linux系統呼叫原理及實現
linux系統呼叫
系統呼叫是linux核心為使用者態程式提供的主要功能介面。通過系統呼叫,使用者態程序能夠臨時切換到核心態,使用核心態才能訪問的硬體和資源完成特定功能。系統呼叫由linux核心和核心模組實現,核心在處理系統呼叫時還會檢查系統呼叫請求和引數是否正確,保證對特權資源和硬體訪問的正確性。通過這種方式,linux在提供核心和硬體資源訪問介面的同時,保證了核心和硬體資源的使用正確性和安全性。
本文主要對linux下系統呼叫的原理和實現進行分析。本文的分析基於x86架構,涉及到的linux核心程式碼版本為4.17.6。
使用者態呼叫介面
使用者態程序主要通過如下方式,直接使用系統呼叫:
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <unistd.h>
#include <sys/syscall.h> /* For SYS_xxx definitions */
int syscall(int number, ...);
syscall介面由glibc提供和實現,第一個引數number表示需要呼叫的系統呼叫編號,後續的可變引數根據系統呼叫型別確定。核心具體支援的系統呼叫號可在<sys/syscall.h>中檢視。函式呼叫失敗會返回-1,具體錯誤原因儲存在errno中,errno的含義可參考<errno.h>
需要注意的是,這裡的返回值和errno是glibc封裝提供的,核心的系統呼叫響應函式本身不提供errno,返回值也不同。
實現原理
一次系統呼叫的完整執行過程如下:
-
通過特定指令發出系統呼叫(int $80、sysenter、syscall)
-
CPU從使用者態切換到核心態,進行一些暫存器和環境設定
-
呼叫system_call核心函式,通過系統呼叫號獲取對應的服務例程
-
呼叫系統呼叫處理例程
-
使用特定指令從系統呼叫返回使用者態(iret、sysexit、sysret)
系統呼叫指令
向核心發起系統呼叫需要使用特定的指令。在Linux中,傳統的方法是使用匯編指令int發起中斷,使用0x80(128)號中斷使CPU進入核心態,之後呼叫對應的中斷響應函式system_call來執行系統呼叫例程。
由於通過中斷方式發起系統呼叫的效能較差,較新的CPU和核心都支援使用sysenter和syscall這兩條專用指令來發起系統呼叫。其中sysenter在32位系統中使用,對應的退出指令為sysexit;syscall在64位系統中使用,對應的退出指令為sysret。
以sysenter為例,使用該指令時,首先呼叫__kernel_vsyscall()函式儲存使用者態堆疊;之後執行sysenter指令切換到核心態;最後開始執行sysenter_entry()函式設定核心態堆疊,並根據系統呼叫號呼叫處理例程,之後的邏輯和system_call相似。
核心實現邏輯
核心呼叫系統呼叫處理例程的核心資料結構是sys_call_table,這個資料結構在<arch/x86/entry/syscall_64.c>中定義如下:
/* this is a lie, but it does not hurt as sys_ni_syscall just returns -EINVAL */
extern asmlinkage long sys_ni_syscall(const struct pt_regs *);
#define __SYSCALL_64(nr, sym, qual) extern asmlinkage long sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64
#define __SYSCALL_64(nr, sym, qual) [nr] = sym,
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>
};
sys_call_table是一個函式指標陣列,其中儲存了所有系統呼叫處理函式的指標。system_call等函式以系統呼叫號作為下標,從sys_call_table中查詢對應的系統呼叫函式執行。
sys_call_table的初始化過程中,第一步是將所有指標陣列元素賦值為sys_ni_syscall。這是為了避免有部分系統呼叫號沒有被使用,沒有定義對應的處理函式。sys_ni_syscall在<kernel/sys_ni.c>中定義,直接返回-ENOSYS,表示系統呼叫不存在。
sys_call_table的具體內容在<asm/syscalls_64.h>中提供,內容類似於:__SYSCALL_64(19, sys_readv, sys_readv)。從之前對__SYSCALL_64巨集的兩處定義可見,syscall_64.c先將__SYSCALL_64巨集展開為函式宣告extern asmlinkage long sym(const struct pt_regs *),再將其展開為陣列元素初始化語句[nr] = sym。
需要注意的是<asm/syscalls_64.h>和提供系統呼叫號巨集定義的標頭檔案<asm/unistd_64.h>等檔案在核心原始碼樹中是不存在的,會在核心編譯的預編譯階段自動生成。核心原始碼中真正定義系統呼叫號和處理函式的檔案,是<arch/x86/entry/syscalls/syscall_64.tbl>,該檔案的內容格式如下:
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The __x64_sys_*() stubs are created on-the-fly for sys_*() system calls
#
# The abi is "common", "64" or "x32" for this file.
#
0 common read __x64_sys_read
1 common write __x64_sys_write
2 common open __x64_sys_open
3 common close __x64_sys_close
4 common stat __x64_sys_newstat
5 common fstat __x64_sys_newfstat
6 common lstat __x64_sys_newlstat
7 common poll __x64_sys_poll
8 common lseek __x64_sys_lseek
9 common mmap __x64_sys_mmap
核心預編譯系統根據這個檔案中提供的系統呼叫號、系統呼叫名稱和對應的處理函式名稱來生成對應的標頭檔案。
新增新的系統呼叫
根據上述分析,如果需要新增一個新的系統呼叫號和處理函式,需要完成如下修改:
-
在syscall_64.tbl中新增新的系統呼叫號、名稱和處理函式名稱。例如“666 common mycall __x64_sys_mycall”
-
提供sys_mycall函式實現。函式應定義為asmlinkage long sys_mycall(...)
-
如果sys_mycall函式實現在獨立的.c檔案中,需要將其加入lib/路徑下的makefile中,在obj-y中新增.c檔案路徑
之後重新編譯核心即可提供自定義的系統呼叫功能。
需要注意的是,sys_call_table資料結構在原始碼中是一個const變數,因此係統呼叫函式指標初始化完成後是不能修改的。如果需要在執行中動態修改或新增系統呼叫處理函式(例如通過可載入核心模組來提供處理函式),可以將const限定去掉,然後在執行中切換呼叫處理函式。