1、建立对系统调用接口的深入认识
2、掌握系统调用的基本过程
3、能完成系统调用的全面控制
4、为后续实验做准备
1、在Linux-0.11
上添加两个系统调用(原始只有72个系统调用)
第一个系统调用:
int iam(const char * name);
作用:
将
name
中存放的字符串拷贝到内核中并保存下来,
要求name
的长度不能超过23
个字符,若超过了,返回-1
.并置errno
为EINVAL
,
如果没有超过就返回拷贝的字符个数。
在kernal/who.c
中实现该系统调用
第二个系统调用:
int whoami(char * name,unsigned int size);
作用:
将内核中存放的字符串(由
iam()
拷贝进去的)重新复制到name
中,
size
是指name
指向的内存空间允许存放的最大长度,
如果size
小于需要的空间,也就是说字符串在name
中放不下,
就返回-1
,并置errno
为EINVAL
2、测试上述添加的两个系统调用
第一个测试点:
iam.c
:测试能不能把字符串放到内核中 - 系统调用:iam
第二个测试点:
whoami.c
:测试能不能把内核中的字符串取出来并打印出来 - 系统调用:whoami
操作系统实现系统调用的基本过程:
应用程序调用系统调用的过程:
从表面上来看,系统调用和普通的函数调用没有区别,都是直接调用然后实现一定的功能,
自定义函数是直接通过call
指令跳转到该函数的地址上去运行,
但是系统调用则是调用为该系统编写的一个接口函数,也就是API(application programming interface)
而API
不能直接的完成系统调用的功能,它只是提供了一个接口去调用真正的系统调用。
系统调用过程:
EAX
0x80
中断(int 0x80
)cd ~/oslab
tar zxvf hit-oslab-linux-20110823.tar.gz
-> 然后出现一个linux0.11文件夹
sudo mount-hdc
-> 可以把虚拟机硬盘挂载在oslab/hdc目录下
hdc/usr/include
目录中的查看unistd.h
的内容并添加两个系统调用号#define _NR_iam 72
#define _NR_whoami 73
linux-0.11
目录中的system_call.s
中的系统调用总数从72
改到74
nr_system_calls = 74
linux-0.11
目录中的sys.h
中添加两个新的系统调用和它们的引用:extern int sys_iam();
extern int sys_whoami();
fn_ptr sys_call_table[] =
{ sys_setup, sys_exit, sys_fork, sys_read,……,sys_iam,sys_whoami },
已经为两个系统调用分配空间,并表明了会在其他的文件中使用这两个系统调用函数,其他文件就是who.c。
可以看到在sys.h中的sys_call_table其实就是一个数组名(首元素的地址)
call sys_call_table(,%eax,4)这一句话,根据汇编寻址方式来解释这句话:
=> call sys_call_table + 4 * %\eax (eax中存放的是系统调用号,系统调用号也就是那些_nr_xxx)
这里就是通过 一个函数指针数组的起始地址 + 4 * %eax
表示从函数起始地址开始跳过%eax个项,而且每个项是4个字节,然后对应到一个函数入口,
所以也就是跳转到从sys_call_table中的第%eax个函数开始执行,
也就是开始执行那个系统调用。
who.c
文件(在kernal目录
下),然后在该文件中实现两个系统调用函数,
who.c:
#include
//#include
#include
#include
char sys_name[24];//系统内核的内存区域
int sys_iam(const char * name)
{
//将字符串name中的内容,存放到系统内核中
int count = 0;
//while(*(name+count) != '\0')//要改成下面的形式
while(get_fs_byte(name+count) != '\0')//在内核态中从用户态取出数据
{
count++;
}
if(count > 23)
{
//如果字符串的长度大于23,则表示不能存放到内核中,返回-1,设置errno
errno = EINVAL;
return -1;
}
else
{
for(int i = 0; i < count; i++)
{
sys_name[i] = get_fs_byte(name+i);
}
sys_name[count] = '\0';
return count;
}
}
int sys_whoami(char * name,unsigned int size)
{
//将内核段中的字符串复制到name中,name能够容纳的最大空间为size
int count = 0;
while(*(sys_name+count) != '\0')
{
count++;
}
if(size < count)//name所能容纳的空间size小于字符串的长度
{
errno = EINVAL;
return -1;
}
else
{
for(int i = 0; i < count; i++)
{
put_fs_byte(sys_name[i],(name+i));//将内核态中的数据拷贝到用户态
}
name[count] = '\0';
return count;
}
}
知识点:
errno是一种传统的错误代码返回机制。当一个函数调用出错时,通常会返回-1给调用者,
但是-1只是表示出错了,但是不知道错是什么,为了解决此问题。全局变量errno登场了,
错误值保存在errno中,就可以判断errno的具体值来判断错误是什么并知道如何应对了,
不同的系统对errno的具体值的含义有不同的定义标准,linux下输入命令man errno可以看到errno返回值的类型(也可以直接看errno.h文件)
who.c
跟其他linux
的代码编译链接到一起,就要修改Makefile:
(Makefile里面记录了所有源程序文件的编译和链接规则)
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o
改为:
OBJS = sched.o system_call.o traps.o asm.o fork.o \
panic.o printk.o vsprintf.o sys.o exit.o \
signal.o mktime.o who.o
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
...
改为:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \
...
然后在已经添加好了两个系统调用的基础上,重新编译内核make all
make all
编译出错,就是表示已经修改了Makefile
的内容。
从报错内容可以看出来:
1.去掉sys.h
和string.h
2.这里面的编译器gcc
不支持C99
,所以变量i
的初始化要放在外面。
who.c:
(代码有问题,在编译内核的时候报错,还需要调试)
#include
// #include
#include
// #include
// #include // 这里好像加加个这个头文件,才能有get...\put...
char sys_name[24];//系统内核的内存区域
int sys_iam(const char * name)
{
//将字符串name中的内容,存放到系统内核中
int count = 0;
while(get_fs_byte(name+count) != '\0')//在内核态中从用户态取出数据
{
count++;
}
if(count > 23)
{
//如果字符串的长度大于23,则表示不能存放到内核中,返回-1,设置errno
errno = EINVAL;
return -1;
}
else
{
int i;
for(i = 0; i < count; i++)
{
sys_name[i] = get_fs_byte(name+i);
}
sys_name[count] = '\0';
return count;
}
}
int sys_whoami(char * name,unsigned int size)
{
//将内核段中的字符串复制到name中,name能够容纳的最大空间为size
int count = 0;
while(*(sys_name+count) != '\0')
{
count++;
}
if(size < count)//name所能容纳的空间size小于字符串的长度
{
errno = EINVAL;
return -1;
}
else
{
int i;
for(i = 0; i < count; i++)
{
put_fs_byte(sys_name[i],(name+i));//将内核态中的数据拷贝到用户态
}
name[count] = '\0';
return count;
}
}
知识点: 可以使用
printk()
来调试调试(就是printf
调试法,只是在内核中需要用printk
)
本函数的核心仍然是调用tty_write()
系统调用,类似于printf()
函数
int printk(const char *fmt, ...)
{
……
__asm__("push %%fs\n\t"
"push %%ds\n\t"
"pop %%fs\n\t"
"pushl %0\n\t"
"pushl $buf\n\t"
"pushl $0\n\t"
"call tty_write\n\t"
"addl $8,%%esp\n\t"
"popl %0\n\t"
"pop %%fs"
::"r" (i):"ax","cx","dx");
……
}
在此之前,需要在目录hdc/usr/include/中创建name.h文件并中添加两个宏定义表示两个系统调用的参数情况(也叫做挂载虚拟机硬盘):
_syscall1(int,iam,const char *,name):
iam()函数返回int,且传入的是一个const char * 的name
_syscall2(int,whoami,char *,name,unsigned int,size):
whoami()函数返回int,且传入的是一个char *的name和unsigned int 的size
_syscall1(int,iam,const char *,name):
_syscall2(int,whoami,char *,name,unsigned int,size):
注: 在unistd.h里面可以看到系统调用的四种情况:1.不传参数 2.一个参数 3.两个参数 4.三个参数
第一个测试点: 编写一个iam.c
函数去测试能不能将字符串存放到内核中
第二个测试点: 编写一个whoami.c
函数去测试能不能从内核中取出字符串并打印出来
两个函数都实现在hdc/usr/root
目录下
iam.c
: (两个代码都有问题,会在虚拟机中报错,还需要调试)
#include
#define __LIBRARY__
#include
#include
_syscall1(int,iam,const char *,name)
int main(int argc,char * argv[])
{
if(argc < 0)
{
printf("No name!");
}
else
{
printf("Please input your name:");
int tag = iam(argv[1]);//应该是sys_iam(argv[1])吧
if(tag != -1)
{
printf("input successfully!\n");
}
}
return 0;
}
whoami.c
:
#include
#define __LIBRARY__
#include
#include
_syscall2(int,whoami,char *,name,unsigned int,size)
int main(int argc,char * argv[])
{
char name[24] = {0};
int tag = sys_whoami(name,24);//应该是sys_whoami(argv[1])吧
if(tag != -1)
{
printf("output successfully!\n",name);
printf("my name is %s\n",name);
}
return 0;
}
如果两个系统调用函数的测试函数已经写好了:
从
linux-0.11
目录返回到oslab/oslab (cd ..)
直接运行这样做会直接把./run
-> 跳转到操作系统运行界面hdc
卸载,我也不知道为啥
可以用./dbg-asm
+ 命令行的c
来替代上述的命令
编译刚才写好的
iam.c
文件(gcc -o iam iam.c -Wall) -Wall
会输出警告信息,如果文件写的没有问题,也就不会出现警告提示,样就表示iam.c
测试程序写对了。
然后测试下看whoami.c
写对了吗(gcc -o whoami whoami.c -Wall)
,
没有报语法错误就表示whoami.c
写对了。
指针参数传递的是应用程序所在地址空间的逻辑地址,
在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。
system_call: //所有的系统调用都从system_call开始
{
……
pushl %edx
pushl %ecx
pushl %ebx # push %ebx,%ecx,%edx,这是传递给系统调用的参数
movl $0x10,%edx # 让ds,es指向GDT,指向核心地址空间
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # 让fs指向的是LDT,指向用户地址空间
mov %dx,%fs
call sys_call_table(,%eax,4) # 即call sys_open
}
include/asm/segment.h:
get_fs_xxx
:在内核态中可以获取用户态的数据(修改iam()
函数,使用get_fs_byte()
从用户态中获取一个字节的数据)
put_fs_xxx
:从内核态将数据拷贝到用户态的内存空间中
这两个类型的函数都是用户空间和内核空间的桥梁
iam.c
:输入字符串并将字符串复制到内核中Please input your name: lqd
input successfully!
运行whoami.c
:从内核中拷贝出字符串并输出到屏幕上
output successfully!
my name is lqd
Linux
的一大特色是可以编写功能强大的的shell
脚本来提高工作效率,
这个实验的部分评分工作就是由shell
脚本完成的。
使用testlab2.sh
来测试iam.c
和whoami.c
在本实验中首先要找到
testlab2.sh
,但是我找不到
然后拷贝到和iam.c\whoami.c
同一目录下(hdc\urs\root
目录)
用命令为脚本添加执行权限:chmod + x testlab2.sh
然后直接运行脚本程序:./testlab2.sh
就可以得出iam.c
和whoami.c
的得分了
本实验涉及到的所有文件:
unistd.h
system_call.s
sys.h
who.c
Makefile
iam.c
whoami.c
testlab2.sh
问题一:
系统调用最多可以传几个参数,如果要增加参数该怎么做?
因为在unsitd.h
已经声明了四种参数传递的方式,在32
位处理器上有四个寄存器(eax,ebx,ecx,edx
),
eax
用于传递中断调用号和返回值,使用ebx,ecx,edx
三个寄存器传递参数,所以最多只可以三个参数,
如果要增加参数需要采用栈来传递参数,寄存器只需要获取栈的地址和参数的长度。
问题二:
添加系统调用并测试的步骤:foo()
- 在
unistd.h
添加#define _nr_(宏定义)
- 在
sys_system.s
中修改系统调用个数+1- 在
sys.h
中添加系统调用的引用(1.写一个extern int foo()
2.在数组中写入系统调用sys_foo
,即函数名称)- 在
kernal
目录下编写foo.c
去实现foo()
函数- 修改
kernal
目录下的Makefile
编译树,使得foo
系统调用被添加到编译树中(在编译的时候得以和其他的文件链接)- 在
hdc/urs/root
下写一个测试函数test.c
用来测试foo()
是不是写好了- 然后运行操作系统,编译
test.c
程序,没有报错,然后运行程序,出现了正确的结果就是对了
注意:博主暂时并为完成此实验,主要是很多代码还需要调试,所以该报告只能做参考。
HIT-OS-LAB参考资料:
1.《操作系统原理、实现与实践》-李治军、刘宏伟 编著
2.《Linux内核完全注释》
3.两个哈工大同学的实验源码
4.Linux-0.11源代码
(上述资料,如果有需要的话,请主动联系我))
该实验的参考资料
网课
官方文档
参考博客
参考实验报告