系统调用
定义
应用通常通过系统调用请求内核提供文件、网络等服务。具体接口和进入内核的机制取决于操作系统与体系结构。这种“用户态程序主动请求内核执行特权操作”的机制就是系统调用。内核给每项服务编一个系统调用号,调用方把号和参数放进约定的寄存器,内核据此分发到对应的处理函数。
系统调用是同步事件:它在执行到陷入指令的那一刻发生,与外部设备是否就绪无关。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(返回时) | 返回值;失败时是负的错误码 |
-
准备参数
应用程序或C库把系统调用号写入
EAX,把参数写入EBX、ECX等寄存器。以write为例,三个参数分别是文件描述符、缓冲区地址和要写入的字节数。 -
触发陷入
执行
int 0x80,产生第128号中断(十六进制0x80等于十进制128)。这个中断在IDT(中断描述符表)中的门允许Ring 3的代码进入,所以用户态可以合法发起。 -
切换特权级并跳转
CPU按IDT找到0x80对应的处理程序,保存用户态的
CS、EIP、EFLAGS和栈指针,把CPL从3改为0,跳转到内核预定义的system_call入口。 -
内核分发并执行
内核按
EAX中的调用号查系统调用表,进入sys_write等具体实现。执行期间内核拥有完整的硬件控制权,可以访问设备寄存器和内核数据结构。 -
返回用户态
内核把返回值或错误码写回
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等返回路径,实际返回方式取决于内核实现。
例子
用int 0x80调用write
下面的片段把字符串写到标准输出,缓冲区地址用msg表示:
mov eax, 4 ; 系统调用号:sys_writemov ebx, 1 ; 参数1:文件描述符1是标准输出mov ecx, msg ; 参数2:缓冲区地址mov edx, 5 ; 参数3:写入5个字节int 0x80 ; 陷入内核,返回后EAX中是实际写入的字节数sys_write执行完后,EAX中存放实际写入的字节数;出错时是一个负的错误码。用户态代码不需要知道页表怎么改、网卡寄存器在哪,它只负责按约定放好号和参数。
printf与系统调用是什么关系
printf是C标准库函数,不是系统调用。它先在用户态的缓冲区里把格式串和参数拼成文本,缓冲区满了或程序主动刷新时才调用write系统调用,把数据交给内核。因此一次printf可能对应零次、一次或多次系统调用,取决于缓冲区当时的状态。
交互探索
以写文件请求为例,逐步查看进入内核、处理请求与返回的过程。
系统调用怎样进入内核并返回?
逐步查看应用请求、内核处理和返回值。
正在准备演示…
练习
int 0x80触发的第128号中断中,0x80对应的十进制中断号是 。参考解答
十六进制
0x80等于 。在32位x86 Linux的
int 0x80约定中,系统调用号和前三个参数分别放在哪里?参考解答
调用号放
EAX,参数依次用EBX、ECX、EDX、ESI、EDI、EBP;返回值也写回EAX。执行
int 0x80后,CPU如何找到内核中的处理程序?参考解答
中断号是IDT的下标,CPU用0x80找到登记的处理程序入口并切换特权级;用户态不能自己跳到内核地址。