您的位置:首页 > 运维架构 > Linux

MOOC《Linux内核分析》——使用库函数API与C代码嵌入汇编完成同一个系统调用

2015-03-28 21:04 591 查看
许松原创,转载请注明出处。

《Linux内核分析》MOOC课程——http://mooc.study.163.com/course/USTC-1000029000

一般情况下,应用程序通过应用编程接口(API)而不是直接通过系通过调用来进行编程。透过系统提供的AP,应用程序可以访问硬件设备以及其他操作系统资源。操作系统通过提供API的方式来避免应用程序不正当地使用硬件设备、错误地访问其他进程的资源等行为,保证了应用程序不会破坏系统本身的稳定性;同时,API也是用户空间访问操作系统内核的唯一手段:除了异常与陷入之外,它们是内核唯一的合法入口。

我们知道CPU对于指令的读取都是通过cs:eip这两个寄存器来完成的,其中cs寄存器是代码段选择寄存器,它的最低两位表明了当前代码的特权级别。一般而言,现代CPU都具有几种不同的指令执行级别:在高执行级别下,代码可以执行特权指令,访问任意的物理地址,这种CPU执行级别就对应内核态;而低级别执行状态下,代码的掌控范围就会受到限制,只能在对应级别允许范围内活动(Linux中0xc0000000以上的地址空间(逻辑地址)只能在内核状态下访问)。

由于处于用户态的程序无法直接执行内核代码,因此它们必须以某种方式通知系统,告诉内核自己希望切换到内核态以调用内核函数。现代操作系统是通过软中断机制实现这一过程的:通过引发一个异常来使系统切换到内核态去执行异常处理程序(也就是系统调用处理程序)。

x86系统上的软中断由int $0x80指令产生。既然提到中断,那么必然要提到中断处理过程:

int $0x80
SAVE_ALL    //保存现场
//...   内核代码,完成中断服务
RESTORE_ALL //恢复现场
iret        //将int $0x80指令保存的寄存器的值出栈


其中,int $0x80指令将会保存一些寄存器的值(包括用户态栈顶地址、当时的状态字、用户态的cs:eip值等)到内核堆栈中,同时将相应中断服务程序的入口地址加载到cs:eip中,将内核堆栈的栈帧加载到ss:esp中。通过前面的学习我们可以知道当些动作完成之后,CPU开始操作内核栈,便进入内核态。当中断返回的时候iret指令将CPU恢复到用户态栈帧上面继续执行,从而切换回用户态。

那么软中断又是如何知道应该调用哪一个内核函数呢?在Linux中,每个系统调用被赋予一个系统调用号,通过这个系统调用号,系统便可以知道到底要调用哪一个内核函数了。内核记录了系统调用表中的所有已经注册过的系统调用列表,存储在system_call_table中。当应用程序由于int $0x80指令而陷入内核态的时候,中断处理函数(名为syst
4000
em_call())开始发挥作用。system_call函数会根据传入的系统调用号(在x86上系统调用号是通过eax寄存器传递给内核的)来调用对应的内核函数,从而完成相应的系统调用。



除了系统调用号之外,大部分的系统调用还需要传入一些外部参数,因此中断发生时必须将这些参数也传递给内核。x86系统上,寄存器ebx,ecx,edx,esi以及edi按照顺序存放前五个参数。对于6个或者6个以上的参数,应当用一个单独的寄存器存指向所有这些参数在用户空间的地址(也就是通过一次间接寻址来达到目的)。给用户空间的返回值也通过寄存器传递,x86上这个值被存放在eax寄存器中。

在有了以上知识的铺垫和C语言内嵌汇编代码的常识之后,我们便可以完成本次课程的目标——用API调用和C内嵌汇编代码实现系统调用了。这里选择了系统调用mkdir作为实验对象,调用该函数可以创建一个新的目录,函数的描述如下:

#include <sys/types.h>
#include <sys/stat.h>

int mkdir(const char* filename, mode_t mode);

参数filename表示新目录的名称,mode表示新目录的权限。
函数返回0表示创建文件夹成功,返回-1则表示出错。


下面是实际代码:

#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>

int main(void)
{
int result = -1;
int dir_mode = S_IRWXU;
const char* dir_name_api = "MadeByAPI";
const char* dir_name_asm = "MadeByASM";

if(0 != mkdir(dir_name_api,dir_mode))
printf("API Failed!");
else
printf("New Dir By API.\n");

result = -1;
asm volatile(
"mov %1,%%ebx\n\t"
"mov %2,%%ecx\n\t"
"mov $0x27,%%eax\n\t"
"int $0x80\n\t"
"mov %%eax,%0\n\t"
:"=m" (result)
:"c"(dir_name_asm),"d"(dir_mode)
);

if(0 != result)
printf("ASM Failed!\n");
else
printf("New Dir By ASM.\n");

return 0;
}


汇编部分出现的0x27是mkdir的系统调用号,系统调用列表参见:http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl

下面是结果截图:

运行上述代码之前:


运行上述代码之后,红框中出现了新创建的两个文件夹:


整个系统调用的过程可以表述为:

应用程序在用户态调用API。

API函数将自己对应的系统调用号以及从应用程序得到的参数保存到寄存器中,并触发软中断,使应用程序陷入内核态。

system_call()函数根据传入的系统调用号在系统调用列表中查找对应的内核函数,并根据寄存器中保存的参数调用该内核函数。

内核函数执行完毕之后将执行结果存放到eax寄存器中。

中断处理程序执行完毕之后返回,将应用程序带回用户态。

被调用的API在最后阶段返回eax中的值并结束API调用。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐