linux011系统调用原理及实验总结.docx
《linux011系统调用原理及实验总结.docx》由会员分享,可在线阅读,更多相关《linux011系统调用原理及实验总结.docx(17页珍藏版)》请在冰豆网上搜索。
linux011系统调用原理及实验总结
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_iam73
intwhoami(void);
intaim(void);
2.2.2在exit.c文件中添加系统调用处理函数的实现
系统调用的函数可以在其他.c文件中添加或在新建文件中添加,只要编辑进image都是可以的,这里为了调试方便就在exit.c文件中添加了。
#defineMAX23
charN_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;
}
else
break;
}
if((c!
='\0')||(i>MAX))
{
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汇编代码中修改系统调用的个数
#If0
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,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_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,%%ax\n\t"\
"movw%0,%%dx\n\t"\
"movl%%eax,%1\n\t"\
"movl%%edx,%2\n\t"\
:
\
:
"i"((short)(0x8000+(dpl<<13)+(type<<8))),\
"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+(dpl<<13)+(type<<8)))
这样,%%edx中高16位为addr的高16位,而低16位的P位为1(因为是0x8000),
DPL位段为DPL(因为dpl<<3),而D位加上类型位段则为type(因为type<<8)其余各位皆为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=0x7119
edx=&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_iam73
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,%edxasparameters
pushl%ebx#tothesystemcall
movl$0x10,%edx#setupds,estokernelspace
mov%dx,%ds#ds,es指向内核数据段(全局描述符表中数据段描述符)。
mov%dx,%es
movl$0x17,%edx#fspointstolocaldataspace
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
jnereschedule
cmpl$0,counter(%eax)#counter
jereschedule
3.4用户态和内核态之间的传递数据
在内核中主要提供了四个函数实现内核态和用户态的数据传递:
copy_to_user(),copy_from_user(),get_fs_byte(),put_fs_byte();上面测试用例中使用的是对字节的操作get_fs_byte(),put_fs_byte()。
4通过bochs环境如何验证系统调用
1.1Bochs+Linux0.11调试环境建立。
可以分为两个部分的工作:
搭建调试环境和Bochs命令的使用;这两部分网上资料较多,就不在此描述了。
1.2测试程序的修改和添加方法。
1.2.1使用mount命令访问文件系统hdc-0.11.img
要想把测试程序test.c运行起来,一定要放入文件系统才行,也就是一定要把test.c程序放进hdc-0.11.img中去才行,可以用如下的方法打开文件系统:
losetup/dev/loop1hdc-0.11.img
losetup-d/dev/loop1
losetup-o512/dev/loop1hdc-0.11.img
mkdir/mnt/tempdir
mount-tminix/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\n\t"\
"nop\n\t"\
"nop\n\t"\
"nop\n\t"\
"movw%%dx,%%ax\n\t"\
"movw%0,%%dx\n\t"\
"nop\n\t"\
"nop\n\t"\
"movl%%eax,%1\n\t"\
"movl%%edx,%2\n\t"\
"nop\n\t"\
"nop\n\t"\
"nop\n\t"\
"nop"\
:
\
:
"i"((short)(0x8000+(dpl<<13)+(type<<8))),\
"o"(*((char*)(gate_addr))),\
"o"(*(4+(char*)(gate_addr))),\
"d"((char*)(addr)),"a"(0x00080000))
1.3.3可以看到运行的效果如下:
在system.map中,看到do_nothing的线性地址是0x6c1d,可以在此处设置断点。
Figure1设置断点
Figure2程序运行到_set_gate宏
Figure3查看此时寄存器中的数值
1.4调试测试程序test.c和sys_whoami和sys_iam函数
1.4.1调试系统调用处理函数sys_whoami和sys_iam.
●在system.map文件中,找到编译kernel后,函数sys_whoami和sys_iam所在的线性地址。
如下所示:
00008dbaTsys_iam
00008e57Tsys_whoami
●在bochs启动kernel后,在0x8dba和0x8e57处设置断点,然后运行test程序,
就会进入系统调用处理函数,运行到设置的断点处,后面可以单步运行,以调试sys_whoami和sys_iam,达到测试系统调用的目的。
如下图所示:
1.4.2在linux0.11的系统中编译运行test程序
在运行起来的linux0.11的系统中,通过maketest命令编译test.c文件,使生产test应用。
运行test程序后,
iam()函数就会通过get_fs_byte()函数把“champion”字符串存入到内存中;
whoami()函数就会把前面存入到内存中的字符串取出,并且通过put_fs_byte()内核函数返回到用户层;并且打印出来。
如下所示: