1. 程式人生 > >系統呼叫與API

系統呼叫與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);

這個擴充套件關鍵字是讓這個函式只從棧上來獲取引數
這裡寫圖片描述