自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Linux操作系統(tǒng)系統(tǒng)編程:x86-64架構下的系統(tǒng)調用

系統(tǒng) Linux
從計算機發(fā)展歷程看,系統(tǒng)調用一直在不斷革新。早期操作系統(tǒng)資源有限,系統(tǒng)調用種類和功能少,程序與內核交互簡單。隨著硬件性能提升、軟件場景變復雜,x86-64 架構持續(xù)演進,系統(tǒng)調用機制也在優(yōu)化,指令集、參數(shù)傳遞方式不斷改進,與內核功能深度融合,推動著 Linux 系統(tǒng)編程不斷進步。

在Linux操作系統(tǒng)里,系統(tǒng)編程如同精密儀器的核心部件,掌控著系統(tǒng)運行的關鍵。而 x86-64 架構下的系統(tǒng)調用,更是連接用戶空間程序與內核的關鍵橋梁。你可以把用戶空間的程序想象成一個個 “工匠”,它們有著各式各樣的需求,比如讀取文件數(shù)據、展示圖像、與其他程序交流信息等。但用戶空間就像被一道無形的屏障圍住,“工匠們” 無法直接觸碰內核掌管的磁盤、內存、網絡接口等底層資源。這時,系統(tǒng)調用就如同 “工匠們” 手中的神奇工具,當他們發(fā)出特定指令,就能突破屏障,讓內核這位 “大管家” 提供相應服務。

從計算機發(fā)展歷程看,系統(tǒng)調用一直在不斷革新。早期操作系統(tǒng)資源有限,系統(tǒng)調用種類和功能少,程序與內核交互簡單。隨著硬件性能提升、軟件場景變復雜,x86-64 架構持續(xù)演進,系統(tǒng)調用機制也在優(yōu)化,指令集、參數(shù)傳遞方式不斷改進,與內核功能深度融合,推動著 Linux 系統(tǒng)編程不斷進步。當下,不管是數(shù)據中心的高性能應用,還是手持設備里的便捷 APP,高效的系統(tǒng)調用機制都是背后的有力支撐。理解 x86-64 架構下的系統(tǒng)調用,不僅是掌握 Linux 系統(tǒng)編程的關鍵,更是開啟現(xiàn)代計算機高效運行奧秘的鑰匙。現(xiàn)在,就讓我們一起深入探索 x86-64 架構下系統(tǒng)調用的精妙之處 。

一、x86-64系統(tǒng)調用初相識

在計算機的世界里,系統(tǒng)調用可謂是連接用戶程序與操作系統(tǒng)內核的橋梁,有著不可或缺的地位。它是操作系統(tǒng)提供給用戶程序的一組 “特殊接口”,用戶程序能夠借助這些接口,請求內核提供各種服務,像文件操作、進程管理、內存分配等等??梢哉f,系統(tǒng)調用是操作系統(tǒng)內核向外提供服務的主要途徑,也是用戶程序與操作系統(tǒng)交互的關鍵方式。

系統(tǒng)調用與常規(guī)函數(shù)調用不同,因為被調用的代碼位于內核中。需要特殊指令來使處理器執(zhí)行從用戶態(tài)切換到特權態(tài)(ring 0)。此外,調用的內核代碼通過系統(tǒng)調用號來標識,而不是函數(shù)地址。

當用戶空間程序需要執(zhí)行一個系統(tǒng)調用時,它會使用特定的指令(例如x86架構中的syscall指令)觸發(fā)從用戶態(tài)到內核態(tài)的切換。在進行切換時,處理器會將當前的上下文保存起來,包括寄存器狀態(tài)和程序計數(shù)器等。然后,處理器會跳轉到預定義的系統(tǒng)調用入口點,該入口點由系統(tǒng)調用號標識。

在內核中,系統(tǒng)調用表(system call table)維護了系統(tǒng)調用號與相應內核函數(shù)的映射關系。當處理器進入內核態(tài)并跳轉到系統(tǒng)調用入口點時,內核會根據系統(tǒng)調用號找到對應的內核函數(shù)來執(zhí)行相應的操作。內核函數(shù)完成后,處理器將恢復之前保存的上下文,并返回到用戶空間程序繼續(xù)執(zhí)行。

通過使用系統(tǒng)調用號而不是函數(shù)地址,內核能夠提供一種標準化的、跨平臺的系統(tǒng)調用接口。不同的系統(tǒng)調用由唯一的系統(tǒng)調用號進行標識,這樣用戶空間程序可以使用相同的系統(tǒng)調用號在不同的操作系統(tǒng)上進行系統(tǒng)調用,而無需關心具體的內核實現(xiàn);Linux 應用程序要與內核通信,需要通過系統(tǒng)調用。系統(tǒng)調用,相當于用戶空間和內核空間之間添加了一個中間層。

圖片圖片

因此,系統(tǒng)調用的機制涉及從用戶態(tài)到內核態(tài)的切換、系統(tǒng)調用號的標識和匹配,以及內核中相應的處理邏輯,以實現(xiàn)用戶空間程序與內核的交互,系統(tǒng)調用作用:

  1. 內核將復雜困難的邏輯封裝起來,用戶程序通過系統(tǒng)來操作硬件,極大簡化了用戶程序開發(fā)。
  2. 降低用戶程序非法操作的風險,保證操作系統(tǒng)能安全,穩(wěn)定地工作。
  3. 系統(tǒng)有效地分離了用戶程序和內核開發(fā)。
  4. 通過接口訪問黑盒操作,使得程序有更好的移植性。

而 x86-64 系統(tǒng)調用,指的是在 x86-64 架構的計算機系統(tǒng)中,用戶空間程序與內核進行交互的主要機制。x86-64 是一種廣泛應用的計算機硬件架構,包括我們日常使用的桌面電腦、服務器等,很多都是基于這個架構。在這個架構下的系統(tǒng)調用,有著特定的實現(xiàn)方式和規(guī)則。

或許你會好奇,x86-64 系統(tǒng)調用與我們平常熟悉的函數(shù)調用有啥不一樣呢?從本質上來說,普通函數(shù)調用是在用戶空間內進行的,執(zhí)行過程相對簡單。當我們在程序里調用一個普通函數(shù)時,程序直接跳轉到函數(shù)的代碼處執(zhí)行,執(zhí)行完畢后再返回調用點繼續(xù)執(zhí)行后續(xù)代碼,整個過程都在用戶空間,不會涉及到系統(tǒng)內核。比如說,在 C 語言中調用一個自定義的函數(shù)add(int a, int b),計算兩個整數(shù)的和,這就是一個普通函數(shù)調用:

#include <stdio.h>

int add(int a, int b) {
    return a + b;
}

int main() {
    int result = add(3, 5);
    printf("結果是: %d\n", result);
    return 0;
}

在這個例子里,add函數(shù)在用戶空間執(zhí)行,調用和返回都很直接。

但 x86-64 系統(tǒng)調用可就復雜多了。由于它涉及到用戶空間程序請求內核服務,所以需要進行特權級別的切換,從用戶態(tài)切換到內核態(tài)。簡單來講,用戶態(tài)下程序的操作權限有限,而內核態(tài)下程序擁有更高的權限,可以訪問系統(tǒng)的關鍵資源和執(zhí)行特權指令。當進行系統(tǒng)調用時,程序需要通過特定的指令(比如syscall指令)來觸發(fā)從用戶態(tài)到內核態(tài)的切換,然后內核根據系統(tǒng)調用號找到對應的內核函數(shù)進行執(zhí)行,執(zhí)行完畢后再切換回用戶態(tài),并返回結果給用戶程序。這就好比你要進入一個高級機密區(qū)域(內核態(tài))獲取某些重要資源(執(zhí)行內核服務),必須先經過嚴格的身份驗證(特權級切換),才能進入并獲取所需。

二、x86-64 系統(tǒng)調用原理

2.1系統(tǒng)調用流程

為了更直觀地理解 x86-64 系統(tǒng)調用的工作過程,我們通過一個詳細的流程圖表(如下)和具體的程序實例來深入剖析。就以一個簡單的文件讀取程序為例,看看它是如何進行系統(tǒng)調用的。

假設我們有一個用 C 語言編寫的簡單文件讀取程序:

#include <stdio.h>

int main() {
    FILE *file = fopen("test.txt", "r");
    if (file == NULL) {
        perror("無法打開文件");
        return 1;
    }
    char buffer[100];
    size_t bytes_read = fread(buffer, 1, sizeof(buffer), file);
    if (bytes_read > 0) {
        printf("讀取的內容: %s\n", buffer);
    }
    fclose(file);
    return 0;
}

在這個程序中,當執(zhí)行fopen函數(shù)時,實際上它會調用底層的系統(tǒng)調用open來打開文件。具體過程如下:

  1. 用戶空間程序發(fā)起系統(tǒng)調用請求:程序執(zhí)行到fopen函數(shù)時,它會向操作系統(tǒng)發(fā)起打開文件的請求,這就觸發(fā)了系統(tǒng)調用。
  2. 設置系統(tǒng)調用號和參數(shù)到寄存器:根據 x86-64 的調用約定,會將系統(tǒng)調用號(比如open系統(tǒng)調用在 x86-64 系統(tǒng)中的調用號是 2)存入%rax寄存器,將文件名(這里是test.txt)的地址存入%rdi寄存器,將打開文件的模式(這里是只讀模式"r"對應的標志)存入%rsi寄存器。
  3. 執(zhí)行 syscall 指令:當所有參數(shù)設置好后,程序執(zhí)行syscall指令,這個指令是觸發(fā)系統(tǒng)調用的關鍵,它會引發(fā)處理器從用戶態(tài)切換到內核態(tài)。
  4. 處理器切換到內核態(tài):syscall指令執(zhí)行后,處理器的特權級別提升,從用戶態(tài)進入內核態(tài),此時程序可以訪問內核的資源和執(zhí)行特權指令。
  5. 內核根據系統(tǒng)調用號查找對應的內核函數(shù):內核接收到系統(tǒng)調用請求后,會從%rax寄存器中讀取系統(tǒng)調用號,然后在內核的系統(tǒng)調用表中查找對應的內核函數(shù)。比如對于open系統(tǒng)調用號 2,內核會找到對應的sys_open函數(shù)。
  6. 執(zhí)行內核函數(shù):內核調用sys_open函數(shù),該函數(shù)會進行一系列的操作,如檢查文件權限、查找文件的 inode 等,最終完成文件的打開操作,并返回一個文件描述符。
  7. 內核函數(shù)執(zhí)行完畢,返回結果到寄存器:sys_open函數(shù)執(zhí)行完成后,會將結果(文件描述符或者錯誤碼)存入%rax寄存器。
  8. 處理器切換回用戶態(tài):內核處理完系統(tǒng)調用后,通過特定的機制(如sysret指令)將處理器的特權級別從內核態(tài)降回用戶態(tài)。
  9. 用戶空間程序從寄存器獲取結果:用戶空間程序繼續(xù)執(zhí)行,從%rax寄存器中獲取系統(tǒng)調用的結果。如果%rax的值是一個有效的文件描述符,那么fopen函數(shù)就可以繼續(xù)進行后續(xù)的文件讀取操作;如果%rax的值是一個錯誤碼,那么fopen函數(shù)會根據錯誤碼進行相應的錯誤處理,比如在程序中通過perror函數(shù)輸出錯誤信息。

2.2調用約定深度剖析

參數(shù)傳遞規(guī)則:依據 x86-64 ABI(應用二進制接口)文檔,在進行系統(tǒng)調用時,參數(shù)的傳遞有著明確的規(guī)則。參數(shù) 1 對應%rdi寄存器,參數(shù) 2 對應%rsi寄存器,參數(shù) 3 對應%rdx寄存器,參數(shù) 4 對應%r10寄存器,參數(shù) 5 對應%r8寄存器,參數(shù) 6 對應%r9寄存器 。例如,在前面提到的open系統(tǒng)調用中,文件名作為參數(shù) 1,就會被傳遞到%rdi寄存器;打開文件的模式作為參數(shù) 2,會被傳遞到%rsi寄存器。

并且,系統(tǒng)調用的參數(shù)數(shù)量限制為 6 個,如果需要傳遞更多參數(shù),可能需要將多個參數(shù)打包成一個結構體,通過內存?zhèn)鬟f。同時,參數(shù)類型限制為INTEGER和MEMORY。INTEGER類型指的是可以存放在通用寄存器中的整型數(shù)據,比如int、long等;MEMORY類型則是指通過內存(堆棧)來傳遞和返回的數(shù)據類型,像結構體、數(shù)組等。

系統(tǒng)調用號作用:系統(tǒng)調用號在 x86-64 系統(tǒng)調用中起著至關重要的作用。它通過%rax寄存器傳遞,是內核識別系統(tǒng)調用的唯一標識。每一個系統(tǒng)調用在內核中都有一個對應的系統(tǒng)調用號,就如同函數(shù)指針一樣,引導程序找到對應的內核函數(shù)執(zhí)行。比如,在 Linux 系統(tǒng)中,write系統(tǒng)調用的系統(tǒng)調用號是 1,exit系統(tǒng)調用的系統(tǒng)調用號是 60。

當用戶空間程序發(fā)起系統(tǒng)調用時,將相應的系統(tǒng)調用號存入%rax寄存器,內核接收到系統(tǒng)調用請求后,首先從%rax寄存器讀取系統(tǒng)調用號,然后根據這個調用號在內核的系統(tǒng)調用表中查找對應的內核函數(shù)。系統(tǒng)調用表是一個存儲著系統(tǒng)調用號和對應內核函數(shù)指針的數(shù)組,通過系統(tǒng)調用號作為索引,內核可以快速定位到要執(zhí)行的內核函數(shù),從而實現(xiàn)對用戶請求的處理。

系統(tǒng)調用指令解析:syscall指令是 x86-64 系統(tǒng)調用的核心指令,它的執(zhí)行過程相當復雜。當程序執(zhí)行syscall指令時,首先會保存返回地址到%rcx寄存器,這個返回地址就是syscall指令的下一條指令的地址,以便系統(tǒng)調用完成后能夠返回正確的位置繼續(xù)執(zhí)行用戶程序。接著,syscall指令會替換指令指針寄存器%rip,將其值替換為 IA32_LSTAR MSR(模型特定寄存器)中存儲的地址,這個地址指向內核中系統(tǒng)調用處理程序的入口。

同時,syscall指令還會保存標志寄存器%rflags到%r11寄存器,并使用 IA32_FMASK MSR 對%rflags進行掩碼操作 ,以確保在特權級切換過程中標志位的正確處理。之后,syscall指令會加載新的CS(代碼段寄存器)和SS(堆棧段寄存器)選擇子,其值來源于 IA32_STAR MSR 的特定比特位。通過這一系列操作,syscall指令實現(xiàn)了從用戶態(tài)到內核態(tài)的快速切換,使得程序能夠進入內核執(zhí)行系統(tǒng)調用對應的內核函數(shù)。

2.3返回值與錯誤碼

當系統(tǒng)調用執(zhí)行完畢,從內核返回用戶空間時,%rax寄存器保存著系統(tǒng)調用的結果。如果系統(tǒng)調用成功執(zhí)行,%rax中存儲的就是正常的返回值,比如對于open系統(tǒng)調用,如果文件成功打開,%rax中會返回一個有效的文件描述符;對于read系統(tǒng)調用,如果讀取文件成功,%rax中會返回實際讀取的字節(jié)數(shù)。然而,如果系統(tǒng)調用過程中發(fā)生了錯誤,%rax的值就會在 -4095 至 -1 之間,這個值表示錯誤碼,并且是實際錯誤碼的相反數(shù)(即-errno) 。例如,如果%rax的值為 -1,表示發(fā)生了EPERM錯誤,即操作不被允許;如果%rax的值為 -2,表示發(fā)生了ENOENT錯誤,即文件或目錄不存在。

在 C 語言中,我們可以通過errno全局變量來獲取具體的錯誤碼,然后通過查閱相關的錯誤碼定義(通常在<errno.h>頭文件中),定位具體的錯誤類型,以便進行相應的錯誤處理。比如在前面的文件讀取程序中,如果fopen函數(shù)返回NULL,我們可以通過perror函數(shù)輸出錯誤信息,perror函數(shù)會根據errno的值查找對應的錯誤描述并輸出,幫助我們快速定位和解決問題。

三、用戶空間

我們以一個 Hello world 程序開始,逐步進入系統(tǒng)調用的學習。下面是用匯編代碼寫的一個簡單的程序:

.section .data
msg:
    .ascii "Hello World!\n"
len = . - msg

.section .text
.globl  main
main:

    # ssize_t write(int fd, const void *buf, size_t count)
    mov $1, %rdi            # fd
    mov $msg, %rsi          # buffer
    mov $len, %rdx          # count
    mov $1, %rax            # write(2)系統(tǒng)調用號,64位系統(tǒng)為1
    syscall

    # exit(status)
    mov $0, %rdi            # status
    mov $60, %rax           # exit(2)系統(tǒng)調用號,64位系統(tǒng)為60
    syscall

編譯并運行:

$ gcc -o helloworld helloworld.s 
$ ./helloworld
Hello world!
$ echo $?
0

上面這段代碼,是直接從我的一篇文章 使用 GNU 匯編語法編寫 Hello World 程序的三種方法拷貝過來的。那篇文章里還提到了使用int 0x80軟中斷和printf函數(shù)實現(xiàn)輸出的方法,有興趣的可以去看下。

四、內核空間

用戶空間通過 syscall 指令,從用戶空間進入內核空間。

4.1內核調試

設置斷點。在內核 write 函數(shù)名下斷點,調試跟蹤函數(shù)的調用堆棧。

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/fs.h>

static ssize_t my_write(struct file *file, const char __user *buf,
                        size_t len, loff_t *offset)
{
    /* 在這里設置斷點 */

    /* 打印調用堆棧 */
    dump_stack();

    /* 寫入操作的具體實現(xiàn) */
    // ...

    return len;
}

static struct file_operations fops = {
    .write = my_write,
};

static int __init my_init(void)
{
    /* 注冊字符設備驅動程序 */
    // ...

    return 0;
}

static void __exit my_exit(void)
{
    /* 注銷字符設備驅動程序 */
    // ...
}

module_init(my_init);
module_exit(my_exit);

MODULE_LICENSE("GPL");

調試觸發(fā)斷點。查看函數(shù)調用堆棧,可以發(fā)現(xiàn) syscall 指令觸發(fā) entry_SYSCALL_64 處理函數(shù)。

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>

int main() {
    pid_t pid = getpid();
    
    // 觸發(fā)系統(tǒng)調用
    syscall(39, pid, NULL, NULL);
    
    return 0;
}

以上代碼是一個簡單的C程序,在執(zhí)行期間會通過syscall函數(shù)觸發(fā)系統(tǒng)調用。你可以將代碼保存為test.c,然后使用gcc進行編譯:gcc -o test test.c。

接下來,你可以使用GDB連接到生成的可執(zhí)行文件并設置斷點以及跟蹤函數(shù)調用堆棧。在終端中輸入gdb ./test啟動GDB調試器。然后按照以下步驟進行操作:

  1. 在GDB提示符下輸入命令:break main,設置一個斷點在程序的main函數(shù)處。
  2. 輸入命令: run ,運行程序。
  3. 當程序運行到syscall指令時,會進入內核并跳轉到相應的系統(tǒng)調用處理函數(shù)(例如entry_SYSCALL_64)。
  4. 在entry_SYSCALL_64處理函數(shù)處會自動停下,此時你可以使用命令: bt(backtrace) 或者 where 來查看函數(shù)調用堆棧信息。

4.2系統(tǒng)調用入口

entry_SYSCALL_64 是 64 位 syscall 指令 入口函數(shù),這個函數(shù)通常是由操作系統(tǒng)提供并負責處理所有來自用戶空間發(fā)起的系統(tǒng)調用請求。具體實現(xiàn)可能因不同的操作系統(tǒng)而有所差異,但其作用都是為了協(xié)調用戶空間和內核空間之間的交互。在不同的架構或操作系統(tǒng)上,對于syscall指令和相應處理函數(shù)名稱可能會有所不同。例如,在32位x86架構上使用entry_INT80_32來處理syscall指令。因此,請根據目標平臺和操作系統(tǒng)環(huán)境選擇正確的符號名稱和相關文檔來進行調試和理解

初始化系統(tǒng)調用。當 linux 內核啟動時,MSR 特殊模塊寄存器會存儲 syscall 指令的入口函數(shù)地址;當 syscall 指令執(zhí)行后,系統(tǒng)從特殊模塊寄存器中取出入口函數(shù)地址進行調用。

#include <linux/kernel.h>
#include <linux/module.h>

MODULE_LICENSE("GPL");

// 聲明一個簡單的系統(tǒng)調用函數(shù)
asmlinkage long my_syscall(void)
{
    printk(KERN_INFO "Hello from custom syscall!\n");
    return 0;
}

// 初始化系統(tǒng)調用表
static void init_syscall_table(void)
{
    // 獲取syscall table地址
    unsigned long *syscall_table = (unsigned long *)kallsyms_lookup_name("sys_call_table");

    // 替換對應系統(tǒng)調用函數(shù)指針
    write_cr0(read_cr0() & (~0x10000));  // 關閉寫保護

    syscall_table[__NR_my_syscall] = (unsigned long)my_syscall;  // 將自定義系統(tǒng)調用函數(shù)指針存儲在syscall table中

    write_cr0(read_cr0() | 0x10000);  // 開啟寫保護
}

static int __init my_module_init(void)
{
    init_syscall_table();
    
    printk(KERN_INFO "Custom syscall module loaded\n");
    
    return 0;
}

static void __exit my_module_exit(void)
{
    printk(KERN_INFO "Custom syscall module unloaded\n");
}

module_init(my_module_init);
module_exit(my_module_exit);

入口函數(shù)工作流程:

  1. 程序從用戶空間進入內核空間,保存用戶態(tài)現(xiàn)場,載入內核態(tài)的信息,程序工作狀態(tài)從用戶態(tài)轉變?yōu)閮群藨B(tài)。
  2. 根據系統(tǒng)調用號,從系統(tǒng)跳轉表中,調用對應的系統(tǒng)調用函數(shù)。
  3. 系統(tǒng)調用函數(shù)完成邏輯后,需要從內核空間回到用戶空間,程序內核態(tài)轉變?yōu)橛脩魬B(tài),需要把之前保存的用戶態(tài)現(xiàn)場進行恢復。
ENTRY(entry_SYSCALL_64)
        TRACE_IRQS_OFF
        subq    $FRAME_SIZE, %rsp          /* Reserve space for pt_regs */
        MOV_LDX(regs, %rsp)                /* Save user stack pointer */
        cmpl    $(nr_syscalls),%eax        /* syscall number valid? */
        jae     badsys                     

        /*
         * Load the syscall table pointer into r10 from a global variable.
         * We stash it in memory at boot time to workaround boot loader
         * address randomization.
         *
         *     movl  sys_call_table(,%rax,8),%r10
         *
         * can be replaced with this:
         *
         *     leal  sys_call_table(%rip),%r10
         *     movq  (%r10,%rax,8),%r10
         */

       .section ".data", "a"
        
sys_call_table:
		.quad   __x64_sys_call_table- sys_call_table

       .section ".text", "ax"

leaq    sys_call_table(%rip),%r10      /* Get the syscall table address into r10 */
movq    (%r10,%rax,8), %r10            /* Load the corresponding system call handler */

在這段代碼中,我們可以看到以下幾個關鍵步驟:

  1. 首先,通過 subq 指令為 pt_regs 結構體在用戶棧上分配空間,用于保存系統(tǒng)調用的參數(shù)和返回值。
  2. 然后,將用戶棧指針 %rsp 的值保存到 regs 寄存器中,以便在系統(tǒng)調用處理函數(shù)中可以訪問到用戶棧上的參數(shù)。
  3. 接下來,通過 cmpl 指令檢查系統(tǒng)調用號是否有效。如果系統(tǒng)調用號大于等于 nr_syscalls(即 sys_call_table 數(shù)組的長度),則跳轉到 badsys 標簽處進行錯誤處理。
  4. 緊接著,使用 leaq 和 movq 指令加載 syscall table 的地址,并從表中獲取對應的系統(tǒng)調用處理函數(shù)地址,存儲在寄存器 %r10 中。這里有兩種不同的實現(xiàn)方式,一種是直接使用全局變量 sys_call_table 獲取 syscall table 的地址;另一種是先通過 RIP 相對尋址獲取 sys_call_table 地址,并再從表中獲取對應的系統(tǒng)調用處理函數(shù)地址。

然后,在代碼中還有其他一些邏輯和錯誤處理部分,在此就不一一列舉了。

gdb 反匯編查看 entry_SYSCALL_64 函數(shù)功能

(1)編譯內核并啟動調試模式:

make menuconfig  # 配置內核選項(可根據需要進行配置)
make -j$(nproc)  # 編譯內核
sudo gdb vmlinux  # 啟動 gdb,并加載編譯好的內核文件

(2)在gdb中設置斷點:

break entry_SYSCALL_64  # 在 entry_SYSCALL_64 函數(shù)處設置斷點

(3)啟動內核調試:

target remote :1234  # 連接到 QEMU 調試服務器(如果使用 QEMU 進行內核調試)
continue             # 繼續(xù)執(zhí)行,使程序運行到設置的斷點處

(4)反匯編查看代碼:

disassemble /m entry_SYSCALL_64  # 使用 disassemble 命令反匯編 entry_SYSCALL_64 函數(shù)

struct pt_regs。程序在系統(tǒng)調用后,從用戶空間進入內核空間,保存用戶態(tài)現(xiàn)場,保存用戶態(tài)傳入參數(shù)。

/* arch/x86/include/asm/ptrace.h */
struct pt_regs {
/*
 * C ABI says these regs are callee-preserved. They aren't saved on kernel entry
 * unless syscall needs a complete, fully filled "struct pt_regs".
 */
    unsigned long r15;
    unsigned long r14;
    unsigned long r13;
    unsigned long r12;
    unsigned long rbp;
    unsigned long rbx;
/* These regs are callee-clobbered. Always saved on kernel entry. */
    unsigned long r11;
    unsigned long r10;  /* 程序傳遞到內核的第 4 個參數(shù)。 */
    unsigned long r9;   /* 程序傳遞到內核的第 6 個參數(shù)。 */
    unsigned long r8;   /* 程序傳遞到內核的第 5 個參數(shù)。 */
    unsigned long ax;   /* 程序傳遞到內核的系統(tǒng)調用號。 */
    unsigned long cx;   /* 程序傳遞到內核的 syscall 的下一條指令地址。 */
    unsigned long dx;   /* 程序傳遞到內核的第 3 個參數(shù)。 */
    unsigned long si;   /* 程序傳遞到內核的第 2 個參數(shù)。 */
    unsigned long di;   /* 程序傳遞到內核的第 1 個參數(shù)。 */
/*
 * On syscall entry, this is syscall#. On CPU exception, this is error code.
 * On hw interrupt, it's IRQ number:
 */
    unsigned long orig_rax; /* 系統(tǒng)調用號。 */
/* Return frame for iretq 
 * 內核態(tài)返回用戶態(tài)需要恢復現(xiàn)場的數(shù)據。*/
    unsigned long ip;       /* 保存程序調用 syscall 的下一條指令地址。 */
    unsigned long cs;       /* 用戶態(tài)代碼起始段地址。 */
    unsigned long flags;    /* 用戶態(tài)的 CPU 標志。 */
    unsigned long sp;       /* 用戶態(tài)的棧頂?shù)刂罚却媸窍蛳略鲩L的)。 */
    unsigned long ss;       /* 用戶態(tài)的數(shù)據段地址。 */
/* top of stack page */
};

4.3do_syscall_64

do_syscall_64 函數(shù)是 Linux 內核中的關鍵函數(shù)之一,它的主要功能是處理 64 位系統(tǒng)調用。當用戶程序通過軟件中斷(syscall)發(fā)起系統(tǒng)調用請求時,內核會將控制轉移到 do_syscall_64 函數(shù)來執(zhí)行相應的操作。

具體而言,do_syscall_64 函數(shù)完成以下主要功能:

  • 獲取系統(tǒng)調用號:從當前進程的 CPU 寄存器或棧中獲取系統(tǒng)調用號,以確定用戶程序請求執(zhí)行哪個特定的系統(tǒng)調用。
  • 參數(shù)傳遞:根據系統(tǒng)調用約定,從當前進程的寄存器或堆棧中提取相應數(shù)量和類型的參數(shù),并將這些參數(shù)傳遞給相應的系統(tǒng)調用處理函數(shù)。
  • 權限檢查:驗證當前進程是否有足夠權限執(zhí)行所請求的系統(tǒng)調用。這可能涉及訪問權限、資源配額、權限級別等方面的檢查。
  • 系統(tǒng)調用執(zhí)行:將控制權轉移給與所請求系統(tǒng)調用對應的內核函數(shù),以便在內核模式下執(zhí)行特定操作。
  • 結果返回:如果需要,將系統(tǒng)調用執(zhí)行結果返回給用戶空間,并更新相應寄存器或內存位置以供用戶程序讀取結果。
ENTRY(entry_SYSCALL_64)
    ...
    call    do_syscall_64           /* returns with IRQs disabled */
    ...
END(entry_SYSCALL_64)
/* arch/x86/entry/common.c */
#ifdef CONFIG_X86_64
__visible void do_syscall_64(unsigned long nr, struct pt_regs *regs) {
    struct thread_info *ti;
    ...
    /*
     * NB: Native and x32 syscalls are dispatched from the same
     * table.  The only functional difference is the x32 bit in
     * regs->orig_ax, which changes the behavior of some syscalls.
     */
    nr &= __SYSCALL_MASK;
    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        /* 通過系統(tǒng)調用跳轉表,調用系統(tǒng)調用號對應的函數(shù)。
         * 函數(shù)返回值保存在 regs->ax 里,最后將這個值,保存到 rax 寄存器傳遞到用戶空間。 */
        regs->ax = sys_call_table[nr](regs);
    }

    syscall_return_slowpath(regs);
}
#endif

4.4系統(tǒng)調用表

系統(tǒng)調用表 syscall_64.tbl,建立了系統(tǒng)調用號與系統(tǒng)調用函數(shù)名的映射關系。腳本會根據這個表,自動生成相關的映射源碼。

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>

// 定義系統(tǒng)調用號與函數(shù)名的映射數(shù)組
static const char *syscall_names[] = {
    [0] = "sys_read",
    [1] = "sys_write",
    [2] = "sys_open",
    // ...
};

int main() {
    int i;

    // 遍歷系統(tǒng)調用號并打印對應的函數(shù)名
    for (i = 0; i < sizeof(syscall_names) / sizeof(syscall_names[0]); i++) {
        printf("Syscall number %d: %s\n", i, syscall_names[i]);
    }

    return 0;
}

4.5系統(tǒng)跳轉表(sys_call_table)

運行流程。系統(tǒng)調用的執(zhí)行流程如下,但是系統(tǒng)調用號、系統(tǒng)跳轉表,系統(tǒng)調用函數(shù),這三者是如何關聯(lián)起來的呢?

系統(tǒng)調用的執(zhí)行流程如下:

  1. 用戶程序通過編寫系統(tǒng)調用號(或者使用對應的庫函數(shù))來請求操作系統(tǒng)提供某項服務。
  2. 當用戶程序發(fā)起系統(tǒng)調用時,會觸發(fā)處理器從用戶態(tài)切換到內核態(tài),進入特權模式。
  3. 處理器將控制權交給操作系統(tǒng)內核,并傳遞系統(tǒng)調用號以及其他必要的參數(shù)。
  4. 操作系統(tǒng)內核根據系統(tǒng)調用號在系統(tǒng)調用表中查找相應的處理函數(shù)地址。
  5. 內核跳轉到對應的系統(tǒng)調用處理函數(shù),開始執(zhí)行具體的操作。
  6. 執(zhí)行完畢后,將結果返回給用戶程序,并再次切換回用戶態(tài)。

關于系統(tǒng)調用號、系統(tǒng)跳轉表和系統(tǒng)調用函數(shù)之間的關聯(lián):

  • 系統(tǒng)調用號:每個系統(tǒng)調用都被賦予一個唯一的編號。例如,在 Linux 中使用 x86_64 架構時,可以在 syscall_64.tbl 文件中找到這些編號定義。它們?yōu)槊總€操作分配了一個特定的數(shù)字標識符。
  • 系統(tǒng)跳轉表:在內核中,有一個稱為“system_call”或類似名稱的特殊位置存儲著一個指向所有系統(tǒng)調用處理函數(shù)地址數(shù)組(也稱為“sys_call_table”)的指針。該數(shù)組包含了所有可能存在的系統(tǒng)調用處理函數(shù)地址。
  • 系統(tǒng)調用函數(shù):每個具體的功能對應一個系統(tǒng)調用函數(shù),它們是內核中的實現(xiàn)代碼。這些函數(shù)通過在系統(tǒng)跳轉表中查找與其對應的位置來進行調用。

當用戶程序觸發(fā)系統(tǒng)調用時,操作系統(tǒng)根據系統(tǒng)調用號從系統(tǒng)跳轉表中獲取對應的處理函數(shù)地址,并執(zhí)行該函數(shù)來完成請求的操作。因此,通過系統(tǒng)調用號和系統(tǒng)跳轉表,操作系統(tǒng)能夠將用戶程序的請求路由到正確的系統(tǒng)調用函數(shù)上。

syscall's number -> syscall -> entry_SYSCALL_64 -> do_syscall_64 -> sys_call_table -> __x64_sys_write

sys_call_table 的定義。#include <asm/syscalls_64.h> 這行源碼對應的文件是在內核編譯的時候,通過腳本創(chuàng)建的。

/* include/generated/asm-offsets.h */
#define __NR_syscall_max 547 /* sizeof(syscalls_64) - 1 */

/* arch/x86/entry/syscall_64.c */
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,

/* arch/x86/entry/syscall_64.c */
asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
#include <asm/syscalls_64.h>
};

Makefile。通過執(zhí)行 syscalltbl.sh 腳本,解析系統(tǒng)調用文件 syscall_64.tbl 數(shù)據,自動生成 syscalls_64.h。

# arch/x86/entry/syscalls/Makefile
syscall64 := $(srctree)/$(src)/syscall_64.tbl
systbl := $(srctree)/$(src)/syscalltbl.sh
quiet_cmd_systbl = SYSTBL  $@
      cmd_systbl = $(CONFIG_SHELL) '$(systbl)' $< $@

syscalltbl.sh

# arch/x86/entry/syscalls/syscalltbl.sh
...
syscall_macro() {
    abi="$1"
    nr="$2"
    entry="$3"

    # Entry can be either just a function name or "function/qualifier"
    real_entry="${entry%%/*}"
    if [ "$entry" = "$real_entry" ]; then
        qualifier=
    else
        qualifier=${entry#*/}
    fi

    echo "__SYSCALL_${abi}($nr, $real_entry, $qualifier)"
}
...

syscalls_64.h 文件內容

/* arch/x86/include/generated/asm/syscalls_64.h */
...
#ifdef CONFIG_X86
__SYSCALL_64(0, __x64_sys_read, )
#else /* CONFIG_UML */
__SYSCALL_64(0, sys_read, )
#endif
#ifdef CONFIG_X86
__SYSCALL_64(1, __x64_sys_write, )
#else /* CONFIG_UML */
__SYSCALL_64(1, sys_write, )
#endif
...

三者關系。通過上述操作,sys_call_table 的定義與 syscalls_64.h 文件內容結合起來就是一個完整的數(shù)組初始化,將系統(tǒng)調用號,系統(tǒng)調用函數(shù),系統(tǒng)跳轉表三者結合起來了。

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    /*
     * Smells like a compiler bug -- it doesn't work
     * when the & below is removed.
     */
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
    [0] = __x64_sys_read,
    [1] = __x64_sys_write,
    ...

系統(tǒng)調用函數(shù)。現(xiàn)在雖然搞清楚了系統(tǒng)調用的關系,但是還沒有發(fā)現(xiàn) __x64_sys_write 這個函數(shù)是在哪里定義的。答案就在這個宏 SYSCALL_DEFINE3,將這個宏展開,回頭再看上面 gdb 調試斷點截斷處的那些函數(shù),整個思路就清晰了。

__do_sys_write() (/root/linux-5.0.1/fs/read_write.c:610)
__se_sys_write() (/root/linux-5.0.1/fs/read_write.c:607)
__x64_sys_write(const struct pt_regs * regs) (/root/linux-5.0.1/fs/read_write.c:607)
...

/* fs/read_write.c */
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
        size_t, count) {
    return ksys_write(fd, buf, count);
}

/* include/linux/syscalls.h */
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

#define SYSCALL_DEFINEx(x, sname, ...)                \
    SYSCALL_METADATA(sname, x, __VA_ARGS__)           \
    __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

/* arch/x86/include/asm/syscall_wrapper.h */
#define __SYSCALL_DEFINEx(x, name, ...)                                \
    asmlinkage long __x64_sys##name(const struct pt_regs *regs);       \
    ALLOW_ERROR_INJECTION(__x64_sys##name, ERRNO);                     \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__));        \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__)); \
    asmlinkage long __x64_sys##name(const struct pt_regs *regs)        \
    {                                                                  \
        return __se_sys##name(SC_X86_64_REGS_TO_ARGS(x,__VA_ARGS__));  \
    }                                                                  \
    __IA32_SYS_STUBx(x, name, __VA_ARGS__)                             \
    static long __se_sys##name(__MAP(x,__SC_LONG,__VA_ARGS__))         \
    {                                                                  \
        long ret = __do_sys##name(__MAP(x,__SC_CAST,__VA_ARGS__));     \
        __MAP(x,__SC_TEST,__VA_ARGS__);                                \
        __PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));              \
        return ret;                                                    \
    }                                                                  \
    static inline long __do_sys##name(__MAP(x,__SC_DECL,__VA_ARGS__))

五、系統(tǒng)調用的定義

read()系統(tǒng)調用是一個很好的初始示例,可以用來探索內核的系統(tǒng)調用機制。它在fs/read_write.c中作為一個簡短的函數(shù)實現(xiàn),大部分工作由vfs_read()函數(shù)處理。從調用的角度來看,這段代碼最有趣的地方是函數(shù)是如何使用SYSCALL_DEFINE3()宏來定義的。實際上,從代碼中,甚至并不立即清楚該函數(shù)被稱為什么。

// linux-3.10/fs/read_write.c

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
	struct fd f = fdget(fd);
	ssize_t ret = -EBADF;

	if (f.file) {
		loff_t pos = file_pos_read(f.file);
		ret = vfs_read(f.file, buf, count, &pos);
		file_pos_write(f.file, pos);
		fdput(f);
	}
	return ret;
}

這些SYSCALL_DEFINEn()宏是內核代碼定義系統(tǒng)調用的標準方式,其中n后綴表示參數(shù)計數(shù)。這些宏的定義(在include/linux/syscalls.h中)為每個系統(tǒng)調用提供了兩個不同的輸出。

// include/linux/syscalls.h

#define SYSCALL_DEFINE1(name, ...) SYSCALL_DEFINEx(1, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE2(name, ...) SYSCALL_DEFINEx(2, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE4(name, ...) SYSCALL_DEFINEx(4, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE5(name, ...) SYSCALL_DEFINEx(5, _##name, __VA_ARGS__)
#define SYSCALL_DEFINE6(name, ...) SYSCALL_DEFINEx(6, _##name, __VA_ARGS__)


// include/linux/syscalls.h

#define SYSCALL_DEFINEx(x, sname, ...)				\
	SYSCALL_METADATA(sname, x, __VA_ARGS__)			\
	__SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

 SYSCALL_METADATA(_read, 3, unsigned int, fd, char __user *, buf, size_t, count)
    __SYSCALL_DEFINEx(3, _read, unsigned int, fd, char __user *, buf, size_t, count)
    {
    	struct fd f = fdget_pos(fd);
    	ssize_t ret = -EBADF;
    	/* ... */

5.1SYSCALL_METADATA

其中之一是SYSCALL_METADATA()宏,用于構建關于系統(tǒng)調用的元數(shù)據,以便進行跟蹤。只有在內核構建時定義了CONFIG_FTRACE_SYSCALLS時才會展開該宏,展開后它會生成描述系統(tǒng)調用及其參數(shù)的數(shù)據的樣板定義。(單獨的頁面詳細描述了這些定義。)

SYSCALL_METADATA()宏主要用于在內核中進行系統(tǒng)調用的跟蹤和分析。當啟用了CONFIG_FTRACE_SYSCALLS配置選項進行內核構建時,宏會展開,并生成一系列用于描述系統(tǒng)調用及其參數(shù)的元數(shù)據定義。這些元數(shù)據包括系統(tǒng)調用號、參數(shù)個數(shù)、參數(shù)類型等信息,用于記錄和分析系統(tǒng)調用的執(zhí)行情況。

通過使用SYSCALL_METADATA()宏,內核能夠在編譯時生成系統(tǒng)調用的元數(shù)據,以支持跟蹤工具對系統(tǒng)調用的監(jiān)控和分析。這些元數(shù)據的定義是一種樣板代碼,提供了系統(tǒng)調用的相關信息,幫助開發(fā)人員和調試工具在系統(tǒng)調用層面進行問題排查和性能優(yōu)化。

5.2__SYSCALL_DEFINEx

__SYSCALL_DEFINEx()部分更加有趣,因為它包含了系統(tǒng)調用的實現(xiàn)。一旦各種宏和GCC類型擴展層層展開,生成的代碼包含一些有趣的特性:

#define __PROTECT(...) asmlinkage_protect(__VA_ARGS__)
#define __SYSCALL_DEFINEx(x, name, ...)					\
	asmlinkage long sys##name(__MAP(x,__SC_DECL,__VA_ARGS__));	\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__));	\
	asmlinkage long SyS##name(__MAP(x,__SC_LONG,__VA_ARGS__))	\
	{								\
		long ret = SYSC##name(__MAP(x,__SC_CAST,__VA_ARGS__));	\
		__MAP(x,__SC_TEST,__VA_ARGS__);				\
		__PROTECT(x, ret,__MAP(x,__SC_ARGS,__VA_ARGS__));	\
		return ret;						\
	}								\
	SYSCALL_ALIAS(sys##name, SyS##name);				\
	static inline long SYSC##name(__MAP(x,__SC_DECL,__VA_ARGS__))

    asmlinkage long sys_read(unsigned int fd, char __user * buf, size_t count)
    	__attribute__((alias(__stringify(SyS_read))));

    static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count);
    asmlinkage long SyS_read(long int fd, long int buf, long int count);

    asmlinkage long SyS_read(long int fd, long int buf, long int count)
    {
    	long ret = SYSC_read((unsigned int) fd, (char __user *) buf, (size_t) count);
    	asmlinkage_protect(3, ret, fd, buf, count);
    	return ret;
    }

    static inline long SYSC_read(unsigned int fd, char __user * buf, size_t count)
    {
    	struct fd f = fdget_pos(fd);
    	ssize_t ret = -EBADF;
    	/* ... */

[root@localhost ~]# uname -r
3.10.0-693.el7.x86_64
[root@localhost ~]# cat /proc/kallsyms | grep '\<sys_read\>'
ffffffff812019e0 T sys_read
[root@localhost ~]# cat /proc/kallsyms | grep '\<SYSC_read\>'
[root@localhost ~]# cat /proc/kallsyms | grep '\<SyS_read\>'
ffffffff812019e0 T SyS_read

5.3SYSCALL_ALIAS

SYSCALL_ALIAS宏定義如下:

// file: include/linux/linkage.h
#ifndef SYSCALL_ALIAS
#define SYSCALL_ALIAS(alias, name) asm(         \
    ".globl " VMLINUX_SYMBOL_STR(alias) "\n\t"  \
    ".set   " VMLINUX_SYMBOL_STR(alias) ","     \
          VMLINUX_SYMBOL_STR(name))
#endif

宏VMLINUX_SYMBOL_STR定義如下:

// file: include/linux/export.h
/*
 * Export symbols from the kernel to modules.  Forked from module.h
 * to reduce the amount of pointless cruft we feed to gcc when only
 * exporting a simple symbol or two.
 *
 * Try not to add #includes here.  It slows compilation and makes kernel
 * hackers place grumpy comments in header files.
 */
/* Indirect, so macros are expanded before pasting. */
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)
#define VMLINUX_SYMBOL_STR(x) __VMLINUX_SYMBOL_STR(x)

#define __VMLINUX_SYMBOL(x) x
#define __VMLINUX_SYMBOL_STR(x) #x

實際效果是給name設置了個別名alias,本例中是給SyS_write設置了別名sys_write。

5.4Syscall table entries

尋找調用sys_read()的函數(shù)還有助于了解用戶空間如何調用該函數(shù)。對于沒有提供自己覆蓋的"通用"架構,include/uapi/asm-generic/unistd.h文件中包含了一個引用sys_read的條目:

// include/uapi/asm-generic/unistd.h

#define __NR_read 63
__SYSCALL(__NR_read, sys_read)

這個定義為read()定義了通用的系統(tǒng)調用號__NR_read(63),并使用__SYSCALL()宏以特定于體系結構的方式將該號碼與sys_read()關聯(lián)起來。例如,arm64使用asm-generic/unistd.h頭文件填充一個表格,將系統(tǒng)調用號映射到實現(xiàn)函數(shù)指針。

然而,我們將集中討論x86_64架構,它不使用這個通用表格。相反,x86_64架構在arch/x86/syscalls/syscall_64.tbl中定義了自己的映射,其中包含sys_read()的條目:

// arch/x86/syscalls/syscall_64.tbl

#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The abi is "common", "64" or "x32" for this file.
#
0	common	read			sys_read
1	common	write			sys_write
2	common	open			sys_open
3	common	close			sys_close
4	common	stat			sys_newstat
......

這表明在x86_64架構上,read()的系統(tǒng)調用號為0(不是63),并且對于x86_64的兩種ABI(應用二進制接口),即sys_read(),有一個共同的實現(xiàn)。(關于不同的ABI將在本系列的第二部分中討論。)syscalltbl.sh腳本從syscall_64.tbl表生成arch/x86/include/generated/asm/syscalls_64.h文件,具體為sys_read()生成對__SYSCALL_COMMON()宏的調用。然后,該頭文件用于填充syscall表sys_call_table,這是一個關鍵的數(shù)據結構,將系統(tǒng)調用號映射到sys_name()函數(shù)。

// arch/x86/syscalls/syscalltbl.sh

#!/bin/sh

in="$1"
out="$2"

grep '^[0-9]' "$in" | sort -n | (
    while read nr abi name entry compat; do
	abi=`echo "$abi" | tr '[a-z]' '[A-Z]'`
	if [ -n "$compat" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $compat)"
	elif [ -n "$entry" ]; then
	    echo "__SYSCALL_${abi}($nr, $entry, $entry)"
	fi
    done
) > "$out"

在x86_64架構中,syscalltbl.sh腳本使用syscall_64.tbl表格生成了arch/x86/include/generated/asm/syscalls_64.h文件。其中,對于sys_read()的定義會包含類似以下的代碼:

__SYSCALL_COMMON(0, sys_read)

這個宏的調用將系統(tǒng)調用號0和sys_read()函數(shù)關聯(lián)起來。然后,arch/x86/include/generated/asm/syscalls_64.h文件會被其他代碼引用,用于填充sys_call_table數(shù)據結構。

即由一個 Makefile文件中在編譯 Linux 系統(tǒng)內核時調用了一個腳本,這個腳本文件會讀取 syscall_64.tbl 文件,根據其中信息生成相應的文件 syscall_64.h。

// arch/x86/syscalls/Makefile

syscall64 := $(srctree)/$(src)/syscall_64.tbl

systbl := $(srctree)/$(src)/syscalltbl.sh

$(out)/syscalls_64.h: $(syscall64) $(systbl)
	$(call if_changed,systbl)

sys_call_table是一個數(shù)組,其中每個元素對應一個系統(tǒng)調用號,它將系統(tǒng)調用號映射到相應的sys_name()函數(shù)。在這種情況下,sys_read()函數(shù)將與系統(tǒng)調用號0關聯(lián)起來,以便當用戶空間發(fā)起sys_read()的系統(tǒng)調用請求時,內核可以根據系統(tǒng)調用號從sys_call_table中找到sys_read()函數(shù)并執(zhí)行。這樣,內核就能正確處理用戶空間對read()的系統(tǒng)調用請求。

六、x86-64系統(tǒng)調用實戰(zhàn)演練

6.1匯編代碼實操

為了更直觀地感受 x86-64 系統(tǒng)調用的實際應用,我們通過具體的匯編代碼示例來深入學習。這里以文件讀寫和進程創(chuàng)建這兩個常見的系統(tǒng)調用為例,詳細剖析每一行代碼的功能和作用。

(1)文件讀取匯編代碼示例

section .data
    filename db 'test.txt', 0   ; 要讀取的文件名,以0結尾表示字符串結束
    buffer times 128 db 0       ; 用于存儲讀取內容的緩沖區(qū),大小為128字節(jié)

section .bss
    fd resq 1                  ; 用于保存文件描述符,resq表示預留8字節(jié)空間(64位系統(tǒng))
    bytes_read resq 1          ; 用于保存實際讀取的字節(jié)數(shù)

section .text
    global _start

_start:
    ; 打開文件,使用O_RDONLY標志表示只讀模式
    mov rax, 2                ; 將系統(tǒng)調用號2(open系統(tǒng)調用號)存入%rax寄存器
    mov rdi, filename         ; 將文件名的地址存入%rdi寄存器,作為open系統(tǒng)調用的第一個參數(shù)
    mov rsi, 0                ; 將打開文件的標志O_RDONLY(值為0)存入%rsi寄存器,作為第二個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)從用戶態(tài)到內核態(tài)的切換,執(zhí)行open系統(tǒng)調用

    mov [fd], rax             ; 將open系統(tǒng)調用返回的文件描述符保存到fd變量中

    ; 讀取文件內容到緩沖區(qū)
    mov rax, 0                ; 將系統(tǒng)調用號0(read系統(tǒng)調用號)存入%rax寄存器
    mov rdi, [fd]             ; 將文件描述符從fd變量中取出,存入%rdi寄存器,作為read系統(tǒng)調用的第一個參數(shù)
    mov rsi, buffer           ; 將緩沖區(qū)的地址存入%rsi寄存器,作為read系統(tǒng)調用的第二個參數(shù)
    mov rdx, 128              ; 將讀取的最大字節(jié)數(shù)128存入%rdx寄存器,作為read系統(tǒng)調用的第三個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)read系統(tǒng)調用,從文件中讀取內容到緩沖區(qū)

    mov [bytes_read], rax     ; 將read系統(tǒng)調用返回的實際讀取的字節(jié)數(shù)保存到bytes_read變量中

    ; 關閉文件
    mov rax, 3                ; 將系統(tǒng)調用號3(close系統(tǒng)調用號)存入%rax寄存器
    mov rdi, [fd]             ; 將文件描述符從fd變量中取出,存入%rdi寄存器,作為close系統(tǒng)調用的第一個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)close系統(tǒng)調用,關閉文件

    ; 退出程序
    mov rax, 60               ; 將系統(tǒng)調用號60(exit系統(tǒng)調用號)存入%rax寄存器
    xor rdi, rdi              ; 將退出狀態(tài)碼0存入%rdi寄存器,作為exit系統(tǒng)調用的第一個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)exit系統(tǒng)調用,程序結束

在這段代碼中,首先定義了要讀取的文件名test.txt和用于存儲讀取內容的緩沖區(qū)buffer。然后通過open系統(tǒng)調用打開文件,獲取文件描述符并保存。接著使用read系統(tǒng)調用從文件中讀取內容到緩沖區(qū),保存實際讀取的字節(jié)數(shù)。最后通過close系統(tǒng)調用關閉文件,并使用exit系統(tǒng)調用退出程序。每一個系統(tǒng)調用都嚴格按照 x86-64 的調用約定,將系統(tǒng)調用號存入%rax寄存器,將參數(shù)依次存入%rdi、%rsi、%rdx等寄存器,通過syscall指令觸發(fā)系統(tǒng)調用。

(2)進程創(chuàng)建匯編代碼示例

section .text
    global _start

_start:                       ; 創(chuàng)建子進程
    mov rax, 57               ; 將系統(tǒng)調用號57(clone系統(tǒng)調用號,用于創(chuàng)建進程,在Linux中clone可用于創(chuàng)建進程、線程等,這里用于創(chuàng)建進程)存入%rax寄存器
    xor rdi, rdi              ; 將%rdi寄存器清零,作為clone系統(tǒng)調用的第一個參數(shù)(這里參數(shù)為0,表示使用默認的克隆標志)
    xor rsi, rsi              ; 將%rsi寄存器清零,作為clone系統(tǒng)調用的第二個參數(shù)(通常用于傳遞棧指針,這里為0表示使用默認棧)
    xor rdx, rdx              ; 將%rdx寄存器清零,作為clone系統(tǒng)調用的第三個參數(shù)(通常用于傳遞父進程的標志,這里為0表示默認)
    xor r10, r10              ; 將%r10寄存器清零,作為clone系統(tǒng)調用的第四個參數(shù)(通常用于傳遞子進程的標志,這里為0表示默認)
    xor r8, r8                ; 將%r8寄存器清零,作為clone系統(tǒng)調用的第五個參數(shù)(通常用于傳遞新的線程組ID,這里為0表示默認)
    xor r9, r9                ; 將%r9寄存器清零,作為clone系統(tǒng)調用的第六個參數(shù)(通常用于傳遞新的父進程ID,這里為0表示默認)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)clone系統(tǒng)調用,創(chuàng)建子進程

    cmp rax, 0                ; 比較clone系統(tǒng)調用的返回值(%rax寄存器)與0
    jz child                  ; 如果返回值為0,說明是子進程,跳轉到child標簽處執(zhí)行

    ; 父進程執(zhí)行的代碼
    mov rax, 60               ; 將系統(tǒng)調用號60(exit系統(tǒng)調用號)存入%rax寄存器
    xor rdi, rdi              ; 將退出狀態(tài)碼0存入%rdi寄存器,作為exit系統(tǒng)調用的第一個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)exit系統(tǒng)調用,父進程結束

child:
    ; 子進程執(zhí)行的代碼
    mov rax, 1                ; 將系統(tǒng)調用號1(write系統(tǒng)調用號)存入%rax寄存器
    mov rdi, 1                ; 將文件描述符1(標準輸出)存入%rdi寄存器,作為write系統(tǒng)調用的第一個參數(shù)
    mov rsi, msg              ; 將要輸出的消息的地址存入%rsi寄存器,作為write系統(tǒng)調用的第二個參數(shù)
    mov rdx, msg_len          ; 將消息的長度存入%rdx寄存器,作為write系統(tǒng)調用的第三個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)write系統(tǒng)調用,子進程向標準輸出打印消息

    mov rax, 60               ; 將系統(tǒng)調用號60(exit系統(tǒng)調用號)存入%rax寄存器
    xor rdi, rdi              ; 將退出狀態(tài)碼0存入%rdi寄存器,作為exit系統(tǒng)調用的第一個參數(shù)
    syscall                   ; 執(zhí)行系統(tǒng)調用,觸發(fā)exit系統(tǒng)調用,子進程結束

section .data
    msg db 'This is a child process!', 0xa, 0  ; 子進程要輸出的消息,0xa表示換行符,0表示字符串結束
    msg_len equ $ - msg                         ; 計算消息的長度

在這段進程創(chuàng)建的匯編代碼中,通過clone系統(tǒng)調用創(chuàng)建一個新的子進程。clone系統(tǒng)調用的參數(shù)較多,這里使用默認值,通過將各個參數(shù)寄存器清零來實現(xiàn)。clone系統(tǒng)調用返回后,根據返回值判斷是父進程還是子進程。如果返回值為 0,則是子進程,子進程會向標準輸出打印一條消息,然后退出;如果返回值不為 0,則是父進程,父進程直接退出。同樣,每個系統(tǒng)調用都遵循 x86-64 的調用約定,準確設置系統(tǒng)調用號和參數(shù)寄存器,通過syscall指令實現(xiàn)系統(tǒng)調用的執(zhí)行。

6.2C 語言調用示范

在 C 語言中,我們通常不會直接使用系統(tǒng)調用的原始方式(如匯編代碼中的方式),而是通過調用 glibc 庫函數(shù)來間接使用系統(tǒng)調用。glibc(GNU C Library)是 GNU 項目中提供的 C 標準庫,它對系統(tǒng)調用進行了封裝,提供了更方便、更高級的接口,使得程序員可以更便捷地使用系統(tǒng)調用。下面以open、read、write等函數(shù)為例,分析 C 語言中如何調用這些庫函數(shù),以及它們內部是如何封裝系統(tǒng)調用的。

(1)C語言文件操作示例

#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFFER_SIZE 128

int main() {
    int fd;
    char buffer[BUFFER_SIZE];
    ssize_t bytes_read;

    // 打開文件,使用O_RDONLY標志表示只讀模式
    fd = open("test.txt", O_RDONLY);
    if (fd == -1) {
        perror("無法打開文件");
        return 1;
    }

    // 讀取文件內容到緩沖區(qū)
    bytes_read = read(fd, buffer, BUFFER_SIZE);
    if (bytes_read == -1) {
        perror("讀取文件失敗");
        close(fd);
        return 1;
    }

    // 輸出讀取到的內容
    write(1, buffer, bytes_read);

    // 關閉文件
    close(fd);

    return 0;
}

在這個 C 語言示例中,首先使用open函數(shù)打開文件test.txt,open函數(shù)的原型定義在<fcntl.h>頭文件中,其函數(shù)聲明為int open(const char *pathname, int flags, mode_t mode);。第一個參數(shù)pathname是要打開的文件名,第二個參數(shù)flags用于指定打開文件的模式,這里使用O_RDONLY表示只讀模式。如果open函數(shù)調用失敗,會返回 -1,并設置errno全局變量來表示具體的錯誤類型,通過perror函數(shù)可以輸出錯誤信息。

接著使用read函數(shù)從文件中讀取內容到緩沖區(qū),read函數(shù)的原型定義在<unistd.h>頭文件中,聲明為ssize_t read(int fd, void *buf, size_t count);。第一個參數(shù)fd是文件描述符,即open函數(shù)返回的值;第二個參數(shù)buf是用于存儲讀取內容的緩沖區(qū);第三個參數(shù)count是要讀取的最大字節(jié)數(shù)。如果read函數(shù)調用失敗,同樣會返回 -1,并設置errno變量。

然后使用write函數(shù)將讀取到的內容輸出到標準輸出,write函數(shù)的原型為ssize_t write(int fd, const void *buf, size_t count);。第一個參數(shù)fd為標準輸出的文件描述符(值為 1),第二個參數(shù)buf是要輸出的內容緩沖區(qū),第三個參數(shù)count是要輸出的字節(jié)數(shù)。

最后使用close函數(shù)關閉文件,close函數(shù)的原型為int close(int fd);,參數(shù)fd為要關閉的文件描述符。

從內部實現(xiàn)來看,這些 glibc 庫函數(shù)實際上是對系統(tǒng)調用的封裝。以open函數(shù)為例,當我們在 C 語言中調用open函數(shù)時,glibc 會將函數(shù)調用轉換為對應的系統(tǒng)調用。在 x86-64 架構下,它會按照系統(tǒng)調用的調用約定,設置好系統(tǒng)調用號和參數(shù)寄存器,然后執(zhí)行syscall指令,觸發(fā)系統(tǒng)調用。

例如,對于open系統(tǒng)調用,glibc 會將系統(tǒng)調用號 2 存入%rax寄存器,將文件名的地址存入%rdi寄存器,將打開文件的標志存入%rsi寄存器,然后執(zhí)行syscall指令。系統(tǒng)調用完成后,glibc 會根據系統(tǒng)調用的返回值進行處理,如果返回錯誤碼,會設置errno全局變量,并返回 -1 給用戶程序。同樣,read、write、close等函數(shù)也都是類似的封裝方式,通過這種方式,glibc 為程序員提供了更簡潔、更易用的接口,隱藏了系統(tǒng)調用的底層細節(jié) 。

七、x86-64系統(tǒng)調用常見問題與優(yōu)化策略

7.1常見問題診斷

在使用 x86-64 系統(tǒng)調用時,可能會遭遇各種棘手的問題,這些問題倘若不能及時解決,就會對程序的正常運行和性能產生嚴重影響。

參數(shù)傳遞錯誤是較為常見的問題之一。比如,在進行文件讀取系統(tǒng)調用時,如果錯誤地將文件名傳遞到了本該存放文件描述符的寄存器,就會導致系統(tǒng)調用失敗。假設在一個文件讀取的匯編代碼中,原本應該將文件描述符存入%rdi寄存器,卻錯誤地存入了文件名:

; 錯誤示例
mov rax, 0    ; read系統(tǒng)調用號
mov rdi, filename ; 錯誤地將文件名存入%rdi寄存器,應該存入文件描述符
mov rsi, buffer
mov rdx, 128
syscall

解決這類問題,需要仔細檢查系統(tǒng)調用的參數(shù)傳遞,嚴格按照 x86-64 的調用約定,將參數(shù)準確無誤地傳遞到對應的寄存器中。在編寫代碼時,可以參考相關的系統(tǒng)調用文檔,明確每個參數(shù)所對應的寄存器。同時,使用調試工具(如 GDB),在程序運行過程中查看寄存器的值,以確保參數(shù)傳遞正確。比如,在 GDB 中,可以使用info registers命令查看寄存器的值,定位參數(shù)傳遞錯誤的位置。

系統(tǒng)調用號錯誤也是一個容易出現(xiàn)的問題。如果傳遞了錯誤的系統(tǒng)調用號,內核將無法找到對應的內核函數(shù),從而引發(fā)未知行為。例如,將open系統(tǒng)調用號誤寫成了其他值:

; 錯誤示例
mov rax, 5    ; 錯誤的系統(tǒng)調用號,open系統(tǒng)調用號應為2
mov rdi, filename
mov rsi, 0
syscall

為了避免這類錯誤,在編寫代碼時,要確保使用正確的系統(tǒng)調用號??梢圆殚喯嚓P的操作系統(tǒng)文檔或頭文件,獲取準確的系統(tǒng)調用號。在 Linux 系統(tǒng)中,系統(tǒng)調用號的定義通??梢栽?usr/include/asm/unistd_64.h頭文件中找到。并且,在程序中使用宏定義來表示系統(tǒng)調用號,這樣不僅可以提高代碼的可讀性,還能減少因手寫系統(tǒng)調用號而導致的錯誤。例如:

; 正確示例,使用宏定義表示系統(tǒng)調用號
%define SYS_OPEN 2
mov rax, SYS_OPEN
mov rdi, filename
mov rsi, 0
syscall

7.2性能優(yōu)化策略

系統(tǒng)調用涉及用戶態(tài)和內核態(tài)的切換,這個過程會帶來一定的開銷,包括保存和恢復寄存器狀態(tài)、切換頁表等。因此,優(yōu)化系統(tǒng)調用的性能對于提高程序的整體效率至關重要。

減少系統(tǒng)調用次數(shù)是一個有效的優(yōu)化策略。以文件讀寫操作為例,如果需要讀取大量的數(shù)據,頻繁地進行小數(shù)據量的系統(tǒng)調用會導致較高的開銷。假設我們要讀取一個大文件的內容,如果每次只讀取 10 個字節(jié),然后進行一次系統(tǒng)調用,那么對于一個 1MB 大小的文件,就需要進行 10 萬次系統(tǒng)調用,這會產生大量的上下文切換開銷。

// 低效的文件讀取方式,頻繁進行系統(tǒng)調用
#include <stdio.h>

int main() {
    FILE *file = fopen("large_file.txt", "r");
    if (file == NULL) {
        perror("無法打開文件");
        return 1;
    }
    char buffer[10];
    while (fread(buffer, 1, 10, file) > 0) {
        // 處理讀取到的數(shù)據
    }
    fclose(file);
    return 0;
}

為了優(yōu)化性能,可以采用批量操作數(shù)據的方式,一次性讀取較大的數(shù)據塊,減少系統(tǒng)調用的次數(shù)。比如將緩沖區(qū)大小設置為 1024 字節(jié),這樣讀取 1MB 大小的文件只需要進行約 1000 次系統(tǒng)調用,大大降低了上下文切換的開銷。

// 優(yōu)化后的文件讀取方式,批量讀取數(shù)據
#include <stdio.h>

int main() {
    FILE *file = fopen("large_file.txt", "r");
    if (file == NULL) {
        perror("無法打開文件");
        return 1;
    }
    char buffer[1024];
    while (fread(buffer, 1, 1024, file) > 0) {
        // 處理讀取到的數(shù)據
    }
    fclose(file);
    return 0;
}

合理選擇系統(tǒng)調用函數(shù)也能提高效率。不同的系統(tǒng)調用函數(shù)在功能和性能上可能存在差異,應根據具體需求選擇最合適的系統(tǒng)調用。例如,在創(chuàng)建進程時,如果只是簡單地創(chuàng)建一個子進程并等待其結束,可以使用fork和wait系統(tǒng)調用;但如果需要創(chuàng)建一個新的進程,并在新進程中執(zhí)行一個新的程序,那么就應該使用execve系統(tǒng)調用。

如果在需要執(zhí)行新程序的情況下錯誤地使用了fork,就無法達到預期的效果,還可能導致性能問題。同時,了解系統(tǒng)調用函數(shù)的底層實現(xiàn)和性能特點,可以幫助我們在編寫程序時做出更優(yōu)的選擇。比如,一些系統(tǒng)調用函數(shù)可能會涉及到復雜的內核操作,而另一些則相對簡單,我們可以根據實際需求選擇更高效的函數(shù)。

責任編輯:武曉燕 來源: 深度Linux
相關推薦

2025-03-26 00:21:00

2011-01-04 14:36:39

LinuxGTK編程

2010-05-07 17:47:12

Unix Solari

2009-07-03 11:57:18

系統(tǒng)編程安全linux

2020-10-08 10:10:51

Linux系統(tǒng)編程信號集

2023-02-22 09:53:55

架構芯片

2009-04-08 20:49:09

nehalem英特爾

2009-08-27 10:23:52

2020-09-26 21:43:59

Linux系統(tǒng)編程條件變量

2020-10-05 22:01:02

Linux系統(tǒng)編程線程屬性

2020-10-10 07:18:14

Linux系統(tǒng)編程管道

2020-10-18 07:13:44

Linux系統(tǒng)編程信號捕捉

2009-12-09 17:25:19

Linux操作系統(tǒng)

2011-01-05 10:32:31

linuxqq

2009-06-23 18:55:40

Linux

2010-02-05 16:04:45

X86 Android

2023-09-21 08:46:44

操作系統(tǒng)windows 10內存

2018-12-19 16:15:35

服務器虛擬化開源

2009-09-01 09:20:24

Linux操作系統(tǒng)聲音設備編程實例

2019-03-15 09:30:09

Linux系統(tǒng)CPU
點贊
收藏

51CTO技術棧公眾號