linux系統調用的過程
- 1. 簡介
- 2. linux的系統調用(以open為例)
- 3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)
-
- 3.1 先寫一個小程式(test_syscall.c)
- 3.2 檢視編譯連結後的彙編代碼
- 3.3 動态庫中的open函數
- 3.4 在動态庫的源碼中查找open函數(當然是在glibc的源碼中)
- 3.5 在核心中找到sys_openat
1. 簡介
系統調用(syscall)是OS提供的用來和應用程式互動的接口,應用程式通過調用系統調用來使用OS所提供的功能。
但是,一般使用者很少直接使用系統調用,應用層庫(如glibc)為我們封裝了一層,是以我們直接使用應用層庫提供的函數就可以了,比如系統調用open函數,在glibc中進行了封裝,給應用程式提供的接口叫fopen。
2. linux的系統調用(以open為例)
linux提供的系統調用都在檔案:include/linux/syscalls.h,在核心代碼中,他們都以sys_開頭。比如常見的系統調用open,在核心代碼中的名稱為sys_open,如下圖:
在上圖中,可以看到sys_open的原型為:
其中,asmlinkage和__user隻是用作簡單的辨別,沒有什麼太大的意義,是以可以不用理它。
下面看一下linux手冊中open函數的原型(http://man7.org/linux/man-pages/man2/open.2.html):
int open(const char *pathname, int flags);
int open(const char *pathname, int flags, mode_t mode);
上面這兩個函數是在頭檔案fcntl.h中聲明的,是以,當使用open這個系統調用時,應該加上這個頭檔案,即#include <fcntl.h>。
還有,會發現這兩個函數和sys_open的原型不一樣,以sys_開頭的函數又叫做系統調用處理函數,最終上面兩個函數都會調用sys_open。
這個文檔的目的,就是為了說明如何從open到達sys_open的。
再者,可以發現open函數是一個變參函數(舉例子舉得複雜了),檢視fcntl.h檔案,可以看到open函數的聲明:
fcntl.h檔案的位置:/usr/include/fcntl.h(該目錄是gcc的預設查找路徑,可以使用gcc -xc -E -v -指令檢視gcc的頭檔案查找路徑)
上面的代碼使用了很多宏,不過不用管它,現在open函數被聲明了,下面我們來看它在那裡被定于,如何最終到達sys_open。
3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)
3.1 先寫一個小程式(test_syscall.c)
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
int main() {
int handle;
handle = open("mytest.c", O_RDONLY);
close(handle);
return 0;
}
3.2 檢視編譯連結後的彙編代碼
$ gcc test_syscall.c
$ objdump -S a.out > aaa
檢視aaa檔案,下面是部分檔案的内容:
從上圖中可以看到,main函數調用了open函數,而open位于動态庫中,在彙編語言中辨別為[email protected]。
是以,下一步,我們應該在動态庫中找到open函數。
3.3 動态庫中的open函數
首先,我們需要确定open函數在哪個動态連結庫中,使用ldd指令:
從上圖中可以看到,這裡有三個動态連結庫,其中最熟悉的是libc.so.6即是libc的動态連結庫;
ld-linux-x86-64.so.2是動态連結庫的解釋器,使得動态連結庫中的函數能夠被正确地調用(這裡貌似有個雞蛋問題,不過通過一定的方法解決了);
最後一個,也就是第一個是linux-vdso.so.1, 這個可以了解為linux提供的一個使用者層庫,類似于glibc。
找到了動态庫,下一步應該在這些動态庫中查找open函數,指令:
第一列是函數的位址,看起來像是一個在檔案中的偏移,第二列是一些辨別(可以使用man nm來檢視它們的意義)。
從上圖中我們便找到了open函數,說明open函數在glibc中有定義(既然找到就不接着往下面找了)。
3.4 在動态庫的源碼中查找open函數(當然是在glibc的源碼中)
直接給出結果吧,open函數最終是在sysdeps/unix/sysv/linux/open64.c檔案中(當然對于32位系統位于相同目錄下的open.c函數中)。
下面是open64.c函數的源碼,其中包含的open函數的實作。會發現open實作的時候使用的是openat系統調用,這是open的一個封裝函數,
參考linux系統調用的手冊:http://man7.org/linux/man-pages/man2/open.2.html。
從上圖中可以看到,SYSCALL_CANCEL宏,它位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:
SYSCALL_CANCEL是一個變參宏(關于變參宏已經忘得幹幹淨淨),總之會調用INLINE_SYSCALL_CALL。
INLINE_SYSCALL_CALL函數位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:
沒什麼好解釋的,我們再去找__INLINE_SYSCALL_DISP,該宏仍然位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:
這裡有兩個宏,分别是__SYSCALL_CONCAT和__INLINE_SYSCALL_NARGS。
先看下__INLINE_SYSCALL_NARGS是為了确定參數的個數,定義如下:
然後看下__SYSCALL_CONCAT,它是用來合并字元串,定義如下:
是以,__INTERNAL_SYSCALL_DISP就是根據參數的個數,使用不同的INLINE_SYSCALL,看了下面的定義就明白了:
這個時候,openat應該使用的是__INLINE_SYSCALL4,接着看下INLINE_SYSCALL (name, a1, a2, a3, a4, a5):
這是一個體系結構相關的函數,就看下x86_64的吧, 位于sysdeps/unix/sysv/linux/x86_64/sysdep.h:檔案中:
從上圖中,可以看到errno被設定。
注:在核心中,系統調用失敗後,系統調用函數傳回的是錯誤碼(error number),在libc的庫中,也就是在這裡,進行了一些封裝,如何系統調用失敗,系統調用的傳回值為-1,而錯誤碼儲存在errno中。
INLINE_SYSCALL宏又使用了INTERNAL_SYSCALL,該宏也在sysdeps/unix/sysv/linux/x86_64/sysdep.h檔案中:
可以看到也是在做字元串的合并,講internal_syscall和nr和在一起,是以,使用這個宏後,結果為:internal_syscall_nr(SYS_ify(name), err, args);
先看下SYS_ify在做什麼,該宏還是在sysdeps/unix/sysv/linux/x86_64/sysdep.h:檔案中,它也是用來合并字元串的:
合并後的結果是__NR_openat(這裡是這樣的)。
注意一下,每個系統調用都有一個唯一的編号(這個編号将在下面用到),__NR_XXX即是該系統調用的編号(沒有找到這個宏具體在那裡定義),
現在看一下:internal_syscall_nr,這裡nr應該是4,是以應用INTERNAL_SYSCALL後結果是:
internal_syscall4(__NR_openat, err, args),找到該宏,定義如下:
從上圖中可以看到,internal_syscall4使用了嵌入式彙編,其中syscall是intel提供了快速調用系統調用的指令,
這裡syscall有5個參數,第一個是系統調用的編号,即是__NR_openat,其他四個依次是openat所需的參數。
linux有一個系統調用,原型為:
long syscall(long number, …);
是以,上圖中的彙編語言就是在調用該函數,然後根據系統調用号,即number,核心再調用具體的系統調用處理函數(即sys_openat)。
3.5 在核心中找到sys_openat
當陷入核心後,作業系統會根據系統調用的編号,調用正确的函數,即是調用sys_openat(可以看到,
不是sys_open,不過這隻是glibc的實作方法)。