系統調用與API

一、系統調用介紹

1.什麼是系統調用

在現代操作系統中,程序運行的時候,本身並沒有權利訪問多少系統資源,系統有限的資源有可能別多個不同的程序同時訪問,爲了保護系統資源,讓應用程序有能力訪問系統資源,每個操作系統都提供了一套接口,以供應用程序使用。這些接口往往通過系統中斷來實現。比如Linux使用0x80號中斷作爲系統調用的入口,window採用0x2E號中斷作爲系統調用接口。

2.Linux 系統調用

在X86下,系統調用由0x80中斷完成,各個通用寄存器用於傳遞參數,EAX寄存器用於表示系統調用的接口號,比如EAX=1表示退出進程(exit),EAX=2表示創建進程(fork),EAX=3表示讀取IO(read);EAX=4表示寫文件或IO(write)等,每個系統調用都對應於內核源代碼中的一個函數,它們都是以“sys_”開頭的,比如exit調用對應內核中的sys_exit函數。當系統調用返回時,EAX又作爲調用結果的返回值。Linux系統有300多個系統調用,這些系統調用都可以在程序裏直接使用,它的C語言形式被定義在”/user/include/unistd.h”

二、系統調用原理

1.特權級與中斷

現代的CPU常常可以在多種截然不同的特權級別下執行指令,在現代操作系統中也據此有兩種特權級別,分別爲用戶模式(User Mode)內核模式(Kernel Mode ),也被稱爲用戶態內核態。運行在高特權級的代碼將自己降低至低特權是允許的,但反過來低特權級的代碼將自己提高至高特權級則不是輕易就能進行的。
操作系統一般是通過中斷(interrupt)來從用戶態切換到內核態。中斷是一個硬件或軟件發出的請求,要求CPU暫停當前的工作轉手去處理更加重要的事情。
中斷一般具有兩個屬性,一個稱爲中斷號(從0開始),一個稱爲中斷處理程序(Iterrupt Service Routine,ISR)。不同的中斷具有不同的中斷號,而同時一箇中斷處理程序對應一箇中斷號。在內核中,有一個數組稱爲中斷向量表(Interrupt Vector Table),這個數組的第n項包含了指向第n號中斷的中斷處理程序的指針。當中斷到來時,CPU會暫停當前行的代碼,根據中斷的中斷號,在中斷向量表中找到對應的中斷處理程序,並調用它。中斷處理程序執行完成之後,CPU會繼續執行之前的代碼。

由於中斷號有限,系統用一個或幾個中斷號來對應所有的系統調用。i386 下Window裏絕大多數系統調用都是由int 0x2e 來觸發的,而Linux則使用int 0x80來觸發所有的系統調用。Linux的系統調用會將系統調用號放在某個固定的寄存器中,對應的中斷代碼會取得這個系統調用號,並且調用正確的函數。

2.基於int的Linux 的經典系統調用實現

這裏寫圖片描述
1.觸發中斷
首先當程序在代碼裏調用一個系統調用時,是以一個函數的形式調用的,例如程序調用fork:

int main(){
    fork();
}

fork函數是一個對系統調用fork的封裝,可以用下列宏來定義它:

_syscall0(pid_t,fork);

_syscall()是一個宏函數,用於定義一個沒有參數的系統調用的封裝。它的第一個參數爲這個系統調用的返回值類型,這裏爲 pid_t,是一個Linux自定義類型,代表進程的id。_syscall()的第二個參數是系統調用的名稱,_syscall()展開之後會形成一個與系統調用名稱同名的函數。下面的代碼是i386版本的syscall()定義:

#define _syscall0(type,name)

type name(void)
{
 long __res;
 __asm__ volatile("int  $0x80"
     : "=a"(__res)
     : "0"(__NR_##name));
 __syscall_return(type,__res);

}

對於syscall(pid_t,fork),上面的宏將展開爲:

pid_t fork(void)
{
    long __res;
    __asm__ volatile(" int $0x80"
    : "=a" (__res)
    : "0" (__NR_fork));
    __syscall_return(pid_t,__res);
}

上面的格式爲AT&T格式的彙編:
1.__asm__是一個gcc的關鍵字,表示接下來將要嵌入彙編代碼。
2.__asm__的第一個參數是一個字符串,代表彙編代碼的文本。這裏的彙編代碼只有一句 int $0x80,這就要調用0x80號中斷。
3.=a __res表示調用eax(a 表示eax) 輸出返回數據並存儲在__res裏。
4.“0”__NR_##name表示__NR_##name爲輸入,“0”指示由編譯器選擇和輸出相同的寄存器(即eax)來傳遞參數
更直觀的,可以把這段彙編改寫爲更爲可讀的格式:
main->fork:

pid_t fork(void){
    long __res;
    $eax =__NR_fork
    int $0x80
    __res = $eax
    __syscall_return(pid_t,__res);
}

__NR_fork是一個宏,表示fork系統調用的調用號,對於x86體系結構,該宏的定義可以在Linux/include/asm-x86/unistd_32.h裏找到:

#define __NR_restart_syscall 0
#define __NR_exit    1
#define __NR_fork    2
........

而__syscall_return 是另一個宏,定義如下:

#define __syscallz_return(type,res)

do{
    if((unsigned long)(res)>=(unsigned long)(-125)){
    errno=-(res);
    res = -1;
}
return (type)(res)
}while(0)

_syscall_return負責將系統調用的返回信息存儲在errno中,將調用失敗信息以-1返回。

fork:
mov eax,2
int 0x80
cmp eax,0xFFFFFF83
jb syscall_noerror
neg eax
mov errno,eax
mov eax,0xFFFFFFFF
syscall_noerror:
ret

如果系統調用本身有參數要如何實現呢?x86 Linux 下的syscall,用於帶一個參數的系統調用:

#define _syscall112(type,name,type1,arg1)

type name(type1,arg1)
{
    long __res;
    __asm__ volatile(
    "int $0x80"
    :  "=a" (__res)
    :  "0" (__NR_##name),"b"((long)(arg1)));
 __syscall_return(type,__res);
}

“b”(long)(arg1),這句的意思是先把arg1強制轉化爲long,然後存放在EBX(b 代表EBX)裏作爲輸入。

push ebx
eax = __NR_##name
ebx = arg1
int 0x80
__res = eax
pop ebx

系統調用有1個參數,那麼參數通過EBX來傳入。x86下Linux支持的系統調用參數至多有6個,分別使用6個寄存器來傳遞,它們分別是EBX、ECX、EDX、ESI、EDI和EBP。
當用戶調用某個系統調用的時候,實際是執行了以上一段彙編代碼。CPU執行到int $0x80時,會保存現場以便恢復,接着會將特權狀態切換到內核狀態。

2.切換堆棧
在實際執行中斷向量表中的第0x80號元素所對應的函數之前,CPU還要進行相應棧的切換。在Linux中,用戶態和內核態使用的是不同的棧,兩者各自負責各自的函數調用,互不干擾。當執行中斷時,當前棧需要從用戶棧切換到內核棧,返回時,需要從內核棧切換回用戶棧。

所謂的"當前棧",指的是ESP的值所在的棧空間。如果ESP的值位於用戶棧的範圍內,那麼程序的當前棧就是用戶棧,反之亦然。此外,寄存器SS的值還應該指向棧所在的頁。
當前棧由用戶棧切換爲內核棧的實際行爲就是:
(1)保存當前的ESP、SS的值
(2)將ESP、SS的值設置爲內核棧的相對值。
反過來,將當前棧由內核棧切換爲用戶棧的實際行爲則是:
(1)恢復原來ESP、SS的值。
(2)用戶態的ESP和SS的值保存在內核棧上。這一行爲由i386的中斷指令自動的由硬件完成。
當0x80號中斷髮生的時候,CPU除了切入內核態之外,還會自動完成下列幾件事情:
(1)找到當前進程的內核棧(每一個進程都有自己的內核棧)
(2)在內核棧中依次壓入用戶態的寄存器SS、ESP、EFLAGS、CS、EIP。
 當內核從系統調用中返回的時候,需要調用iret指令來回到用戶態,iret指令則會從內核棧裏彈出寄存器SS、ESP、EFLAGS\CS、EIP的值,使得棧恢復到用戶態的狀態。

這裏寫圖片描述
3.中斷處理程序
在int指令合理地切換了棧之後,程序就切換到了中斷向量表中記錄的0x80號中斷處理程序。
這裏寫圖片描述
i386的中斷向量表在Linux源代碼的Linux/arch/i386/kernel/trap.c裏可見一部分。文件的末尾 trap_init函數用於初始化中斷向量表。在trap_init函數結尾最後一行set_system_gate(SYSCALL_VECTOR,$system_call)設置了系統調用中斷號。Linux/include/asm-i386/mach-default/irq_vectors.h裏可以找到SYSCALL_VECTOR的定義:

#define SYSCALL_VECTOR 0X80

用戶調用 int 0x80 之後,最後執行的函數是system_call,該函數在Linux/arch/i386/kernel/entry.S裏可以找到定義。
main -> fork -> int 0x80 ->system_call

ENTRY(system_call)
    ......
    SAVE_ALL//
    ......
    cmpl $(nr_syscalls),%eax
    jae syscall_badsys

上面是system_call的開頭,在這裏一開始使用宏將SAVE_ALL將各種寄存器壓入棧中,然後比較nr_syscalls與eax的值,nr_syscalls是比最大系統調用號大1的值,小於它的話跳到syscall_badsys執行:

syscall_call:
    call *sys_call_table(0,%eax,4)
    ...
  RESTORE_REGS
    ...
   iret

sys_call_table(0,%eax,4)定義在 Linux/arch/i386/kernel/systable.S裏

.data
ENTRY(sys_call_table)
    .long sys_restart_syscall
    .long sys_exit
    .long sys_fork
    .long sys_read
    .long sys_write
    ......

這就是Linux的i386系統調用表。*sys_call_table(0,%eax,4)指的是sys_call_table上偏移量爲0+%eax*4上的那個元素的值指向的函數。
內核裏的系統調用函數往往以sys_加上系統調用函數名來名,例如
sys_fork、sys_open等。
這裏寫圖片描述
系統調用從用戶那裏獲取參數的方式

用戶調用系統調用時,根據參數的數量不同,依次將參數放入EBX、ECX、EDX、ESI、EDI和EBP這六個寄存器中。
而在進入系統調用的服務程序system_call的時候,system_call調用了一個宏SAVE_ALL來保存各個寄存器的值到棧中。SAVE_ALL的大致內容如下:

#define SAVE_ALL
 ......
 push %eax
 push %ebp
 push %esi
 push %edx
 push %ecx
 push %ebx
 mov $(KERNEL_DS),%edx
 mov %edx,%ds
 mov %edx,%es

入棧順序與函數參數順序一樣
參數被放在了棧上。
這裏寫圖片描述
另一反面,所有以sys開頭的內核系統調用函數,都有一個asmlinkage的標識

asmlinkage pid_t sys_fork(void);

這個擴展關鍵字是讓這個函數只從棧上來獲取參數
這裏寫圖片描述

發佈了45 篇原創文章 · 獲贊 31 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章