- A+
最新 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 了。后面就是常规操作了。