2015年2月24日 星期二

[翻譯] 系統呼叫(system call)的剖析(上)

  • 原文標題:Anatomy of a system call, part 1
  • 原文網址:http://lwn.net/Articles/604287/
  • 原文作者:David Drysdale
  • 原文發表時間:2014 年 07 月 09 日

譯註:
  • 本文內容與圖片皆自原網址修改。
  • 根據原文使用的參考資料連結,原文應該是使用 3.14 版核心做為依據。

↓↓↓↓↓↓ 正文開始 ↓↓↓↓↓↓

系統呼叫是使用者空間(user-space)下的程式與 Linux 核心互動的主要機制,既然它們這麼重要,那麼我們一點都不奇怪能夠發現核心引入各式各樣的機制,來確保系統呼叫能跨架構做一般性的實作,並能以一種既有效率,又有一致性的方法讓使用者空間可以使用。

致力於讓 FreeBSD 的 Capsicum 安全性框架在 Linux 上運作,為此參與了幾個新系統呼叫的新增(包括稍微不常用的系統呼叫 execveat()),而且我發現自己對它們實作的細節有所研究。做為結果,這篇文章是探索核心對於系統呼叫(system call,或 syscall)之實作細節的兩篇文章中的第一篇,我們將在本文中著眼於主流情況:一個常態的系統呼叫(read())的技術,以及允許 x86_64 使用者程式去調用它的機制;第二篇文章將從主流情況脫離,進一步涵蓋比較少見的系統呼叫,以及其他類系統呼叫的調用機制。

因為系統呼叫的程式碼是落在核心內部的,所以與常規的函式呼叫是不一樣的;為了讓處理器運行切換到 ring 0(privileged mode,特權模式)的轉換,需要特殊的指令,此外,辨別被呼叫之核心程式碼的方法是系統呼叫號碼(syscall number),而不是函式位址。

SYSCALL_DEFINEn() 定義系統呼叫


系統呼叫 read() 為探索核心的系統呼叫構造提供了一個很好的起步案例,它實作於 fs/read_write.c,本身是一個把大部分工作交給 vfs_read() 的簡短函式。從調用的立場來看,這段程式碼最有意思的方面是利用巨集 SYSCALL_DEFINE3() 來定義函式的方法,當然啦,光從這段程式碼,還不足以弄明白到底函式到底叫什麼名字。
這些 SYSCALL_DEFINEn() 巨集是核心的程式碼定義系統呼叫的標準方法,其結尾 n 代表引數(argument)的個數,對於各個系統呼叫,這些巨集(在 include/linux/syscalls.h 中)會產生 2 個分開的輸出。
其中第一部分,SYSCALL_METADATA(),為進行追蹤的用途,建立了一組有關於系統呼叫的中繼資料(metadata),只有在定義了 CONFIG_FTRACE_SYSCALLS 來做核心編譯(kernel build)時,SYSCALL_METADATA() 才會展開,展開時則會提供用來描述該系統呼叫以及其參數的資料之模板定義式(boilerplate definition)。(有個另外的頁面對這些定義式做了更詳細的描述。)

__SYSCALL_DEFINEx() 這部分就更加有意思了,因為它掌握系統呼叫的實作方式,一旦各種不同層的的巨集和 GCC 型別擴充(GCC type extension)都展開完畢,結果的程式碼包含了一些有意思的功能:
首先,我們注意到系統呼叫的實作本身被賦予了 SYSC_read() 這個名字,但它是 static 的,所以一旦出了這個模塊它就是不可存取的譯註 1;取而代之的,有個包裹函式(wrapper function),叫做 SyS_read()別名(alias)sys_read(),是外部可見的(external visible)譯註 2,仔細看看這些別名,會注意到在它們的參數型態裡有個不一樣的地方——sys_read() 預期的是之前明確宣告的型態(例如預期第二個引數是 char __user *),而 SyS_read() 所預期的則是一串的(長)整數。深掘此處的歷史,會查出使用 long 的這個版本確保了對於 64 位元的核心平台,32 個位元長的值會有正確的符號擴展(sign-extended),藉此防止一個歷史中曾出現的弱點 譯註 3

最後一件在包裹函式 SyS_read() 中注意到的事情,是指引指令(directive)asmlinkage 和對 asmlinkage_protect() 的呼叫,Kernel Newbies FAQ 幫助解釋了 asmlinkage,它意指函式應當預期它的引數會落在堆疊上而不是在暫存器中譯註 4;而 asmlinkage_protect()通用定義式則解釋其防止編譯器假設它自己可以安全的再利用堆疊上屬於引數的那些區塊。

伴隨著 sys_read() (附有精確型別的那個變體)的定義,在 include/linux/syscalls.h 也有著一份宣告,這允許其他的核心程式碼直接呼叫這個系統呼叫的實作(大概發生在有五、六個地方);一般來說,從核心內其他地方直接呼叫系統呼叫是不鼓勵的,而且也並不常見譯註 5

系統呼叫表(syscall table)項目


追尋 sys_read() 的呼叫者的同時,也指明了使用者空間的程式如何觸及這個函式的方法。對於沒有自行覆寫的「通用」(generic)架構來說,檔案 include/uapi/asm-generic/unistd.h 包含了一筆參考到 sys_read項目
這為 read() 定義了通用系統呼叫號碼 __NR_read(63),並利用巨集 __SYSCALL() ,依照特定於硬體架構的方法,把號碼跟 sys_read() 關聯起來;例如 arm64 就利用檔案 asm-generic/unistd.h填充一張表 譯註 6,該表用來將系統呼叫號碼映射到實作函式之指標。

然而,我們要專注於 x86_64 架構,其並不使用這張通用的表,取而代之的,x86_64 在 arch/x86/syscalls/syscall_64.tbl 定義它自己的映射表,當然也包括了給 sys_read()項目
這指定了在 x86_64,read() 的系統呼叫號碼是 0(不是 63),並且對於 x86_64 上的所有 ABI(application binary interface,應用程式二進位介面)都有 common 式的實作,名為 sys_read()。腳本 syscalltbl.sh 會從表格 syscall_64.tbl 產生 arch/x86/include/generated/asm/syscalls_64.h,特別是會為 sys_read() 產生一個對巨集 __SYSCALL_COMMON() 的調用,這個標頭檔被用來逐項操作系統呼叫表,sys_call_table,這張表是將系統呼叫號碼映射到 sys_name() 函式的關鍵資料結構。

x86_64 系統呼叫的調用


現在我們來看看使用者空間的程式是如何調用系統呼叫的,這方面天生就是特定於硬體架構的,所以在本文剩下的部分要專注於 x86_64 架構(x86 的其他架構將在本系列的第二篇文章中探討)。調用的程序含括了幾個步驟,所以在左邊的可點擊流程圖,也許能以導覽來提供協助。

[系統呼叫流程圖]
在前一節中,我們發現了由系統呼叫函式的指標所構成的一張表;x86_64 的這張表看起來大概像下面這樣(利用 GCC 對於陣列初始化的擴充來確保任何缺失的項目都指向 sys_ni_syscall()):
對於 64 位元程式碼,此表會在 arch/x86/kernel/entry_64.S 中被存取,準確的說是在組語入口點(entry point)system_call;它利用 RAX 暫存器從陣列挑選相關的項目,然後呼叫該項目,而在同個函式裡面稍早之前,巨集 SAVE_ARGS 把各種暫存器推到堆疊上,以便符合在前述看到的指引指令 asmlinkage 譯註 7

進一步往外層看,入口點 system_call 它自己會在 syscall_init()參考到,函式 syscall_init() 則會在核心啟動流程的早期被呼叫
硬體指令 wrmsrl 寫入值到一個特定模組暫存器(model-specific register,MSR)
在這個情況,通用的系統呼叫處理函式 system_call 的位址被寫到暫存器 MSR_LSTAR0xc000008),這是用來處理硬體指令 SYSCALL 的 x86_64 特定模組暫存器。

這提供了我們,為了把從使用者空間到核心空間串聯連起來所需的一切。對於 x86_64 的使用者程式如何調用系統呼叫,標準 ABI 是將系統呼叫號碼(對 read 來說是 0)放進暫存器 RAX,而其他參數放進特定的暫存器(對前三個參數是 RDI、RSI、RDX),然後發起硬體指令 SYSCALL,這道指令會導致處理器轉移到 ring 0,並且調用由特定模組暫存器 MSR_LSTAR 所參考指向的程式碼——也就是 system_callsystem_call 的程式碼會把暫存器推到核心堆疊(kernel stack)上,並呼叫陣列表 sys_call_table 內第 RAX 項的函式指標——也就是 sys_read(),它是為了真正的實作 SYSC_read(),所做的一個既輕薄,且帶有 asmlinkage 的包裹函式。

現在,我們已經見識了在最普及之平台上的系統呼叫的標準實作,有了一個良好的起點,以便理解在其他架構和比較不常見的情況到底做了什麼,而這,將是本系列第二篇文章的主題。

↑↑↑↑↑↑ 正文結束 ↑↑↑↑↑↑

譯註 1
其實指的就是 C 的靜態函式(static function),不了解靜態函式的話可以看這裡。   
譯註 2
早期的系統呼叫實作是以 sys_ 為開頭的函式,例如在 2.6.24 中,系統呼叫 read() 的實作像這樣,現今這樣設計與相容性不無關係。   
譯註 3
這篇文章中,對該弱點的成因有比較通俗易懂的解釋。   
譯註 4
就我所知,Linux Kernel Newbies 的這段解釋其實僅適用於 x86_32 的核心,在 x86_64 其實其並沒有這樣的意義。就我的理解,asmlinkage 的意義其實表示「此函式需遵從 C 語言在此平台下的呼叫慣例」,因為 x86_32 下的慣例是堆疊,因此才有了本文的解釋,其定義在這裡(可以明顯看到它只在 x86_32 下起作用);然而 x86_64 的慣例本來就是暫存器,所以沒有這種限制,其定義在這裡
  
譯註 5
其實關於「不鼓勵」這句話,似乎找不到明確的證據,在原文的推文中對於這點的討論,大家也都只是「認為」在核心內調用系統呼叫,違反了系統呼叫本身的意義。
  
譯註 6
在原始碼中引入的是 arch/arm64/include/asm/unistd.h,而此檔案最終會引入 include/uapi/asm-generic/unistd.h
  
譯註 7
接續譯註 4 的資料,此處將暫存器推到堆疊,並非為了符合 asmlinkage;而是核心對於系統呼叫的要求完成時,在返回使用者空間前,必須將暫存器回復原狀,以符合 ABI 的規定(其實也避免了一般程式窺探系統行為的痕跡),為此才對暫存器進行「備份」。 而實際上對於符合 C 在 x86_64 上的函式呼叫慣例,在維基百科可以查到,「對於系統調用,R10 用來替代 RCX」的暫存器差異,而在此處,即可看到 system_call 對於暫存器 R10 與 RCX 的調整,使暫存器從符合「系統呼叫」變成符合「函式呼叫」。

沒有留言 :

張貼留言