首页 > 其他分享 >x86_64系统调用过程

x86_64系统调用过程

时间:2024-06-07 21:59:57浏览次数:20  
标签:调用 x86 regs 64 内核 寄存器 rsp

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
  • 内核态中的页表包括用户空间内存的页表和内核空间内存的页表。
  • 用户态的页表只包括用户空间内存的页表以及必要的内核空间内存的页表,如用于处理系统调用、中断等信息的内存。

标签:调用,x86,regs,64,内核,寄存器,rsp
From: https://www.cnblogs.com/Natsunobourei/p/18237929

相关文章

  • Ctranslate2 调用翻译模型 M2M100
    点击下载完整代码:完整代码importctranslate2importsentencepieceasspmimportosdeftokenize(sp,queries):ifisinstance(queries,list):returnsp.encode(queries,out_type=str)else:return[sp.encode(queries,out_type=str)]def......
  • 264 Exception Handling Middleware
    示例CRUDExample项目新建Middlewares文件夹,下面新建ExceptionHandlingMiddleware.cs(VS中有Middleware模板)usingMicrosoft.AspNetCore.Builder;usingMicrosoft.AspNetCore.Http;usingSerilog;usingSystem.Threading.Tasks;namespaceCRUDExample.Middlewares{ ......
  • Java中实现图片和Base64的互相转化
    前言公司项目中用到了实名认证此,采用的第三方平台。后端中用到的单项功能为身份证信息人像对比功能,在写demo的过程中发现,它们所要求的图片信息为base64编码格式。一、代码packagecom.bajiao.wyq.tools.chuanglan;importjava.awt.image.BufferedImage;importjava.io.ByteArray......
  • 8644 堆排序
    Description用函数实现堆排序,并输出每趟排序的结果输入格式第一行:键盘输入待排序关键的个数n第二行:输入n个待排序关键字,用空格分隔数据输出格式第一行:初始建堆后的结果其后各行输出交换堆顶元素并调整堆的结果,数据之间用一个空格分隔输入样例10548093267......
  • 调用文心一言API询问httpx的使用方法2
    [importrequestsimportjsondefget_access_token():url="https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=输入自己的id&client_secret=输入自己id的密码"payload=json.dumps("")headers={"Content-Typ......
  • 调用文心一言API询问httpx的使用方法
    importrequestsimportjsondefget_access_token():url="https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=输入自己的id&client_secret=输入自己id的密码"payload=json.dumps("")headers={"Content-Type......
  • RPC--远程过程调用协议
    什么是RPC?RPC的全称是:RemoteProcedureCall,远程过程调用。它的作用就是允许一台机器上的程序去调用另一台机器上的程序,而不会意识到这个过程是远程的,也就是程序员不需要知道网络通信中的任何细节。为什么要使用RPC?提高开发效率:程序员不需要再关心网络中实现的细节,可以直接......
  • 如何优雅的编写Java接口(安全性,可重复调用,稳定性,追溯性)
    接口编写注意事项签名:对外提供的接口要做签名认证,认证不通过的请求不允许访问接口、提供服务。加密:敏感数据在网络传输过程中应该加密。IP白名单:限制请求的IP,增加IP白名单,一般在网关层处理。限流:尤其是对外提供的接口,无法保障调用的频率,应该做限流处理,保障接口服务正......
  • C语言杂谈:函数栈帧,函数调用时到底发生了什么
            我们都知道在调用函数时,要为函数在栈上开辟空间,函数后续内容都会在栈帧空间中保存,如非静态局部变量,返回值等。这段空间就叫栈帧。    当函数调用,就会开辟栈帧空间,函数返回时,栈帧空间就会被释放。这里的释放并非清空,而是让其无效化,可以后续的使用。1,......
  • P6419 COCI2014-2015#1 Kamp
    P6419COCI2014-2015#1Kamp换根\(dp\)的trick。题面钦定\(k\)个关键点,求每个点出发,访问完所有关键点的距离最小值。思路设\(g_u\)为从点\(u\)出发,访问完子树内所有关键点后,回到点\(u\)的距离最小值。\(s_u\)为点\(u\)子树内关键点个数,\(E(u,v)\)为边权。\[......