最新 x86_64 系统调用入口分析 (基于 5.7.0)

  • 最新 x86_64 系统调用入口分析 (基于 5.7.0)已关闭评论
  • 207 次浏览
  • A+
所属分类:linux技术
摘要

最近的工作涉及系统调用入口,但网上的一些分析都比较老了,这里把自己的分析过程记录一下,仅供参考。


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