- 问题描述
通过这一周的学习,我们初步学习了操作系统中一个非常重要的概念——系统调用,同时学习了保证操作系统可以正常运行的法宝,系统调用的三层机制,以及系统调用的实现过程。下面通过使用库函数API和C代码中嵌入汇编代码触发一个系统调用函数来具体阐述这种机制。
2. 解决步骤
2.1 系统调用三层机制简述
2.1.1 用户态,内核态和中断
宏观上linux操作系统的体系架构分为用户态和内核态,计算机的硬件资源有限,为了减少有限资源的访问和冲突,CPU和操作系统必须提供一些机制对用户程序进行权限划分。现代的CPU一般都有几种不同的指令执行级别,就是什么样的程序执行什么样的指令是有权限的。在高的执行级别下,代码可以执行特权指令,访问任意的物理内存,这时CPU的执行级别对应的就是内核态,对所有的指令包括特权指令都可以执行。相应的,在用户态(低级别指令),代码能够掌控的范围就会受到限制。
在linux操作系统中,用户态和内核态很显著的区分方法就是CS:EIP的指向范围,在内核态时,CS:EIP的值可以是任意的地址,例如在32位的x86机器上有4GB的进程地址空间,内核态下的4GB的地址空间全都可以访问。但在用户态下,只能访问0x00000000~0xbfffffff的地址空间。
系统调用也是一种中断(中断处理是从用户态进入内核态的主要方式)。一般来说,进入内核态是由中断触发的,可能是硬件中断,在用户态进程执行时,硬件中断信号到来,进入内核态,就会执行这个中断对应的中断服务例程。也可能是用户态程序执行的过程中,调用了一个系统调用,陷入了内核态,叫做Trap(系统调用只是特殊的中断)。这时就会有从用户态到内核态的寄存器上下文保存起来,同时要把内核态的寄存器的值放到当前CPU中。
2.1.2 系统调用概述
系统调用的意义是操作系统为用户态进程与硬件设备进行交互提供了一组接口。
1. 操作系统提供的API和系统调用的关系
系统调用的库函数就是操作系统提供的API(应用程序编程接口),API是函数定义。系统调用通过软中断向内核发出中断请求,int指令(32位操作系统)的执行就会触发一个中断请求。libc函数库定义的一些API内部使用了系统调用的封装例程,其主要目的是发布系统调用,使程序员在写代码时不需要用汇编指令和寄存器传递参数来触发系统调用。一般每个系统调用对应一个系统调用的封装例程,函数库再用这些封装例程定义出给程序员调用的API,这样把系统调用最终封装成方便程序员使用的库函数。
2.系统调用三层机制
如图所示,User Mode表示用户态,Kernel Mode表示内核态。xyz()就是一个API函数,是系统调用对应的API,其中封装了一个系统调用,会触发中断指令,对应system_call内核代码的起点,即中断向量对应的中断服务程序入口,内部会有sys_xyz()系统调用处理函数,执行完sys_xyz()后会ret from_sys_call,这里是进程调度最常见的调度时间点。如果没有发生进程调度,就会再返回到用户态接着执行。
2.2编写代码触发一个系统调用函数
在这里我选择的系统调用为107号系统调用,geteuid(这里使用的是64位操作系统,32位操作系统该系统调用号为49)。geteuid()用来取得执行目前进程有效的用户ID。有效用户ID(EUID)是用户最初执行程序时所用的ID,表示该ID是程序的所有者 。
2.2.1 使用库函数API触发一个系统调用
编写代码如下
64位操作系统中geteuid函数定义与头文件如下
头文件:#include <unistd.h> #include <sys/types.h>
定义函数:uid_t geteuid(void);
编译执行结果如下
可以看到euid此时的值为1000。
2.2.2 C代码中嵌入汇编代码触发一个系统调用函数
编写代码如下
重点阐述汇编代码段
"mov $0,%%rbx
"
把寄存器RBX清零,系统调用传递第一个参数使用EBX寄存器为0。
"mov $0x6b,%%rax
"
把0x6b放到了RAX寄存器里面,RAX寄存器用于传递系统调用号,RAX寄存器用于传递系统调用号,这里十六进制的6b是107,系统调用号为107。
"syscall
"
"syscall"是触发系统调用陷入内核执行107号系统调用的内核处理函数(32位操作系统指令位“int $0x80"。
"mov %%rax,%0
"
系统调用会有一个返回值,通过RAX寄存器返回,把RAX寄存器的值放入到变量a中。
编译执行代码
与使用库函数API所得的值相同。
3. 总结
通过这一周的学习,我更进一步的了解了操作系统的工作机制,对系统调用的三层机制有了一个初步的了解,在接下来的学习中,将更深入地学习系统调用的三层机制的内容。