深入理解系統調用


系統調用概念

1. 什么是系統調用

簡單來說,系統調用就是用戶程序和硬件設備之間的橋梁。

用戶程序在需要的時候,通過系統調用來使用硬件設備。

系統調用的存在,有以下重要的意義:

1)用戶程序通過系統調用來使用硬件,而不用關心具體的硬件設備,這樣大大簡化了用戶程序的開發。

比如:用戶程序通過write()系統調用就可以將數據寫入文件,而不必關心文件是在磁盤上還是軟盤上,或者其他存儲上。

2)系統調用使得用戶程序有更好的可移植性。

只要操作系統提供的系統調用接口相同,用戶程序就可在不用修改的情況下,從一個系統遷移到另一個操作系統。

3)系統調用使得內核能更好的管理用戶程序,增強了系統的穩定性。

因為系統調用是內核實現的,內核通過系統調用來控制開放什么功能及什么權限給用戶程序。

這樣可以避免用戶程序不正確的使用硬件設備,從而破壞了其他程序。

4)系統調用有效的分離了用戶程序和內核的開發。

用戶程序只需關心系統調用API,通過這些API來開發自己的應用,不用關心API的具體實現。

內核則只要關心系統調用API的實現,而不必管它們是被如何調用的。

系統調用在系統中的關系如下圖所示:

2. Linux上的系統調用實現原理

要想實現系統調用,主要實現以下幾個方面:

1. 通知內核調用一個哪個系統調用

2. 用戶程序把系統調用的參數傳遞給內核

3. 用戶程序獲取內核返回的系統調用返回值

下面看看Linux是如何實現上面3個功能的。

2.1 通知內核調用一個哪個系統調用

每個系統調用都有一個系統調用號,系統調用發生時,內核就是根據傳入的系統調用號來知道是哪個系統調用的。

在x86架構中,用戶空間將系統調用號是放在eax中的,系統調用處理程序通過eax取得系統調用號。

系統調用號定義在內核代碼:arch/x86/include/asm/unistd.h 中,可以看出linux的系統調用不是很多。

2.2 用戶程序把系統調用的參數傳遞給內核

系統調用的參數也是通過寄存器傳給內核的,在x86系統上,系統調用的前5個參數放在ebx,ecx,edx,esi和edi中,如果參數多的話,還需要用個單獨的寄存器存放指向所有參數在用戶空間地址的指針。

一般的系統調用都是通過C庫(最常用的是glibc庫)來訪問的,Linux內核提供一個從用戶程序直接訪問系統調用的方法。

參見內核代碼:rch/x86/include/asm/unistd.h :

里面定義了6個宏,分別可以調用參數個數為0~6的系統調用

syscall0(type,name)

_syscall1(type,name,type1,arg1)

_syscall2(type,name,type1,arg1,type2,arg2)

_syscall3(type,name,type1,arg1,type2,arg2,type3,arg3)

_syscall4(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4)

_syscall5(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5)

_syscall6(type,name,type1,arg1,type2,arg2,type3,arg3,type4,arg4,type5,arg5,type6,arg6)

超過6個參數的系統調用很罕見,所以這里只定義了6個。

2.3 用戶程序獲取內核返回的系統調用返回值

獲取系統調用的返回值也是通過寄存器,在x86系統上,返回值放在eax中。

3. 一個簡單的系統調用的實現

了解了Linux上系統調用的原理,下面就可以自己來實現一個簡單的系統調用。

環境准備

首先配置內核選項如下圖所示(調試內核必須這樣配置):

之后使用busybox制作根文件系統,我下載的是1.31.1版本。

首先配置busybox使用靜態鏈接,否則在根文件系統頂層目錄下面要拷貝編譯器庫文件,很麻煩。

之后使用命令make -j$(nproc) && make install編譯安裝busybox。默認安裝路徑為源碼目錄的_install下面。

再到家目錄下面新建rootfs文件夾,將_install目錄中的所有文件拷貝過去。並且新建幾個目錄(dev proc sys home等)和文件(dev/console dev/null dev/tty*)。

准備init腳本⽂件放在根⽂件系統跟⽬錄下(rootfs/init),在init⽂件中添加這些內容:

之后給init腳本添加可執⾏權限:chmod +x init

打包成內存根⽂件系統鏡像:

測試掛載根⽂件系統,看內核啟動完成后是否執⾏init腳本:

qemu運行部分截圖如下所示:

查看系統調用表和匯編改寫

打開/linux-5.4.34/arch/x86/entry/syscalls/syscall_64.tbl,查看要選擇進行實驗的系統調用。

我的學號為48,看到shutdown的時候我心中一陣竊喜。但是又無從下手,所以換了個系統調用,最近在看write系統調用,就從writev下手吧,正好csdn上也有個博客可參考。

 

系統調用writev,函數入口為__x64_sys_writev。下面通過一個函數來簡單了解writev的使用:

gcc編譯(這里采用靜態編譯)后運行,輸出結果:

簡單分析,writev的作用是將多個緩沖區的內容一次性輸出到某個位置/內容。

匯編改寫手動觸發系統調用
新建在 rootfs/home 目錄下新建文件 write-asm.c,在后面添加以下內容:

gcc編譯后查看執行結果,和write.c效果一樣。改寫成功。

gdb調試與分析

重新打包根文件目錄,純命令⾏下啟動虛擬機。

qemu-system-x86_64 -kernel shiyan/linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0",此時虛擬機會暫停在啟動界面。在另一個terminal中開啟gdb調試 gdb vmlinux,連接進行調試,target remote:1234。結果如下圖所示:

gdb輸入命令c,使得虛擬機繼續執行,到初始界面。

由之前分析可知,wirtev系統調用觸發的函數是__x64_sys_writev,通過gdb在函數入口下斷點,然后監聽。

在虛擬機中執行write-file,會卡住,在gdb界面查看斷點分析。

使用 l 命令查看代碼情況, n 命令單步執行, step 命令進入函數內部 bt查看堆棧。

首先斷點定位為到/home/howin/shiyan/linux-5.4.34/fs/read_write.c 中的1128行:

進入do_writev函數查看,可知,這里是完成程序內容的地方,前期的保存現場工作已經完成。

執行完這個函數,發現回到了函數堆棧上一層的do_sys_call_64 中 ,接下來要執行的 syscall_return_slowpath 函數要為恢復現場做准備。

繼續執行,發現再次回到了函數堆棧的上一層,entry_SYSCALL_64 ,接下來執行的是用於恢復現場的匯編指令。

最后伴隨着兩個pop指令,恢復了rdirsp寄存器。系統調用完成。

總結

1.匯編指令syscall 觸發系統調用,通過MSR寄存器找到了中斷函數入口(具體細節不考慮),此時,代碼執行到/home/howin/shiyan/linux-5.4.34/arch/x86/entry/entry_64.S 目錄下的ENTRY(entry_SYSCALL_64)入口,然后開始通過swapgs 和壓棧動作保存現場。

 

2.然后跳轉到了/linux-5.4.34/arch/x86/entry/common.c 目錄下的 do_syscall_64 函數,在ax寄存器中獲取到系統調用號,然后去執行系統調用內容.

3.然后程序跳轉到/linux-5.4.34/fs/read_write.c 下的do_writev 函數,開始執行。

4.函數執行完后回到步驟3中的syscall_return_slowpath(regs); 准備進行現場恢復操作。

5.接着程序再次回到arch/x86/entry/entry_64.S,執行現場的恢復,最后兩句,完成了堆棧的切換。

參考博客:https://blog.csdn.net/Alan_cqu_cj/article/details/106272204


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM