课程笔记

6.4软件中断与系统调用

阅读入口

特权级与系统调用

x86在硬件层面把代码的运行权限分成4级,编号0到3,通常称为保护环。当前代码所在的环记在段寄存器的CPL(当前特权级)字段中,CPU执行指令和访问内存时都按它检查权限。

特权级常称可执行的指令内存访问典型代码
Ring 0内核态包括特权指令在内的全部指令由页表及硬件配置控制内核核心代码:调度、内存管理、驱动
Ring 3用户态只有非特权指令由映射和权限控制浏览器、编辑器、Python解释器
Ring 1、Ring 2—介于两者之间介于两者之间原本为驱动等中间层预留,主流系统不使用

运行在Ring 0的代码处于内核态,运行在Ring 3的代码处于用户态。Ring 1和Ring 2的设计初衷是给设备驱动一类中间层使用,但现代Linux与Windows基本只用Ring 0和Ring 3,把需要特权的操作全部收进内核。

部分需要较高特权级才能执行的指令称为特权指令。修改页表、用cli关中断、用in和out访问I/O端口都属于这一类。用户态执行特权指令不是被忽略,而是由CPU报错,交给内核处理。

应用通常通过系统调用请求内核提供文件、网络等服务。具体接口和进入内核的机制取决于操作系统与体系结构。这种“用户态程序主动请求内核执行特权操作”的机制就是系统调用。内核给每项服务编一个系统调用号,调用方把号和参数放进约定的寄存器,内核据此分发到对应的处理函数。

系统调用是同步事件:它在执行到陷入指令的那一刻发生,与外部设备是否就绪无关。32位x86 Linux用int 0x80陷入,x86-64用syscall陷入。

教材用32位x86 Linux的int 0x80讲这段内容,保护环的划分与知识点一致。读定义时先分清“内核态能做什么”和“用户态被禁了什么”,后面五步流程才有落点。

一次系统调用的流程

以32位x86 Linux的int 0x80为例,寄存器的分工是固定的:

寄存器用途
EAX系统调用号,例如exit为1,write为4
EBX、ECX、EDX、ESI、EDI、EBP依次存放最多6个参数
EAX(返回时)返回值;失败时是负的错误码
  1. 准备参数

    应用程序或C库把系统调用号写入EAX,把参数写入EBX、ECX等寄存器。以write为例,三个参数分别是文件描述符、缓冲区地址和要写入的字节数。

  2. 触发陷入

    执行int 0x80,产生第128号中断(十六进制0x80等于十进制128)。这个中断在IDT(中断描述符表)中的门允许Ring 3的代码进入,所以用户态可以合法发起。

  3. 切换特权级并跳转

    CPU按IDT找到0x80对应的处理程序,保存用户态的CS、EIP、EFLAGS和栈指针,把CPL从3改为0,跳转到内核预定义的system_call入口。

  4. 内核分发并执行

    内核按EAX中的调用号查系统调用表,进入sys_write等具体实现。执行期间内核拥有完整的硬件控制权,可以访问设备寄存器和内核数据结构。

  5. 返回用户态

    内核把返回值或错误码写回EAX,执行iret恢复之前保存的CS、EIP、EFLAGS和栈,CPL回到3。程序从int 0x80的下一条指令继续执行。

现代指令

int 0x80要走完整的中断门流程:查IDT、检查门权限、保存现场,通常需要更多通用中断门处理步骤。AMD64的syscall和Intel的sysenter通过专门配置的寄存器直接进入内核入口,采用不同的专用入口机制。它们做的仍然是同一件事:用户态按约定放好调用号和参数,一条指令陷入,内核执行,再返回。

x86-64的约定与32位不同:调用号放在RAX,参数依次放在RDI、RSI、RDX、R10、R8、R9,返回值放在RAX,可使用sysret等返回路径,实际返回方式取决于内核实现。

这五步与6.3的中断处理共用“保存现场、恢复现场”的骨架,区别在于入口由程序主动执行指令触发,而不是设备发信号。

学习安排

6.3硬件中断与异常 把外部设备的I/O中断、CPU内部的时钟中断归为异步事件,把除零、缺页和系统调用归为同步的异常。这一节接着“陷阱 / 软中断”这一类往下讲,看用户态程序怎样主动进入内核请求服务;6.5进程 中Running → Blocked的状态转换,很多就是由系统调用触发的。

读的时候抓住一条主线:权限被切成Ring 0与Ring 3,用户态代码通过陷入指令把受限请求交给内核,内核执行完再返回。32位x86 Linux用int 0x80完成这次进入,x86-64使用syscall。

课程约定

  • 32位x86使用EAX、EBX、ECX,x86-64使用RAX、RDI、RSI等;答题时按题目给出的架构选择寄存器名。
  • 0x80是中断向量号,十六进制0x80等于十进制128。
  • iret是从中断处理程序返回用户态的通用指令;syscall的返回可使用sysret等路径,取决于内核实现。
  • “用户态 → 内核态 → 用户态”称为模式切换,它改变CPU的权限级别;进程上下文切换保存和恢复的是执行现场。

易错点

  • 系统调用不是函数调用。C库的printf、fopen是用户态函数,真正陷入内核的是它们内部调用的write、open。
  • int 0x80是陷入指令,不是系统调用本身;系统调用是这次陷入所请求的服务。
  • 用户态执行受限特权指令会触发硬件异常,后续由操作系统处理。判断一条指令是否只能在内核态执行,看它是否修改机器全局状态或直接访问硬件。
  • 系统调用进入内核不等于进程切换;调用期间若发生等待,调度器可能切换线程,之后调用线程才恢复。
  • 不要把中断都当成系统调用:时钟中断是异步的硬件事件,系统调用是同步的、由指令主动触发的陷入。