HIT-OSLab2
实验目标:在 Linux-0.11 上添加两个系统调用:(原始只有72个系统调用)
1 | int iam(const char * name); |
- 将
name
中存放的字符串拷贝到内核中并保存下来,要求name
的长度不能超过 23 个字符,若超过了,返回 -1 并置errno
为EINVAL
,如果没有超过就返回拷贝的字符个数
1 | int whoami(char * name,unsigned int size); |
- 将内核中存放的字符串(由
iam
拷贝进去的)重新复制到name
中,size
是指name
指向的内存空间允许存放的最大长度,如果size
小于需要的空间,就返回 -1,并置errno
为EINVAL
,否则返回拷贝的字节数
实验过程
系统调用执行的详细过程如下:
- 调用中断处理程序:当进程需要执行系统调用时,进程会主动调用中断处理程序 interrupt handler
- 保存现场信息:中断处理程序会保存当前进程的寄存器值到堆栈中,以便在系统调用结束后恢复进程状态
- 切换控制权:将 CPU 控制权交给操作系统,操作系统开始处理系统调用请求
- 解析系统调用请求:操作系统根据系统调用请求的代码指针等信息,解析出需要调用的系统服务函数的地址
- 调用系统服务函数:操作系统将解析出的系统服务函数地址作为参数,调用系统服务函数执行系统调用请求
- 处理系统调用结果:系统服务函数处理完系统调用请求后,会将结果返回给操作系统
- 恢复现场信息:操作系统将保存的寄存器值从堆栈中取出,恢复当前进程的原始状态
- 切换控制权:操作系统将 CPU 控制权还给进程,进程继续执行
在代码层中的实现如下:
- 把系统调用的编号存入 ax
- 把函数参数存入其他通用寄存器
- 触发 0x80 中断(
int 0x80
)
在 linux-0.11 中,系统调用的源码存放在两处地方:
linux-0.11/lib/
linux-0.11/kernel/
(实验要求我们把文件放入这里)
在系统调用中用户态程序不能直接和内核态程序交换数据,必须使用内核提供的 API,在 linux-0.11 中的 API 如下:
1 | static inline unsigned char get_fs_byte(const char * addr) |
编写 sys_iam 和 sys_whoami 的代码:
1 |
|
系统调用号在 linux-0.11/include/unistd.h
文件中,我们可以在其末尾添加新的系统调用号:
1 |
- 需要注意的是:这里同时需要修改
hdc-0.11.img
中的hdc/usr/include/unistd.h
文件,如果想在虚拟机中使用 gcc 编译的话,会导入虚拟机hdc/usr/include/
中的文件为头文件
系统调用总数量则记录在 linux-0.11/kernel/system_call.s
中,将其改为74:
1 | nr_system_calls = 74 |
接下来需要在系统调用中添加我们实现的程序,为此我们需要先分析一下系统调用的代码
在 linux-0.11 中,system_call.s
文件为 Linux 内核中所有系统调用的实现提供了框架,主要包含以下内容:
- 系统调用函数原型声明:定义了每个系统调用的函数原型,以便其他汇编文件可以方便地调用这些函数
- 系统调用处理函数:定义了实际处理每个系统调用的函数,这些函数由内核驱动,并通过中断处理机制执行
- 系统调用参数转换函数:在调用用户空间应用程序之前,系统调用参数需要进行转换,这些函数负责将内核提供的参数转换为应用程序需要的格式
所有系统调用的入口如下:
1 | system_call: |
在 sys_call_table 中记录了所有的系统调用,其位置在 linux-0.11/include/linux/sys.h
:
1 | extern int sys_iam(); |
- 在其中添加 sys_iam,sys_whoami 即可完成注册
最后需要在 makefile 中添加 sys_iam,sys_whoami 的链接,不然就会引发如下报错:
1 | ld: kernel/kernel.o:(.data+0x120): undefined reference to `sys_iam' |
修改 makefile:
1 | OBJS = sched.o system_call.o traps.o asm.o fork.o \ |
1 | ### Dependencies: |
为了测试系统调用,我们可以将 hdc-0.11.img 挂载(使用 mount-hdc 脚本),然后再往其中写入测试脚本的代码
不过在此之前我们需要先了解一下 _syscallN 宏:
1 |
1 |
N
是一个整数(表示系统调用的编号),...
是一个可变参数列表(用于传递系统调用的参数),##
是一个 C 语言宏展开的符号(##name
将被替换name
)"=a"
用于指定返回寄存器__res
的输出模式(返回寄存器__res
用于存储系统调用的返回值),这段代码的作用是:在执行完汇编代码后应该将结果存储在 eax 寄存器中- 将系统调用号
__NR_##name
和参数a b c
压入栈中,最后执行int 0x80
在 linux-0.11 上函数调用和系统调用的底层代码是不同的,_syscallN 宏的出现可以使我们以函数调用的方式去写系统调用的C代码
下面是利用 _syscallN 执行系统调用的测试脚本:
1 |
|
1 |
|
使用如下命令进行编译:(最好在 linux-0.11 中进行编译)
1 | gcc -o whoami whoami.c -Wall |
最终效果如下: