# 系统调用

## 定义

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

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

## 性质

以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`等返回路径，实际返回方式取决于内核实现。

> [!NOTE]
> 系统调用进入内核与进程或线程切换是不同概念。调用过程中若需要等待，调度器可以运行其他线程；之后调用线程恢复并返回用户态。

## 例子

**用`int 0x80`调用`write`**

下面的片段把字符串写到标准输出，缓冲区地址用`msg`表示：

```asm title="write(1, msg, 5) via int 0x80"
mov eax, 4          ; 系统调用号：sys_write
mov ebx, 1          ; 参数1：文件描述符1是标准输出
mov ecx, msg        ; 参数2：缓冲区地址
mov edx, 5          ; 参数3：写入5个字节
int 0x80            ; 陷入内核，返回后EAX中是实际写入的字节数
```

`sys_write`执行完后，`EAX`中存放实际写入的字节数；出错时是一个负的错误码。用户态代码不需要知道页表怎么改、网卡寄存器在哪，它只负责按约定放好号和参数。

**`printf`与系统调用是什么关系**

`printf`是C标准库函数，不是系统调用。它先在用户态的缓冲区里把格式串和参数拼成文本，缓冲区满了或程序主动刷新时才调用`write`系统调用，把数据交给内核。因此一次`printf`可能对应零次、一次或多次系统调用，取决于缓冲区当时的状态。

## 交互探索

以写文件请求为例，逐步查看进入内核、处理请求与返回的过程。

交互演示：系统调用怎样进入内核并返回？。逐步查看应用请求、内核处理和返回值。 请在站点操作演示。

## 练习

1. `int 0x80`触发的第128号中断中，`0x80`对应的十进制中断号是 \_\_\_\_\_\_（number）。

   答案：128

   十六进制`0x80`等于 $8 \times 16 = 128$。

2. 在32位x86 Linux的`int 0x80`约定中，系统调用号和前三个参数分别放在哪里？

   * A.  调用号在`EAX`，参数在`EBX`、`ECX`、`EDX`
   * B.  调用号在`EBX`，参数在`EAX`、`ECX`、`EDX`
   * C.  调用号在`EAX`，参数在`ESP`、`EBP`、`EIP`
   * D.  调用号在`EDX`，参数在`EAX`、`EBX`、`ECX`

   答案：A

   调用号放`EAX`，参数依次用`EBX`、`ECX`、`EDX`、`ESI`、`EDI`、`EBP`；返回值也写回`EAX`。

3. 执行`int 0x80`后，CPU如何找到内核中的处理程序？

   * A.  查IDT中0x80号中断门，跳转到它登记的处理程序
   * B.  查页表，从虚拟地址换算物理地址后直接执行
   * C.  读取`EAX`指向的内存，把那里的内容当作指令执行
   * D.  由用户态程序直接跳转到内核入口地址

   答案：A

   中断号是IDT的下标，CPU用0x80找到登记的处理程序入口并切换特权级；用户态不能自己跳到内核地址。
