哈工大操作系统实验---lab2:系统调用

实验目的:

1、建立对系统调用接口的深入认识
2、掌握系统调用的基本过程
3、能完成系统调用的全面控制
4、为后续实验做准备


实验内容:

1、在Linux-0.11上添加两个系统调用(原始只有72个系统调用)
第一个系统调用:

int iam(const char * name);

作用:

name中存放的字符串拷贝到内核中并保存下来,
要求name的长度不能超过23个字符,若超过了,返回-1.并置errnoEINVAL
如果没有超过就返回拷贝的字符个数。
kernal/who.c中实现该系统调用

第二个系统调用:

int whoami(char * name,unsigned int size);

作用:

将内核中存放的字符串(由iam()拷贝进去的)重新复制到name中,
size是指name指向的内存空间允许存放的最大长度,
如果size小于需要的空间,也就是说字符串在name中放不下,
就返回-1,并置errnoEINVAL

2、测试上述添加的两个系统调用
第一个测试点:

iam.c:测试能不能把字符串放到内核中 - 系统调用:iam

第二个测试点:

whoami.c:测试能不能把内核中的字符串取出来并打印出来 - 系统调用:whoami


实验过程:

知识点:

操作系统实现系统调用的基本过程:

  • 1.应用程序调用库函数(API)
  • 2.API将系统调用号存入EAX,然后通过中断调用使系统进入到内核态
  • 3.内核的中断处理函数根据系统调用号,调用对应的内核函数(系统调用)
  • 4.系统调用完成相应的功能,将返回值存入EAX,返回到中断处理函数
  • 5.中断处理函数返回到API中
  • 6.API将EAX返回给应用程序

应用程序调用系统调用的过程:
从表面上来看,系统调用和普通的函数调用没有区别,都是直接调用然后实现一定的功能,
自定义函数是直接通过call指令跳转到该函数的地址上去运行,
但是系统调用则是调用为该系统编写的一个接口函数,也就是API(application programming interface)
API不能直接的完成系统调用的功能,它只是提供了一个接口去调用真正的系统调用。

系统调用过程:

  • 1.把系统调用的编号存入EAX
  • 2.把函数参数存入其他通用寄存器
  • 3.触发0x80中断(int 0x80)

步骤:

  • 准备实验环境:
cd ~/oslab
tar zxvf hit-oslab-linux-20110823.tar.gz
-> 然后出现一个linux0.11文件夹
sudo mount-hdc
-> 可以把虚拟机硬盘挂载在oslab/hdc目录下

  • 查看includehdc/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的内容。
哈工大操作系统实验---lab2:系统调用_第1张图片
从报错内容可以看出来:
1.去掉sys.hstring.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.cwhoami.c

在本实验中首先要找到testlab2.sh,但是我找不到
然后拷贝到和iam.c\whoami.c同一目录下(hdc\urs\root目录)
用命令为脚本添加执行权限: chmod + x testlab2.sh
然后直接运行脚本程序: ./testlab2.sh
就可以得出iam.cwhoami.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源代码
(上述资料,如果有需要的话,请主动联系我))

该实验的参考资料
网课
官方文档
参考博客
参考实验报告

你可能感兴趣的:(哈工大操作系统实验---lab2:系统调用)