0x03. 运行状态切换与控制流转移
计算机运行时总会经历无数次的用户态与内核态之间的转换,进行无数次的控制流转移
- 用户进程往往需要使用内核所提供的各种功能,此时就需要通过系统调用等接口陷入内核,待任务完成之后再“着陆”回用户态
- 操作系统往往也需要“主动地”暂停当前进程的运行,让 CPU 陷入到内核态并获取控制权,这通常是因为有需要内核主动处理的事件(如外部中断)
通常而言,CPU 由用户态陷入到内核态主要有以下几种途径:
- 系统调用(通常通过 int 0x80/syscall/sysenter 指令)
- 异常 (例如进行了除 0 操作)
- 外设产生中断 (例如时钟中断)
- ...
主要的一个过程如下:
-
切换 GS 段寄存器:通过
swapgs切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用 -
保存用户态栈帧信息:将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里(由 GS 寄存器所指定的
percpu段),将 CPU 独占区域里记录的内核栈顶放入 rsp/esp -
保存用户态寄存器信息: 通过 push 保存各寄存器值到栈上,以便后续“着陆”回用户态
-
通过汇编指令判断是否为32位
-
控制权转交内核,执行相应的操作
由内核态重新“着陆”回用户态只需要恢复用户空间信息即可:
swapgs指令恢复用户态GS寄存器sysretq或者iretq系列指令让 CPU 运行模式回到 ring 3,恢复用户空间程序的继续运行
一、中断
中断即硬件/软件向 CPU 发送的特殊信号,CPU 接收到中断后会停下当前工作转而执行中断处理程序,完成中断处理后再重新恢复原工作流程
实模式下使用中断向量表(interrupt vector table,该表通常位于物理地址 0~1k 处)存放不同中断号对应的中断处理程序的地址,自保护模式起引入中断描述符表(Interrupt Descriptor Table)用以存放 「门描述符」(gate descriptor),中断描述符表地址存放在 IDTR 寄存器中,CPU 通过中断描述符表访问对应门
「门」(gate)可以理解为中断的前置检查物件,当中断发生时会先通过这些「门」,主要有如下三种门:
- 中断门(Interrupt gate):用以进行硬中断处理,其类型码为 110;中断门的 DPL(Descriptor Priviledge Level)为 0,故只能在内核态下访问,即中断处理程序应当由内核激活;进入中断门会清除 IF 标志位以关闭中断,防止中断嵌套的发生
- 陷阱门(Trap gate):类型码为 111,类似于中断门,主要用以处理 CPU 异常,但不会清除 IF 标志位
- 系统门(System gate):Linux 特有门,类型码为 3、4、5、128;其 DPL 为 3,用以供用户进程访问,主要用以进行系统调用(int 0x80)

二、系统调用
系统调用(system call)是由操作系统内核向上层应用程式提供的应用接口,操作系统负责调度一切的资源,当用户进程想要请求更高权限的服务时,便需要通过由系统提供的应用接口,使用系统调用以陷入内核态,再由操作系统完成请求
Windows系统下将系统调用封装在win32 API中,不过本篇博文主要讨论Linux
I. 系统调用表
所有的系统调用被声明于内核源码arch/x86/entry/syscalls/syscall_64.tbl 中,在该表中声明了系统调用的标号、类型、名称、内核态函数名称
在内核中使用 系统调用表(System Call Table) 对系统调用进行索引,该表中储存了不同标号的系统调用函数的地址,当进程进行系统调用时会根据该表寻找到不同系统调用对应的处理函数
II. 进行系统调用
Linux下的系统调用以eax/rax寄存器作为系统调用号,参数传递约束如下:
- 32 位:
ebx、ecx、edx、esi、edi、ebp作为第一个参数、第二个参数...进行参数传递 - 64 位:
rdi、rsi、rdx、r10、r8、r9作为第一个参数、第二个参数...进行参数传递
对于 32 位系统调用而言,Linux 实际上通过 0x80 号软中断实现系统调用的基本功能,即用户态程序通过 int 0x80 指令触发一个软中断陷入内核题,对应的中断处理程序会根据系统调用号在系统调用表中调用对应的处理

对于 64 位系统调用而言,CPU 提供了 syscall/sysenter 指令来替代开销巨大的中断,内核启动时会将系统调用函数入口(entry_SYSCALL_64)写入 MSR 寄存器组中,当执行 syscall/sysenter 指令时 CPU 会进入 ring0 并自动跳转到 MSR 寄存器所指定的系统调用入口中:
- 通过
swapgs指令将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用 - 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里(由 GS 寄存器所指定的
percpu段),将 CPU 独占区域里记录的内核栈顶赋给 rsp/esp 寄存器 - 将用户态各寄存器值到栈上形成一个
pt_regs结构体,以便后续“着陆”回用户态 - 根据系统调用号在系统调用表中取出对应的系统调用函数指针执行

III. 系统调用返回
同样地,内核执行完系统调用后退出系统调用也有对应的两种方式:
- 执行
iret汇编指令(其实就是中断返回的方式) - 执行
sysret汇编指令 / 执行sysexit汇编指令(only Intel)
三、信号机制
Signals 机制(又称之为软中断信号)是UNIX及类UNIX系统中的一种异步的进程间通信方式,用以通知一个进程发生了某个事件,通常情况下常见的流程如下图所示:

- 源程序通过
kill()系统调用向目标程序发送信号,该信号会被存储到进程描述符的信号队列当中 - ① 当目标进程重新被调度时内核首先会检查该进程的信号队列,若存在未处理信号则会进行信号处理流程:内核会将用户态进程的寄存器逐一压入【用户态进程的栈上】,形成一个
sigcontext结构体,接下来压入 SIGNALINFO 以及指向系统调用 sigreturn 的代码,用以在后续返回时恢复用户态进程上下文;压入栈上的这一大块内容称之为一个SigreturnFrame,同时也是一个ucontext_t结构体 - ② 控制权回到用户态进程,用户态进程跳转到相应的 signal handler 函数以处理不同的信号,完成之后将会执行位于其栈上的第一条指令——
sigreturn系统调用 - ③ 进程通过 sigreturn 系统调用重新陷入内核,恢复原有工作的用户态上下文信息
- ④ 控制权重新返还给用户态进程,恢复进程原上下文
需要注意的是信号处理机制并不是即时的,即发送信号并不能直接中断一个正在运行中的进程,但 Linux 进程调度的频率是非常频繁的,因此信号通常都能很快被处理