當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > Linux閱碼場(chǎng)
[導(dǎo)讀]我們?cè)诤芏鄷稀⒉┛蜕隙紝W(xué)過(guò)或者聽說(shuō)過(guò)系統(tǒng)調(diào)用與API這兩個(gè)概念,那么這兩個(gè)概念究竟是什么意思,它們之間是什么關(guān)系呢?如果我們閱讀過(guò)《操作系統(tǒng)導(dǎo)論》,就會(huì)明白操作系統(tǒng)的目的與作用,就會(huì)知道內(nèi)核是要向進(jìn)程提供服務(wù)的,那么內(nèi)核是如何向進(jìn)程提供服務(wù)的呢?下面我們就來(lái)一探究竟。


作者簡(jiǎn)介:

程磊,一線碼農(nóng),在某手機(jī)公司擔(dān)任系統(tǒng)開發(fā)工程師,閱碼場(chǎng)榮譽(yù)總編輯,日常喜歡研究?jī)?nèi)核基本原理。


目錄:

一、基本概念解析

1.1 系統(tǒng)調(diào)用的來(lái)源與作用

1.2 API的來(lái)源與作用

1.3 API與系統(tǒng)調(diào)用的關(guān)系

1.4 系統(tǒng)調(diào)用機(jī)制的基本原理

二、API的制定與實(shí)現(xiàn)

2.1 POSIX API

2.2 Windows API

2.3 API的實(shí)現(xiàn)

三、系統(tǒng)調(diào)用的實(shí)現(xiàn)

3.1 x86平臺(tái)的實(shí)現(xiàn)

3.2 指令基本原理

3.3系統(tǒng)調(diào)用編號(hào)

3.4系統(tǒng)調(diào)用入口函數(shù)

3.5匯編程序演示

3.6vsyscall與vdso

四、總結(jié)回顧



一、基本概念解析


我們?cè)诤芏鄷稀⒉┛蜕隙紝W(xué)過(guò)或者聽說(shuō)過(guò)系統(tǒng)調(diào)用與API這兩個(gè)概念,那么這兩個(gè)概念究竟是什么意思,它們之間是什么關(guān)系呢?如果我們閱讀過(guò)《操作系統(tǒng)導(dǎo)論》,就會(huì)明白操作系統(tǒng)的目的與作用,就會(huì)知道內(nèi)核是要向進(jìn)程提供服務(wù)的,那么內(nèi)核是如何向進(jìn)程提供服務(wù)的呢?下面我們就來(lái)一探究竟。

1.1 系統(tǒng)調(diào)用的來(lái)源與作用


我們先來(lái)看一下進(jìn)程的虛擬內(nèi)存空間布局,我們以32位為例,64位的邏輯也是一樣的。

可以看到一個(gè)進(jìn)程的內(nèi)存空間分為用戶空間和內(nèi)核空間兩部分。每個(gè)進(jìn)程都有自己獨(dú)立的用戶空間,但是所有進(jìn)程都共享同一個(gè)內(nèi)核空間,所以所有進(jìn)程都可以請(qǐng)求內(nèi)核的服務(wù)。不過(guò)內(nèi)核空間運(yùn)行在特權(quán)級(jí),用戶空間運(yùn)行在非特權(quán)級(jí),所以用戶空間是不能直接訪問(wèn)內(nèi)核空間的。為此,內(nèi)核向用戶空間提供了有限制的訪問(wèn),系統(tǒng)調(diào)用。用戶空間可以通過(guò)系統(tǒng)調(diào)用來(lái)調(diào)用內(nèi)核里一些特定的函數(shù)。這樣的話,進(jìn)程就可以通過(guò)系統(tǒng)調(diào)用來(lái)請(qǐng)求內(nèi)核的服務(wù)了。系統(tǒng)調(diào)用是如何實(shí)現(xiàn)的呢?這是需要硬件的特殊支持的,第三章節(jié)會(huì)講。

1.2 API的來(lái)源與作用


既然有了系統(tǒng)調(diào)用,進(jìn)程可以通過(guò)系統(tǒng)調(diào)用來(lái)請(qǐng)求內(nèi)核的服務(wù),那么為什么還會(huì)有API呢?因?yàn)橄到y(tǒng)調(diào)用是偏底層的,有很多細(xì)節(jié)要處理,而且不同的平臺(tái)其系統(tǒng)調(diào)用并不相同;就算是同一個(gè)平臺(tái),其提供的系統(tǒng)調(diào)用功能以及系統(tǒng)調(diào)用的實(shí)現(xiàn)方法都有可能會(huì)發(fā)生變化。因此為了屏蔽系統(tǒng)調(diào)用的各種細(xì)節(jié),增加通用性和跨平臺(tái)性,操作系統(tǒng)又向用戶進(jìn)程提供了API。API,Application Programming Interface,應(yīng)用程序編程接口,它的意思就是它的字面意思,就是指操作系統(tǒng)向應(yīng)用程序提供的編程接口?,F(xiàn)實(shí)中有很多人把API當(dāng)做I(Interface)接口的意思來(lái)用,本文所說(shuō)的API都是指它的本意。有了API你就不用考慮系統(tǒng)調(diào)用了,無(wú)論在任何平臺(tái)、任何OS,你只管使用API,只要它們的API是相同的,你的源碼就是兼容的、跨平臺(tái)的。


1.3 API與系統(tǒng)調(diào)用的關(guān)系


API和系統(tǒng)調(diào)用具體是什么關(guān)系呢?系統(tǒng)調(diào)用是偏底層、偏實(shí)現(xiàn)的,API是偏上層、偏接口的。系統(tǒng)調(diào)用是實(shí)現(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)委員會(huì)來(lái)制定和發(fā)展API。API的實(shí)現(xiàn)是在用戶空間庫(kù)里面,一般都是在libc中實(shí)現(xiàn)。API的底層實(shí)現(xiàn)一般使用的是系統(tǒng)調(diào)用,很多API和系統(tǒng)調(diào)用是一對(duì)一關(guān)系。但也有特殊情況,比如有的API并不使用系統(tǒng)調(diào)用,有的系統(tǒng)調(diào)用沒(méi)有對(duì)應(yīng)的API,有的API可能調(diào)用了多個(gè)系統(tǒng)調(diào)用,有的系統(tǒng)調(diào)用可能被多個(gè)API使用。也就是說(shuō)大部分情況下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,而且它們的名字也相同時(shí),我們不能把它們看做是同一個(gè)事物,而應(yīng)當(dāng)把它們看做不同的事物,只不過(guò)是名字相同而已,是同名的API使用了同名的系統(tǒng)調(diào)用。就好比有兩種情況,第一種情況是,有兩個(gè)人都叫張偉,一個(gè)是副市長(zhǎng),一個(gè)是公安局局長(zhǎng),張偉副市長(zhǎng)安排張偉局長(zhǎng)去做某件事情。第二種情況是,有一個(gè)人叫張偉,他是副市長(zhǎng)兼任公安局局長(zhǎng),張偉副市長(zhǎng)兼局長(zhǎng)去做某件事情。這兩種情況是不一樣的,同名的API與系統(tǒng)調(diào)用的關(guān)系類似于前者。


下面我們舉例來(lái)說(shuō)明一下API與系統(tǒng)調(diào)用的關(guān)系。我們來(lái)寫一個(gè)最簡(jiǎn)單的hello world程序,代碼如下。

#include #include  int main(int argc, char *argv[]){ char str[] = "hello, world\n"; write(1, str, strlen(str));} 

編譯:gcc -o hello hello.c

運(yùn)行:./hello

會(huì)在屏幕上輸出 hello, world。


這個(gè)程序非常簡(jiǎn)單,我們調(diào)用了兩個(gè)API(strlen 和 write),在屏幕上輸出了一行文字。同樣是API,strlen沒(méi)有使用系統(tǒng)調(diào)用,自己直接在用戶空間就把功能實(shí)現(xiàn)了,而write API則使用了write系統(tǒng)調(diào)用。有些API的功能比較簡(jiǎn)單,自己在用戶空間就能實(shí)現(xiàn),沒(méi)必要麻煩內(nèi)核。但是有些API的功能在用戶空間是不可能實(shí)現(xiàn)或者很難實(shí)現(xiàn)的,必須要求助于內(nèi)核。我們把write API與write系統(tǒng)調(diào)用畫成圖,如下所示:


API函數(shù)通過(guò)系統(tǒng)調(diào)用機(jī)制調(diào)用系統(tǒng)調(diào)用函數(shù)。那么系統(tǒng)調(diào)用機(jī)制要做的事情有哪些呢?有兩件事,一是實(shí)現(xiàn)CPU特權(quán)級(jí)的轉(zhuǎn)變,把CPU設(shè)置為特權(quán)模式之后才能執(zhí)行內(nèi)核的代碼。二是傳遞系統(tǒng)調(diào)用的編號(hào)和函數(shù)參數(shù),系統(tǒng)調(diào)用函數(shù)有很多,怎么知道你想調(diào)用的是哪個(gè)系統(tǒng)調(diào)用函數(shù)呢,通過(guò)編號(hào)來(lái)區(qū)分。系統(tǒng)調(diào)用函數(shù)大部分都是有參數(shù)的,所以還需要傳遞參數(shù),參數(shù)怎么傳遞是和具體硬件相關(guān)的,由相應(yīng)的ABI來(lái)規(guī)定。


1.4 系統(tǒng)調(diào)用機(jī)制的基本原理


那么系統(tǒng)調(diào)用機(jī)制該怎么實(shí)現(xiàn)呢?答案是要靠CPU提供的特殊指令(系統(tǒng)調(diào)用指令)來(lái)實(shí)現(xiàn),雖然不同架構(gòu)的CPU實(shí)現(xiàn)不盡相同,但是大概模式都是一樣的,都是往某個(gè)寄存器寫入系統(tǒng)調(diào)用編號(hào),在約定的寄存器或者棧上寫入?yún)?shù),然后調(diào)用特殊指令(系統(tǒng)調(diào)用指令),此時(shí)CPU就會(huì)切換到特權(quán)模式并進(jìn)入內(nèi)核執(zhí)行一段預(yù)先設(shè)定的代碼(系統(tǒng)調(diào)用入口函數(shù)),這段代碼會(huì)根據(jù)系統(tǒng)調(diào)用編號(hào)調(diào)用相應(yīng)的系統(tǒng)調(diào)用函數(shù)。畫成圖如下所示:
可以看出完成一個(gè)系統(tǒng)調(diào)用有兩個(gè)關(guān)鍵點(diǎn),一是系統(tǒng)調(diào)用編號(hào)要能對(duì)應(yīng)上,二是系統(tǒng)調(diào)用入口函數(shù)要提前設(shè)置好。這樣系統(tǒng)調(diào)用入口函數(shù)才能根據(jù)系統(tǒng)調(diào)用編號(hào)找到正確的系統(tǒng)調(diào)用函數(shù)。

需要說(shuō)明的是,一個(gè)平臺(tái)提供的系統(tǒng)調(diào)用指令不一定只有一個(gè),不同系統(tǒng)調(diào)用指令對(duì)應(yīng)的系統(tǒng)調(diào)用入口函數(shù)也不相同,這個(gè)第三章會(huì)詳細(xì)講解。



二、API的制定和實(shí)現(xiàn)


最開始的時(shí)候是沒(méi)有操作系統(tǒng)的,后來(lái)逐漸產(chǎn)生了操作系統(tǒng)。操作系統(tǒng)對(duì)應(yīng)用程序提供的用戶空間接口就叫做API(應(yīng)用程序編程接口)。API剛開始是看著缺啥就添啥,沒(méi)有一定的標(biāo)準(zhǔn)。后來(lái)隨著操作系統(tǒng)的發(fā)展,再野蠻生長(zhǎng)就不行了,于是就有了操作系統(tǒng)API標(biāo)準(zhǔn)規(guī)范。不同的操作系統(tǒng),它們的API并不相同,API的制定與維護(hù)方法也不相同。

2.1 POSIX API


UNIX操作系統(tǒng)家族的API叫做POSIX(Portable Operating System Interface)。POSIX是IEEE制定的規(guī)范,POSIX這個(gè)名字是GNU的倡導(dǎo)者Richard Stallman建議的,按照當(dāng)時(shí)的命名習(xí)慣在最后加了個(gè)X。UNIX還有另外一個(gè)規(guī)范叫做Single UNIX Specification,簡(jiǎn)稱SUS,是由Open Group發(fā)布的。后來(lái)POSIX和SUS合并開發(fā),內(nèi)容一樣,但是對(duì)外還是用兩個(gè)名字。想要了解POSIX S最新的標(biāo)準(zhǔn),請(qǐng)查看網(wǎng)站https://unix.org/online.html。


Linux本身僅僅是個(gè)內(nèi)核,并不是個(gè)操作系統(tǒng)。GNU/Linux或者Linux發(fā)行版才是個(gè)完整的操作系統(tǒng)。Linux發(fā)行版都遵循POSIX API。網(wǎng)站https://man7.org 和書籍《The Linux Programming Interface》非常全面詳細(xì)地介紹了POSIX API的語(yǔ)義以及它在Linux上的一些實(shí)現(xiàn)情況,非常值得大家認(rèn)真學(xué)習(xí)或者經(jīng)常查閱。

2.2 Windows API


Windows的API在16位的時(shí)候叫做Windows API。后來(lái)到了32位的時(shí)候,重新設(shè)計(jì)了API,由于16的API和32位的API差別非常大,所以就重新命名為Win32 API。到了64位的時(shí)候,API基本沒(méi)啥變化,就是把有些參數(shù)從32位提升到了64位,所以64位的Windows的API也依然被人們叫做Win32 API。當(dāng)然64位的API也被Windows命名為Windows API,因?yàn)?6位Windows早已成為歷史,這么命名也不會(huì)引起歧義?,F(xiàn)在Windows API和Win32 API幾乎是同義詞,區(qū)別不大。


由于Windows操作系統(tǒng)是微軟一家的閉源產(chǎn)品,所以它的API規(guī)范是由公司制定的。這和POSIX是由標(biāo)準(zhǔn)委員會(huì)制定的是不一樣的。


2.3 API的實(shí)現(xiàn)


API本身僅僅是個(gè)規(guī)范,是個(gè)概念性的東西,它具體是怎么實(shí)現(xiàn)的呢?目前業(yè)界都是把API放在libc里面來(lái)實(shí)現(xiàn)的。所以libc里面不僅有C標(biāo)準(zhǔn)庫(kù)的實(shí)現(xiàn),還有操作系統(tǒng)API的實(shí)現(xiàn)。所以大家不能認(rèn)為libc就是一個(gè)普通的lib,它的作用是非常重要的,沒(méi)有l(wèi)ibc,幾乎所有的進(jìn)程都無(wú)法運(yùn)行,libc是進(jìn)程通向內(nèi)核的必經(jīng)之路。幾乎所有的進(jìn)程都鏈接了libc,大部分都是動(dòng)態(tài)鏈接的,通過(guò)ldd命令可以查到,通過(guò)/proc/$pid/maps也可以查到;少部分是靜態(tài)鏈接libc的,是查不到libc.so的,但是程序本身還是包含libc的代碼的。當(dāng)然你也可以自己調(diào)用系統(tǒng)調(diào)用就不用libc,一般只有演示程序會(huì)這么做。


Libc在不同操作系統(tǒng)上的實(shí)現(xiàn)是不同的,在同一個(gè)操作系統(tǒng)也可能有多個(gè)不同的實(shí)現(xiàn)。Linux發(fā)行版上最流行的libc實(shí)現(xiàn)是Glibc,Android上的libc實(shí)現(xiàn)是bionic。



三、系統(tǒng)調(diào)用的實(shí)現(xiàn)

系統(tǒng)調(diào)用機(jī)制的實(shí)現(xiàn)原理都是相同的,但是不同操作系統(tǒng)、不同硬件平臺(tái)上的實(shí)現(xiàn)細(xì)節(jié)又不盡相同。下面我們分別來(lái)講一下Linux在x86平臺(tái)和arm平臺(tái)上實(shí)現(xiàn)細(xì)節(jié)。

3.1 x86平臺(tái)的實(shí)現(xiàn)


X86平臺(tái)的系統(tǒng)調(diào)用的實(shí)現(xiàn)方法經(jīng)歷了三代的變遷,每次改變都提高了系統(tǒng)調(diào)用的執(zhí)行效率。第一代系統(tǒng)調(diào)用指令,借用了中斷機(jī)制的指令,int 0x80、iret。第二代系統(tǒng)調(diào)用指令sysenter、sysexit。第三代系統(tǒng)調(diào)用指令syscall、sysret。三代指令在內(nèi)核中的使用情況如下圖所示:

下面我們分別講一下這三代指令的基本原理。

3.2 指令基本原理


第一代系統(tǒng)調(diào)用指令使用的是中斷指令,基本原理如下。中斷發(fā)生時(shí),CPU會(huì)切換到特權(quán)模式并跳到內(nèi)核執(zhí)行預(yù)先指定的一段程序。執(zhí)行哪段程序呢,要根據(jù)中斷源來(lái)決定,不同的中斷源執(zhí)行不同的程序,每個(gè)中斷源都對(duì)應(yīng)一個(gè)整數(shù)來(lái)標(biāo)識(shí)自己,這個(gè)整數(shù)就叫做中斷向量。中斷源有三類,外設(shè)中斷、CPU異常、指令中斷,前兩種都有自己的方法來(lái)指定中斷向量,指令中斷是在指令的操作數(shù)里面指定中斷向量號(hào)的。我們的系統(tǒng)調(diào)用就是利用指令中斷,用向量號(hào)0x80,也就是十進(jìn)制的128當(dāng)做自己的中斷向量,來(lái)執(zhí)行系統(tǒng)調(diào)用的。我們?cè)谟脩艨臻g,先把系統(tǒng)調(diào)用編號(hào)賦值給寄存器EAX,然后執(zhí)行int 0x80,CPU就會(huì)跳轉(zhuǎn)到內(nèi)核執(zhí)行內(nèi)核預(yù)先設(shè)定的中斷處理程序(也就是系統(tǒng)調(diào)用入口函數(shù))。系統(tǒng)調(diào)用入口函數(shù)根據(jù)EAX的值調(diào)用對(duì)應(yīng)的系統(tǒng)調(diào)用函數(shù)。系統(tǒng)調(diào)用函數(shù)執(zhí)行完成之后返回系統(tǒng)調(diào)用入口函數(shù),入口函數(shù)再執(zhí)行iret返回到用戶空間,一個(gè)系統(tǒng)調(diào)用就完成了。


第二代系統(tǒng)調(diào)用指令sysenter/sysexit,由于通過(guò)中斷流程進(jìn)行系統(tǒng)調(diào)用開銷太大了,很多操作對(duì)系統(tǒng)調(diào)用來(lái)說(shuō)又是沒(méi)有意義的,因此Intel專門開發(fā)了只用于系統(tǒng)調(diào)用的指令。由于sysenter是專用指令,它可以把很多中斷相關(guān)的操作都省略掉,具體來(lái)說(shuō)有以下幾點(diǎn),1.不再自動(dòng)把寄存器信息保存到內(nèi)核棧上,2.不再自動(dòng)從內(nèi)核棧上加載esp的值,3.不再走中斷處理流程。


使用sysenter指令需要提前設(shè)置一些MSR寄存器,具體來(lái)說(shuō)要做以下一些設(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í)行時(shí)CPU就會(huì)切換到特權(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還要快一些,因?yàn)?.它不再保存和恢復(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í)行的時(shí)候會(huì)把MSR IA32_STAR的47:32位加載到CS和SS,把MSR IA32_LSTR的值加載到RIP。在執(zhí)行sysret之前把要返回到的用戶空間指令的地址寫入RCX,sysret執(zhí)行時(shí)會(huì)把MSR IA32_STAR的63:48位加載到CS和SS,把RCX加載到RIP。


3.3 系統(tǒng)調(diào)用編號(hào)


我們先來(lái)解決第一個(gè)問(wèn)題,系統(tǒng)調(diào)用編號(hào)是怎么確定的。不同架構(gòu)不同位數(shù)的系統(tǒng),系統(tǒng)調(diào)用編號(hào)是不一樣的。如果用戶空間傳遞的系統(tǒng)調(diào)用編號(hào)和內(nèi)核里的系統(tǒng)調(diào)用編號(hào)對(duì)不上,那問(wèn)題就嚴(yán)重了。Linux內(nèi)核在編譯時(shí)會(huì)生成一個(gè)文件,arch/x86/include/generated/uapi/asm/unistd_64.h,這個(gè)文件是生成的,不是本來(lái)就有的,這個(gè)文件里面有所有系統(tǒng)調(diào)用的編號(hào)。在安裝操作系統(tǒng)時(shí)或者單獨(dú)安裝內(nèi)核和內(nèi)核頭文件時(shí),這個(gè)文件會(huì)被安裝在/usr/include/asm/unistd_64.h,libc會(huì)使用這個(gè)文件,這樣用戶空間傳遞的編號(hào)和內(nèi)核里面的系統(tǒng)調(diào)用編號(hào)就是一致的了。


3.4 系統(tǒng)調(diào)用入口函數(shù)


下面我們來(lái)說(shuō)說(shuō)系統(tǒng)調(diào)用入口函數(shù)是怎么設(shè)置的。X86_64對(duì)于64位的進(jìn)程來(lái)說(shuō)只有一個(gè)系統(tǒng)調(diào)用指令,就是syscall,它的入口函數(shù)在linux-src/arch/x86/entry/entry_64.S, 函數(shù)名叫entry_SYSCALL_64。對(duì)于32位的進(jìn)程來(lái)說(shuō)有三個(gè)系統(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è)置它們的代碼在兩個(gè)地方,syscall(64)、syscall(32)、sysenter 這三個(gè)設(shè)置在一個(gè)地方,在文件linux-src/arch/x86/kernel/cpu/common.c中的函數(shù) syscall_init


#ifdef CONFIG_X86_64 void syscall_init(void){ wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS); wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64); #ifdef CONFIG_IA32_EMULATION 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);#else 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);#endif  /* * 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);} #else /* CONFIG_X86_64 */......#endif /* CONFIG_X86_64 */


從代碼中可以看出只有在64位的情況下才會(huì)設(shè)置syscall指令的入口函數(shù),只有在系統(tǒng)兼容32位進(jìn)程(CONFIG_IA32_EMULATION)的情況下才會(huì)設(shè)置syscall(32)、sysenter的兼容入口函數(shù)。大部分linux發(fā)行版都支持32位進(jìn)程兼容。


兼容int 0x80的代碼設(shè)置在另外一個(gè)地方,因?yàn)閕nt 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), #ifdef CONFIG_X86_32 TSKG(X86_TRAP_DF,    GDT_ENTRY_DOUBLEFAULT_TSS),#else ISTG(X86_TRAP_DF,    asm_exc_double_fault, IST_INDEX_DF),#endif ISTG(X86_TRAP_DB,    asm_exc_debug, IST_INDEX_DB), #ifdef CONFIG_X86_MCE ISTG(X86_TRAP_MC,    asm_exc_machine_check, IST_INDEX_MCE),#endif #ifdef CONFIG_AMD_MEM_ENCRYPT ISTG(X86_TRAP_VC,    asm_exc_vmm_communication, IST_INDEX_VC),#endif  SYSG(X86_TRAP_OF,    asm_exc_overflow),#if defined(CONFIG_IA32_EMULATION) SYSG(IA32_SYSCALL_VECTOR,  entry_INT80_compat),#elif defined(CONFIG_X86_32) SYSG(IA32_SYSCALL_VECTOR,  entry_INT80_32),#endif};void __init idt_setup_traps(void){ idt_setup_from_table(idt_table, def_idts, ARRAY_SIZE(def_idts), true);}


從代碼中可以看出,只有系統(tǒng)支持32位進(jìn)程兼容(CONFIG_IA32_EMULATION)才會(huì)去設(shè)置entry_INT80_compat。


我們?cè)O(shè)置好了這些系統(tǒng)調(diào)用指令的入口函數(shù)之后,當(dāng)用戶空間調(diào)用這些指令的時(shí)候就會(huì)調(diào)用這些函數(shù)。那么這些函數(shù)又是怎樣去調(diào)用具體對(duì)應(yīng)的系統(tǒng)調(diào)用函數(shù)呢?我們以64位進(jìn)程的syscall指令為例來(lái)看一看。先看它的入口函數(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 */ sysretqSYM_CODE_END(entry_SYSCALL_64)


我們對(duì)代碼做了精簡(jiǎn)只留下最關(guān)鍵的??梢钥吹胶瘮?shù)先把__USER_DS和__USER_CS都push到了棧上,這是為了執(zhí)行最后面的那條sysretq時(shí)可以返回用戶空間把特權(quán)級(jí)也轉(zhuǎn)為用戶級(jí)。函數(shù)的主體就是調(diào)用函數(shù)do_syscall_64,我們?cè)賮?lái)看一個(gè)這個(gè)函數(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ù)用戶空間傳來(lái)的系統(tǒng)調(diào)用編號(hào)在sys_call_table數(shù)組中調(diào)用相應(yīng)的函數(shù)。那么這個(gè)sys_call_table數(shù)組是怎么來(lái)的呢?它是在文件linux-5.15.28/arch/x86/entry/syscall_64.c中定義的,如下:


#include #include #include #include #include  #define __SYSCALL(nr, sym) extern long __x64_##sym(const struct pt_regs *);#include #undef __SYSCALL #define __SYSCALL(nr, sym) __x64_##sym, asmlinkage const sys_call_ptr_t sys_call_table[] = {#include <asm/syscalls_64.h>};


那么syscalls_64.h的內(nèi)容是什么,它是怎么來(lái)的呢?這個(gè)文件并不是手寫的,而是在編譯時(shí)由腳本生成的,它是根據(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)


對(duì)syscall_64.c進(jìn)行預(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,};


也就是說(shuō)這是由一堆函數(shù)名構(gòu)成的函數(shù)指針數(shù)組,那么這些函數(shù)名是怎么生成的呢?它是由一系列的SYSCALL_DEFINEx宏生成的,x代表函數(shù)的參數(shù)個(gè)數(shù)。我們以open系統(tǒng)調(diào)用來(lái)講解一下,open系統(tǒng)調(diào)用的實(shí)現(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);}


可以看出這個(gè)宏會(huì)生成函數(shù)__x64_sys_open,這個(gè)函數(shù)正好是sys_call_table數(shù)組里面的函數(shù)名。__x64_sys_open接受的參數(shù)是一個(gè)寄存器集的指針,然后提取寄存器中的值再調(diào)用函數(shù)__se_sys_open,函數(shù)__se_sys_open對(duì)參數(shù)進(jìn)行強(qiáng)轉(zhuǎn)再調(diào)用__do_sys_open,這個(gè)函數(shù)是最終的函數(shù)。我們可以看到這里面還生成了函數(shù)__ia32_sys_open,這個(gè)函數(shù)是32位進(jìn)程兼容的系統(tǒng)調(diào)用所使用的數(shù)組ia32_sys_call_table的成員。


3.5 匯編程序演示


下面我們用匯編語(yǔ)言來(lái)試一試執(zhí)行系統(tǒng)調(diào)用,一般情況下我們都不會(huì)直接使用系統(tǒng)調(diào)用指令,下面的例子僅僅是為了演示,標(biāo)準(zhǔn)編程中請(qǐng)使用API。


.data msg: .ascii "Hello from syscall !\n" 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
然后運(yùn)行程序
./hello-syscall64
可以看到運(yùn)行成功,命令行輸出了 Hello from syscall !

下面我們?cè)賮?lái)演示一下32位進(jìn)程兼容模式的系統(tǒng)調(diào)用,匯編代碼如下:


.datamsg1: .ascii "Hello from int 0x80 !\n" len1 = . - msg1msg2: .ascii "Hello from sysenter !\n" 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
然后運(yùn)行程序
./hello-syscall32
可以看到運(yùn)行成功,命令行輸出了
Hello from int 0x80 !
Hello from sysenter !

從上面的匯編代碼示例中我們看到了用戶空間是如何調(diào)用系統(tǒng)調(diào)用的,這也正是libc中的做法。我們前面有個(gè)內(nèi)容沒(méi)有講,那就是執(zhí)行了系統(tǒng)調(diào)用指令,CPU是如何切換到特權(quán)模式的。其實(shí)前面的系統(tǒng)調(diào)用入口函數(shù)設(shè)置里面也在相應(yīng)的寄存器里面設(shè)置了__KERNEL_CS,這個(gè)會(huì)導(dǎo)致CPU切到特權(quán)模式來(lái)執(zhí)行。


上述代碼放到了github上:https://github.com/orangeboyye/hello-syscall


3.6 vsyscall與vdso


最剛開始的時(shí)候只有一種系統(tǒng)調(diào)用方式int 0x80,這時(shí)候libc都是直接使用這個(gè)指令。后來(lái)有個(gè)sysenter系統(tǒng)調(diào)用指令,libc就要考慮系統(tǒng)有沒(méi)有sysenter指令,有的話就用sysenter,沒(méi)有的話就用int 0x80。但是這對(duì)libc來(lái)說(shuō)太難了,因此內(nèi)核想了一個(gè)辦法,把內(nèi)核的一個(gè)page設(shè)置為用戶空間可訪問(wèn)的,叫做vsyscall,libc通過(guò)這個(gè)vsyscall來(lái)進(jìn)行系統(tǒng)調(diào)用,就不用有那么復(fù)雜的考慮了。對(duì)于內(nèi)核來(lái)說(shuō),如果CPU支持sysenter并且內(nèi)核自己也支持sysenter,就把vsyscall設(shè)置為sysenter,否則就設(shè)置為int 0x80。這對(duì)內(nèi)核來(lái)說(shuō)是一件非常簡(jiǎn)單的事。后來(lái)人們發(fā)現(xiàn)可以把一些系統(tǒng)調(diào)用的函數(shù)放到vsyscall里面,如果獲取系統(tǒng)時(shí)間,這是一個(gè)只讀的操作,而且對(duì)系統(tǒng)沒(méi)有啥影響,放到vsyscall之后,libc就可以直接調(diào)用了,沒(méi)有額外的開銷。后來(lái)人們又覺(jué)得vsyscall的地址在內(nèi)核空間,而且vsyscall沒(méi)有一定的格式,這不太好。于是又開發(fā)了vdso,它是so的格式,在進(jìn)程創(chuàng)建的時(shí)候映射到進(jìn)程的地址空間,這樣進(jìn)程就可以像使用so一樣使用vdso。再后來(lái),64位的進(jìn)程下只有一個(gè)系統(tǒng)調(diào)用指令,vsyscall的最初的作用就沒(méi)有了意義,所以64位進(jìn)程下的vsyscall和vdso就沒(méi)有了系統(tǒng)調(diào)用指令兼容層的功能,就只剩下了可以直接調(diào)用一些系統(tǒng)調(diào)用函數(shù)的功能。



四、總結(jié)回顧


內(nèi)核為了向用戶空間提供服務(wù),設(shè)計(jì)出了系統(tǒng)調(diào)用機(jī)制,系統(tǒng)調(diào)用機(jī)制可以讓用戶空間調(diào)用內(nèi)核里的某些特定的函數(shù)。要實(shí)現(xiàn)系統(tǒng)調(diào)用機(jī)制需要有CPU提供的特殊指令才行。由于歷史原因,系統(tǒng)調(diào)用指令在x86平臺(tái)上不止有一個(gè)。系統(tǒng)調(diào)用指令的作用是把CPU模式切換到特權(quán)模式、讓CPU跳到指定的入口函數(shù)來(lái)執(zhí)行,并把用戶空間提供的系統(tǒng)調(diào)用編號(hào)和參數(shù)傳遞進(jìn)內(nèi)核。入口函數(shù)根據(jù)系統(tǒng)調(diào)用編號(hào)調(diào)用相應(yīng)的函數(shù)并傳遞參數(shù),執(zhí)行完畢后再返回用戶空間。

我們一般情況下并不會(huì)直接使用系統(tǒng)調(diào)用,操作系統(tǒng)為我們提供了非常豐富的API,用起來(lái)更方便。

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車。 SODA V工具的開發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱,數(shù)字世界的話語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉