? ? ?
?
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ??
?
作者簡介:程磊,一線碼農(nóng),在某手機公司擔(dān)任系統(tǒng)開發(fā)工程師,閱碼場榮譽總編輯,日常喜歡研究內(nèi)核基本原理。
?
目錄:
一、基本概念解析
? ? ? 1.1 系統(tǒng)調(diào)用的來源與作用
? ? ? 1.2 API的來源與作用
? ? ? 1.3 API與系統(tǒng)調(diào)用的關(guān)系
? ? ? 1.4 系統(tǒng)調(diào)用機制的基本原理
二、API的制定與實現(xiàn)
? ? ? 2.1 POSIX API
? ? ? 2.2 Windows API
? ? ? 2.3 API的實現(xiàn)
三、系統(tǒng)調(diào)用的實現(xiàn)
? ? ? 3.1 x86平臺的實現(xiàn)
? ? ? 3.2?指令基本原理
? ? ? 3.3系統(tǒng)調(diào)用編號
? ? ? 3.4系統(tǒng)調(diào)用入口函數(shù)
? ? ? 3.5匯編程序演示
? ? ? 3.6vsyscall與vdso
四、總結(jié)回顧
?
?
?一、基本概念解析 ? ?我們在很多書籍上、博客上都學(xué)過或者聽說過系統(tǒng)調(diào)用與API這兩個概念,那么這兩個概念究竟是什么意思,它們之間是什么關(guān)系呢?如果我們閱讀過《操作系統(tǒng)導(dǎo)論》,就會明白操作系統(tǒng)的目的與作用,就會知道內(nèi)核是要向進程提供服務(wù)的,那么內(nèi)核是如何向進程提供服務(wù)的呢?下面我們就來一探究竟。
1.1 系統(tǒng)調(diào)用的來源與作用
?
我們先來看一下進程的虛擬內(nèi)存空間布局,我們以32位為例,64位的邏輯也是一樣的。
可以看到一個進程的內(nèi)存空間分為用戶空間和內(nèi)核空間兩部分。每個進程都有自己獨立的用戶空間,但是所有進程都共享同一個內(nèi)核空間,所以所有進程都可以請求內(nèi)核的服務(wù)。不過內(nèi)核空間運行在特權(quán)級,用戶空間運行在非特權(quán)級,所以用戶空間是不能直接訪問內(nèi)核空間的。為此,內(nèi)核向用戶空間提供了有限制的訪問,系統(tǒng)調(diào)用。用戶空間可以通過系統(tǒng)調(diào)用來調(diào)用內(nèi)核里一些特定的函數(shù)。這樣的話,進程就可以通過系統(tǒng)調(diào)用來請求內(nèi)核的服務(wù)了。系統(tǒng)調(diào)用是如何實現(xiàn)的呢?這是需要硬件的特殊支持的,第三章節(jié)會講。
1.2 API的來源與作用
?
既然有了系統(tǒng)調(diào)用,進程可以通過系統(tǒng)調(diào)用來請求內(nèi)核的服務(wù),那么為什么還會有API呢?因為系統(tǒng)調(diào)用是偏底層的,有很多細節(jié)要處理,而且不同的平臺其系統(tǒng)調(diào)用并不相同;就算是同一個平臺,其提供的系統(tǒng)調(diào)用功能以及系統(tǒng)調(diào)用的實現(xiàn)方法都有可能會發(fā)生變化。因此為了屏蔽系統(tǒng)調(diào)用的各種細節(jié),增加通用性和跨平臺性,操作系統(tǒng)又向用戶進程提供了API。API,Application Programming Interface,應(yīng)用程序編程接口,它的意思就是它的字面意思,就是指操作系統(tǒng)向應(yīng)用程序提供的編程接口。現(xiàn)實中有很多人把API當(dāng)做I(Interface)接口的意思來用,本文所說的API都是指它的本意。有了API你就不用考慮系統(tǒng)調(diào)用了,無論在任何平臺、任何OS,你只管使用API,只要它們的API是相同的,你的源碼就是兼容的、跨平臺的。
?
1.3 API與系統(tǒng)調(diào)用的關(guān)系
?
API和系統(tǒng)調(diào)用具體是什么關(guān)系呢?系統(tǒng)調(diào)用是偏底層、偏實現(xiàn)的,API是偏上層、偏接口的。系統(tǒng)調(diào)用是實現(xiàn)在內(nèi)核里的,它的修改只要符合內(nèi)核的規(guī)范、只要內(nèi)核的主要管理者同意就可以。API它首先是行業(yè)標(biāo)準(zhǔn)或者業(yè)內(nèi)標(biāo)準(zhǔn),是不能隨意改變的,一般都有相應(yīng)的標(biāo)準(zhǔn)委員會來制定和發(fā)展API。API的實現(xiàn)是在用戶空間庫里面,一般都是在libc中實現(xiàn)。API的底層實現(xiàn)一般使用的是系統(tǒng)調(diào)用,很多API和系統(tǒng)調(diào)用是一對一關(guān)系。但也有特殊情況,比如有的API并不使用系統(tǒng)調(diào)用,有的系統(tǒng)調(diào)用沒有對應(yīng)的API,有的API可能調(diào)用了多個系統(tǒng)調(diào)用,有的系統(tǒng)調(diào)用可能被多個API使用。也就是說大部分情況下API和系統(tǒng)調(diào)用是1:1的關(guān)系,但有些情況下是1:0、0:1、1:n、n:1、m:n的關(guān)系。當(dāng)API和系統(tǒng)調(diào)用的關(guān)系是1:1,而且它們的名字也相同時,我們不能把它們看做是同一個事物,而應(yīng)當(dāng)把它們看做不同的事物,只不過是名字相同而已,是同名的API使用了同名的系統(tǒng)調(diào)用。就好比有兩種情況,第一種情況是,有兩個人都叫張偉,一個是副市長,一個是公安局局長,張偉副市長安排張偉局長去做某件事情。第二種情況是,有一個人叫張偉,他是副市長兼任公安局局長,張偉副市長兼局長去做某件事情。這兩種情況是不一樣的,同名的API與系統(tǒng)調(diào)用的關(guān)系類似于前者。
?
下面我們舉例來說明一下API與系統(tǒng)調(diào)用的關(guān)系。我們來寫一個最簡單的hello world程序,代碼如下。
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
int main(int argc, char *argv[])
{
char str[] = "hello, world ";
write(1, str, strlen(str));
}?
編譯:gcc -o hello hello.c
運行:./hello
會在屏幕上輸出 hello, world。
?
這個程序非常簡單,我們調(diào)用了兩個API(strlen 和 write),在屏幕上輸出了一行文字。同樣是API,strlen沒有使用系統(tǒng)調(diào)用,自己直接在用戶空間就把功能實現(xiàn)了,而write API則使用了write系統(tǒng)調(diào)用。有些API的功能比較簡單,自己在用戶空間就能實現(xiàn),沒必要麻煩內(nèi)核。但是有些API的功能在用戶空間是不可能實現(xiàn)或者很難實現(xiàn)的,必須要求助于內(nèi)核。我們把write API與write系統(tǒng)調(diào)用畫成圖,如下所示:
API函數(shù)通過系統(tǒng)調(diào)用機制調(diào)用系統(tǒng)調(diào)用函數(shù)。那么系統(tǒng)調(diào)用機制要做的事情有哪些呢?有兩件事,一是實現(xiàn)CPU特權(quán)級的轉(zhuǎn)變,把CPU設(shè)置為特權(quán)模式之后才能執(zhí)行內(nèi)核的代碼。二是傳遞系統(tǒng)調(diào)用的編號和函數(shù)參數(shù),系統(tǒng)調(diào)用函數(shù)有很多,怎么知道你想調(diào)用的是哪個系統(tǒng)調(diào)用函數(shù)呢,通過編號來區(qū)分。系統(tǒng)調(diào)用函數(shù)大部分都是有參數(shù)的,所以還需要傳遞參數(shù),參數(shù)怎么傳遞是和具體硬件相關(guān)的,由相應(yīng)的ABI來規(guī)定。
?
1.4 系統(tǒng)調(diào)用機制的基本原理
?
那么系統(tǒng)調(diào)用機制該怎么實現(xiàn)呢?答案是要靠CPU提供的特殊指令(系統(tǒng)調(diào)用指令)來實現(xiàn),雖然不同架構(gòu)的CPU實現(xiàn)不盡相同,但是大概模式都是一樣的,都是往某個寄存器寫入系統(tǒng)調(diào)用編號,在約定的寄存器或者棧上寫入?yún)?shù),然后調(diào)用特殊指令(系統(tǒng)調(diào)用指令),此時CPU就會切換到特權(quán)模式并進入內(nèi)核執(zhí)行一段預(yù)先設(shè)定的代碼(系統(tǒng)調(diào)用入口函數(shù)),這段代碼會根據(jù)系統(tǒng)調(diào)用編號調(diào)用相應(yīng)的系統(tǒng)調(diào)用函數(shù)。畫成圖如下所示:
可以看出完成一個系統(tǒng)調(diào)用有兩個關(guān)鍵點,一是系統(tǒng)調(diào)用編號要能對應(yīng)上,二是系統(tǒng)調(diào)用入口函數(shù)要提前設(shè)置好。這樣系統(tǒng)調(diào)用入口函數(shù)才能根據(jù)系統(tǒng)調(diào)用編號找到正確的系統(tǒng)調(diào)用函數(shù)。
需要說明的是,一個平臺提供的系統(tǒng)調(diào)用指令不一定只有一個,不同系統(tǒng)調(diào)用指令對應(yīng)的系統(tǒng)調(diào)用入口函數(shù)也不相同,這個第三章會詳細講解。
?
?
?二、API的制定和實現(xiàn) ? ?最開始的時候是沒有操作系統(tǒng)的,后來逐漸產(chǎn)生了操作系統(tǒng)。操作系統(tǒng)對應(yīng)用程序提供的用戶空間接口就叫做API(應(yīng)用程序編程接口)。API剛開始是看著缺啥就添啥,沒有一定的標(biāo)準(zhǔn)。后來隨著操作系統(tǒng)的發(fā)展,再野蠻生長就不行了,于是就有了操作系統(tǒng)API標(biāo)準(zhǔn)規(guī)范。不同的操作系統(tǒng),它們的API并不相同,API的制定與維護方法也不相同。2.1 POSIX API
?
UNIX操作系統(tǒng)家族的API叫做POSIX(Portable Operating System Interface)。POSIX是IEEE制定的規(guī)范,POSIX這個名字是GNU的倡導(dǎo)者Richard Stallman建議的,按照當(dāng)時的命名習(xí)慣在最后加了個X。UNIX還有另外一個規(guī)范叫做Single UNIX Specification,簡稱SUS,是由Open Group發(fā)布的。后來POSIX和SUS合并開發(fā),內(nèi)容一樣,但是對外還是用兩個名字。想要了解POSIX S最新的標(biāo)準(zhǔn),請查看網(wǎng)站https://unix.org/online.html。
?
Linux本身僅僅是個內(nèi)核,并不是個操作系統(tǒng)。GNU/Linux或者Linux發(fā)行版才是個完整的操作系統(tǒng)。Linux發(fā)行版都遵循POSIX API。網(wǎng)站https://man7.org 和書籍《The Linux Programming Interface》非常全面詳細地介紹了POSIX API的語義以及它在Linux上的一些實現(xiàn)情況,非常值得大家認真學(xué)習(xí)或者經(jīng)常查閱。
2.2 Windows API
?
Windows的API在16位的時候叫做Windows API。后來到了32位的時候,重新設(shè)計了API,由于16的API和32位的API差別非常大,所以就重新命名為Win32 API。到了64位的時候,API基本沒啥變化,就是把有些參數(shù)從32位提升到了64位,所以64位的Windows的API也依然被人們叫做Win32 API。當(dāng)然64位的API也被Windows命名為Windows API,因為16位Windows早已成為歷史,這么命名也不會引起歧義。現(xiàn)在Windows API和Win32 API幾乎是同義詞,區(qū)別不大。
?
由于Windows操作系統(tǒng)是微軟一家的閉源產(chǎn)品,所以它的API規(guī)范是由公司制定的。這和POSIX是由標(biāo)準(zhǔn)委員會制定的是不一樣的。
?
2.3 API的實現(xiàn)
?
API本身僅僅是個規(guī)范,是個概念性的東西,它具體是怎么實現(xiàn)的呢?目前業(yè)界都是把API放在libc里面來實現(xiàn)的。所以libc里面不僅有C標(biāo)準(zhǔn)庫的實現(xiàn),還有操作系統(tǒng)API的實現(xiàn)。所以大家不能認為libc就是一個普通的lib,它的作用是非常重要的,沒有l(wèi)ibc,幾乎所有的進程都無法運行,libc是進程通向內(nèi)核的必經(jīng)之路。幾乎所有的進程都鏈接了libc,大部分都是動態(tài)鏈接的,通過ldd命令可以查到,通過/proc/$pid/maps也可以查到;少部分是靜態(tài)鏈接libc的,是查不到libc.so的,但是程序本身還是包含libc的代碼的。當(dāng)然你也可以自己調(diào)用系統(tǒng)調(diào)用就不用libc,一般只有演示程序會這么做。
?
Libc在不同操作系統(tǒng)上的實現(xiàn)是不同的,在同一個操作系統(tǒng)也可能有多個不同的實現(xiàn)。Linux發(fā)行版上最流行的libc實現(xiàn)是Glibc,Android上的libc實現(xiàn)是bionic。
?
?
?三、系統(tǒng)調(diào)用的實現(xiàn)系統(tǒng)調(diào)用機制的實現(xiàn)原理都是相同的,但是不同操作系統(tǒng)、不同硬件平臺上的實現(xiàn)細節(jié)又不盡相同。下面我們分別來講一下Linux在x86平臺和arm平臺上實現(xiàn)細節(jié)。
3.1 x86平臺的實現(xiàn)
?
X86平臺的系統(tǒng)調(diào)用的實現(xiàn)方法經(jīng)歷了三代的變遷,每次改變都提高了系統(tǒng)調(diào)用的執(zhí)行效率。第一代系統(tǒng)調(diào)用指令,借用了中斷機制的指令,int 0x80、iret。第二代系統(tǒng)調(diào)用指令sysenter、sysexit。第三代系統(tǒng)調(diào)用指令syscall、sysret。三代指令在內(nèi)核中的使用情況如下圖所示:
?下面我們分別講一下這三代指令的基本原理。
3.2 指令基本原理
?
第一代系統(tǒng)調(diào)用指令使用的是中斷指令,基本原理如下。中斷發(fā)生時,CPU會切換到特權(quán)模式并跳到內(nèi)核執(zhí)行預(yù)先指定的一段程序。執(zhí)行哪段程序呢,要根據(jù)中斷源來決定,不同的中斷源執(zhí)行不同的程序,每個中斷源都對應(yīng)一個整數(shù)來標(biāo)識自己,這個整數(shù)就叫做中斷向量。中斷源有三類,外設(shè)中斷、CPU異常、指令中斷,前兩種都有自己的方法來指定中斷向量,指令中斷是在指令的操作數(shù)里面指定中斷向量號的。我們的系統(tǒng)調(diào)用就是利用指令中斷,用向量號0x80,也就是十進制的128當(dāng)做自己的中斷向量,來執(zhí)行系統(tǒng)調(diào)用的。我們在用戶空間,先把系統(tǒng)調(diào)用編號賦值給寄存器EAX,然后執(zhí)行int 0x80,CPU就會跳轉(zhuǎn)到內(nèi)核執(zhí)行內(nèi)核預(yù)先設(shè)定的中斷處理程序(也就是系統(tǒng)調(diào)用入口函數(shù))。系統(tǒng)調(diào)用入口函數(shù)根據(jù)EAX的值調(diào)用對應(yīng)的系統(tǒng)調(diào)用函數(shù)。系統(tǒng)調(diào)用函數(shù)執(zhí)行完成之后返回系統(tǒng)調(diào)用入口函數(shù),入口函數(shù)再執(zhí)行iret返回到用戶空間,一個系統(tǒng)調(diào)用就完成了。
?
第二代系統(tǒng)調(diào)用指令sysenter/sysexit,由于通過中斷流程進行系統(tǒng)調(diào)用開銷太大了,很多操作對系統(tǒng)調(diào)用來說又是沒有意義的,因此Intel專門開發(fā)了只用于系統(tǒng)調(diào)用的指令。由于sysenter是專用指令,它可以把很多中斷相關(guān)的操作都省略掉,具體來說有以下幾點,1.不再自動把寄存器信息保存到內(nèi)核棧上,2.不再自動從內(nèi)核棧上加載esp的值,3.不再走中斷處理流程。
?
使用sysenter指令需要提前設(shè)置一些MSR寄存器,具體來說要做以下一些設(shè)置。把內(nèi)核代碼段的選擇符寫入MSR IA32_SYSENTER_CS,把系統(tǒng)調(diào)用入口函數(shù)寫入MSR IA32_SYSENTER_EIP,內(nèi)核棧段的選擇符要放在緊挨著內(nèi)核棧段的后面,把內(nèi)核棧的地址寫入MSR IA32_SYSENTER_ESP,這樣sysenter執(zhí)行時CPU就會切換到特權(quán)模式,然后執(zhí)行系統(tǒng)調(diào)用入口函數(shù)。在執(zhí)行sysexit之前把要返回到的用戶空間指令的地址寫入EDX,用戶空間棧的值寫入ECX。
?
sysenter/sysexit指令也可以用于64位模式,但是Linux選擇在64位上只使用syscall/sysret。
?
第三代系統(tǒng)調(diào)用指令syscall/sysret,是AMD開發(fā)的,它只能用于64位模式,比sysenter/sysexit還要快一些,因為1.它不再保存和恢復(fù)用戶空間RSP,2.它只能用于平坦內(nèi)存,因此省略了分段單元的開銷。
?
使用syscall/sysret前要提前設(shè)置一些MSR。要在MSR IA32_STAR中設(shè)置內(nèi)核空間和用戶空間的代碼段,其中內(nèi)核空間CS、SS在47:32位,用戶空間CS、SS在63:48位。系統(tǒng)調(diào)用入口函數(shù)的地址要寫人MSR IA32_LSTR。syscall執(zhí)行的時候會把MSR IA32_STAR的47:32位加載到CS和SS,把MSR IA32_LSTR的值加載到RIP。在執(zhí)行sysret之前把要返回到的用戶空間指令的地址寫入RCX,sysret執(zhí)行時會把MSR IA32_STAR的63:48位加載到CS和SS,把RCX加載到RIP。
?
3.3?系統(tǒng)調(diào)用編號
?
我們先來解決第一個問題,系統(tǒng)調(diào)用編號是怎么確定的。不同架構(gòu)不同位數(shù)的系統(tǒng),系統(tǒng)調(diào)用編號是不一樣的。如果用戶空間傳遞的系統(tǒng)調(diào)用編號和內(nèi)核里的系統(tǒng)調(diào)用編號對不上,那問題就嚴重了。Linux內(nèi)核在編譯時會生成一個文件,arch/x86/include/generated/uapi/asm/unistd_64.h,這個文件是生成的,不是本來就有的,這個文件里面有所有系統(tǒng)調(diào)用的編號。在安裝操作系統(tǒng)時或者單獨安裝內(nèi)核和內(nèi)核頭文件時,這個文件會被安裝在/usr/include/asm/unistd_64.h,libc會使用這個文件,這樣用戶空間傳遞的編號和內(nèi)核里面的系統(tǒng)調(diào)用編號就是一致的了。
?
3.4?系統(tǒng)調(diào)用入口函數(shù)
?
下面我們來說說系統(tǒng)調(diào)用入口函數(shù)是怎么設(shè)置的。X86_64對于64位的進程來說只有一個系統(tǒng)調(diào)用指令,就是syscall,它的入口函數(shù)在linux-src/arch/x86/entry/entry_64.S, 函數(shù)名叫entry_SYSCALL_64。對于32位的進程來說有三個系統(tǒng)調(diào)用指令 int 0x80、sysenter、syscall,它們的入口函數(shù)都在 linux-src/arch/x86/entry/entry_64_compat.S,函數(shù)名分別叫做entry_INT80_compat、entry_SYSENTER_compat、entry_SYSCALL_compat。設(shè)置它們的代碼在兩個地方,syscall(64)、syscall(32)、sysenter 這三個設(shè)置在一個地方,在文件linux-src/arch/x86/kernel/cpu/common.c中的函數(shù) syscall_init
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
void syscall_init(void)
{
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
wrmsrl(MSR_CSTAR, (unsigned long)entry_SYSCALL_compat);
/*
* This only works on Intel CPUs.
* On AMD CPUs these MSRs are 32-bit, CPU truncates MSR_IA32_SYSENTER_EIP.
* This does not cause SYSENTER to jump to the wrong location, because
* AMD doesn't allow SYSENTER in long mode (either 32- or 64-bit).
*/
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)__KERNEL_CS);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP,
(unsigned long)(cpu_entry_stack(smp_processor_id()) + 1));
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, (u64)entry_SYSENTER_compat);
wrmsrl(MSR_CSTAR, (unsigned long)ignore_sysret);
wrmsrl_safe(MSR_IA32_SYSENTER_CS, (u64)GDT_ENTRY_INVALID_SEG);
wrmsrl_safe(MSR_IA32_SYSENTER_ESP, 0ULL);
wrmsrl_safe(MSR_IA32_SYSENTER_EIP, 0ULL);
/*
* Flags to clear on syscall; clear as much as possible
* to minimize user space-kernel interference.
*/
wrmsrl(MSR_SYSCALL_MASK,
X86_EFLAGS_CF|X86_EFLAGS_PF|X86_EFLAGS_AF|
X86_EFLAGS_ZF|X86_EFLAGS_SF|X86_EFLAGS_TF|
X86_EFLAGS_IF|X86_EFLAGS_DF|X86_EFLAGS_OF|
X86_EFLAGS_IOPL|X86_EFLAGS_NT|X86_EFLAGS_RF|
X86_EFLAGS_AC|X86_EFLAGS_ID);
}
......
?
從代碼中可以看出只有在64位的情況下才會設(shè)置syscall指令的入口函數(shù),只有在系統(tǒng)兼容32位進程(CONFIG_IA32_EMULATION)的情況下才會設(shè)置syscall(32)、sysenter的兼容入口函數(shù)。大部分linux發(fā)行版都支持32位進程兼容。
?
兼容int 0x80的代碼設(shè)置在另外一個地方,因為int 0x80是中斷指令,所以它是在設(shè)置中斷的地方設(shè)置的,具體位置是linux-src/arch/x86/kernel/idt.c中的函數(shù)idt_setup_traps。
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
tatic const __initconst struct idt_data def_idts[] = {
INTG(X86_TRAP_DE, asm_exc_divide_error),
ISTG(X86_TRAP_NMI, asm_exc_nmi, IST_INDEX_NMI),
INTG(X86_TRAP_BR, asm_exc_bounds),
INTG(X86_TRAP_UD, asm_exc_invalid_op),
INTG(X86_TRAP_NM, asm_exc_device_not_available),
INTG(X86_TRAP_OLD_MF, asm_exc_coproc_segment_overrun),
INTG(X86_TRAP_TS, asm_exc_invalid_tss),
INTG(X86_TRAP_NP, asm_exc_segment_not_present),
INTG(X86_TRAP_SS, asm_exc_stack_segment),
INTG(X86_TRAP_GP, asm_exc_general_protection),
INTG(X86_TRAP_SPURIOUS, asm_exc_spurious_interrupt_bug),
INTG(X86_TRAP_MF, asm_exc_coprocessor_error),
INTG(X86_TRAP_AC, asm_exc_alignment_check),
INTG(X86_TRAP_XF, asm_exc_simd_coprocessor_error),
TSKG(X86_TRAP_DF, GDT_ENTRY_DOUBLEFAULT_TSS),
ISTG(X86_TRAP_DF, asm_exc_double_fault, IST_INDEX_DF),
ISTG(X86_TRAP_DB, asm_exc_debug, IST_INDEX_DB),
ISTG(X86_TRAP_MC, asm_exc_machine_check, IST_INDEX_MCE),
ISTG(X86_TRAP_VC, asm_exc_vmm_communication, IST_INDEX_VC),
SYSG(X86_TRAP_OF, asm_exc_overflow),
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_compat),
SYSG(IA32_SYSCALL_VECTOR, entry_INT80_32),
};
void __init idt_setup_traps(void)
{
idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);
}
?
從代碼中可以看出,只有系統(tǒng)支持32位進程兼容(CONFIG_IA32_EMULATION)才會去設(shè)置entry_INT80_compat。
?
我們設(shè)置好了這些系統(tǒng)調(diào)用指令的入口函數(shù)之后,當(dāng)用戶空間調(diào)用這些指令的時候就會調(diào)用這些函數(shù)。那么這些函數(shù)又是怎樣去調(diào)用具體對應(yīng)的系統(tǒng)調(diào)用函數(shù)呢?我們以64位進程的syscall指令為例來看一看。先看它的入口函數(shù),linux-src/arch/x86/entry/entry_64.S:entry_SYSCALL_64
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
SYM_CODE_START(entry_SYSCALL_64)
pushq $__USER_DS /* pt_regs->ss */
pushq PER_CPU_VAR(cpu_tss_rw + TSS_sp2) /* pt_regs->sp */
pushq %r11 /* pt_regs->flags */
pushq $__USER_CS /* pt_regs->cs */
pushq %rcx /* pt_regs->ip */
pushq %rax /* pt_regs->orig_ax */
call do_syscall_64 /* returns with IRQs disabled */
sysretq
SYM_CODE_END(entry_SYSCALL_64)
?
我們對代碼做了精簡只留下最關(guān)鍵的。可以看到函數(shù)先把__USER_DS和__USER_CS都push到了棧上,這是為了執(zhí)行最后面的那條sysretq時可以返回用戶空間把特權(quán)級也轉(zhuǎn)為用戶級。函數(shù)的主體就是調(diào)用函數(shù)do_syscall_64,我們再來看一個這個函數(shù),linux-src/arch/x86/entry/common.c
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
static __always_inline bool do_syscall_x64(struct pt_regs *regs, int nr)
{
/*
* Convert negative numbers to very high and thus out of range
* numbers for comparisons.
*/
unsigned int unr = nr;
if (likely(unr < NR_syscalls)) {
unr = array_index_nospec(unr, NR_syscalls);
regs->ax = sys_call_table[unr](regs);
return true;
}
return false;
}
__visible noinstr void do_syscall_64(struct pt_regs *regs, int nr)
{
add_random_kstack_offset();
nr = syscall_enter_from_user_mode(regs, nr);
instrumentation_begin();
if (!do_syscall_x64(regs, nr) && !do_syscall_x32(regs, nr) && nr != -1) {
/* Invalid system call, but still a system call. */
regs->ax = __x64_sys_ni_syscall(regs);
}
instrumentation_end();
syscall_exit_to_user_mode(regs);
}
?
可以看到do_syscall_64就是調(diào)用do_syscall_x64,do_syscall_x64就是根據(jù)用戶空間傳來的系統(tǒng)調(diào)用編號在sys_call_table數(shù)組中調(diào)用相應(yīng)的函數(shù)。那么這個sys_call_table數(shù)組是怎么來的呢?它是在文件linux-5.15.28/arch/x86/entry/syscall_64.c中定義的,如下:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
asmlinkage const sys_call_ptr_t sys_call_table[] = {
#include <asm/syscalls_64.h>
};
?
那么syscalls_64.h的內(nèi)容是什么,它是怎么來的呢?這個文件并不是手寫的,而是在編譯時由腳本生成的,它是根據(jù)文件linux-src/arch/x86/entry/syscalls/syscall_64.tbl 生成的。我們截取一段syscalls_64.h的內(nèi)容如下:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
__SYSCALL(0, sys_read)
__SYSCALL(1, sys_write)
__SYSCALL(2, sys_open)
__SYSCALL(3, sys_close)
__SYSCALL(4, sys_newstat)
__SYSCALL(5, sys_newfstat)
__SYSCALL(6, sys_newlstat)
__SYSCALL(7, sys_poll)
__SYSCALL(8, sys_lseek)
__SYSCALL(9, sys_mmap)
......
__SYSCALL(442, sys_mount_setattr)
__SYSCALL(443, sys_quotactl_fd)
__SYSCALL(444, sys_landlock_create_ruleset)
__SYSCALL(445, sys_landlock_add_rule)
__SYSCALL(446, sys_landlock_restrict_self)
__SYSCALL(447, sys_memfd_secret)
__SYSCALL(448,?sys_process_mrelease)
?
對syscall_64.c進行預(yù)編譯之后我們可以發(fā)現(xiàn)sys_call_table數(shù)組的內(nèi)容如下:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
const sys_call_ptr_t sys_call_table[] = {
__x64_sys_read,
__x64_sys_write,
__x64_sys_open,
__x64_sys_close,
__x64_sys_newstat,
__x64_sys_newfstat,
__x64_sys_newlstat,
__x64_sys_poll,
__x64_sys_lseek,
__x64_sys_mmap,
__x64_sys_mprotect,
__x64_sys_munmap,
__x64_sys_brk,
......
__x64_sys_openat2,
__x64_sys_pidfd_getfd,
__x64_sys_faccessat2,
__x64_sys_process_madvise,
__x64_sys_epoll_pwait2,
__x64_sys_mount_setattr,
__x64_sys_quotactl_fd,
__x64_sys_landlock_create_ruleset,
__x64_sys_landlock_add_rule,
__x64_sys_landlock_restrict_self,
__x64_sys_memfd_secret,
__x64_sys_process_mrelease,
};
?
也就是說這是由一堆函數(shù)名構(gòu)成的函數(shù)指針數(shù)組,那么這些函數(shù)名是怎么生成的呢?它是由一系列的SYSCALL_DEFINEx宏生成的,x代表函數(shù)的參數(shù)個數(shù)。我們以open系統(tǒng)調(diào)用來講解一下,open系統(tǒng)調(diào)用的實現(xiàn)是在文件linux-src/fs/open.c
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
struct open_how how = build_open_how(flags, mode);
return do_sys_openat2(dfd, filename, &how);
}
SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, umode_t, mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
?
我們把宏SYSCALL_DEFINE3展開之后大致可以得到如下的代碼:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
long __x64_sys_open(const struct pt_regs *regs)
{ return __se_sys_open(regs->di, regs->si, regs->dx); }
long __ia32_sys_open(const struct pt_regs *regs)
{ return __se_sys_open((unsigned int)regs->bx, (unsigned int)regs->cx, (unsigned int)regs->dx); }
static long __se_sys_open(__typeof(filename), __typeof(flags), __typeof(mode) )
{
long ret = __do_sys_open(( const char *) filename, ( int) flags, ( umode_t) mode);
return ret;
}
long __do_sys_open(const char * filename, int flags, umode_t mode)
{
if (force_o_largefile())
flags |= O_LARGEFILE;
return do_sys_open(AT_FDCWD, filename, flags, mode);
}
?
可以看出這個宏會生成函數(shù)__x64_sys_open,這個函數(shù)正好是sys_call_table數(shù)組里面的函數(shù)名。__x64_sys_open接受的參數(shù)是一個寄存器集的指針,然后提取寄存器中的值再調(diào)用函數(shù)__se_sys_open,函數(shù)__se_sys_open對參數(shù)進行強轉(zhuǎn)再調(diào)用__do_sys_open,這個函數(shù)是最終的函數(shù)。我們可以看到這里面還生成了函數(shù)__ia32_sys_open,這個函數(shù)是32位進程兼容的系統(tǒng)調(diào)用所使用的數(shù)組ia32_sys_call_table的成員。
?
3.5?匯編程序演示
?
下面我們用匯編語言來試一試執(zhí)行系統(tǒng)調(diào)用,一般情況下我們都不會直接使用系統(tǒng)調(diào)用指令,下面的例子僅僅是為了演示,標(biāo)準(zhǔn)編程中請使用API。
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
.data
msg:
.ascii "Hello from syscall ! "
len = . - msg
.text
.global _start
_start:
movq $1, %rax
movq $1, %rdi
movq $msg, %rsi
movq $len, %rdx
syscall
movq $60, %rax
xorq %rdi, %rdi
????syscall
?
執(zhí)行如下命令,先匯編后鏈接
gcc -c -o hello-syscall64.o hello-syscall64.S
ld -entry _start hello-syscall64.o -o hello-syscall64
然后運行程序
./hello-syscall64
可以看到運行成功,命令行輸出了 Hello from syscall !
下面我們再來演示一下32位進程兼容模式的系統(tǒng)調(diào)用,匯編代碼如下:
?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
- ?
.data
msg1:
.ascii "Hello from int 0x80 ! "
len1 = . - msg1
msg2:
.ascii "Hello from sysenter ! "
len2 = . - msg2
.text
.globl _start
_start:
movl $4, %eax
movl $1, %ebx
movl $msg1, %ecx
movl $len1, %edx
int $0x80
movl $4, %eax
movl $1, %ebx
movl $msg2, %ecx
movl $len2, %edx
call sys
movl $1, %eax
movl $0, %ebx
int $0x80
sys:
pushl %ecx
pushl %edx
pushl %ebp
movl %esp, %ebp
sysenter
popl %ebp
popl %edx
popl %ecx
????ret
?
執(zhí)行如下命令,先匯編后鏈接
gcc -m32 -c -o hello-syscall32.o hello-syscall32.S
ld -melf_i386 -entry _start hello-syscall32.o -o hello-syscall32
然后運行程序
./hello-syscall32
可以看到運行成功,命令行輸出了
Hello from int 0x80 !
Hello from sysenter !
從上面的匯編代碼示例中我們看到了用戶空間是如何調(diào)用系統(tǒng)調(diào)用的,這也正是libc中的做法。我們前面有個內(nèi)容沒有講,那就是執(zhí)行了系統(tǒng)調(diào)用指令,CPU是如何切換到特權(quán)模式的。其實前面的系統(tǒng)調(diào)用入口函數(shù)設(shè)置里面也在相應(yīng)的寄存器里面設(shè)置了__KERNEL_CS,這個會導(dǎo)致CPU切到特權(quán)模式來執(zhí)行。
?
上述代碼放到了github上:https://github.com/orangeboyye/hello-syscall
?
3.6?vsyscall與vdso
?
最剛開始的時候只有一種系統(tǒng)調(diào)用方式int 0x80,這時候libc都是直接使用這個指令。后來有個sysenter系統(tǒng)調(diào)用指令,libc就要考慮系統(tǒng)有沒有sysenter指令,有的話就用sysenter,沒有的話就用int 0x80。但是這對libc來說太難了,因此內(nèi)核想了一個辦法,把內(nèi)核的一個page設(shè)置為用戶空間可訪問的,叫做vsyscall,libc通過這個vsyscall來進行系統(tǒng)調(diào)用,就不用有那么復(fù)雜的考慮了。對于內(nèi)核來說,如果CPU支持sysenter并且內(nèi)核自己也支持sysenter,就把vsyscall設(shè)置為sysenter,否則就設(shè)置為int 0x80。這對內(nèi)核來說是一件非常簡單的事。后來人們發(fā)現(xiàn)可以把一些系統(tǒng)調(diào)用的函數(shù)放到vsyscall里面,如果獲取系統(tǒng)時間,這是一個只讀的操作,而且對系統(tǒng)沒有啥影響,放到vsyscall之后,libc就可以直接調(diào)用了,沒有額外的開銷。后來人們又覺得vsyscall的地址在內(nèi)核空間,而且vsyscall沒有一定的格式,這不太好。于是又開發(fā)了vdso,它是so的格式,在進程創(chuàng)建的時候映射到進程的地址空間,這樣進程就可以像使用so一樣使用vdso。再后來,64位的進程下只有一個系統(tǒng)調(diào)用指令,vsyscall的最初的作用就沒有了意義,所以64位進程下的vsyscall和vdso就沒有了系統(tǒng)調(diào)用指令兼容層的功能,就只剩下了可以直接調(diào)用一些系統(tǒng)調(diào)用函數(shù)的功能。
?
?
?四、總結(jié)回顧 ? ?內(nèi)核為了向用戶空間提供服務(wù),設(shè)計出了系統(tǒng)調(diào)用機制,系統(tǒng)調(diào)用機制可以讓用戶空間調(diào)用內(nèi)核里的某些特定的函數(shù)。要實現(xiàn)系統(tǒng)調(diào)用機制需要有CPU提供的特殊指令才行。由于歷史原因,系統(tǒng)調(diào)用指令在x86平臺上不止有一個。系統(tǒng)調(diào)用指令的作用是把CPU模式切換到特權(quán)模式、讓CPU跳到指定的入口函數(shù)來執(zhí)行,并把用戶空間提供的系統(tǒng)調(diào)用編號和參數(shù)傳遞進內(nèi)核。入口函數(shù)根據(jù)系統(tǒng)調(diào)用編號調(diào)用相應(yīng)的函數(shù)并傳遞參數(shù),執(zhí)行完畢后再返回用戶空間。
我們一般情況下并不會直接使用系統(tǒng)調(diào)用,操作系統(tǒng)為我們提供了非常豐富的API,用起來更方便。
?
審核編輯:湯梓紅
?
評論
查看更多