系统调用的三层机制
基础知识
- 用户态、内核态和中断
- 用户态:在低的执行级别下,代码能够掌控的范围有所限制,只能访问部分内存。
- 内核态:在高的执行级别下,代码可以执行特权指令,访问任意的物理内存。
- 中断:从用户态进入内核态的主要方式。
- 中断类别
- 硬件中断:在用户态进程执行时,硬件中断信号到来,进入内核态,就会执行这个中断对应的中断服务例程。
- 软中断:在用户态进程执行过程中,调用了一个系统调用(一种特殊中断),进入内核态。
•寄存器上下文切换
当用户态切换到内核态时,就要把用户态寄存器上下文保存起来,同时把内核态的寄存器的值放到当前CPU中。int指令触发中断机制会在堆栈上保存一些寄存器的值,会保存用户态栈顶地址、当时的状态字、当时的CS:EIP的值。同时会将内核态的栈顶地址、内核态的状态字放入CPU对应的寄存器,并且CS:EIP寄存器的值会指向中断处理程序的入口,对于系统调用来说是指向system_call。int指令或中断信号发生之后,第一件事就是保存现场,进入中断处理程序,执行SAVE_ALL。中断处理程序结束后,中断处理结束前的最后一件事是恢复现场,执行RESTORE_ALL。
•API和系统调用关系 •API:应用程序编程接口,只是函数定义。
•系统调用:是通过软中断向内核发出了中断请求,int指令的执行就会触发一个中断请求。
libc函数库定义的一些API内部使用了系统调用的封装例程,其主要目的是发布系统调用,使程序员在写代码时不需要用汇编指令和寄存器传递参数来触发系统调用。一个API可能只对应一个系统调用,亦可能内部由多个系统调用实现,一个系统调用也可能被多个API调用。
•Intel x86 CPU定义了4种不同的执行级别0、1、2、3,数字越小特权越高。Linux系统采用了其中的0、3两个特权级别。
使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
方法一:使用API
- 创建hello20199306.c并编译
- 运行编译的程序
方法二:使用C内嵌汇编代码
- 代码如下
int main(){
char* msg = "hello";
int len = 11;
int result = 0;
__asm__ __volatile__("movl %2, %%edx;
" /*传入参数:要显示的字符串长度*/
"movl %1, %%ecx;
" /*传入参赛:文件描述符(stdout)*/
"movl $1, %%ebx;
" /*传入参数:要显示的字符串*/
"movl $4, %%eax;
" /*系统调用号:4 sys_write*/
"int $0x80" /*触发系统调用中断*/
:"=m"(result) /*输出部分:本例并未使用*/
:"m"(msg),"r"(len) /*输入部分:绑定字符串和字符串长度变量*/
:"%eax");
return 0;
}
-
编译程序
-
运行
总结
Linux 下的系统调用是通过中断(int 0x80)来实现的。在执行 int 80 指令时,寄存器 eax 中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器 ebx,ecx,edx,esi,edi 中,当系统调用完成之后,返回值可以在寄存器 eax 中获得。
所有的系统调用功能号都可以在文件 /usr/include/bits/syscall.h 中找到,为了便于使用,它们是用 SYS_
ssize_t write(int fd, const void *buf, size_t count);
该函数的功能最终是通过 SYS_write 这一系统调用来实现的。根据上面的约定,参数 fb、buf 和 count 分别存在寄存器 ebx、ecx 和 edx 中,而系统调用号 SYS_write 则放在寄存器 eax 中,当 int 0x80 指令执行完毕后,返回值可以从寄存器 eax 中获得。