-------------------------------------------------------------------
使用库函数API和C代码中嵌入汇编代码两种方式使用同一个系统调用
-------------------------------------------------------------------
这个实验要求使用一个系统调用,那我们应该弄清楚:什么是系统调用?为什么要系统调用?
一、系统调用
简单讲,系统调用linux内核中自带的一组用于实现系统功能的子程序,与普通的函数调用非常相似,只不过,为了安全起见,系统调用只有在内核态下才能进行。
系统调用的原因也很简单:
系统调用在用户空间进程和硬件设备之间添加了一个中间层。这就是计算机行业中那句著名的话:“计算机科学领域的任何问题都可以通过增加一个中间层来解决”。
该层主要作用有三个:
(1)它为用户空间提供了一种统一的硬件的抽象接口。比如当需要读些文件的时候,应用程序就可以不去管磁盘类型和介质,甚至不用去管文件所在的文件系统到底是哪种类型。
(2)系统调用保证了系统的稳定和安全。作为硬件设备和应用程序之间的中间人,内核可以基于权限和其他一些规则对需要进行的访问进行裁决。举例来说,这样可以避免应用程序不正确地使用硬件设备,窃取其他进程的资源,或做出其他什么危害系统的事情。
(3)每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口,也是出于这种考虑。如果应用程序可以随意访问硬件而内核又对此一无所知的话,几乎就没法实现多任务和虚拟内存,当然也不可能实现良好的稳定性和安全性。
在Linux中,系统调用是用户空间访问内核的惟一手段;除异常和中断外,它们是内核惟一的合法入口。
而kernel留给用户层的接口其实就只有一个:软中断(int 0x80)。用户态通过它来陷入内核态,完成系统调用。为了方便使用,kernel与用户层之间又增加了API与一些库(例如libc库)来封装这一过程。因此,目前的资料里大部分都写,调用一个系统调用有三种方法:
(1)通过 glibc 提供的库函数
(2)使用 syscall 函数直接调用
(3)通过 int 0x80指令陷入
这种说法只是使用系统调用的人总结的,并不准确(前两个本质上是也是由第三个实现的),大家一定要记住,只有软中断才是用户态到内核态转变的真正入口。而从内核态转变用户态的真正出口是iret。
二、API/libc与系统调用的关系
既然API与libc对软中断进行了封装,那我们先一起看看它们之间的具体关系。一般情况下,应用程序通过应用编程接口(API)而不是直接通过系统调用来编程。这点很重要,因为应用程序使用的这种编程接口实际上并不需要和内核提供的系统调用一一对应。一个API定义了一组应用程序使用的编程接口。从程序员的角度看,系统调用无关紧要,他们只需要跟API打交道就可以了。相反,内核只跟系统调用打交道;库函数及应用程序是怎么使用系统调用不是内核所关心的。
三、系统调用实现机制
与调用函数一样,系统调用也需要输入输出参数。
每个系统调用至少有一个参数,即系统调用号(由eax传递),其他参数依次由ebx、ecx、edx、esi、edi、ebp传入。
由于使用寄存器传递参数,因此对参数的长度做了限制:
(1)每个参数的长度不能超过寄存器的长度,即32位
(2)在系统调用号(eax)之外,参数的个数不能超过6个(ebx、ecx、edx、esi、edi、ebp)如果超过六个,可以传入一个地址,地址所在地存放多个参数(原理有些类似于Java中对String的保存)。
系统调用的三个层次依次是:xyz函数(API)、system_ call(中断向量)和 sys_ xyz(中断服务程序)。
四、开始实验
这次实验以一个较简单的系统调用getpid(系统调用号为0x14)为例,先写两个c文件,分别为api.c和asm.c具体代码如下:
编译运行,两个程序的执行与结果都符合我们的预期。
现在我们不妨用gdb调试一下,来亲自看看API的调用是否就是像我们之前分析的对系统调用的封装那样执行的。
在getpid处设置断点,运行到断点处。
使用ni命令,对汇编指令逐条运行。
前面的清理寄存器的值此处先不讨论,直接一直ni运行到传递系统调用号处(箭头所指的为下一条将要运行的指令,所以此时系统调用号还未传入)。
利用info r命令来查看当前寄存器的值(系统调用号还未传入,eax还为0)。
ni一下,再次查看,发现系统调用号已经传入,确实是利用eax的。
再ni一下,此时该系统调用的实际工作已经完成了,结果(也就是pid)保存在eax中。
一直ni,直到从系统调用里回到main中。可以看到,系统调用的结果确实由eax传出,然后交给printf函数打印。
五、总结
经过这次实验,我们可以看到:
(1)要查看系统资源(如pid等),只有处于内核态(0级)的时候才可以。
(2)用户要从用户态(3级)切换到内核态(0级),就要通过软中断(int 0x80)来进行系统调用。
(3)通过eax传递系统调用号,然后由system_call交给system_service完成工作。
(4)system_service完成工作后,结果又由eax传递给用户态堆栈。
大致过程可以用下图表示:
-----------------------------END--------------------------------
刘建鑫 + 原创作品转载请注明出处 + 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000
------------------------------------------------------------------