视频:课程视频
实验环境:实验楼
(实验楼有个很坑的地方是代码不能保存,今天写的东西关了就没了,如果要保存就要花钱.可以参考我的这篇文章自己搭一个环境)
配套建材:<<操作系统原理、实现与实践>>
推荐两个比较不错的实验合集:第一个 第二个
在实验楼和实验合集里面已经有比较完善的实验代码,本篇文章不再详细叙述.主要讲一下整个系统调用的流程以及自己遇到的一些问题,
要用到的一些文件:
步骤:
1.include/unistd.h 添加 __NR_iam 72 和 __NR_whoami 73
2.include/linux/sys.h 添加 extern int sys_iam(); 和 extern int sys_whoiam();
在sys_call_table 添加 sys_iam,sys_whoiam
3.kernel/system_call.s 修改nr_system_calls = 74
4.kernel/who.c 完成sys_iam()和 sys_whoami()具体函数实现
5.修改kernel/Makefile,否则make all不会把 who.c 加入到内核中了。
6.在linux-0.11目录下重新编译
7.在linux-0.11里面编写测试代码测试
测试代码调用iam()的执行过程:
将_syscall1(int, iam, const char *,name)宏展开:
#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; \
}
这是一个C语言的内嵌汇编,然后将参数代入得到下面
#define _syscall1(int,iam,const char *,name) \
int iam(const char * name) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_iam),"b" ((long)(name))); \
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
}
: “0” (__NR_iam),“b” ((long)(a))); 这个代表输入参数,把__NR_iam的值赋给eax,把参数name的值赋给ebx
然后执行 int 0x80中断
执行完了之后将结果赋值给eax,然后再赋值给__res
然后继续执行if (__res >= 0) 以及后面的东西
这里我们在include/unistd.h已经定义了__NR_iam的值为72,所以相当于把eax赋值为72,并将name赋值给ebx,然后调用int 0x80
这里再讲一下int 0x80对应的函数入口地址的初始化过程
系统启动的时候会将OS的代码读取到内存0开始的地方.然后会执行main.c文件里面的若干个init函数,用于进行一系列的初始化.在一个sched_init()的函数里面,有一个宏set_system_gate(0x80,&system_call).这个宏就将int 0x80的中断函数入口地址指向了system_call
再来看system_call
system_call:
! # 检查系统调用编号是否在合法范围内
cmpl \$nr_system_calls-1,%eax
ja bad_sys_call
push %ds
push %es
push %fs
pushl %edx
pushl %ecx
pushl %ebx
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
mov %dx,%fs
call sys_call_table(,%eax,4)
call sys_call_table(,%eax,4)表示找到入口地址sys_call_table,然后偏移4*eax.因为sys_call_table里面放的都是系统调用的入口地址,都是四个字节.前面知道eax是72,这里就是到第73个系统调用的入口地址
前面push ebx,ecx,edx就是在传递参数,对于 iam函数来说,ebx里面存储着name
然后让ds es指向0x10就是内核地址空间的段地址, fs指向0x17 是用户地址空间的段地址 这里的作用是实现用户地址空间和内核地址空间之间的数据传递(后面会具体说怎么传递的)
在sys_call_table里面找到sys_iam函数的入口地址并执行结束后返回到system_call,然后执行iret,回到上面的宏
也就是#define _syscall1(int,iam,const char *,name) .将sys_iam()的返回值赋给__res,执行
if (__res >= 0) \
return (type) __res; \
errno = -__res; \
return -1; \
然后iam()函数执行结束.
通过流程图来表示就是这样的:
讲到这里,流程就完成了.下面再说一说自己做实验的时候遇到的问题
1.iam函数需要实现的功能是:将字符串参数 name 的内容拷贝到内核中保存下来
但是我在做的时候就在想我怎么知道内核中那一段内存是可以使用的呢,因为在学习汇编语言的时候曾经看到过,内核的代码是不能随便改的,如果随便修改的话可能会造成系统死机.后来看了提示才发现原来自己蠢了,既然都已经在写系统调用了,随便在who.c文件用一个数组不就可以了.
2.
在编译的时候发现上面的报错.这里的原因是没有在虚拟机操作系统中的/usr/include/unistd.h文件里面新增的系统调用的调用号.就是#define __NR_iam 72和 #define __NR_whoami 73
这里的虚拟机不是指的实验楼,而是利用./run启动的那个虚拟机
3.编译执行实验楼里面的测试程序(testlab2.c)发现当字符串长度大于24的时候,返回结果不正确
注:上图里面的good name和fail name是我sys_iam()里面的输出.根据提示信息可以发现,当字符串长度>=24的时候,errno的值是1而不是22
这里其实受到了实验描述的干扰:
实验是这样说的:如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。
所以在写sys_iam函数的时候,就想当然的这样写了
if(length>=24){
errno=EINVAL ;
return -1 ;
}
这时候就要回过头来再看看那个宏了 我们知道 返回值会赋给__res.所以如果我们按照上面这样写的话,会得到errno=1 return -1 这样是不对的.要想让errno=EINVAL 只有在前面返回的时候 返回-EINVAL
if (__res >= 0)
return (type) __res;
errno = -__res;
return -1; \
其实后来看其他文件的时候,也就明白了为什么要这样写了
这是kernel/fork.c里面的一个函数
4.为什么get_fs_byte(const char*addr)可以实现内核区和用户区数据的交互
在编写sys_iam()的时候,如果不用get_fs_byte,直接将name的值赋给内核中的缓冲区域,再执行这个函数的时候是会报错的,segment fault.为什么呢?这是因为任何数据的地址都是根据段:偏移得到的.参数name只是给出了它的偏移,而它的段地址默认是段寄存器DS里面的值.我们在system_call里面已经得知:
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
mov %dx,%fs
ds指向的是0x10,这是指向内核段的地址.那么DS:name肯定就不是用户态缓冲区的数据了
不过没关系后面将0x17赋给了fs,0x17就是用户态的段地址
我们来看.get_fs_byte干了什么
extern inline unsigned char get_fs_byte(const char * addr)
{
unsigned register char _v;
__asm__ ("movb %%fs:%1,%0":"=r" (_v):"m" (*addr));
return _v;
}
这句内嵌汇编的意思就是 mov fs:addr,_v 将fs段偏移地址为addr的数据赋值给_v.显然这里_v就是内核缓冲区里面的那个数组,而fs段由指向的是用户段.所以实现了用户段将数据传给内核段.
实验到这里就告一段落了,从完成这个实验到后面总结以及写这篇文章花费了挺多的时间的,不过感觉还是学到不少东西.也不得不感叹Linus不愧是大佬,很多设计真的很巧妙(当然也可能是我太菜,哈哈)