【技術分享】Linux的系統調用機制
1、概述
處于用戶態的程序只能執行非特權指令, 如果需要使用某些特權指令, 比如: 通過io指令與硬盤交互來讀取文件, 則必須通過系統調用向內核發起請求, 內核會檢查請求是否安全, 從而保證用戶態進程不會威脅整個系統
write(1, ptr, 0x10)系統調用為例子, 匯編可以寫為如下, 內核收到請求后會向顯存中寫入數據, 從而在顯示器上顯示出來
mov rax, 1mov rdi, 1mov rsi, ptrmov rdx, 0x10syscall
C庫會首先實現一個write的包裹函數, 為這個系統調用進行一些簡單的參數檢查和錯誤處理
由于write的功能十分簡單, 不方面使用因此還會根據write衍生出更高級的函數printf()供用戶使用
整體結構如下:

接下來我們主要研究系統調用是怎么進入和退出的, 并不研究具體處理函數的實現
2、i386下處理系統調用
i386的系統調用是通過中斷實現的, 因此放在了arch/i386/traps.c里面, 通過system_call()處理int 0x80的中斷

system_call()聲明

system_call()定義

sys_call_table就是一個函數指針數組, 定義在arch/i386/syscall.c中, 通過包含文件完成數組的初始化

unistd.h中定義了系統調用號與處理函數的句柄, 這個文件位于源碼頂層, 是所有架構都必須滿足的, 處理函數的舉報由各個架構自己實現

總結

3、預備知識
段選擇子

實模式下下: 實際物理地址 = (段地址<<4) + 偏移地址
保護模式下: 邏輯地址由兩部分組成
段標識符: 16位字段, 放在段寄存器中, 為全局段表的索引
段內偏移: 32位
保護模式下尋址過程
先根據段寄存器找到段選擇子(16bit)
再根據GDT找到段表, 用選擇子作為索引, 從而找到段描述符(64bit)
根據段描述符找到段基址
段基址+ 偏移地址 得到線性地址
線性地址到物理地址:
如果沒有分頁那么線性地址就是物理地址;如果啟用了分頁, 那么還要通過頁表得到物理地址
物理地址才是類似于實模式中地址的概念, 可以直接送到總線上用于CPU訪問內存

段選擇子/段描述符索引
為 段描述表 中 段描述符 的 序號
由于一共13bit可用, 因此可以區分8192個段
TI: Table Indicator, 引用描述表的指示位( T1 = 0表示從全局段表GDT中讀取 TI = 1 表示從局部段表LDT中讀取)
RPL: Requested Privilege Level, 表示請求者的特權等級, 當試圖訪問一個段時, 會自動當前特權等級和段要求的特權等級
由于分頁機制比分段機制更加靈活, 因此現在的操作系統并不開啟分段, 但是處于兼容性的考慮, 段寄存器還是被保留了下來
對于分段linux采用平坦模式, 也就是說所有的段的基址都是0, 地址空間相同, 分段只用于鑒權: 每當執行某些特權指令時CPU就會自動檢查CS寄存器的RPL
如果為0則說明當前是內核態, 允許執行
如果不為0則說明是用戶態, CPU會拋出一個異常, 交由內核的異常處理程序處理, 通常會向此用戶進程發送SIGSEV信號
因此狹義上來說陷入內核態就是CPU令CS的RPL為0, 從而可以執行特權指令. 切換到內核態的執行環境則就是后話了
syscall指令
64位下的系統調用就和中斷沒關系了, 主要依賴于syscall指令的支持, syscall指令依靠MSR寄存器找到處理系統的入口點
MSR寄存器用來對CPU進行設置, 通過WRMSR和RDMSR指令讀寫

x86_64寄存器架構

當syscall指令執行時, 有如下操作
RCX保存用戶態的RIP
從MSR寄存器中的IA32_LASAR獲取RIP
R11保存標志寄存器
用IA32_STAR[47:32]設置CS的選擇子, 同時把RPL設置為0, 表示現在開始執行內核態代碼, 這是進入內核態的第一步, 由CPU完成
用IA32_STAR[47:32]+8設置SS的選擇子, 這也就要求GDT中棧段描述符就在代碼段描述符上面

指令操作

https://www.felixcloutier.com/x86/syscall.html
swapgs指令
swapgs指令: 把gs的值與IA32_KERNEL_GS_BASE MSR進行交換

https://www.felixcloutier.com/x86/swapgs
剛剛切換到內核態時, 所有的通用寄存器與段寄存器都被用戶使用, 內核需要想辦法找到內核相關信息, 解決方法為:
令gs指向描述每個cpu相關的數據結構
當要切換到用戶態時就調用swapgs把值保存在MSR寄存器中. 由于操作MSR的指令為特權指令, 因此用戶態下是無法修改的MSR
4、x86-64下處理系統調用
kernel初始化時, 調用arch/x86/kernel/s.c:syscall_init()對MSR進行初始化, 設置entry_SYSCALL_64為處理系統調用的入口點

由于有些指令entry_SYSCALL_64的任務可以分為三部分
進入路徑: 匯編實現, 目的是保存syscall的現場, 切換到內核態的執行環境, 創建一個適當的環境, 然后調用處理程序
處理程序: C實現, 負責具體的處理工作
退出路徑: 匯編實現, 目的是從中斷環境中退出, 切換到用戶態, 恢復用戶態的程序執行
進入路徑部分:
先通過swapgs指令切換到內核態的gs, 并保存用戶態的gs
這是一個特權指令, 但是CPU處理system指令時已經把CS的RPL設為00, 因此現在運行在內核態, 可以執行特權指令
然后通過gs保存用戶的rsp, 并找到內核態的rsp, 至此切換到內核態堆棧
然后保存所有內核態會使用的寄存器到棧上

接下來涉及到slow_path和fast_path相關, 這只是一個優化, 其本質工作就是下面這條指令

sys_call_table是一個函數指針數組, 指向各個系統調用的處理函數


處理函數結束后會ret到entry_SYSCALL_64中, 進入退出路徑部分, 這部分進行的工作為
把處理函數的返回值寫入到內核棧上的pt_regs中
利用pt_regs結構恢復用戶態的執行環境
交換gs, 切回用戶態的gs, 并把內核態的gs保存在MSR中
sysretq, 從syscall中退出


5、總結
i386:

x86_64
