向操作系统中添加系统调用
一, 实验内容
在Linux 0.11上添加两个系统调用, 在kernel/who.c中实现.
第一个系统调用是iam(),其原型为:int iam(const char * name);
完成的功能是将字符串参数name的内容保存到内核中。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回-1,并置errno为EINVAL。
第二个系统调用是whoami(),其原型为:int whoami(char* name, unsigned int size);
它将内核中由iam()保存的名字拷贝到name指向的用户地址空间中,同时确保不会对name越界访存(name的大小由size说明)。返回值是拷贝的字符数。如果size小于需要的空间,则返回-1,并置errno为EINVAL。
运行结果应该是这样的:
\$ ./iam aixiangfei
\$ ./whoami
aixiangfei
二, 实验步骤
1, 根据printf()函数的实现原理, 首先是调用库函数printf(), 然后在库函数中调用write()函数, 把系统调用号__NR_write传递给0x80号中断. 由于这次实验只要求实现系统调用, 而不是实现库函数, 所以就不需要第一步了. 所以现在应该先添加一个类似于__NR_write的系统调用号, 可以在include/unistd.h中添加两个宏:
#define __NR_whoami 72 #define __NR_iam 73
2, 当发生0x80号中断时, 就会调用中断处理函数system_call, 这个函数在在system_call.s中, 注意到system_call函数中有一个判断语句:
......
nr_system_calls = 72
......
system_call: cmpl $nr_system_calls-1,%eax ja bad_sys_call
......
call sys_call_table(,%eax,4)
......
system_call函数首先会判断nr_system_calls-1和%eax的大小, 如果%eax更大, 那么就会跳到bad_sys_call 发生系统调用错误! 如果调用whoami, 那么传递给eax的值就是72, 如果调用iam, 那么传递给eax的值就是73, 所以, 如果不修改这个值, 系统调用肯定会失败的! 所以nr_system_calls应该改为74
3, 中断处理函数system_call会从sys_call_table函数表中查找系统调用的入口地址, 所以还要修改include/linux/sys.h文件.
首先在数组中的末尾添加两个函数名:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, ...... , sys_whoami, sys_iam };
由于sys_whoami和sys_iam是函数名, 编译器必须要知道函数名的声明才能编译通过, 所以在前面必须要加上这两个函数的声明:
......
extern int sys_whoami(); extern int sys_iam();
4, 接下来就应该来实现sys_whoami()和sys_iam()函数了. 根据实验的要求, 这两个系统调用必须在kernel/who.c中实现.
首先应该考虑一个问题: int iam(const char * name)和int whoami(char* name, unsigned int size)中的指针参数传递的是应用程序所在地址空间的逻辑地址,而现在是在内核中, 在内核中如果直接访问这个地址,访问到的是内核空间中的数据,而不会是原来用户空间的数据. 所以必须借助于内核提供的get_fs_byte()和put_fs_byte(), get_fs_byte()的功能是从用户地址空间中获得一个字节的的数据, put_fs_byte()的功能是从核心态拷贝数据到用核心态.
这两个函数都是在头文件include/asm/segment.h中声明的, 所以who.c文件必须先包含asm/segment.h
然后考虑到出错处理的问题, 实验要求函出错之后应该返回-1,并置errno为EINVAL, 但是在进入0x80中断之前, 其实已经做了一些错误处理的工作的, 比如说write的定义:
int write(int fd, const char * buf, off_t count) { long __res; __asm__ volatile ( "int $0x80" // 汇编语句 : "=a" (__res) // 输出寄存器 : "0" (__NR_write), "b" (long fd), "c" (long buf) "d" (long count) // 输入寄存器 if (__res >= 0) return (int)__res; errno = -__res; return -1; ) }
可以看到如果返回值__res>=0, 函数才正确返回, 否则就要先设置errno为 -__res, 并返回-1, 所以在系统调用处理出错情况时, 只需要返回-EINVAL即可.
也就是说, 如果系统调用没有出错, 那么返回值肯定是一个正数, 如果系统调用出错了, 就返回-EINVAL, EINVAL是在errno.h中定义的是, 是一个正数, 当返回到用户态的时候, errno的值就是EINVAL了, 最后会返回-1表示出错.
所以最终的who.c代码如下所示:
#include <errno.h> #include <asm/segment.h> char buf[24]; int sys_iam(const char *name) { int count; int i; char temp[24]; for (count = 0; count < 24; count++) { temp[count] = get_fs_byte(name + count); if (temp[count] == '\0') break; } if (count == 24) return -EINVAL; for (i = 0; i <= count; i++) buf[i] = temp[i]; return count; } int sys_whoami(char *name, unsigned int size) { int count = 0; int i = 0; while (buf[count] != '\0') count++; if (size < count) return -EINVAL; while (i < count) { put_fs_byte(buf[i], &name[i]); i++; } return count; }
5, 由于修改了内核文件, 所以必须要修改Makefile文件, 把添加的文件编译进操作系统镜像中. 因为对于整个代码树来说, 仅仅添加了who.c文件, 所以只需要修改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
修改为:
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: exit.s exit.o: exit.c ../include/errno.h ../include/signal.h \ ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \ ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \ ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \ ../include/asm/segment.h
修改为:
\### Dependencies: 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 \ ../include/sys/types.h ../include/sys/wait.h ../include/linux/sched.h \ ../include/linux/head.h ../include/linux/fs.h ../include/linux/mm.h \ ../include/linux/kernel.h ../include/linux/tty.h ../include/termios.h \ ../include/asm/segment.h
即可.
接下来就可以make了
三, 验证结果
上面仅仅是修改了linux内核相关文件, 还没有修改linux的文件系统, 而实验文件给的是一个编译好的hdc-0.11.img镜像文件, 不能直接修改, 所以只好挂载到Ubuntu系统上再来修改了.
先挂载linux 0.11文件系统, 然后在/usr/include/unistd.h中添加两个两个系统调用号, 跟实验步骤1是一样的.
然后就可以./run了!
最后可以编写测试函数了. 需要注意的是, 由于不能修改直接修改文件系统的源码, 库里面还没有iam()和whoami()这两个库函数, 可以向库里面增加这两个函数, 但是为了方便, 就直接在测试程序中添加函数的定义了.
// iam.c
#define __LIBRARY__ #include <unistd.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> #include <stdio.h> _syscall2(int, whoami, char *, name, unsigned int, size) int main() { char arr[24]; whoami(arr, 24); printf("%s", arr); return 0; }
用gcc编译之后, 运行结果如图所示:

浙公网安备 33010602011771号