1. 程式人生 > >linux系統呼叫原理及實現

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,返回值也不同。

實現原理

一次系統呼叫的完整執行過程如下:

  1. 通過特定指令發出系統呼叫(int $80、sysenter、syscall)

  2. CPU從使用者態切換到核心態,進行一些暫存器和環境設定

  3. 呼叫system_call核心函式,通過系統呼叫號獲取對應的服務例程

  4. 呼叫系統呼叫處理例程

  5. 使用特定指令從系統呼叫返回使用者態(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

核心預編譯系統根據這個檔案中提供的系統呼叫號、系統呼叫名稱和對應的處理函式名稱來生成對應的標頭檔案。

新增新的系統呼叫

根據上述分析,如果需要新增一個新的系統呼叫號和處理函式,需要完成如下修改:

  1. 在syscall_64.tbl中新增新的系統呼叫號、名稱和處理函式名稱。例如“666  common  mycall   __x64_sys_mycall”

  2. 提供sys_mycall函式實現。函式應定義為asmlinkage long sys_mycall(...)

  3. 如果sys_mycall函式實現在獨立的.c檔案中,需要將其加入lib/路徑下的makefile中,在obj-y中新增.c檔案路徑

之後重新編譯核心即可提供自定義的系統呼叫功能。

需要注意的是,sys_call_table資料結構在原始碼中是一個const變數,因此係統呼叫函式指標初始化完成後是不能修改的。如果需要在執行中動態修改或新增系統呼叫處理函式(例如通過可載入核心模組來提供處理函式),可以將const限定去掉,然後在執行中切換呼叫處理函式。