本节将更新哈工大《操作系统》课程第三个 Lab 实验 系统调用。按照实验书要求,介绍了非常详细的实验操作流程,并提供了超级无敌详细的代码注释。文末附完整标准答案代码,包括系统调用实现 who.c
和测试函数 iam.c
、whoami.c
以及超详细注释。
实验目的:
- 建立对系统调用接口的深入认识;
- 掌握系统调用的基本过程;
- 能完成系统调用的全面控制;
- 为后续实验做准备。
实验任务:
在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
1、第一个系统调用是 iam(),完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为EINVAL。
2、第二个系统调用是 whoami(),它将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)。返回值是拷贝的字符数。如果 size 小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。
3、运行添加过新系统调用的 Linux 0.11,在其环境下编写两个测试程序 iam.c 和 whoami.c。
实验工具准备:
文件名 | 介绍 |
---|---|
hit-操作系统实验指导书.pdf | 哈工大OS实验指导书 |
Linux内核完全注释(修正版v3.0).pdf | 赵博士对Linux v0.11 OS进行了详细全面的注释和说明 |
file1615.pdf | BIOS 涉及的中断数据手册 |
hit-oslab-linux-20110823.tar.gz | hit-oslab 实验环境 |
gcc-3.4-ubuntu.tar.gz | Linux v0.11 所使用的编译器 |
Bochs 汇编级调试指令 | bochs 基本调试指令大全 |
最全ASCII码对照表0-255 | 屏幕输出字符对照的 ASCII 码 |
x86_64 常用寄存器大全 | x86_64 常用寄存器大全 |
一、应用程序如何调用系统调用
1、创建实验工程 oslab_Lab2
- 解压
hit-oslab-linux-20110823.tar.gz
,并命名为os_lab_Lab2
,在此源码基础上我们进一步完成实验。 - 编译及运行:
cd ~/my_space/OS_HIT/oslab_Lab2/linux-0.11/ // 工程文件夹
make all // 编译
../run // 启动 Bochs
2、创建 NR_whoami 和 NR_iam 两个宏
==注意:==此时不是修改内核目录里的,而是修改在 v0.11 的开发环境里的这个文件。(这步很多同学会犯错,导致后续在 linux-v0.11 环境下使用 gcc
编译测试函数时报错找不到这两个宏)
具体原因:可以参照《Linux内核完全注释(修正版v3.0).pdf》
- 挂载 linux-v0.11 的磁盘,并在 include/unistd.h 中添加宏
cd oulab_Lab2
sudo ./mount-hdc
cd hdc/usr/include
gedit unistd.h
- 通过文本编辑器,在 unistd.h 头文件中手动添加宏
#define __NR_iam 72
#define __NR_whoami 73
二、从“int 0x80”进入内核函数
用户程序调用进入内核的步骤
- init/main.c 内核初始化时,调用量 sched_init() 初始化函数
- kernel/sched.c 中定义sched_init:
void sched_init(void) { // …… set_system_gate(0x80,&system_call); }
- include/asm/system.h 定义宏 set_system_gate :
#define set_system_gate(n,addr) _set_gate(&idt[n],15,3,addr)
- include/asm/system.h 定义宏 _ set_gate :填写IDT(中断描述符表),将 system_call 函数地址写到 0x80 对应的中断描述符中,也就是在中断 0x80 发生后,自动调用函数 system_call。
1、修改 kernel/system_call.s 文件中的系统调用总数
// 由原来的 72 修改为 74
nr_system_calls = 74
2、在 include/linux/sys.h 文件中增加 whoami() 和 iam() 两个函数声明
extern int sys_whoami();
extern int sys_iam();
fn_ptr sys_call_table[] = { sys_setup,sys_exit, sys_fork, sys_read,..., sys_iam, sys_whoami } // 表中添加的函数顺序必须和 _NR_xxxxxx 的值对应
三、实现 sys_iam() 和 sys_whoami()
我需要编写系统调用函数来实现这两个功能。值得注意的是,每个系统调用都有一个 sys_xxxxxx() 与之对应,它们都是我们学习和模仿的好对象。
- 此处的要点是如何实现:在用户态和核心态之间传递数据。其难点是:指针参数传递的是应用程序所在地址空间的逻辑地址,在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。
以 open
为例:
lib/open.c:
- 系统调用是用 eax、ebx、ecx、edx 寄存器来传递参数的
- 获取用户地址空间(用户数据段)中的数据依靠的就是段寄存器 fs,下面该转到 sys_open 执行了
fs/open.c:
- 将参数传给了 open_namei()
- open_namei( ) 用 ==get_fs_byte()== 获得一个字节的用户空间中的数据
所以说,在whoami() 中实现用户态和和心态之间的数据传递,即通过:put_fs_xxx() 和 get_fs_xxx() 都是用户空间和内核空间之间的桥梁
- 在
oslab_Lab2/linux-0.11/kernel/
文件夹下创建系统调用实现who.c
。具体代码如下:(已有详细注释,此处将不再赘述)
// sys_iam 和 sys_whoami 系统调用函数
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
// 内核中存放name,23字符 + '\0' = 24
char msg[24];
// 将字符串参数 name 拷贝到内核中保存下来
// return:拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。
int sys_iam(const char* name)
{
int i;
char tmp[30]; // 临时存储,操作失败不影响 msg
// 从用户态内存获取数据,存入 tmp
for(i = 0; i < 30; i++) {
tmp[i] = get_fs_byte(name+i);
if(tmp[i] == '\0')
break;
}
// printk(tmp);
i = 0;
// 计算 name 的长度
while(tmp[i] != '\0' && i < 30) {
i++;
}
int len = i;
// 长度大于 23 返回报错
if(len > 23) {
return -(EINVAL);
}
strcpy(msg, tmp);
return i;
}
// 将由 iam() 保存的名字拷贝到 name 指向的用户地址空间,size 指定 name 的大小
// return:如果size小于需要的空间,则返回“-1”,并置errno为 EINVAL
int sys_whoami(char* name, unsigned int size)
{
int len = 0;
// 计算长度
for(; msg[len] != '\0'; len++);
// 长度大于则返回
if(len > size) {
return -(EINVAL);
}
// 从内核态获取数据,存入 name
int i = 0;
for(; i < size; i++) {
put_fs_byte(msg[i], name+i);
if(msg[i] == '\0')
break;
}
return i;
}
值得注意的是,在编写代码过程中,我们可以通过
printk()
函数在屏幕上输出数据来进行调试,适当地向屏幕输出一些程序运行状态的信息,也是一种很高效、便捷的调试方法。
四、修改编译规则 Makefile
我们需要为新建的
who.c
文件添加编译规则来创建编译对象以及添加响应的依赖,后续即可使用make all
一键编译。
- 修改
oslab_Lab2/linux-0.11/kernel/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 who.o
### Dependencies:
who.s who.o: who.c ../include/linux/kernel.h ../include/unistd.h
五、编写测试函数
编写两个测试函数,来测试我们编写的系统调用函数 sys_iam() 和 sys_whoami。
- iam.c:将字符串参数 name 的内容拷贝到内核中保存下来
- whoami.c:将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中并显示。
- 值得说明的是,我们编写后的两个测试函数需要在 linux-v0.11 环境下进行编译。由于 linux-v0.11 作为一个很小的操作系统,只有编译工具 vi ,使用起来非常不方便。因此,我们可以直接在 ubuntu 环境下进行编写,再通过挂载
hdc
磁盘来将这两个文件拷贝到 linux-v0.11 系统下,然后编译、运行。
1、在 oslab_Lab2/linux-0.11/kernel
文件夹下创建两个测试函数
- iam.c:
#include <errno.h>
#define __LIBRARY__
#include <unistd.h>
#include <stdio.h>
_syscall1(int, iam, const char*, name);
int main(int argc,char ** argv)
{
iam(argv[1]);
return 0;
}
- whoami.c:
#define __LIBRARY__
#include <unistd.h>
_syscall2(int, whoami, char*, name, unsigned int, size);
int main(int argc, char* agrv[])
{
char s[30];
whoami(s, 30);
printf("%s\n", s);
return 0;
}
2、将两个测试函数移动到 linux-v0.11 系统下
通过挂载 hdc 磁盘,将编写的两个测试函数移动到 linux-v0.11 系统下,以便下一步进行编译。由于我们每在 ubuntu 系统下修改一次测试函数都要将其移动到 linux-v0.11 系统下进行编译,为了方便,我们可以编写一个 bash 脚本,存放如下命令。
cd ~/my_space/OS_HIT/oslab_Lab2
touch cp.sh // 创建脚本
脚本内容如下:
sudo ./mount-hdc // 挂载磁盘
sudo cp ./linux-0.11/kernel/iam.c ./hdc/iam.c // 将 iam.c 移动到 hdc 磁盘
sudo cp ./linux-0.11/kernel/whoami.c ./hdc/whoami.c // 将 whoami.c 移动到 hdc 磁盘
sudo umount hdc // 卸载
./run // 启动 linux-v0.11
3、编译与运行
在编译之前,尤其注意,上述创建的 iam.c 和 whoami.c 两个文件中不能含有任何
//
之类的注释,否则在使用 linux-v0.11 系统进行编译时会报错。
- 在 linux-v0.11 系统下,我们运行如下命令回到根目录,可以看到移动过来的
iam.c
和whoami.c
文件。
cd ../..
ls
- 编译,运行如下指令,可以生成
iam
和whoami
两个可执行文件。
gcc -o iam iam.c -Wall // 编译 iam.c 文件
gcc -o whoami whoami.c -Wall // 编译 whoami.c 文件
// -Wall 是 GCC 的一个选项,用来开启所有警告
- 运行,分别运行两个文件,得到如下所示结果
./iam Joker // 将 Joker 存入内核
./whoami // 从内核中取出 ./iam 存入的字符并显示
至此,Lab3 实验介绍完毕。
也许有人觉得比较实验步骤比较多比较复杂,特此总结一下每个步骤:
- 在
hdc/usr/include/unistd.h
中添加系统调用号__NR_iam
和__NR_whoami
- 在
kernel/system_call.s
修改系统调用总数 nr_system_calls = 74 - 在
include/linux/sys.h
添加函数声明extern int sys_whoami();
和extern int sys_iam();
- 创建两个函数的实现:
/kernel/who.c
- 在
kernel/Makefile
中添加编译规则 - 在
/kernel
中创建测试函数 iam.c 和 whoami.c - 编写 bash 脚本,将测试函数移到 linux-v0.11 下
- 在 Linux-v0.11 下编译 iam.c 和 whoami.c