一、实验目的和内容
实验目的:深入掌握操作系统内核程序开发方法。
实验内容:以版本 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结构体的兼容性较差。因为内核并没有关于此结构体的定义,所以只
能参照测试文件的结构体定义来设定一些常量。一旦结构体定义修改,常量也要修改。