Linux综合实验1:系统调用扩充

一、实验目的和内容

实验目的:深入掌握操作系统内核程序开发方法。

实验内容:以版本 0 内核为基础,增加一组系统调用(详情如下),并通过给定的测试用例。

execve2:以“立即加载”方式执行一个可执行文件,要求加载完后运行时该进程不产生缺页异常。

getdents:获取一组目录项

sleep:进程睡眠

getcwd:获取当前工作目录

二、操作方法与实验步骤

首先要为内核添加这四个系统调用,做法类似课内实训:(以添加 g e t c w d getcwd getcwd 为例)

(0)修改nr_system_calls为92 (必须大于当前最大的系统调用号91)

(1)在 unistd.h 添加系统调用号的定义和函数声明:

#define __NR_getcwd 91
char * getcwd(char * buf, size_t size);

(2)在 sys.h添加 sys_getcwd的定义,并在系统调用表的对应调用号的位置添加函数名。

(3)任选 *.c 文件 实现 sys_getcwd函数。

1、getcwd:

(1)首先思考如何根据文件系统格式手动寻找当前工作目录。

思路起点是当前进程控制块中的成员pwd:

struct m_inode * pwd; /*当前进程工作目录的索引节点指针*/

我们知道此索引节点就可以类似课内分析hello.c的方法逐层向上推:

在函数 s y s sys sys_ g e t c w d getcwd getcwd处设断点,显示pwd指向的索引节点值。
在这里插入图片描述

找到数据块5201,读取该数据块内容:

在这里插入图片描述

找到名称为 . . .. .. 的目录项,即为父目录,得到父目录索引节点号 0x83,得到父目录索引节点信息。

在这里插入图片描述
得到父节点数据块0x1271,找到对应数据块:

在这里插入图片描述

找到其中匹配当前目录0xce的目录项,从而得到当前目录的名称并记录。

然后将当前目录改为父目录,重复上述过程直到为根目录不能向上跳。

(2)代码实现:

核心的 s y s sys sys_ g e t c w d getcwd getcwd 代码如下:

char * sys_getcwd(char * buf, size_t size){
	char temp[256]; int len=0;
	struct m_inode* now=current->pwd; int idx=now->i_num,dev=now->i_dev;
	/*idx:当前目录索引节点号; now:当前索引节点*/
	char* pt=bread(now->i_dev,1)->b_data;
	int sta=( (int)(*(unsigned short*)(pt+4)) + (int)(*(unsigned short*)(pt+6)) + 2 )*0x400; /*索引节点起始*/
	while(1){
		char* pt=bread(dev,now->i_zone[0])->b_data;
		int faidx=-1,num=0;
		while(num<1024){
			if((*(unsigned short*)(pt+2))==0x2e2e){
				faidx=*(unsigned short*)pt;
				break;
			}
			pt+=16; num+=16;
		}
		if(faidx==-1){
			printk("Can't find ..!\n");
			return NULL;
		}
		int pos=(faidx-1)*0x20+sta;
		pt=bread(dev,pos/1024)->b_data; pt+=pos%1024;
		struct m_inode* fa=pt;
		pt=bread(dev,fa->i_zone[0])->b_data;
		num=0;
		while(num<1024){
			if((*(unsigned short*)pt)==idx){
				int cnt=2;
				while(*(pt+cnt))++cnt;
				while(cnt>2){
					--cnt;
					temp[++len]=*(pt+cnt);
				}
				temp[++len]='/';
				break;
			}
			pt+=16; num+=16;
		}
		if(faidx==1)break;  /*当前为根目录就不在往上跳了*/
		now=fa; idx=faidx;
	}
	int top=0,i;
	temp[len+1]='\0';
	for(i=len;i>0;--i){
		put_fs_byte(temp[i],buf+top);
		++top;
	}
	put_fs_byte('\0',buf+top);
	return buf;
}

首先3行记录当前索引节点和索引节点标号,5、6行根据超级块得到索引节点起始地址,记为sta。

然后进行while循环,每次循环有两个任务:找到当前目录名,跳转到父目录。先用 b r e a d bread bread 函数读取

当前目录第一个数据块的首地址,记为 p t pt pt ,然后从 p t pt pt 开始遍历每个目录项,判断其名称是否为

. . .. .. ,如是则找到了父目录索引节点,记为 f a i d x faidx faidx。算出父目录索引节点并读取索引节点内容:

pt=bread(dev,pos/1024)->b_data; pt+=pos%1024;

这里需特别注意,若仍采用bread读取,由于索引节点起始地址不为0x400整数倍,块号为其对0x400

的商,起始地址还要加上对0x400的余数。 创建索引节点类型指针 f a fa fa 指向 p t pt pt,省去很多不必要的计算。

接下来读取父目录第一个数据块,用while循环比对每个目录项,若找到索引节点与当前目录索引节点

相同,即得到当前目录名,倒序计入答案temp。

最后更新当前目录索引节点与下标。直到父目录索引节点为1,即为根目录时退出循环。

最后将temp的答案借助 put_fs_byte 倒序写入buf即可。

2、getdents:

linux_dirent 结构体形式(要打开测试的getdents.c,在其include的 “new.h” 中):

struct linux_dirent {
	long           d_ino;
	off_t          d_off;
	unsigned short d_reclen;
	char           d_name[14];
};

首先根据当前进程的打开文件表与描述符找到对应的读写信息状态项,得到inode节点。类似getcwd的

方法,可以得到所有的目录项。逐个读取目录项即可。

代码如下:

int sys_getdents(int fd,struct linux_dirent *dirp,unsigned long len){
	char buf[512]; int top=0;
	int sz=sizeof(long)+sizeof(off_t);
	struct m_inode* now=current->filp[fd]->f_inode;
	char* pt=bread(now->i_dev,now->i_zone[0])->b_data;
	int cnt=0;
	while(cnt<1024){
		if((*(unsigned short*)pt)==0)break;
		int reclen=sz+2+14;
		*(unsigned short*)buf = *(unsigned short*)pt;
		*(unsigned short*)(buf+top+sz)=reclen;
		top+=sz+2;
		pt+=2; int num=0;
		for(;num<14;++num,++pt)buf[top++]=*pt;
		cnt+=16;
	}
	if(cnt==32)printk("This is an empty directory!\n");
	int i;
	for(i=0;i<top;++i)put_fs_byte(buf[i],(char*)dirp+i);
	return top;
}

更新dirp实质上就是更新该指针后面的一片连续空间,可以用buf数组来记录答案。更新每个

linux_dirent项,直到目录项对应索引节点号为0. 最后将buf逐字节写入dirp。

3、execve2

大致思路:类似execve,添加系统调用,只是 call do_execve2。 关注do_execve2:

大体上照搬 d o _ e x e c v e do\_execve do_execve 代码,除此之外要在修改eip之前提前加载可执行文件的代码段,数据段,

bss段等即可。原先的加载是do_no_page函数,我们以一样的内容在 memory.c 中定义

do_no_page2函数即可,形为:

void do_no_page2(unsigned long address);

do_execve2 函数改动的部分:

	current->euid = e_uid;
	current->egid = e_gid;
	/*从这里开始改动*/
	i = ex.a_text+ex.a_data+ex.a_bss+ex.a_syms;
	/*加载代码段,数据段*/
	int sta=current->start_code+ex.a_entry,end=sta+i,j;
	for(j=sta;j<end;j+=0x1000)do_no_page2(j);
	/*下面和原来的一样*/	
	while (i&0xfff)
		put_fs_byte(0,(char *) (i++));

注意,do_no_page2的参数是线性地址,所以要将段内偏移加上 start_code 。而一个页表项对应4KB大小的页面,故参数地址每次 + 4KB即可。

4、sleep:

unsigned int sys_sleep(unsigned int seconds){
	sys_signal(SIGALRM,SIG_IGN,NULL);
	sys_alarm(seconds);
	sys_pause();
	return 0;
}

我们只需要睡眠当前进程seconds秒即可。而睡眠分为两步,修改控制块中的alarm,执行pause重新

调度。同时要注意调用sys_signal忽略SIGALRM信号,否则进程会被SIGALRM信号杀死。

sys_signal后,不用担心信号处理函数被修改无法复原的问题,源码中

tmp.sa_flags = SA_ONESHOT | SA_NOMASK;

SA_ONESHOT 的意义是,使此处理句柄单次有效,下次即恢复默认值。如果想要多次有效要调用 sys_sigaction.

三、实验结果与分析

实验结果截图:

在这里插入图片描述
在这里插入图片描述
与预期效果一致。

为了更好的测试,我们修改当前目录来测试 getcwd,getdents,结果与预期一致:

在这里插入图片描述

修改sleep秒数,测得结果仍符合预期。

修改execve2测试程序中的可执行文件名为 ./getdents,再编译运行 ,结果与预期一致:

在这里插入图片描述

以上结果说明正确实现了为内核增加四个系统调用的功能。

分析与注意事项:

1、内核与用户态的起始地址不同,任何尝试在内核调用用户态函数、修改用户态指针对应地址数据都

会出错!

解决方案:只调用内核态函数,如 sys_alarm,sys_pause; 需要在内核修改用户态指针对应处的

数据时,调用函数 put_fs_byte(val,用户态地址).

2、寻址方式为小端寻址!例:读取 char指针pt 处的一个双字:

法一:(简单)

*(unsigned short*)pt

法二:(复杂,需要理解寻址方式,同时规避溢出)

((int)((*(pt+1))&0xff)<<8) + ((*pt)&0xff)

3、关于do_execve的理解:

该函数修改进程控制块成要执行可执行文件所需要的模式,同时将页表清空。最后两句设置 eip[0]=可

执行文件起始指令地址,从核心态返回后,eip将变为此地址并开始执行文件的第一条指令;又修改了

eip[3],为用户态栈指针。可以发现前后进程并没有发生改变。

四、问题与建议

1、在 sys_getcwd 中,暂时必须提前将设备块号记录。尝试根据新索引节点数据块号读取会出

错。即行24中,bread的第一个参数dev不能修改为i_dev。

2、getdents中,对于linux_dirent结构体的兼容性较差。因为内核并没有关于此结构体的定义,所以只

能参照测试文件的结构体定义来设定一些常量。一旦结构体定义修改,常量也要修改。

  • 4
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
杭电实验1所涉及的内容是Linux内核编译及添加系统调用。 在这个实验中,首先需要了解Linux内核的编译过程。Linux内核是操作系统的核心部分,由C语言编写。编译内核的目的是为了生成可执行的内核镜像文件,即vmlinuz。内核编译的一个重要环节是配置内核,确定编译时的各种选项和功能。 首先,需要从官方网站(https://www.kernel.org/)上获取最新的Linux内核源代码。在命令行中使用wget命令下载源代码文件,并解压缩到合适的目录。 接下来,进入源代码目录,使用make menuconfig命令配置内核。此命令会打开一个文本界面菜单,可以选择需要的功能和驱动,并进行一些优化设置。可以根据实验要求选择需要的功能,也可以使用默认配置。 配置完成后,使用make命令开始编译内核。编译过程需要一定时间,取决于计算机性能和内核大小。编译完成后会生成可执行的内核镜像文件vmlinuz。 接下来是添加新的系统调用系统调用是应用程序与操作系统之间的接口,用于请求操作系统提供服务。在实验中,需要在内核中添加一个自定义的系统调用。 首先,在内核源代码中找到系统调用表文件syscalls.h,添加新的系统调用的函数原型。然后,在内核的核心文件kernel/sys.c中实现该系统调用的功能代码。 在添加完系统调用的功能代码后,需要编译内核并安装。使用make命令编译内核,并使用make install命令将编译好的内核安装到系统中。安装完成后,重启计算机,系统就会使用新的内核和新添加的系统调用。 总结来说,杭电实验1是介绍了Linux内核的编译过程和系统调用的添加方法。通过这个实验,可以了解Linux内核的基本结构和编译过程,以及如何在内核中添加新的系统调用。这对于深入理解操作系统和内核开发是非常有帮助的。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值