這張圖畫了挺久的,主要是想讓大家可以從全局角度,看下linux內(nèi)核中系統(tǒng)調(diào)用的實現(xiàn)。
在講具體的細節(jié)之前,我們先根據(jù)上圖,從整體上看一下系統(tǒng)調(diào)用的實現(xiàn)。
系統(tǒng)調(diào)用的實現(xiàn)基礎(chǔ),其實就是兩條匯編指令,分別是syscall和sysret。
syscall使執(zhí)行邏輯從用戶態(tài)切換到內(nèi)核態(tài),在進入到內(nèi)核態(tài)之后,cpu會從 MSR_LSTAR 寄存器中,獲取處理系統(tǒng)調(diào)用內(nèi)核代碼的起始地址,即上面的 entry_SYSCALL_64。
在執(zhí)行 entry_SYSCALL_64 函數(shù)時,內(nèi)核代碼會根據(jù)約定,先從rax寄存器中獲取想要執(zhí)行的系統(tǒng)調(diào)用的編號,然后根據(jù)該編號從sys_call_table數(shù)組中找到對應(yīng)的系統(tǒng)調(diào)用函數(shù)。
接著,從 rdi, rsi, rdx, r10, r8, r9 寄存器中獲取該系統(tǒng)調(diào)用函數(shù)所需的參數(shù),然后調(diào)用該函數(shù),把這些參數(shù)傳入其中。
在系統(tǒng)調(diào)用函數(shù)執(zhí)行完畢之后,執(zhí)行結(jié)果會被放到rax寄存器中。
最后,執(zhí)行sysret匯編指令,從內(nèi)核態(tài)切換回用戶態(tài),用戶程序繼續(xù)執(zhí)行。
如果用戶程序需要該系統(tǒng)調(diào)用的返回結(jié)果,則從rax中獲取。
總體流程就是這樣,相對來說,還是比較簡單的,主要就是先去理解syscall和sysret這兩條匯編指令,在理解這兩條匯編指令的基礎(chǔ)上,再去看內(nèi)核源碼,就會容易很多。
有關(guān)syscall和sysret指令的詳細介紹,請參考Intel 64 and IA-32 Architectures Software Developer’s Manual。
有了上面對系統(tǒng)調(diào)用的整理理解,我們接下來看下其具體的實現(xiàn)細節(jié)。
以write系統(tǒng)調(diào)用為例,其對應(yīng)的內(nèi)核源碼為:
在內(nèi)核中,所有的系統(tǒng)調(diào)用函數(shù)都是通過 SYSCALL_DEFINE 等宏定義的,比如上面的write函數(shù),使用的是 SYSCALL_DEFINE3。
將該宏展開后,我們可以得到如下的函數(shù)定義:
由上可見,SYSCALL_DEFINE3宏展開后為三個函數(shù),其中只有__x64_sys_write是外部可訪問的,其它兩個都有被static修飾,不能被外部訪問,所以注冊到上文中提到的sys_call_table數(shù)組里的函數(shù),應(yīng)該就是這個函數(shù)。
那該函數(shù)是怎么注冊到這個數(shù)組的呢?
我們先不說答案,先來看下sys_call_table數(shù)組的定義:
由上可見,該數(shù)組各元素的默認值都是 __x64_sys_ni_syscall:
該函數(shù)也非常簡單,就是直接返回錯誤碼-ENOSYS,表示系統(tǒng)調(diào)用非法。
sys_call_table數(shù)組定義的地方好像只設(shè)置了默認值,并沒有設(shè)置真正的系統(tǒng)調(diào)用函數(shù)。
我們再看看其他地方,看是否有代碼會注冊真正的系統(tǒng)調(diào)用函數(shù)到sys_call_table數(shù)組里。
可惜,并沒有。
這就奇怪了,那各系統(tǒng)調(diào)用函數(shù)到底是在哪里注冊的呢?
我們再回頭仔細看下sys_call_table數(shù)組的定義,它在設(shè)置完默認值之后,后面還include了一個名為asm/syscalls_64.h的頭文件,這個位置include頭文件還是比較奇怪的,我們看下它里面是什么內(nèi)容。
但是,這個文件居然不存在。
那我們只能初步懷疑這個頭文件是編譯時生成的,帶著這個疑問,我們?nèi)ニ阉飨嚓P(guān)內(nèi)容,確實發(fā)現(xiàn)了一些線索:
這個文件確實是編譯時生成的,上面的makefile中使用了syscalltbl.sh腳本和syscall_64.tbl模板文件來生成這個syscalls_64.h頭文件。
我們來看下syscall_64.tbl模板文件的內(nèi)容:
這里確實定義了write系統(tǒng)調(diào)用,且標明了它的編號是1。
我們再來看下生成的syscalls_64.h頭文件:
這里面定義了很多好像宏調(diào)用一樣的東西。
__SYSCALL_COMMON,這個不就是sys_call_table數(shù)組定義那里define的那個宏嘛。
再去上面看下__SYSCALL_COMMON這個宏定義,它的作用是將sym表示的函數(shù)賦值到sys_call_table數(shù)組的nr下標處。
所以對于__SYSCALL_COMMON(1, sys_write)來說,它就是注冊__x64_sys_write函數(shù)到sys_call_table數(shù)組下標為1的槽位處。
而這個__x64_sys_write函數(shù),正是我們上面猜測的,SYSCALL_DEFINE3定義的write系統(tǒng)調(diào)用,展開之后的一個外部可訪問的函數(shù)。
這樣就豁然開朗了,原來真正的系統(tǒng)調(diào)用函數(shù)的注冊,是通過先定義__SYSCALL_COMMON宏,再include那個根據(jù)syscall_64.tbl模板生成的syscalls_64.h頭文件來完成的,非常巧妙。
系統(tǒng)調(diào)用函數(shù)注冊到sys_call_table數(shù)組的過程,到這里已經(jīng)非常清楚了。
下面我們繼續(xù)來看下哪里在使用這個數(shù)組:
do_syscall_64在使用,方式是先通過nr在sys_call_table數(shù)組中找到對應(yīng)的系統(tǒng)調(diào)用函數(shù),然后再調(diào)用該函數(shù),將regs傳入其中。
這個流程和我們上面預(yù)估的一樣,且傳入的regs參數(shù)類型,和我們上面注冊的系統(tǒng)調(diào)用函數(shù)所需的類型也一樣。
那也就是說,regs參數(shù)的字段里,是帶著各系統(tǒng)調(diào)用函數(shù)所需的參數(shù)的,SYSCALL_DEFINE等宏展開出來的一系列函數(shù),會從這些字段中提取出真正的參數(shù),然后對其進行類型轉(zhuǎn)換,最后這些參數(shù)被傳入到最終的系統(tǒng)調(diào)用函數(shù)中。
對于上面的write系統(tǒng)調(diào)用宏展開后的那些函數(shù),__x64_sys_write會先從regs中提取出di, si, dx字段作為真正參數(shù),然后__se_sys_write會將這些參數(shù)轉(zhuǎn)成正確的類型,最后__do_sys_write函數(shù)被調(diào)用,轉(zhuǎn)換后的這些參數(shù)被傳入其中。
在系統(tǒng)調(diào)用函數(shù)執(zhí)行完畢后,其結(jié)果會被賦值到了regs的ax字段里。
由上可見,系統(tǒng)調(diào)用函數(shù)的參數(shù)及返回值的傳遞,都是通過regs來完成的。
但文章開始的時候不是說,系統(tǒng)調(diào)用的參數(shù)及返回值的傳遞,是通過寄存器來完成的嗎,這里怎么是通過struct pt_regs的字段呢?
先別急,先來看下struct pt_regs的定義:
你有沒有發(fā)現(xiàn),這里面的字段名都是寄存器的名字。
那是不是說,在執(zhí)行系統(tǒng)調(diào)用的代碼里,有邏輯把各寄存器里的值放到了這個結(jié)構(gòu)體的對應(yīng)字段里,在結(jié)束系統(tǒng)調(diào)用時,這些字段里的值又被賦值到各個對應(yīng)的寄存器里呢?
離真相越來越近。
我們繼續(xù)看使用了do_syscall_64的地方:
上圖中的entry_SYSCALL_64方法,就是系統(tǒng)調(diào)用流程中最重要的一個方法了,為了便于理解,我對該方法做了很多修改,并添加了很多注釋。
這里需要注意的是100行到121行這段邏輯,它將各寄存器的值壓入到棧中,以此來構(gòu)建struct pt_regs對象。
這就能構(gòu)建出一個struct pt_regs對象了?
是的。
我們回上面看下struct pt_regs的定義,看其字段名字及順序是不是和這里的壓棧順序正好相反。
我們再想下,當(dāng)我們要構(gòu)建一個struct pt_regs對象時,我們要為其在內(nèi)存中分配一塊空間,然后用一個地址來指向這段空間,這個地址就是該struct pt_regs對象的指針,這里需要注意的是,這個指針里存放的地址,是這段內(nèi)存空間的最小地址。
再看上面的壓棧過程,每一次壓棧操作我們都可以認為是在分配內(nèi)存空間并賦值,當(dāng)r15被最終壓入到棧中后,整個內(nèi)存空間分配完畢,且數(shù)據(jù)也初始化完畢,此時,rsp指向的棧頂?shù)刂罚褪沁@段內(nèi)存空間的最小地址,因為壓棧過程中,棧頂?shù)牡刂肥且恢痹谧冃〉摹?/p>
綜上可知,在壓棧完畢后,rsp里的地址就是一個struct pt_regs對象的地址,即該對象的指針。
在構(gòu)建完struct pt_regs對象后,123行將rax中存放的系統(tǒng)調(diào)用編號賦值到了rdx里,124行將rsp里存放的struct pt_regs對象的地址,即該對象的指針,賦值到了rsi中,接著后面執(zhí)行了call指令,來調(diào)用do_syscall_64方法。
調(diào)用do_syscall_64方法之前,對rdi和rsi的賦值,是為了遵守c calling convention,因為在該calling convention中約定,在調(diào)用c方法時,第一個參數(shù)要放到rdi里,第二個參數(shù)要放到rsi里。
我們再去上面看下do_syscall_64方法的定義,參數(shù)類型及順序是不是和我們這里說的是完全一樣的。
在調(diào)用完do_syscall_64方法后,系統(tǒng)調(diào)用的整個流程基本上就快結(jié)束了,上圖中的129行到133行做的都是一些寄存器恢復(fù)的工作,比如從棧中彈出對應(yīng)的值到rax,rip,rsp等等。
這里需要注意的是,棧中rax的值是在上面do_syscall_64方法里設(shè)置的,其存放的是系統(tǒng)調(diào)用的最終結(jié)果。
另外,在棧中彈出的rip和rsp的值,分別是用戶態(tài)程序的后續(xù)指令地址及其堆棧地址。
最后執(zhí)行sysret,從內(nèi)核態(tài)切換回用戶態(tài),繼續(xù)執(zhí)行syscall后面邏輯。
到這里,完整的系統(tǒng)調(diào)用處理流程就已經(jīng)差不多說完了,不過這里還差一小步,就是syscall指令在進入到內(nèi)核態(tài)之后,是如何找到entry_SYSCALL_64方法的:
它其實是注冊到了MSR_LSTAR寄存器里了,syscall指令在進入到內(nèi)核態(tài)之后,會直接從這個寄存器里拿系統(tǒng)調(diào)用處理函數(shù)的地址,并開始執(zhí)行。
系統(tǒng)調(diào)用內(nèi)核態(tài)的邏輯處理就是這些。
下面我們用一個例子來演示下用戶態(tài)部分:
編譯并執(zhí)行:
我們用syscall來執(zhí)行write系統(tǒng)調(diào)用,寫的字符串為Hi ,syscall執(zhí)行完畢后,我們直接使用ret指令將write的返回結(jié)果當(dāng)作程序的退出碼返回。
所以在上圖中,輸出了Hi,且程序的退出碼是3。
如果對上面的匯編不太理解,可以把它想像成下面這個樣子:
在這里,我們使用的是glibc中的write方法來執(zhí)行該系統(tǒng)調(diào)用,其實該方法就是對syscall指令做的一層封裝,本質(zhì)上使用的還是我們上面的匯編代碼。
這個例子到這里就結(jié)束了。
有沒有覺得不太盡興?
我們分析了這么多的代碼,最終就用了這么個小例子就結(jié)束了,不行,我們要再做點什么。
要不我們來自己寫個系統(tǒng)調(diào)用?
說干就干。
我們先在write系統(tǒng)調(diào)用下面定義一個我們自己的系統(tǒng)調(diào)用:
該方法很簡單,就是將參數(shù)加10,然后返回。
再把這個系統(tǒng)調(diào)用在syscall_64.tbl里注冊一下,編號為442:
編譯內(nèi)核,等待執(zhí)行。
我們再把上面寫的那個hi程序改下并編譯好:
然后在虛擬機中啟動新編譯的linux內(nèi)核,并執(zhí)行上面的程序:
看結(jié)果,正好就是20。
搞定,收工。
原文標題:精致全景圖 | 系統(tǒng)調(diào)用是如何實現(xiàn)的
文章出處:【微信公眾號:Linuxer】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
責(zé)任編輯:haq
-
寄存器
+關(guān)注
關(guān)注
31文章
5294瀏覽量
119816 -
系統(tǒng)調(diào)用
+關(guān)注
關(guān)注
0文章
28瀏覽量
8317
原文標題:精致全景圖 | 系統(tǒng)調(diào)用是如何實現(xiàn)的
文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關(guān)注!文章轉(zhuǎn)載請注明出處。
發(fā)布評論請先 登錄
相關(guān)推薦
評論