最新 x86_64 系統調用入口分析 (基於5.7.0) 整體概覽 最近的工作涉及系統調用入口,但網上的一些分析都比較老了,這裡把自己的分析過程記錄一下,僅供參考。 x86_64位系統調用使用 SYSCALL 指令進入內核空間,使CPU切換到ring 0。SYSCALL 指令主要工作為從MSR寄存器 ...
最新 x86_64 系統調用入口分析 (基於5.7.0)
整體概覽
最近的工作涉及系統調用入口,但網上的一些分析都比較老了,這裡把自己的分析過程記錄一下,僅供參考。
x86_64位系統調用使用 SYSCALL 指令進入內核空間,使CPU切換到ring 0。SYSCALL 指令主要工作為從MSR寄存器載入CS/SS,以及系統調用入口(entry_SYSCALL_64),從而進入系統調用處理流程。
MSR寄存器相關這裡不再介紹,需要相關知識的指路 寄存器總結 以及
Model-specific register。
SYSCALL 指令
IF (CS.L ≠ 1 ) or (IA32_EFER.LMA ≠ 1) or (IA32_EFER.SCE ≠ 1)
(* Not in 64-Bit Mode or SYSCALL/SYSRET not enabled in IA32_EFER *)
THEN #UD;
FI;
RCX ← RIP; (* Will contain address of next instruction *)
RIP ← IA32_LSTAR;
R11 ← RFLAGS;
RFLAGS ← RFLAGS AND NOT(IA32_FMASK);
CS.Selector ← IA32_STAR[47:32] AND FFFCH (* Operating system provides CS; RPL forced to 0 *)
(* Set rest of CS to a fixed value *)
CS.Base ← 0;
(* Flat segment *)
CS.Limit ← FFFFFH;
(* With 4-KByte granularity, implies a 4-GByte limit *)
CS.Type ← 11;
(* Execute/read code, accessed *)
CS.S ← 1;
CS.DPL ← 0;
CS.P ← 1;
CS.L ← 1;
(* Entry is to 64-bit mode *)
CS.D ← 0;
(* Required if CS.L = 1 *)
CS.G ← 1;
(* 4-KByte granularity *)
CPL ← 0;
SS.Selector ← IA32_STAR[47:32] + 8;
(* SS just above CS *)
(* Set rest of SS to a fixed value *)
SS.Base ← 0;
(* Flat segment *)
SS.Limit ← FFFFFH;
(* With 4-KByte granularity, implies a 4-GByte limit *)
SS.Type ← 3;
(* Read/write data, accessed *)
SS.S ← 1;
SS.DPL ← 0;
SS.P ← 1;
SS.B ← 1;
(* 32-bit stack segment *)
SS.G ← 1;
(* 4-KByte granularity *)
(代碼引自 https://www.felixcloutier.com/x86/syscall)
這裡主要做了三個工作:
- 將RIP保存到RCX寄存器,即將SYSCALL指令下一條指令地址保存到RCX,後續用到。
- 從 IA32_LSTAR MSR 寄存器載入系統調用入口地址。64 位寄存器名為MSR_LSTAR。
- 從 IA32_STAR MSR 寄存器47-32到載入CS/SS段。64 位寄存器名為 MSR_STAR,其在內核啟動過程中初始化。
MSR寄存器初始化源碼點這
核心為:
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);
入口地址
接下來就是進入 entry_SYSCALL_64處理流程,源碼在這。
但是這裡有一個問題:在較新版內核中,都已支持 PTI 機制,用戶態與內核態使用不同頁表,而這裡 entry_SYSCALL_64 已經屬於內核代碼,而我們仔細觀察entry_SYSCALL_64 實現,在第四行才切換內核頁表。想要 entry_SYSCALL_64 能被執行,就需要 cpu_entry_area 的作用了。
SYM_CODE_START(entry_SYSCALL_64)
UNWIND_HINT_EMPTY
/* * Interrupts are off on entry. * We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON, * it is too small to ever cause noticeable irq latency. */
swapgs
/* tss.sp2 is scratch space. */
movq %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
cpu_entry_area 包括了CPU進入內核需要的所有數據/代碼,會被映射到用戶態頁表。瞭解點著,但是要註意較新版本cpu_entry_area已經不包含其中的 a set of trampolines;
,至於為什麼看這。
那又是怎麼實現?
翻來覆去,終於在 pti 初始化處找到了關鍵點,其實現為
/* * Clone the populated PMDs of the entry and irqentry text and force it RO. */
static void pti_clone_entry_text(void){
pti_clone_pgtable((unsigned long) __entry_text_start,
(unsigned long) __irqentry_text_end,
PTI_CLONE_PMD);}
其將 __entry_text_start
開頭的地址複製,而這又與 entry_SYSCALL_64 有什麼關係?我們繼續往下找
#define ENTRY_TEXT \
ALIGN_FUNCTION(); \
__entry_text_start = .; \
*(.entry.text) \
__entry_text_end = .;
而再看 entry_SYSCALL_64 定義的文件頭部
.code64
.section .entry.text, "ax"
所以這裡就會把 entry_SYSCALL_64 等一眾函數地址拷貝到用戶頁表,從而實現可訪問。具體定義展開這裡就不進行了。
繼續執行
回到 entry_SYSCALL_64,我們跳過一系列處理,可以看到一個關鍵點:
call do_syscall_64
很顯然了,接下來就是執行 do_syscall_64 了。後面就是常規操作了。