Linux0.11系统调用原理及实验总结1系统调用的原理1.1概述系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。通过int0x80,就可使用内核资源。不过,通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数库中再通过int0x80进行系统调用。所以,系统调用过程是这样的:应用程序调用libc中的函数-libc中的函数引用系统调用宏-系统调用宏中使用int0x80完成系统调用并返回。另外一种访问内核的方式是直接添加一个系统调用,供自己的应用程序使用,这样就不再使用库函数了,变得更为直接,效率也会更高。1.2相关的数据结构在说具体的调用过程之前,这里先要说几个数据结构。1.2.1系统调用函数表系统调用函数表sys_call_table是在sys.h中定义的,它是一个函数指针数组,每个元素是一个函数指针,它的值是各个系统提供的供上层调用的系统函数的入口地址。也就是说通过查询这个表就可以调用软中断0x80所有的系统函数处理函数。1.2.2函数指针偏移宏这是一系列宏,它们的定义在unistd.h中,基本形式为#define_NR_namevalue,name为系统函数名字,value是一个整数值,是name所对应的系统函数指针在sys_call_table中的偏移量。1.2.3系统调用宏系统调用宏_syscalln(type,name)在内核的unistd.h文件中定义的,对它展开就是:typename(参数列表){调用过程;};其中,n为参数个数,type为函数返回值类型,name为所要调用的系统函数的名字。在unistd.h中共定义了4个这样的宏(n从0到3),也就是说,0.11核中系统调用最多可带3个参数。那么下面就说这个宏干了什么,也就是说上面的那个“调用过程”是怎么样的呢?在这个宏中嵌入了汇编代码,做的工作就是int0x80,其中将字符串“_NR_”和name连接,组成一个宏并将这个宏的值,也就是被调用的系统函数在sys_call_table中偏移量送到eax寄存器中;同时指明系统函数将来的返回值放到eax中。1.3系统调用处理过程下面我再说一下系统调用的核心软中断int0x80具体干了什么。这条指令会引起CPU的软件中断,cpu会根据中断号找到中断处理程序。这个中断处理程序是在System_call.s中。在中断处理程序的工作过程大致是这样的:1.3.1将寄存器ds,es,fs以及存有参数的edx,ecx,ebx入栈,再ds,es,指向内核段,fs指向用户段。1.3.2根据eax中的偏移值,在函数表sys_call_table中找到对应的系统函数指针(函数的入口地址)。并利用call指令调用系统函数,返回后,程序把返回值加入堆栈。1.3.3检查执行本次系统调用的进程的状态,如果发现由于某种原因原进程没处在就绪状态或者时间片到了,就会执行进程调度函数schedule()。1.3.4通过执行这次调用的程序的代码选择符判断它是不是普通用户程序,如果是就调用信号处理函数。若不是就直接弹出栈内容,并返回2添加一个系统调用的实验2.1实验内容在linux0.11版本中添加两个系统调用,并编写一个简单的应用程序测试它们。第一个系统调用是iam(),其原型为:intiam(constchar*name);完成的功能是将字符串参数name的内容拷贝到内核中保存下来。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。第二个系统调用是whoami(),其原型为:intwhoami(char*name,unsignedintsize);它将内核中由iam()保存的名字拷贝到name指向的用户地址空间中,同时确保不会对name越界访存(name的大小由size说明)。返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errno为EINVAL。2.2代码添加修改步骤2.2.1在unistd.h中添加系统调用接口#define__NR_whoami72#define__NR_iam73intwhoami(void);intaim(void);2.2.2在exit.c文件中添加系统调用处理函数的实现系统调用的函数可以在其他.c文件中添加或在新建文件中添加,只要编辑进image都是可以的,这里为了调试方便就在exit.c文件中添加了。#defineMAX23charN_MAX[26];intsys_whoami(char*name,unsignedintsize){if(strlen(N_MAX)size)return-EINVAL;inti;for(i=0;N_MAX[i]!='\0';i++){put_fs_byte(N_MAX[i],&name[i]);}returnstrlen(N_MAX);}intsys_iam(char*name){charc;charstr[100];memset(str,'\0',sizeof(str));inti;for(i=0;i=MAX;i++){if((c=get_fs_byte(&name[i]))!='\0'){str[i]=c;}elsebreak;}if((c!='\0')||(iMAX)){return-EINVAL;}memset(N_MAX,'\0',sizeof(N_MAX));for(i=0;str[i]!='\0';i++){N_MAX[i]=str[i];}returni;}2.2.3在system_call.s汇编代码中修改系统调用的个数#If0nr_system_calls=72#elsenr_system_calls=74#endif2.2.4测试代码的编写test.c的代码如下:#define__LIBRARY__#includeunistd.h_syscall1(int,iam,char*,name)_syscall2(int,whoami,char*,name,unsignedint,size)intmain(){inta=0;charbb[26]=champion;charcc[26]=;a=iam(bb);printf(a=%d,a);a=whoami(cc,8);printf(iam=%s\n,cc);return(1);}3系统调用相关代码的分析3.1初始化软件中断门。3.1.1函数调用层次初始化软件中断门,就是把0x80软件中断的处理函数system_call挂载到中断向量表idt中,以确保发生软件中断时会运行system_call函数,这个函数在system_call.s实现。初始化的流程如下:main()sched_init()set_system_gate(0x80,&system_call)_set_gate3.1.2初始化宏_set_gate的原型/*传入的四个参数说明如下:gate_addr=&idt[0x80]软件中断门的地址。type=15type为门类型dpl=3dpl为请求特权级addr=&system_call=0x7119通过查找system.map可以查到中断处理程序的地址*/#define_set_gate(gate_addr,type,dpl,addr)\__asm__(movw%%dx,%%ax\n\t\movw%0,%%dx\n\t\movl%%eax,%1\n\t\movl%%edx,%2\n\t\:\:i((short)(0x8000+(dpl13)+(type8))),\o(*((char*)(gate_addr))),\o(*(4+(char*)(gate_addr))),\d((char*)(addr)),a(0x00080000))3.1.3分析初始化宏_set_gate的实现__asm__格式为嵌入式汇编的格式,分析可知代码有5个传入的参数%0,%1,%2,%3,%4如下:%0,立即数i((short)(0x8000+(dpl13)+(type8)))这样,%%edx中高16位为addr的高16位,而低16位的P位为1(因为是0x8000),DPL位段为DPL(因为dpl3),而D位加上类型位段则为type(因为type8)其余各位皆为0%1是用内存地址,并且可以加偏移量o(*((char*)(gate_addr))),gate_addr=&idt[0x80]%2是用内存地址,并且可以加偏移量o(*(4+(char*)(gate_addr))),gate_addr=&idt[0x80]%3,edx做为参数d((char*)(addr)),&system_call=0x7119edx=&system_call=0x7119%4,eax做为参数a(0x00080000))eax=0x00080000__asm__格式为嵌入式汇编的格式,分析可知四条命令含义如下:movw%%dx,%%ax\n\t先将%%dx的低16位移入%%ax的低16位这样,在%eax中就形成了所需要的中断门的第一个长整数,其高16位为_KERNEL_CS,而低16位为addr的低16位。movw%0,%%dx\n\t字操作16位操作,操作低16位,高16位已经有调用函数的地址.movl%%eax,%1\n\t双字操作,为门的0--31位赋值movl%%edx,%2\n\t双字操作,为门的32--63位赋值3.2以_syscall1为例,分析系统调用入口宏的含义。其中_syscall1是一个宏,在include/unistd.h中定义。将_syscall1(int,close,int,fd)进行宏展开,可以得到:#define_syscall1(type,name,atype,a)\typename(atypea)\{\long__res;\__asm__volatile(int$0x80\:=a(__res)\:0(__NR_##name),b((long)(a)));\if(__res=0)\return(type)__res;\errno=-__res;\return-1;\}传入参数说明:其中type表示系统调用的返回值类型,name表示该系统调用的名称,atype、a分别表示第1个参数的类型和名称;可以有n个系统调用的传入参数,它们的数目和_syscall后面的数字一样大。调用接口宏含义说明:它先将宏__NR_##name存入EAX,将参数fd存入EBX,然后进行0x80中断调用。调用返回后,从EAX取出返回值,存入__res,再通过对__res的判断决定传给API的调用者什么样的返回值。__NR_##name就是系统调用的编号,在include/unistd.h中定义;在上面的例子中,我们添加了两个自己的系统调用接口,如下:#define__NR_whoami72#define__NR_iam733.3对_system_call函数的分析处理流程图处理流程分析_system_call:cmpl$nr_system_calls-1,%eax#调用号如果超出范围的话就在eax中置-1并退出。jabad_sys_callpush%ds#保存原段寄存器值。push%espush%fspushl%edx#ebx,ecx,edx中放着系统调用相应的C语言函数的调用参数。pushl%ecx#push%ebx,%ecx,%edxasparameterspushl%ebx#tothesystemcallmovl$0x10,%edx#setupds,estokernelspacemov%dx,%ds#ds,es指向内核数据段(全局描述符表中数据段描述符)。mov%dx,%esmovl$0x17,%edx#fspointstolocaldataspacemov%dx,%fs#f