深入理解Linux系統調用過程


深入理解Linux系統調用過程


一、操作說明

  • 以40號系統調用sendfile為例
  • 通過匯編指令觸發該系統調用
  • 通過gdb跟蹤該系統調用的內核處理過程
  • 重點閱讀分析系統調用入口的保存現場和恢復現場

相關參考:
孟寧老師課件 以及
https://cloud.tencent.com/developer/article/1492374

二、系統調用知識預備

2.1 中斷

我們知道,中斷是操作系統的一個重要概念,是操作系統並發操作的的基石。下面是中斷的大致分類。

  • 外部中斷(硬件中斷)
  • 內部中斷(軟件中斷)/異常
    • 故障(fault)
    • 陷阱(trap)【系統調用從用戶態進入內核態的方式】

2.2 用戶態和內核態

在Linux 中分為用戶態和內核態兩種運行狀態。
對於普通進程,平時都是運行在用戶態下,僅擁有基本的運行能力。當進行一些特殊操作,比如說要打開文件(open)然后進行寫入(write)、分配內存(malloc)時,就會切換到內核態。
內核態進行相應的檢查,如果通過了,則按照進程的要求執行相應的操作,分配相應的資源。
這種機制就被稱為系統調用,用戶態進程發起調用,切換到內核態,內核態完成,返回用戶態繼續執行,是用戶態唯一主動切換到內核態的合法手段(exception 和 interrupt 是被動切換)。

2.3 系統調用

系統調⽤的庫函數就是我們使⽤的操作系統提供的 API(應⽤程序編程接⼝),API 只是 函數定義。系統調⽤是通過特定的軟件中斷(陷阱 trap) 向內核發出服務請求,int $0x80 和syscall指令的執⾏就會觸發⼀個系統調⽤。C庫函數內部使⽤了系統調⽤的封裝例程, 其主要⽬的是發布系統調⽤,使程序員在寫代碼時不需要⽤匯編指令和寄存器傳遞參數來 觸發系統調⽤。⼀般每個系統調⽤對應⼀個系統調⽤的封裝例程,函數庫再⽤這些封裝例 程定義出給程序員調⽤的 API ,這樣把系統調⽤終封裝成⽅便程序員使⽤的C庫函數。

Linux系統調用過程
  • 當⽤戶態進程調⽤⼀個系統調⽤時,CPU切換到內核態並開始執⾏system_call(entry_INT80_32或entry_SYSCALL_64)匯編代碼,其 中根據系統調⽤號調⽤對應的內核處理函數
  • 保存現場,執行中斷函數,恢復現場,中斷返回(簡要來說就是這么些)
Linux系統調用傳參(為編寫嵌入式匯編做准備)
  • 32位x86體系結構下普通的函數調⽤是通過將參數壓棧的⽅式傳遞的。系統調⽤從⽤戶 態切換到內核態,在⽤戶態和內核態這兩種執⾏模式下使⽤的是不同的堆棧,即進程的⽤戶態堆棧和進程的內核態堆棧,傳遞參數⽅法⽆法通過參數壓棧的⽅式,⽽是通過寄存器 傳遞參數的方式。

  • 32位x86體系結構下寄存器的⻓度⼤32位。除了EAX⽤於傳遞系統調⽤號外,參數按順序賦值給EBX、ECX、EDX、ESI、EDI、EBP,參數的個數不能超過6個, 即上述6個寄存器。如果超過6個就把某⼀個寄存器作為指針,指向內存,就可以通過內 存來傳遞更多的參數。

  • 64位x86體系結構下普通的函數調⽤和系統調⽤都是通過寄存器傳遞參數,RDI、RSI、RDX、RCX、R8、R9這6個寄存器⽤ 作函數/系統調⽤參數傳遞,依次對應第 1 參數到第 6 個參數。

三、具體實驗過程

3.1 運行環境

  • macOS
  • 虛擬機: Parallels Desktop
  • 虛擬機環境: Ubuntu 1804

3.2 環境准備

  • 查詢系統調用號
    學號340,通過查閱Linux源代碼中的arch/x86/entry/syscalls/syscall_64.tbl 可以找 到40號sendfile系統調用對應的內核處理函數為__x64_sys_sendfile64.

sendfile 相關介紹:
sendfile系統調用在內核版本2.1中被引入,目的是簡化通過網絡在兩個本地文件之間進行的數據傳輸過程。sendfile系統調用的引入,不僅減少了數據復制,還減少了上下文切換的次數。
sendfile(socket, file, len);

  • 安裝開發工具及下載內核源代碼
# 安裝相關依賴
sudo apt install build-essential
sudo apt install qemu # install QEMU 
sudo apt install libncurses5-dev bison flex libssl-dev libelf-dev
# 下載解壓linux內核源碼
sudo apt install axel
axel -n 20 https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/ linux-5.4.34.tar.xz 
xz -d linux-5.4.34.tar.xz 
tar -xvf linux-5.4.34.tar cd linux-5.4.34
  • 配置內核選項
make defconfig # Default configuration is based on 'x86_64_defconfig'
make menuconfig  
# 打開debug相關選項
Kernel hacking  ---> 
    Compile-time checks and compiler options  ---> 
       [*] Compile the kernel with debug info 
       [*]   Provide GDB scripts for kernel debugging
 [*] Kernel debugging 
# 關閉KASLR,否則會導致打斷點失敗
Processor type and features ----> 
   [] Randomize the address of the kernel image (KASLR)

配置相關:

  • 編譯和運行內核
make -j$(nproc) 
# nproc gives the number of CPU cores/threads available
# 測試內核是否正常加載運⾏,因為沒有⽂件系統終會kernel panic 
qemu-system-x86_64 -kernel arch/x86/boot/bzImage  
# 此時還不能正常運行
  • 制作根⽂件系統
# 下載 busybox源代碼解壓,解壓完成后,配置編譯,並安裝。
axel -n 20 https://busybox.net/downloads/busybox-1.31.1.tar.bz2 
tar -jxvf busybox-1.31.1.tar.bz2 
cd busybox-1.31.1

make menuconfig 
#記得要編譯成靜態鏈接,不⽤動態鏈接庫。
Settings  --->
    [*] Build static binary (no shared libs) 
#然后編譯安裝,默認會安裝到源碼⽬錄下的 _install ⽬錄中。 
make -j$(nproc) && make install

#pwd = ~
mkdir rootfs
cd rootfs
cp ../busybox-1.31.1/_install/* ./ -rf
mkdir dev proc sys home 
sudo cp -a /dev/{null,console,tty,tty1,tty2,tty3,tty4} dev/
  • 准備init腳本⽂件放在根⽂件系統跟⽬錄下(rootfs/init),init⽂件內容如下。記得給init腳本添加可執⾏權限
#!/bin/sh
 mount -t proc none /proc 
 mount -t sysfs none /sys
 echo "Wellcome MyOS!"
 echo "--------------------" 
 cd home
 /bin/sh 

chmod +x init
  • 打包成內存根⽂件系統鏡像
#打包成內存根⽂件系統鏡像 
find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz 
#測試掛載根⽂件系統,看內核啟動完成后是否執⾏init腳本 
# cd.. 退到rootfs.cpio.gz所在的目錄
qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz
  • 運行結果

3.3 匯編改寫手動觸發系統調用

  • 在 rootfs/home 目錄下新建文件 sendfile-asm.c

我們通過寫一個小程序觸發這一系統調用。使用內聯匯編小程序sendfile-asm.c如下:

int main()
{
    asm volatile(
    "movl $0x28,%eax\n\t" //使⽤EAX傳遞系統調⽤號40
    "syscall\n\t" //觸發系統調⽤ 
    );
    return 0;
}
  • gcc編譯(這里采用靜態編譯)
    gcc -o sendfile-asm sendfile-asm.c -static

  • 重新打包成內存根文件系統鏡像。
    find . -print0 | cpio --null -ov --format=newc | gzip -9 > ../ rootfs.cpio.gz

  • 啟動虛擬機
    qemu-system-x86_64 -kernel linux-5.4.34/arch/x86/boot/bzImage -initrd rootfs.cpio.gz -S -s -nographic -append "console=ttyS0"

  • 觀察結果

在我們的構建系統的根目錄下可發現可執行文件sendfile

3.4 通過GDB進行調試

  • 連接進行調試

接上步啟動虛擬機,此時虛擬機會暫停在啟動界面。
在另一個terminal中開啟gdb調試 gdb vmlinux。連接進行調試,target remote:1234。

  • 為40號系統調用打斷點b __64x_sys_sendfile64,通過c繼續運行,此時在qemu虛擬機運行可執行文件sendfile。就可發現該文件確實觸發了系統調用。即我們通過匯編實現了系統調用。

3.5 系統調用入口的保存現場和恢復現場

  • 通過bt可觀察當前堆棧信息
    • 第一層/ 頂層 __x64_sys_sendfile 系統調用函數所在
    • 第二層 do_syscall_64 獲取系統調用號, 前往系統調用函數
    • 第三層 entry_syscall_64 中斷入口,做保存線程工作,調用 do_syscall_64
    • 第四層 OS相關

從中,我們可發現該系統調用涉及到do_syscall_64和entry_SYSCALL_64兩個內核函數。


  • 首先斷點定位到/home/tx/linux-5.4.34/fs/read_write.c的1511行:

  • 進入do_sendfile函數查看,在這里是運行程序的代碼段,前期的保存現場工作已經完成。
  • 執行完這個函數,發現回到了函數堆棧上一層的do_sys_call_64 中,接下來要執行的 syscall_return_slowpath 函數要為恢復現場做准備。

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

  • 最后伴隨着pop指令,恢復了rdi和rsp寄存器。系統調用完成。

四、總結

最后,我們來總結下系統調用的整個過程:

    1. 通過匯編指令syscall 觸發系統調用,並從MSR寄存器找到中斷函數入口,此時,代碼執行到/home/tx/linux-5.4.34/arch/x86/entry/entry_64.S 目錄下的ENTRY(entry_SYSCALL_64)入口,然后開始通過swapgs 和壓棧動作保存現場。
    1. 接着跳轉到了/linux-5.4.34/arch/x86/entry/common.c 目錄下的 do_syscall_64 函數,在ax寄存器中獲取到系統調用號,接着去執行系統調用的具體內容。
    1. 接着程序跳轉到/linux-5.4.34/fs/read_write.c 下的do_writev 函數,並開始執行
    1. 在函數執行完后回到步驟3中的syscall_return_slowpath(regs); 准備進行現場恢復操作,
    1. 接着程序再次回到arch/x86/entry/entry_64.S,執行現場的恢復,最后兩句,完成了堆棧的切換。

保存和恢復現場過程:syscall指令觸發系統調用 --> entry_SYSCALL_64( )執行現場保存 --> do_syscall_64( )查找調用入口並執行 --> 准備恢復現場 --> entry_SYSCALL_64( )最后完成現場恢復


免責聲明!

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



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