天天看點

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

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,如下圖:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

在上圖中,可以看到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的頭檔案查找路徑)

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

上面的代碼使用了很多宏,不過不用管它,現在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檔案,下面是部分檔案的内容:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

從上圖中可以看到,main函數調用了open函數,而open位于動态庫中,在彙編語言中辨別為[email protected]。

是以,下一步,我們應該在動态庫中找到open函數。

3.3 動态庫中的open函數

首先,我們需要确定open函數在哪個動态連結庫中,使用ldd指令:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

從上圖中可以看到,這裡有三個動态連結庫,其中最熟悉的是libc.so.6即是libc的動态連結庫;

ld-linux-x86-64.so.2是動态連結庫的解釋器,使得動态連結庫中的函數能夠被正确地調用(這裡貌似有個雞蛋問題,不過通過一定的方法解決了);

最後一個,也就是第一個是linux-vdso.so.1, 這個可以了解為linux提供的一個使用者層庫,類似于glibc。

找到了動态庫,下一步應該在這些動态庫中查找open函數,指令:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

第一列是函數的位址,看起來像是一個在檔案中的偏移,第二列是一些辨別(可以使用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。

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

從上圖中可以看到,SYSCALL_CANCEL宏,它位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

SYSCALL_CANCEL是一個變參宏(關于變參宏已經忘得幹幹淨淨),總之會調用INLINE_SYSCALL_CALL。

INLINE_SYSCALL_CALL函數位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

沒什麼好解釋的,我們再去找__INLINE_SYSCALL_DISP,該宏仍然位于sysdeps/unix/sysdep.h檔案中,下面是它的定義:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

這裡有兩個宏,分别是__SYSCALL_CONCAT和__INLINE_SYSCALL_NARGS。

先看下__INLINE_SYSCALL_NARGS是為了确定參數的個數,定義如下:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

然後看下__SYSCALL_CONCAT,它是用來合并字元串,定義如下:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

是以,__INTERNAL_SYSCALL_DISP就是根據參數的個數,使用不同的INLINE_SYSCALL,看了下面的定義就明白了:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

這個時候,openat應該使用的是__INLINE_SYSCALL4,接着看下INLINE_SYSCALL (name, a1, a2, a3, a4, a5):

這是一個體系結構相關的函數,就看下x86_64的吧, 位于sysdeps/unix/sysv/linux/x86_64/sysdep.h:檔案中:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

從上圖中,可以看到errno被設定。

注:在核心中,系統調用失敗後,系統調用函數傳回的是錯誤碼(error number),在libc的庫中,也就是在這裡,進行了一些封裝,如何系統調用失敗,系統調用的傳回值為-1,而錯誤碼儲存在errno中。

INLINE_SYSCALL宏又使用了INTERNAL_SYSCALL,該宏也在sysdeps/unix/sysv/linux/x86_64/sysdep.h檔案中:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

可以看到也是在做字元串的合并,講internal_syscall和nr和在一起,是以,使用這個宏後,結果為:internal_syscall_nr(SYS_ify(name), err, args);

先看下SYS_ify在做什麼,該宏還是在sysdeps/unix/sysv/linux/x86_64/sysdep.h:檔案中,它也是用來合并字元串的:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

合并後的結果是__NR_openat(這裡是這樣的)。

注意一下,每個系統調用都有一個唯一的編号(這個編号将在下面用到),__NR_XXX即是該系統調用的編号(沒有找到這個宏具體在那裡定義),

現在看一下:internal_syscall_nr,這裡nr應該是4,是以應用INTERNAL_SYSCALL後結果是:

internal_syscall4(__NR_openat, err, args),找到該宏,定義如下:

linux系統調用的過程1. 簡介2. linux的系統調用(以open為例)3. 系統調用的過程(從open到sys_open,其實glibc使用的是sys_openat後面會說)

從上圖中可以看到,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的實作方法)。

繼續閱讀