linux0.11系统调用原理及实验总结由刀豆文库小编整理,希望给你工作、学习、生活带来方便,猜你可能喜欢“编译原理实验报告总结”。
Linux0.11系统调用原理及实验总结
系统调用的原理
1.1 概述
系统调用是一个软中断,中断号是0x80,它是上层应用程序与Linux系统内核进行交互通信的唯一接口。通过int 0x80,就可使用内核资源。不过,通常应用程序都是使用具有标准接口定义的C函数库间接的使用内核的系统调用,即应用程序调用C函数库中的函数,C函数库中再通过int 0x80进行系统调用。
所以,系统调用过程是这样的:
应用程序调用libc中的函数->libc中的函数引用系统调用宏->系统调用宏中使用int 0x80完成系统调用并返回。
另外一种访问内核的方式是直接添加一个系统调用,供自己的应用程序使用,这样就不再使用库函数了,变得更为直接,效率也会更高。
1.2 相关的数据结构
在说具体的调用过程之前,这里先要说几个数据结构。
1.2.1 系统调用函数表
系统调用函数表sys_call_table是在sys.h中定义的,它是一个函数指针数组,每个元素是一个函数指针,它的值是各个系统提供的供上层调用的系统函数的入口地址。也就是说通过查询这个表就可以调用软中断0x80所有的系统函数处理函数。
1.2.2 函数指针偏移宏
这是一系列宏,它们的定义在unistd.h中,基本形式为#define _NR_name value,name为系统函数名字,value是一个整数值,是name所对应的系统函数指针在sys_call_table中的偏移量。
1.2.3 系统调用宏
系统调用宏_syscalln(type,name)在内核的unistd.h文件中定义的,对它展开就是: type name(参数列表){ 调用过程; }; 其中,n为参数个数,type为函数返回值类型,name为所要调用的系统函数的名字。在unistd.h中共定义了4个这样的宏(n从0到3),也就是说,0.11核中系统调用最多可带3个参数。那么下面就说这个宏干了什么,也就是说上面的那个“调用过程”是怎么样的呢?在这个宏中嵌入了汇编代码,做的工作就是int 0x80,其中将字符串“_NR_”和name连接,组成一个宏并将这个宏的值,也就是被调用的系统函数在sys_call_table中偏移量送到eax寄存器中;同时指明系统函数将来的返回值放到eax中。
1.3 系统调用处理过程
下面我再说一下系统调用的核心软中断int 0x80具体干了什么。这条指令会引起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.1 实验内容
在linux0.11版本中添加两个系统调用,并编写一个简单的应用程序测试它们。
第一个系统调用是iam(),其原型为:
intiam(const char * name);完成的功能是将字符串参数name的内容拷贝到内核中保存下来。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。
第二个系统调用是whoami(),其原型为:
intwhoami(char* name, unsigned int size);它将内核中由iam()保存的名字拷贝到name指向的用户地址空间中,同时确保不会对name越界访存(name的大小由size说明)。返回值是拷贝的字符数。如果size小于需要的空间,则返回“-1”,并置errno为EINVAL。
2.2 代码添加修改步骤
2.2.1 在unistd.h中添加系统调用接口
#define __NR_whoami 72 #define __NR_iam 73 intwhoami(void);intaim(void);
2.2.2 在exit.c文件中添加系统调用处理函数的实现
系统调用的函数可以在其他.c文件中添加或在新建文件中添加,只要编辑进image都是可以的,这里为了调试方便就在exit.c文件中添加了。
#define MAX 23 char N_MAX[26];
intsys_whoami(char* name, unsigned int size){ if(strlen(N_MAX)>size)return-EINVAL;
int i;
for(i=0;N_MAX[i]!=' ';i++){
put_fs_byte(N_MAX[i],&name[i]);}
returnstrlen(N_MAX);}
intsys_iam(char *name){
char c;charstr[100];memset(str,' ',sizeof(str));int i;
for(i = 0;i
{ if((c=get_fs_byte(&name[i]))!=' ')
{ str[i] = c;
} else break;}
if((c!= ' ')||(i>MAX))
{ return-EINVAL;}
memset(N_MAX,' ',sizeof(N_MAX));
for(i=0;str[i]!=' ';i++){
N_MAX[i]=str[i];} return i;} 2.2.3 在system_call.s汇编代码中修改系统调用的个数
#If 0 nr_system_calls = 72 #else
nr_system_calls = 74 #endif
2.2.4 测试代码的编写test.c的代码如下:
#define __LIBRARY__ #include _syscall1(int,iam,char*,name)_syscall2(int,whoami,char*,name, unsigned int,size)
int main(){ int a = 0;char bb[26] = “champion”;char cc[26] = “”;
a = iam(bb);printf(“a=%d”, a);
a = whoami(cc,8);printf(“iam=%sn”,cc);return(1);} 系统调用相关代码的分析
3.1 初始化软件中断门。
3.1.1 函数调用层次
初始化软件中断门,就是把0x80软件中断的处理函数system_call挂载到中断向量表idt中,以确保发生软件中断时会运行system_call函数,这个函数在system_call.s实现。初始化的流程如下:
main()sched_init()set_system_gate(0x80, &system_call)_set_gate
3.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,%%axnt”
“movw %0,%%dxnt” “movl %%eax,%1nt” “movl %%edx,%2nt” : : “i”((short)(0x8000+(dpl
__asm__格式为嵌入式汇编的格式,分析可知代码有5个传入的参数%0,%1,%2,%3,%4如下: %0,立即数
“i”((short)(0x8000+(dpl
DPL位段为DPL(因为dpl
%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 = 0x7119 edx = &system_call = 0x7119
%4,eax做为参数 “a”(0x00080000))eax = 0x00080000
__asm__格式为嵌入式汇编的格式,分析可知四条命令含义如下:
“movw %%dx,%%axnt”
先将%%dx的低16位移入%%ax的低16位
这样,在%eax中就形成了所需要的中断门的第一个长整数,其高16位为_KERNEL_CS,而低16位为addr的低16位。
“movw %0,%%dxnt”
字操作
16位操作,操作低16位,高16位已经有调用函数的地址.“movl %%eax,%1nt”
双字操作,为门的0--31位赋值
“movl %%edx,%2nt”
双字操作,为门的32--63位赋值
3.2 以_syscall1为例,分析系统调用入口宏的含义。
其中_syscall1是一个宏,在include/unistd.h中定义。将_syscall1(int,close,int,fd)进行宏展开,可以得到:
#define _syscall1(type,name,atype,a)type name(atype a){ 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_whoami 72 #define __NR_iam 73
3.3 对_system_call函数的分析
处理流程图
处理流程分析 _system_call: cmpl $nr_system_calls-1,%eax # 调用号如果超出范围的话就在eax中置-1 并退出。jabad_sys_call push %ds # 保存原段寄存器值。push %es push %fs pushl %edx # ebx,ecx,edx中放着系统调用相应的C 语言函数的调用参数。pushl %ecx# push %ebx,%ecx,%edx as parameters pushl %ebx # to the system call movl $0x10,%edx# set up ds,es to kernel space mov %dx,%ds # ds,es指向内核数据段(全局描述符表中数据段描述符)。mov %dx,%es movl $0x17,%edx # fs points to local data space mov %dx,%fs# fs指向局部数据段(局部描述符表中数据段描述符)。
# 下面这句操作数的含义是:调用地址 = _sys_call_table + %eax * 4。参见列表后的说明。# 对应的C 程序中的sys_call_table在include/linux/sys.h中,其中定义了一个包括72 个 # 系统调用C 处理函数的地址数组表。call _sys_call_table(,%eax,4)pushl %eax# 把系统调用号入栈。
movl _current,%eax# 取当前任务(进程)数据结构地址。
# 下面查看当前任务的运行状态。如果不在就绪状态(state 不等于0)就去执行调度程序。# 如果该任务在就绪状态但counter值等于0,则也去执行调度程序。cmpl $0,state(%eax)# state jne reschedule cmpl $0,counter(%eax)# counter je reschedule
3.4 用户态和内核态之间的传递数据
在内核中主要提供了四个函数实现内核态和用户态的数据传递:copy_to_user(),copy_from_user(),get_fs_byte(),put_fs_byte();上面测试用例中使用的是对字节的操作get_fs_byte(),put_fs_byte()。
通过bochs环境如何验证系统调用
1.1 Bochs+Linux0.11调试环境建立。
可以分为两个部分的工作:搭建调试环境和Bochs命令的使用;这两部分网上资料较多,就不在此描述了。
1.2 测试程序的修改和添加方法。
1.2.1 使用mount命令访问文件系统hdc-0.11.img 要想把测试程序test.c运行起来,一定要放入文件系统才行,也就是一定要把test.c程序放进hdc-0.11.img中去才行,可以用如下的方法打开文件系统:
losetup /dev/loop1 hdc-0.11.img losetup-d /dev/loop1
losetup-o 512 /dev/loop1 hdc-0.11.img
mkdir/mnt/tempdir mount-t minix /dev/loop1 /mnt/tempdir
说明:用losetup的-d选项把hdc-0.11.img文件与loop1的关联解除,用losetup的-o选项,该选项指明关联的起始字节偏移位置。由上面分区信息可知,这里第1个分区的起始偏移位置是1 * 512 字节。
在把第1个分区与loop1重新关联后,我们就可以使用mount命令来访问其中的文件了。在对分区中文件系统访问结束后,最后请卸载和解除关联。umount /dev/loop1 losetup-d /dev/loop1 1.2.2 编译test.c测试程序
把测试程序放到/mnt/tempdir/user/root目录下,这样就可以任意修改test.c文件的内容,并可以把修改的内容保存到hdc-0.11.img文件系统中去了。
1.3 通过bochs调试观察,是如何把0x80的中断函数system_call的地址挂载上去的。1.3.1 通过添加do_nothing()函数,然后在此函数设置断点,可以查看0x80中断处理函数是如何放到中断向量表中去的。
加入调试辅助代码如下:
因为set_system_gate是一个宏,没有办法添加断点,所以就添加了一个函数do_nothing(),在此处设置断点,以方便观察后面宏的运行情况;并且加入了几个nop命令,以方便观察运行情况。
voidsched_init(void){ do_nothing();set_system_gate(0x80,&system_call);} 1.3.2 修改_set_gate宏如下,加入了nop命令,以便调试观察。
#define _set_gate(gate_addr,type,dpl,addr)__asm__(“nop nt” “nop nt” “nop nt” “nop nt” “movw %%dx,%%axnt” “movw %0,%%dxnt” “nop nt” “nop nt” “movl %%eax,%1nt” “movl %%edx,%2nt” “nop nt” “nop nt” “nop nt” “nop ” : : “i”((short)(0x8000+(dpl
1.3.3 可以看到运行的效果如下:
在system.map中,看到do_nothing的线性地址是0x6c1d,可以在此处设置断点。
Figure 1设置断点
Figure 2程序运行到_set_gate宏
Figure 3查看此时寄存器中的数值
调试测试程序test.c和sys_whoami和sys_iam函数 1.4.1 调试系统调用处理函数sys_whoami和sys_iam. 在system.map文件中,找到编译kernel后,函数sys_whoami和sys_iam所在的线性地址。如下所示: 00008dba T sys_iam 00008e57 T sys_whoami 在bochs启动kernel后,在0x8dba和0x8e57处设置断点,然后运行test程序,就会进入系统调用处理函数,运行到设置的断点处,后面可以单步运行,以调试sys_whoami和sys_iam,达到测试系统调用的目的。
如下图所示: 1.4
1.4.2 在linux0.11的系统中编译运行test程序
在运行起来的linux0.11的系统中,通过make test命令编译test.c文件,使生产test应用。运行test程序后,iam()函数就会通过get_fs_byte()函数把“champion”字符串存入到内存中;
whoami()函数就会把前面存入到内存中的字符串取出,并且通过put_fs_byte()内核函数返回到用户层;并且打印出来。如下所示: