大家好,欢迎来到IT知识分享网。
特别说明:该文章前两天发布过,但一直在审核中。看头条网友说字数太多可能一直处于审核中状态,我把该文章拆分成几个章节发布,如影响阅读体验还请见谅。
八、系统调用处理程序
本文只会讲解正常的系统调用流程,涉及到调试、追踪及异常相关的处理,并没有涉及。另外代码比较长,全贴上去是为了让大家有一个全局视角,下面我们会逐句来分析。
/* * Register setup: * rax system call number * rdi arg0 * rcx return address for syscall/sysret, C arg3 * rsi arg1 * rdx arg2 * r10 arg3 (--> moved to rcx for C) * r8 arg4 * r9 arg5 * r11 eflags for syscall/sysret, temporary for C * r12-r15,rbp,rbx saved by C code, not touched. * * Interrupts are off on entry. * Only called from user space. * * XXX if we had a free scratch register we could save the RSP into the stack frame * and report it properly in ps. Unfortunately we haven't. * * When user can change the frames always force IRET. That is because * it deals with uncanonical addresses better. SYSRET has trouble * with them due to bugs in both AMD and Intel CPUs. */ ENTRY(system_call) CFI_STARTPROC simple CFI_SIGNAL_FRAME CFI_DEF_CFA rsp,KERNEL_STACK_OFFSET CFI_REGISTER rip,rcx /*CFI_REGISTER rflags,r11*/ SWAPGS_UNSAFE_STACK /* * A hypervisor implementation might want to use a label * after the swapgs, so that it can do the swapgs * for the guest and jump here on syscall. */ GLOBAL(system_call_after_swapgs) movq %rsp,PER_CPU_VAR(old_rsp) movq PER_CPU_VAR(kernel_stack),%rsp /* * No need to follow this irqs off/on section - it's straight * and short: */ ENABLE_INTERRUPTS(CLBR_NONE) SAVE_ARGS 8,0 movq %rax,ORIG_RAX-ARGOFFSET(%rsp) movq %rcx,RIP-ARGOFFSET(%rsp) CFI_REL_OFFSET rip,RIP-ARGOFFSET testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET) jnz tracesys system_call_fastpath: #if __SYSCALL_MASK == ~0 cmpq $__NR_syscall_max,%rax #else andl $__SYSCALL_MASK,%eax cmpl $__NR_syscall_max,%eax #endif ja badsys movq %r10,%rcx call *sys_call_table(,%rax,8) # XXX: rip relative movq %rax,RAX-ARGOFFSET(%rsp) /* * Syscall return path ending with SYSRET (fast path) * Has incomplete stack frame and undefined top of stack. */ ret_from_sys_call: movl $_TIF_ALLWORK_MASK,%edi /* edi: flagmask */ sysret_check: LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx andl %edi,%edx jnz sysret_careful CFI_REMEMBER_STATE /* * sysretq will re-enable interrupts: */ TRACE_IRQS_ON movq RIP-ARGOFFSET(%rsp),%rcx CFI_REGISTER rip,rcx RESTORE_ARGS 1,-ARG_SKIP,0 /*CFI_REGISTER rflags,r11*/ movq PER_CPU_VAR(old_rsp), %rsp USERGS_SYSRET64 END(system_call)
先来看下ENTRY、GLOBAL、END这三个宏,其中ENTRY、END定义在include/linux/linkage.h文件中,ENTRY宏中又引用了ALIGN宏。ALIGN与GLOBAL宏一起,定义在arch/x86/include/asm/linkage.h文件中。
// file: include/linux/linkage.h #define ALIGN __ALIGN #define ALIGN_STR __ALIGN_STR #define ENTRY(name) \ .globl name; \ ALIGN; \ name: #endif #define END(name) \ .size name, .-name
// file: arch/x86/include/asm/linkage.h #define GLOBAL(name) \ .globl name; \ name: #if defined(CONFIG_X86_64) || defined(CONFIG_X86_ALIGNMENT_16) #define __ALIGN .p2align 4, 0x90 #define __ALIGN_STR __stringify(__ALIGN) #endif
解释一下这些宏的内容:
- ENTRY宏定义了一个全局符号,并标明了地址对齐方式及符号的起始地址。
- GLOBAL宏的作用与ENTRY宏类似,只不过没有标明地址对齐方式。
- END宏定义了一个符号的字节大小。
- ALIGN宏定义了对齐方式,最终被扩展成.p2align 4, 0x90指令。该指令指示编译器按照16字节对齐(2^4),对齐产生的空洞使用字节0x90来填充。
- .size、.p2align等伪指令的详细说明,请参考 gas 官方文档。
继续往下看,会遇到一堆以CFI_开头的宏。这些宏,最后都会扩展到 cfi 相关的指令,这部分指令主要是用来调试、追踪用的,我们在本文中不会涉及到这些指令的细节,下文中遇到这些指令也会直接跳过。大家有兴趣的话,可以查阅 gas官方文档中 CFI-directives 这一节。
下一步是SWAPGS_UNSAFE_STACK,该宏定义在arch/x86/include/asm/irqflags.h头文件中,会扩展成swapgs指令:
// file: arch/x86/include/asm/irqflags.h #define SWAPGS_UNSAFE_STACK swapgs
我们来看一下 Intel SDM Volume 2B 中对swapgs指令的说明:
SWAPGS exchanges the current GS base register value with the value contained in MSR address C0000102H (IA32_KERNEL_GS_BASE). The SWAPGS instruction is a privileged instruction intended for use by system software.
When using SYSCALL to implement system calls, there is no kernel stack at the OS entry point. Neither is there a straightforward method to obtain a pointer to kernel structures from which the kernel stack pointer could be read. Thus, the kernel cannot save general purpose registers or reference memory.
By design, SWAPGS does not require any general purpose registers or memory operands. No registers need to be saved before using the instruction. SWAPGS exchanges the CPL 0 data pointer from the IA32_KERNEL_GS_BASE MSR with the GS base register. The kernel can then use the GS prefix on normal memory references to access kernel data structures. Similarly, when the OS kernel is entered using an interrupt or exception (where the kernel stack is already set up), SWAPGS can be used to quickly get a pointer to the kernel data structures.
该指令会交换当前 GS 基址寄存器和 IA32_KERNEL_GS_BASE 寄存器的值,交换后 GS 基址寄存器会指向内核的数据结构。
接下来,我们用GLOBAL定义了一个全局符号system_call_after_swapgs。从名字上也能看出来,它表示的是swapgs之后的系统调用执行过程。
GLOBAL(system_call_after_swapgs)
再往后的两条指令,先把用户空间的栈指针保存起来,然后用内核栈指针填充%rsp寄存器。之后,%rsp指向内核栈的栈顶位置,我们就可以访问内核栈了。
movq %rsp,PER_CPU_VAR(old_rsp) movq PER_CPU_VAR(kernel_stack),%rsp
再往下,使用ENABLE_INTERRUPTS打开中断,该宏定义在arch/x86/include/asm/irqflags.h文件中。与其一起定义的还有DISABLE_INTERRUPTS宏,该宏会禁止中断,我们下文中会遇到。
// file: arch/x86/include/asm/irqflags.h #define ENABLE_INTERRUPTS(x) sti #define DISABLE_INTERRUPTS(x) cli
接下来,SAVE_ARGS 8,0会将部分通用寄存器保存到内核栈中,其中SAVE_ARGS宏定义于arch/x86/include/asm/calling.h文件中。该宏有三个参数addskip、 save_rcx和save_r,其中addskip表示跳过的字节数,save_rcx指示是否保存 %rcx寄存器,save_r指示是否保存r8~r11这四个寄存器。从调用指令中可以看到,入参addskip为 8、save_rcx为 0,save_r参数未指定,按默认值1处理。
SAVE_ARGS宏中又引入了movq_cfi宏,该宏定义于arch/x86/include/asm/dwarf2.h文件中,其功能是把指定寄存器的值复制到栈中指定的偏移地址处。
// file:arch/x86/include/asm/dwarf2.h .macro movq_cfi reg offset=0 movq %\reg, \offset(%rsp) CFI_REL_OFFSET \reg, \offset .endm
SAVE_ARGS 8,0执行时,%rsp指针先向下移动9*8 + 8 = 80个字节,然后按地址从高到低依次填充%rdi、%rsi …… %r11寄存器的值。根据入参要求,有的寄存器值可以不保存,但空间会预留出来。
// file: arch/x86/include/asm/calling.h .macro SAVE_ARGS addskip=0, save_rcx=1, save_r=1 subq $9*8+\addskip, %rsp CFI_ADJUST_CFA_OFFSET 9*8+\addskip movq_cfi rdi, 8*8 movq_cfi rsi, 7*8 movq_cfi rdx, 6*8 .if \save_rcx movq_cfi rcx, 5*8 .endif movq_cfi rax, 4*8 .if \save_r movq_cfi r8, 3*8 movq_cfi r9, 2*8 movq_cfi r10, 1*8 movq_cfi r11, 0*8 .endif .endm
SAVE_ARGS 8,0指令执行完成后,内核栈的结构示意如下:
下一步,把%rax和%rcx寄存器的值保存到内核栈中。因为在执行syscall时,会把返回地址存入%rcx,所以%rcx会被破坏,我们要把它提前保存起来。%rax寄存器后面也会被修改,所以一起保存起来。
movq %rax,ORIG_RAX-ARGOFFSET(%rsp) movq %rcx,RIP-ARGOFFSET(%rsp)
ORIG_RAX、ARGOFFSET、ARGOFFSET这三个宏定义于arch/x86/include/asm/calling.h头文件中。这个文件主要是根据 x86 函数调用习惯,使用宏定义了调用时各通用寄存器在栈中的偏移量以及一些寄存器操作,比如刚才我们用到的SAVE_ARGS宏。
可以看到,该文件开头就描述了x86-64 函数调用习惯。在x86-64 函数调用中,%rdi、%rsi、%rdx、%rcx、%r8、%r9是作为参数传递用的,属于调用者保存的寄存器;另外%r10和%r11也是调用者保存的寄存器。%rbx、%rbp,%r12~%r15这6个寄存器是被调用者保存的。%rax和%rdx这两个寄存器是存放函数返回值的。所谓调用者保存,就是说在调用发生时,被调用方有权利破坏这些寄存器而不通知调用方。所以调用方为了保证调用返回后能顺利执行,就要自己来保存这些值。所谓被调用方保存,是指这些寄存器你可以随便用,但有一个前提,就是在返回前要把这些值复原。另外,也可以看到,在把寄存器值复制到内核栈时,其顺序和偏移量跟文件中定义的值是对应的。
ARGOFFSET宏定义值为48,与R11以一致,表示的是最后入栈的参数的偏移量。ORIG_RAX宏定义值为120,表示的是原%rax的保存位置。RIP宏为128,是指返回地址的偏移量。ORIG_RAX-ARGOFFSET计算后为72,所以movq %rax,ORIG_RAX-ARGOFFSET(%rsp)会把原始 %rax的值存入到 %rsp+72所指向的地址,也就是上图中的保留位置。RIP-ARGOFFSET计算结果为80,所以movq %rcx,RIP-ARGOFFSET(%rsp)会把原始%rcx的值存入到%rsp+80所指向的的地址。
// file: arch/x86/include/asm/calling.h /* x86 function call convention, 64-bit: ------------------------------------- arguments | callee-saved | extra caller-saved | return [callee-clobbered] | | [callee-clobbered] | --------------------------------------------------------------------------- rdi rsi rdx rcx r8-9 | rbx rbp [*] r12-15 | r10-11 | rax, rdx [] */ /* * 64-bit system call stack frame layout defines and helpers, * for assembly code: */ #define R15 0 #define R14 8 #define R13 16 #define R12 24 #define RBP 32 #define RBX 40 /* arguments: interrupts/non tracing syscalls only save up to here: */ #define R11 48 #define R10 56 #define R9 64 #define R8 72 #define RAX 80 #define RCX 88 #define RDX 96 #define RSI 104 #define RDI 112 #define ORIG_RAX 120 /* + error_code */ /* end of arguments */ /* cpu exception frame or undefined in case of fast syscall: */ #define RIP 128 #define CS 136 #define EFLAGS 144 #define RSP 152 #define SS 160 #define ARGOFFSET R11 #define SWFRAME ORIG_RAX
执行完成后,内核栈示意图如下:
继续往下,是测试和跳转指令。
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET) jnz tracesys
_TIF_WORK_SYSCALL_ENTRY宏(TIF 是 Thread Info Flag的缩写)和THREAD_INFO宏定义于arch/x86/include/asm/thread_info.h文件中。该文件定义了线程中使用到的标志位及基本操作。
// file: arch/x86/include/asm/thread_info.h #define KERNEL_STACK_OFFSET (5*8) /* work to do in syscall_trace_enter() */ #define _TIF_WORK_SYSCALL_ENTRY \ (_TIF_SYSCALL_TRACE | _TIF_SYSCALL_EMU | _TIF_SYSCALL_AUDIT | \ _TIF_SECCOMP | _TIF_SINGLESTEP | _TIF_SYSCALL_TRACEPOINT | \ _TIF_NOHZ) /* * Same if PER_CPU_VAR(kernel_stack) is, perhaps with some offset, already in * a certain register (to be used in assembler memory operands). */ #define THREAD_INFO(reg, off) KERNEL_STACK_OFFSET+(off)-THREAD_SIZE(reg)
TI_flags宏定义在include/generated/asm-offsets.h文件中,这个文件是由Kbuild自动生成的,定义了一些偏移常量。
// file: include/generated/asm-offsets.h #define TI_flags 16 /* offsetof(struct thread_info, flags) # */
THREAD_INFO宏中,又引用了THREAD_SIZE宏,THREAD_SIZE定义的是线程内核栈的大小,在文件arch/x86/include/asm/page_64_types.h中。可以看到,THREAD_SIZE是把PAGE_SIZE左移一位得到的,也就是说,THREAD_SIZE是PAGE_SIZE的2倍。PAGE_SIZE表示的是内存页的大小,该宏定义在arch/x86/include/asm/page_types.h文件中,其值通过计算为4096。所以,THREAD_SIZE的值为8192。到目前为止,我们计算出了内核栈的大小。
// file: arch/x86/include/asm/page_64_types.h #define THREAD_SIZE_ORDER 1 #define THREAD_SIZE (PAGE_SIZE << THREAD_SIZE_ORDER) #define CURRENT_MASK (~(THREAD_SIZE - 1))
// file: arch/x86/include/asm/page_types.h /* PAGE_SHIFT determines the page size */ #define PAGE_SHIFT 12 #define PAGE_SIZE (_AC(1,UL) << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE-1))
总结一下:
- _TIF_WORK_SYSCALL_ENTRY宏表示的是在进入系统调用追踪时,有哪些状态位要置位。
- THREAD_INFO宏根据传入的寄存器和偏移量,计算出 thread_info结构体的地址。
- TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)计算出线程状态
写到这里,可能会有同学带有疑问:为什么TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)能够计算出线程的状态呢。这涉及到进程(线程)的数据结构。Linux内核中使用thread_info结构体来存储线程的相关信息,thread_info结构体定义在arch/x86/include/asm/thread_info.h文件中。thread_info有个成员变量flags,表示的是线程的标志位。系统利用这些标志位来做一些特殊处理。thread_info符号本身表示结构体起始的地址,flags变量与起始地址之间有两个成员变量task和exec_domain,这两个变量都是8字节的指针,所以flags变量相对thread_info的偏移量为16,跟我们看到的的TI_flags宏定义是一致的。所以TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET)表示的是变量flags的地址。
// file: arch/x86/include/asm/thread_info.h struct thread_info { struct task_struct *task; /* main task structure */ struct exec_domain *exec_domain; /* execution domain */ __u32 flags; /* low level flags */ __u32 status; /* thread synchronous flags */ __u32 cpu; /* current CPU */ int preempt_count; /* 0 => preemptable, <0 => BUG */ mm_segment_t addr_limit; struct restart_block restart_block; void __user *sysenter_return; #ifdef CONFIG_X86_32 unsigned long previous_esp; /* ESP of the previous stack in case of nested (IRQ) stacks */ __u8 supervisor_stack[0]; #endif unsigned int sig_on_uaccess_error:1; unsigned int uaccess_err:1; /* uaccess failed */ };
thread_info并不孤单,它跟内核栈是共生的关系,这点从thread_union结构体中可以看到。thread_union结构体定义在include/linux/sched.h文件中,包含两个成员变量,一个是stack,一个就是thread_info。stack是一个数组,可以看到,其包含THREAD_SIZE个字节,也就是8192字节。另外,Linux内核中,用task_struct结构体来表示进程。task_struct结构体中,有一个成员变量stack,该变量是一个指针,会指向内核栈。
// file: include/linux/sched.h struct task_struct { ...... void *stack; ...... } union thread_union { struct thread_info thread_info; unsigned long stack[THREAD_SIZE/sizeof(long)]; };
thread_info、task_struct和内核栈的关系见下图:
回来继续说 testl指令,因为_TIF_WORK_SYSCALL_ENTRY以及thread_info里flags变量,都是32位整数,所以使用了带 l 后缀的的testl指令。 该指令会会对两个操作数做逻辑与(AND)运算,然后把执行结果丢弃,但会根据执行结果设置 SF、 ZF 和 PF 状态位。 所以这行代码通过testl指令判断线程状态信息里有没有设置跟踪、调试相关的状态位,有的话执行结果非0,ZF 位被清除,接下来的jnz tracesys会跳到tracesys去执行。在本文中,我们不关心追踪调试相关的流程,所以我们跳过这一部分。
testl $_TIF_WORK_SYSCALL_ENTRY,TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET) jnz tracesys
继续往下走,进入system_call_fastpath标签。进入该标签后,我们会执行以下几行代码。这段代码的作用,是判断系统调用号是否超出了最大值,如果超出了,跳转到badsys执行。
#if __SYSCALL_MASK == ~0 cmpq $__NR_syscall_max,%rax #else andl $__SYSCALL_MASK,%eax cmpl $__NR_syscall_max,%eax #endif ja badsys
__SYSCALL_MASK宏定义在arch/x86/include/asm/unistd.h文件中,根据系统配置选项CONFIG_X86_X32_ABI来决定。如果系统不支x32 ABI,那么该宏被扩展为(~0);否则被扩展(~(__X32_SYSCALL_BIT))。__X32_SYSCALL_BIT定义在arch/x86/include/uapi/asm/unistd.h中,其值为0x。__NR_syscall_max宏定义于include/generated/asm-offsets.h文件中,这是一个Kbuild编译时动态生成的文件。
// file: arch/x86/include/asm/unistd.h # ifdef CONFIG_X86_X32_ABI # define __SYSCALL_MASK (~(__X32_SYSCALL_BIT)) # else # define __SYSCALL_MASK (~0) # endif
/* x32 syscall flag bit */ #define __X32_SYSCALL_BIT 0x
// file: include/generated/asm-offsets.h #define __NR_syscall_max 542 /* sizeof(syscalls_64) - 1 # */
我们已经知道,在x86-64系统中,系统调用号是通过%rax寄存器来传递的(32位系统通过%eax来传递)。cmp指令会用第二个操作数减去第一个操作数,计算结果丢弃,但会根据计算结果设置 CF、OF、SF、ZF、AF 和 PF 状态位。ja指令会检查比较后的 ZF 和 CF 状态位,如果全为 0 就会执行跳转。同样的,对于异常处理本文不做解析。
继续往下,就会执行到下面两行代码:
movq %r10,%rcx call *sys_call_table(,%rax,8) # XXX: rip relative
虽然在执行syscall指令时,第四个参数要求使用%r10来传递。但是根据x86_64 ABI,按照 C 调用习惯,使用call指令进行函数调用时,第四个参数需要使用%rcx来传递,所以需要把第四个参数从 %r10复制到%rcx,然后才能发起函数调用。sys_call_table是系统调用表的地址,我们已经知道,系统调用表是一个数组,数组的每个元素都是一个8字节的指针,保存的是函数地址;另外,%rax里保存的是系统调用号;所以sys_call_table(,%rax,8)表示该系统调用号对应的函数入口地址。call *Operand是一个间接调用,表示操作数是从寄存器或内存中读出的。最终,call *sys_call_table(,%rax,8)会会切换到系统调用号对应的函数去执行。
函数调用完成后,程序会返回到arch/x86/kernel/entry_64.S继续执行。此时,被调用函数的执行结果已经保存到 %rax寄存器。接下来,会把 %rax的值 保存到内核栈对应的位置。
movq %rax,RAX-ARGOFFSET(%rsp)
接下来,进入ret_from_sys_call标签。
ret_from_sys_call: movl $_TIF_ALLWORK_MASK,%edi
_TIF_ALLWORK_MASKh宏定义在arch/x86/include/asm/thread_info.h文件中,表示在返回用户空间时需要处理的一些工作,比如说有信号等待处理(TIF_SIGPENDING)或者需要重新调度(TIF_NEED_RESCHED)等。
// file: arch/x86/include/asm/thread_info.h /* work to do on any return to user space */ #define _TIF_ALLWORK_MASK \ ((0x0000FFFF & ~_TIF_SECCOMP) | _TIF_SYSCALL_TRACEPOINT | \ _TIF_NOHZ)
然后进入sysret_check标签,表示系统调用返回前要做的一些检查工作。
sysret_check: LOCKDEP_SYS_EXIT DISABLE_INTERRUPTS(CLBR_NONE) TRACE_IRQS_OFF
这三个宏都定义在arch/x86/include/asm/irqflags.h文件中,LOCKDEP_SYS_EXIT宏的具体实现依赖于内核配置选项CONFIG_DEBUG_LOCK_ALLOC,它允许我们从系统调用返回时调试锁信息。本文不会涉及到调试相关的细节,所以略过。DISABLE_INTERRUPTS宏被直接扩展成cli指令,禁止中断。TRACE_IRQS_OFF宏的具体实现依赖于内核配置选项CONFIG_TRACE_IRQFLAGS,该宏跟中断追踪有关,本文暂略过。
// file: arch/x86/include/asm/irqflags.h #define DISABLE_INTERRUPTS(x) cli #ifdef CONFIG_TRACE_IRQFLAGS # define TRACE_IRQS_ON call trace_hardirqs_on_thunk; # define TRACE_IRQS_OFF call trace_hardirqs_off_thunk; #else # define TRACE_IRQS_ON # define TRACE_IRQS_OFF #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC # define LOCKDEP_SYS_EXIT ARCH_LOCKDEP_SYS_EXIT # define LOCKDEP_SYS_EXIT_IRQ ARCH_LOCKDEP_SYS_EXIT_IRQ # else # define LOCKDEP_SYS_EXIT # define LOCKDEP_SYS_EXIT_IRQ # endif
再接下来,会判断系统调用返回前,有没有需要处理的工作。movl把线程当前的标志位信息复制到%edx,然后与%edi进行逻辑与操作。%edi里保存的是返回前需要处理的标志位组合。andl指令执行后,如果结果为0,eflags里的ZF 位为1,表示没有额外的工作要处理;如果不为0,会清除ZF位, 说明有工作要处理。jnz指令会判断 ZF 标志位的值,ZF为0时,跳转到sysret_careful执行。sysret_careful处的执行流程本文不涉及。
movl TI_flags+THREAD_INFO(%rsp,RIP-ARGOFFSET),%edx andl %edi,%edx jnz sysret_careful
检查通过之后,会对寄存器和栈进行恢复。syscall指令会把返回地址保存到 %rcx,把 rflags 的值保存到%r11;sysret指令执行相反的操作,会用%rcx和%r11的值恢复%rip和rflags。所以,调用sysret之前,我们要先用保存在栈中的值去恢复%rcx和%r11;还要把%rsp恢复成用户空间的栈指针。
movq RIP-ARGOFFSET(%rsp),%rcx RESTORE_ARGS 1,-ARG_SKIP,0 /*CFI_REGISTER rflags,r11*/ movq PER_CPU_VAR(old_rsp), %rsp
movq RIP-ARGOFFSET(%rsp),%rcx恢复%rcx的值;RESTORE_ARGS 1,-ARG_SKIP,0使用保存到内核栈中值恢复各寄存器,其中%rcx的值在上一指令已经恢复过了,此处忽略未恢复;movq PER_CPU_VAR(old_rsp), %rsp恢复用户空间的栈指针。
RESTORE_ARGS宏定义于arch/x86/include/asm/calling.h文件,其又引入了movq_cfi_restore宏。movq_cfi_restore宏定义在arch/x86/include/asm/dwarf2.h文件中。
// file: arch/x86/include/asm/calling.h #define ARG_SKIP (9*8) .macro RESTORE_ARGS rstor_rax=1, addskip=0, rstor_rcx=1, rstor_r11=1, \ rstor_r8910=1, rstor_rdx=1 .if \rstor_r11 movq_cfi_restore 0*8, r11 .endif .if \rstor_r8910 movq_cfi_restore 1*8, r10 movq_cfi_restore 2*8, r9 movq_cfi_restore 3*8, r8 .endif .if \rstor_rax movq_cfi_restore 4*8, rax .endif .if \rstor_rcx movq_cfi_restore 5*8, rcx .endif .if \rstor_rdx movq_cfi_restore 6*8, rdx .endif movq_cfi_restore 7*8, rsi movq_cfi_restore 8*8, rdi .if ARG_SKIP+\addskip > 0 addq $ARG_SKIP+\addskip, %rsp CFI_ADJUST_CFA_OFFSET -(ARG_SKIP+\addskip) .endif .endm
// file: arch/x86/include/asm/dwarf2.h .macro movq_cfi_restore offset reg movq \offset(%rsp), %\reg CFI_RESTORE \reg .endm
主流程最后一步,执行到USERGS_SYSRET64。
USERGS_SYSRET64
该宏定义于arch/x86/include/asm/irqflags.h,会扩展成swapgs和sysretq。swapgs交换用户空间GS段和内核空间GS段的值,然后执行sysretq返回用户空间。
// file: arch/x86/include/asm/irqflags.h #define USERGS_SYSRET64 \ swapgs; \ sysretq;
至此,我们已经分析完了系统调用的主流程。总结一下,执行系统调用时主要有以下几个步骤:
- 在用户空间将系统调用号及参数传入指定的寄存器。
- 使用syscall从用户态切换到内核态,然后从入口system_call开始执行。
- 保存用户态栈指针,然后切换到内核栈。接着,将传参用通用寄存器、返回地址及调用号存入内核栈。
- 检查系统调用号,检查通过后,在系统调用表里根据调用号找到对应的函数入口,执行函数调用;否则在%rax里放入错误码-ENOSYS直接返回。
- 恢复现场,包括通用寄存器、rflags寄存器,返回地址,栈指针。
- 使用sysret从内核态返回到用户态。
关联链接:
Linux Kernel源码阅读: x86-64 系统调用实现细节(一)
Linux Kernel源码阅读: x86-64 系统调用实现细节(二)
九、参考文献:
1、Linux Inside-Syscall( https://0xax.gitbooks.io/linux-insides/content/SysCall/)
2、The Definitive Guide to Linux System Calls(https://blog.packagecloud.io/the-definitive-guide-to-linux-system-calls/)
3、Anatomy of a system call, part 1(https://lwn.net/Articles//)
4、Anatomy of a system call, additional content(https://lwn.net/Articles//)
5、《Linux 内核设计与实现》(原书第3版)
6、《x86汇编语言:从实模式到保护模式》
7、Intel 64 and IA-32 Architectures Software Developer Manuals(https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html)
8、x86_64 ABI(https://gitlab.com/x86-psABIs/x86-64-ABI)
9、gas 官方文档( https://sourceware.org/binutils/docs/as/index.html)
10、What is ":-!!" in C code?(https://stackoverflow.com/questions//what-is-in-c-code)
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/90455.html