x86_64系統呼叫過程

道成空發表於2024-06-07

x86_64系統呼叫過程

本文所述Linux核心版本為v6.4.0

一、概述

在x86_64架構下,系統呼叫會經歷以下過程:

  1. 將系統呼叫號存入rax暫存器,引數依次存入rdirsirdxr10r8r9暫存器,第7個及之後的引數會透過棧傳遞。
  2. 執行syscall指令,該指令會儲存syscall指令下一條指令的地址,然後將許可權從使用者態轉換到核心態,並將rip設定為entry_SYSCALL_64程式的入口地址。
  3. 執行entry_SYSCALL_64程式,核心會儲存使用者態的上下文,包括暫存器和堆疊指標,然後呼叫do_syscall_64函式來完成系統呼叫功能。
  4. 系統呼叫處理函式執行完畢後,核心將返回值放入rax暫存器,然後核心恢復之前儲存的使用者態上下文,包括暫存器和堆疊指標。
  5. 核心執行sysret指令,將控制權返回給使用者態程式。

二、MSR暫存器

從80486之後的x86架構CPU,內部增加了一組新的暫存器,統稱為MSR暫存器(Model Specific Registers),這些暫存器不像上面列出的暫存器是固定的,這些暫存器可能隨著不同的版本有所變化,主要用來支援一些新的功能。

隨著x86CPU不斷更新換代,MSR暫存器變的越來越多,但與此同時,有一部分MSR暫存器隨著版本迭代,慢慢固化下來,成為了變化中那部分不變的。

在早期的x86架構CPU上,系統呼叫依賴於軟中斷實現,如Linux中的int 80。軟中斷是一個比較慢的操作,因為執行軟中斷就需要記憶體查表,透過IDTR定位到IDT,再取出函式地址進行執行。

而系統呼叫是一個頻繁觸發的動作,如此這般勢必對效能有所影響。在進入奔騰時代後,就使用幾個特定的MSR暫存器,分別儲存了執行系統呼叫時核心系統呼叫入口函式所需要的引數,不再需要記憶體查表。快速系統呼叫還提供了專門的CPU指令sysenter/sysexit用來發起系統呼叫和退出系統呼叫(在64位上,這一對指令升級為syscall/sysret)。

三、段選擇符

段選擇符結構如下:

image-20240607154222921
  • Index:所對應的段描述符處於GDTLDT中的索引。

  • TI:表示對應段描述符儲存在GDT中還是LDT中,0表示全域性描述符表GDT,1表示區域性描述符表LDT

  • RPL:當該段選擇符裝入cs暫存器時,設定CPU當前的特權級CPL的值為RPL,也就是cs暫存器中的RPL就是CPL

CPL值為0,表示CPU當前特權級別為Ring0(核心態),值為3,表示表示CPU當前特權級別為Ring3(使用者態)。

四、段描述符

GDT全域性段描述符表中的每個條目都有一個這樣的複雜的結構:

image-20240607003309591

  • BASE :段首地址的線性地址。

  • LIMIT :該段最後一個地址的偏移量。

  • MORE:包括段的各種標誌(如型別、特權級別等),結構如下:

image-20240607003958353

  • DPL:表示訪問這個段CPU要求的最小優先順序(儲存在cs暫存器的CPL特權級)。當DPL為0時,只有CPL為0才能訪問,DPL為3時,CPL為0為3都可以訪問這個段。

五、SYSCALL指令

syscall指令主要做了三個工作:

  • rip暫存器內容儲存到rcx暫存器。
  • MSR_LSTAR暫存器中的系統呼叫處理程式入口地址存入rip暫存器。
  • MSR_STAR 暫存器的 [47:32] 存入 csss段選擇暫存器。

MSR暫存器初始化核心程式碼為:

// MSR_STAR的[63:48]存入使用者程式碼段選擇符,[47:32]存入核心程式碼段選擇符
// wrmsr函式第一個參數列示要寫入的MSR編號,第二個參數列示要寫入低32位的值,第三個參數列示要寫入高32位的值
wrmsr(MSR_STAR, 0, (__USER32_CS << 16) | __KERNEL_CS);
// 使用系統呼叫處理程式entry_SYSCALL_64地址填充MSR_LSTAR暫存器
wrmsrl(MSR_LSTAR, (unsigned long)entry_SYSCALL_64);

cs程式碼段暫存器指向包含程式指令的段,在cs暫存器中RPL用於表示當前CPU的特權級CPL

CPL為0是最高許可權(核心態使用),CPL為3是使用者態使用。

  • __USER32_CS 是使用者程式碼段選擇符的值,低兩位為 0b11

  • __KERNEL_CS 是核心程式碼段選擇符的值,低兩位為 0b00

由於syscall指令將核心程式碼段選擇符的值存入了 csss段選擇暫存器,當前CPU特權級別從Ring3變為Ring0,即由使用者態轉變為了核心態。

接下來就是進入entry_SYSCALL_64處理流程。

六、entry_SYSCALL_64

arch/x86/entry/entry_64.S中的entry_SYSCALL_64程式原始碼如下:

SYM_CODE_START(entry_SYSCALL_64)
	UNWIND_HINT_ENTRY
	ENDBR

	/* 交換gs暫存器的值 */
	swapgs
	/* tss.sp2 is scratch space. */
	/* 將當前的棧指標儲存到tss中的sp2欄位 */
	movq	%rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
	/* 使用%rsp作為臨時暫存器來切換到核心態頁表(KPTI核心頁表隔離) */
	SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
	/* 從使用者棧切換到核心棧 */
	movq	PER_CPU_VAR(pcpu_hot + X86_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR

	/* 構建使用者態暫存器上下文(struct pt_regs) */
	/* Construct struct pt_regs on stack */
	pushq	$__USER_DS				/* pt_regs->ss */
	pushq	PER_CPU_VAR(cpu_tss_rw + TSS_sp2)	/* pt_regs->sp */
	pushq	%r11					/* pt_regs->flags */
	pushq	$__USER_CS				/* pt_regs->cs */
	pushq	%rcx					/* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
	pushq	%rax					/* pt_regs->orig_ax */
	
	/* 儲存剩餘暫存器 */
	PUSH_AND_CLEAR_REGS rax=$-ENOSYS

	/* IRQs are off. */
	/* 將當前核心棧指標作為引數,相當於傳遞了一個使用者態的pt_regs */
	movq	%rsp, %rdi
	/* Sign extend the lower 32bit as syscall numbers are treated as int */
	/* 將系統呼叫號也作為引數傳遞 */
	movslq	%eax, %rsi

	/* clobbers %rax, make sure it is after saving the syscall nr */
	/* 關閉分支預測 */
	IBRS_ENTER
	UNTRAIN_RET

	/* 函式執行系統呼叫功能,並將返回值存入rax暫存器 */
	call	do_syscall_64		/* returns with IRQs disabled */

	/*
	 * Try to use SYSRET instead of IRET if we're returning to
	 * a completely clean 64-bit userspace context.  If we're not,
	 * go to the slow exit path.
	 * In the Xen PV case we must use iret anyway.
	 */

	/* do_syscall_64執行過程中產生異常或其他特殊情況,會跳轉到慢退出路徑 */
	
	ALTERNATIVE "", "jmp	swapgs_restore_regs_and_return_to_usermode", \
		X86_FEATURE_XENPV

	movq	RCX(%rsp), %rcx
	movq	RIP(%rsp), %r11
	
	cmpq	%rcx, %r11	/* SYSRET requires RCX == RIP */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * On Intel CPUs, SYSRET with non-canonical RCX/RIP will #GP
	 * in kernel space.  This essentially lets the user take over
	 * the kernel, since userspace controls RSP.
	 *
	 * If width of "canonical tail" ever becomes variable, this will need
	 * to be updated to remain correct on both old and new CPUs.
	 *
	 * Change top bits to match most significant bit (47th or 56th bit
	 * depending on paging mode) in the address.
	 */
#ifdef CONFIG_X86_5LEVEL
	ALTERNATIVE "shl $(64 - 48), %rcx; sar $(64 - 48), %rcx", \
		"shl $(64 - 57), %rcx; sar $(64 - 57), %rcx", X86_FEATURE_LA57
#else
	shl	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
	sar	$(64 - (__VIRTUAL_MASK_SHIFT+1)), %rcx
#endif

	/* If this changed %rcx, it was not canonical */
	cmpq	%rcx, %r11
	jne	swapgs_restore_regs_and_return_to_usermode

	cmpq	$__USER_CS, CS(%rsp)		/* CS must match SYSRET */
	jne	swapgs_restore_regs_and_return_to_usermode

	movq	R11(%rsp), %r11
	cmpq	%r11, EFLAGS(%rsp)		/* R11 == RFLAGS */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * SYSCALL clears RF when it saves RFLAGS in R11 and SYSRET cannot
	 * restore RF properly. If the slowpath sets it for whatever reason, we
	 * need to restore it correctly.
	 *
	 * SYSRET can restore TF, but unlike IRET, restoring TF results in a
	 * trap from userspace immediately after SYSRET.  This would cause an
	 * infinite loop whenever #DB happens with register state that satisfies
	 * the opportunistic SYSRET conditions.  For example, single-stepping
	 * this user code:
	 *
	 *           movq	$stuck_here, %rcx
	 *           pushfq
	 *           popq %r11
	 *   stuck_here:
	 *
	 * would never get past 'stuck_here'.
	 */
	testq	$(X86_EFLAGS_RF|X86_EFLAGS_TF), %r11
	jnz	swapgs_restore_regs_and_return_to_usermode

	/* nothing to check for RSP */

	cmpq	$__USER_DS, SS(%rsp)		/* SS must match SYSRET */
	jne	swapgs_restore_regs_and_return_to_usermode

	/*
	 * We win! This label is here just for ease of understanding
	 * perf profiles. Nothing jumps here.
	 */
	/* 若透過所有檢查,使用sysret來返回使用者態 */
syscall_return_via_sysret:
	/* 恢復分支預測 */
	IBRS_EXIT
	/* 從棧中恢復暫存器的值 */
	POP_REGS pop_rdi=0

	/*
	 * Now all regs are restored except RSP and RDI.
	 * Save old stack pointer and switch to trampoline stack.
	 */
	movq	%rsp, %rdi
	/* 切換回使用者棧 */
	movq	PER_CPU_VAR(cpu_tss_rw + TSS_sp0), %rsp
	UNWIND_HINT_END_OF_STACK

	pushq	RSP-RDI(%rdi)	/* RSP */
	pushq	(%rdi)		/* RDI */

	/*
	 * We are on the trampoline stack.  All regs except RDI are live.
	 * We can do future final exit work right here.
	 */
	 /* 清除核心棧內容 */
	STACKLEAK_ERASE_NOCLOBBER
	
	/* 切換回使用者態頁表 */
	SWITCH_TO_USER_CR3_STACK scratch_reg=%rdi

	popq	%rdi
	popq	%rsp
SYM_INNER_LABEL(entry_SYSRETQ_unsafe_stack, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR
	swapgs
	/* 切換回使用者態,Ring0 -> Ring3 */
	sysretq
SYM_INNER_LABEL(entry_SYSRETQ_end, SYM_L_GLOBAL)
	ANNOTATE_NOENDBR
	/* 正常返回情況不會被執行 */
	int3
SYM_CODE_END(entry_SYSCALL_64)

七、核心頁表隔離KPTI

核心頁表隔離(Kernel page-table isolation,縮寫KPTI,也簡稱PTI,舊稱KAISER)是Linux核心中的一種強化技術,旨在更好地隔離使用者空間與核心空間的記憶體來提高安全性,緩解現代x86CPU中的“熔斷(Meltdown)”硬體安全缺陷。

在 KPTI機制中,核心態空間的記憶體和使用者態空間的記憶體的隔離進一步得到了增強。

image-20240606003335605
  • 核心態中的頁表包括使用者空間記憶體的頁表和核心空間記憶體的頁表。
  • 使用者態的頁表只包括使用者空間記憶體的頁表以及必要的核心空間記憶體的頁表,如用於處理系統呼叫、中斷等資訊的記憶體。

相關文章