说明
欧长坤 原创作品转载请注明出处 《Linux内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000 这学期学校恰好有操作系统的课程,上个学习就开始寻思研究研究Linux内核代码,恰好MOOC有这个课程,遂选了此课。
一、准备工作
本周的实验比起前三周的实验稍微容易得多。我们可以在http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl中查看系统调用号。
二、分析
在这里,我们决定使用sysinfo这个库函数API,首先,我们得会使用这个API。
在 Linux 中,sysinfo可以用来获取系统相关信息的结构体。 函数声明和原型如下所示:
|
|
那么,这个sysinfo的结构体长什么样?
|
|
其实我们都并不关心这个sysinfo的结构到底长什么样,我们目前所关心的是如何能够成功的调用。 实际上看到这里,我们已经能够完成使用库函数调用的C代码了。
那么怎么用汇编来实现呢? 我们得知道sysinfo的系统调用号是多少,容易知道sysinfo的系统调用号是116。所以,嵌入汇编时的值应该为0x74。
三、实验过程
我们先写好使用库函数API调用的版本,使用vi来编辑一个syscall.c的代码:
|
|
代码如下:
|
|
接下来编译和执行都不用说,然后就是输出结果:
|
|
至此,我们完成了一个使用库函数调用的版本。
好,那么我们现在要来编写嵌入汇编的版本。
正常情况下,我们一般会对老师编写time函数的汇编版本产生下面的疑惑: 在老师的代码中,
|
|
最开始这一行: mov $0,%%ebx
为什么要将%%ebx
清零呢?
事实上在系统调用时,system_call是linux
系统调用的入口点。每个系统调用至少有一个参数,那就是eax
,它负责传递系统调用号,同时获取返回值。
除了eax
外,还允许至多6个参数,分别是ebx,ecx,edx,esi,edi,ebp
。
另一方面,容易观察到,实际上time()
函数除了自身的传入系统调用号(同时接收返回值)外,还传入了一个参数NULL
。
结合上面的叙述,应该可以猜到,其实代码mov $0,%%ebx\n\t
是相当于向ebx
传入了一个参数NULL
,也就是0
。
同理,对于sysinfo
这个库函数API,它也有一个返回值,表示是否成功,并且传递进来一个参数sys_info
来接收系统的相关信息。因此,我们可以编写下面的代码:
|
|
所以,最后的执行结果我们可以看到,是一样的:
|
|
四、总结
我们来总结一下:
一般的,进程是不能访问内核的。它不能访问内核所占内存空间也不能调用内核函数。CPU硬件决定了这些(这就是为什么它被称作"保护模式")。系统调用是这些规则的一个唯一例外。其原理是进程先用适当的值填充寄存器,然后调用一个特殊的指令(系统调用号),这个指令会跳到一个事先定义的内核中的一个位置(显然,这个位置是用户进程可读但是不可写的)。在Intel CPU中,这个由中断0x80实现。硬件知道一旦你跳到这个位置,你就不是在限制模式下运行的用户,而是作为操作系统的内核,所以你就可以为所欲为了。
进程可以跳转到的内核位置叫做sysem_call
。这个过程检查系统调用号,这个号码告诉内核进程请求哪种服务。然后,它查看系统调用表(sys_call_table
)找到所调用的内核函数入口地址。接着,就调用函数,等返回后,做一些系统检查,最后返回到进程(或到其他进程,如果这个进程时间用尽)。
调用内核的参数传递最多涉及七个寄存器:eax,ebx,ecx,edx,esi,edi,ebp
。其中,eax用来传入系统调用号,并用来接收返回值。其他六个参数作为函数的输入或输出参数被传递进去。