標籤:

系統調用深度剖析(上)

原文:Anatomy of a system call, part 1

翻譯:RobotCode俱樂部

系統調用是用戶空間程序與Linux內核交互的主要機制。考慮到它們的重要性,不難發現內核包含了各種各樣的機制,以確保系統調用能夠跨體系結構通用地實現,並且能夠以一種高效且一致的方式提供給用戶空間。

我一直致力於將FreeBSD的Capsicum安全框架移植到Linux上,由於這涉及到添加幾個新的系統調用(包括稍微不同尋常的execveat()系統調用),我發現自己正在研究它們實現的細節。因此,本文是探索內核實現系統調用(或系統調用)的詳細信息的兩篇文章中的第一篇。在本文中,我們將重點討論主流情況:普通syscall (read())的機制,以及允許x86_64用戶程序調用它的機制。第二篇文章將脫離主流案例,介紹更多不常見的系統調用和其他系統調用機制。

系統調用不同於常規函數調用,因為被調用的代碼在內核中。需要特殊的指令使處理器執行到ring 0(特權模式)的轉換。此外,被調用的內核代碼由一個syscall編號標識,而不是由一個函數地址標識。

使用SYSCALL_DEFINEn()定義一個syscall

read()系統調用為研究內核的syscall機制提供了一個很好的初始示例。它是在fs/read_write.c中實現的。,作為一個短函數,它將大部分工作傳遞給vfs_read()。從調用的角度來看,這段代碼最有趣的方面是使用SYSCALL_DEFINE3()宏定義函數的方式。實際上,從代碼中,甚至不能立即清楚地知道函數的調用。

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

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

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;
/* ... */

第一個是SYSCALL_METADATA(),它構建了一個關於系統調用的元數據集合,用於跟蹤。只有在為內核構建定義CONFIG_FTRACE_SYSCALLS時,它才會展開,它的展開給出了描述syscall及其參數的數據的樣板定義。(另一頁更詳細地描述了這些定義。)

__SYSCALL_DEFINEx()部分更有趣,因為它包含系統調用實現。一旦擴展了宏和GCC類型擴展的各個層,得到的代碼包括一些有趣的特性:

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;
/* ... */

首先,我們注意到系統調用實現實際上名為SYSC_read(),但它是靜態的,因此在此模塊之外是不可訪問的。相反,一個名為SyS_read()並別名為SyS_read()的包裝器函數在外部是可見的。仔細觀察這些別名,我們注意到它們的參數類型的不同——sys_read()期望顯式聲明的類型(例如第二個參數是char __user *),而sys_read()只期望一組(長)整數。深入研究這個問題的歷史,可以發現長版本可以確保32位值對某些64位內核平台進行正確的符號擴展,從而防止出現歷史上的漏洞。

使用SyS_read()包裝器,我們注意到的最後一點是asmlinkage指令和asmlinkage_protect()調用。內核新手FAQ很好地解釋了asmlinkage意味著函數期望它的參數在堆棧中而不是寄存器中,並且asmlinkage_protect()的通用定義解釋了它的作用是防止編譯器假設它可以安全地重用堆棧的這些區域。

在定義sys_read()(具有精確類型的變體)的同時,還在include/linux/syscalls.h中聲明。這允許其他內核代碼直接調用系統調用實現(在6個地方發生)。直接從內核的其他地方調用系統調用通常是不鼓勵的,而且也不常見。

系統調用表條目

尋找sys_read()的調用者還指出了用戶空間如何到達這個函數。對於不提供自己覆蓋的「通用」架構,可以使用include/uapi/asm-generic/unistd.h文件包括一個引用sys_read的條目:

#define __NR_read 63
__SYSCALL(__NR_read, sys_read)

它為read()定義了通用的syscall編號__NR_read(63),並使用__SYSCALL()宏以特定於體系結構的方式將該編號與sys_read()關聯起來。例如,arm64使用asm-generic/unistd.h頭文件,以填充將syscall編號映射到實現函數指針的表。

但是,我們將集中討論x86_64體系結構,它不使用這個通用表。相反,x86_64在arch/x86/syscalls/syscall_64.tbl中定義了自己的映射,其中有一個sys_read()條目:

0 common read sys_read

這表明x86_64上的read()具有syscall號0(不是63),並且對於x86_64的兩個ABIs都有一個公共實現,即sys_read()。(本系列的第二部分將討論不同的ABIs。)syscalltbl.sh腳本生成來自syscall_64.tbl表的arch/x86/include/generated/asm/syscalls_64.h,特別是為sys_read()生成__SYSCALL_COMMON()宏的調用。這個頭文件依次用於填充syscall表sys_call_table, sys_call_table是將syscall編號映射到sys_name()函數的關鍵數據結構。

x86_64調用系統調用

現在我們來看看用戶空間程序如何調用系統調用。這本質上是特定於體系結構的,因此在本文的其餘部分中,我們將集中討論x86_64體系結構(本系列的第二篇文章將研究其他x86體系結構)。調用過程還涉及幾個步驟,因此下圖可能有助於導航。(建議訪問原文,這裡圖中的鏈接沒有保留

在上一節中,我們發現了一個系統調用函數指針表;x86_64的表如下所示(使用GCC擴展進行數組初始化,以確保任何缺少的條目都指向sys_ni_syscall()):

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
[0 ... __NR_syscall_max] = &sys_ni_syscall,
[0] = sys_read,
[1] = sys_write,
/*... */
};

對於64位代碼,可以從arch/x86/kernel/entry_64.S訪問該表。,從system_call程序集入口點;它使用RAX寄存器在數組中選擇相關的條目,然後調用它。在函數的前面,SAVE_ARGS宏將各種寄存器壓入堆棧,以匹配前面看到的asmlinkage指令。

往外看,system_call入口點本身在syscall_init()中引用,這是一個在內核啟動早期調用的函數:

void syscall_init(void)
{
/*
* LSTAR and STAR live in a bit strange symbiosis.
* They both write to the same internal register. STAR allows to
* set CS/DS but only a 32bit target. LSTAR sets the 64bit rip.
*/
wrmsrl(MSR_STAR, ((u64)__USER32_CS)<<48 | ((u64)__KERNEL_CS)<<32);
wrmsrl(MSR_LSTAR, system_call);
wrmsrl(MSR_CSTAR, ignore_sysret);
/* ... */

wrmsrl指令將一個值寫入特定於模型的寄存器;在本例中,通用system_call 處理函數的地址被寫入寄存器MSR_LSTAR (0xc0000082),這是處理syscall指令的特定於x86_64模型的寄存器。

這就為我們提供了從用戶空間連接到內核代碼所需的一切。x86_64用戶程序如何調用系統調用的標準ABI是將系統調用號(0表示讀取)放入RAX寄存器,將其他參數放入特定寄存器(前3個參數為RDI、RSI、RDX),然後發出SYSCALL指令。該指令導致處理器轉換到ring 0並調用MSR_LSTAR寄存器(即system_call)引用的代碼。system_call代碼將寄存器壓入內核堆棧,並調用sys_call_table表中RAX條目上的函數指針——即sys_read(),它是SYSC_read()中實際實現的asmlinkage 包裝器。

既然我們已經看到了在最常見平台上的系統調用的標準實現,我們就能夠更好地理解其他體系結構和不太常見的情況。這將是本系列第二篇文章的主題。

--未完待續

ps:這篇翻譯的實在粗糙,將就看看吧

由於本人水平有限,翻譯必然有很多不妥的地方,歡迎指正。

同時,歡迎關注下方微信公眾號,一起交流學習:)


推薦閱讀:

TAG:Linux內核 |