实验内容
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
iam()
int iam(const char * name);
完成的功能是将字符串参数 name
的内容拷贝到内核中保存下来。要求 name
的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name
的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。
在 kernal/who.c
中实现此系统调用。
whoami()
int whoami(char* name, unsigned int size);
它将内核中由 iam()
保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name
越界访存(name
的大小由 size
说明)。返回值是拷贝的字符数。如果 size
小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。也是在 kernal/who.c
中实现。
测试程序
运行添加过新系统调用的 Linux 0.11,在其环境下编写两个测试程序 iam.c 和 whoami.c
具体流程
应用程序如何调用系统调用
在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。
调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。
而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
-
把系统调用的编号存入 EAX;
-
把函数参数存入其它通用寄存器;
-
触发 0x80 号中断(int 0x80)。
linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
close()
_syscall1()
按照参数展开
int close(int fd) { long __res; __asm__ volatile ("int $0x80" : "=a" (__res) : "0" (__NR_close),"b" ((long)(fd))); if (__res >= 0) return (int) __res; errno = -__res; return -1; }
它先将宏 __NR_close
存入 EAX,将参数 fd 存入 EBX,然后进行 0x80 中断调用。调用返回后,从 EAX 取出返回值,存入 __res
,再通过对 __res
的判断决定传给 API 的调用者什么样的返回值。
其中 __NR_close
就是系统调用的编号,在 include/unistd.h
中定义:
#define __NR_close 6 /* 所以添加系统调用时需要修改include/unistd.h文件, 使其包含__NR_whoami和__NR_iam。 */
/* 而在应用程序中,要有: */ /* 有它,_syscall1 等才有效。详见unistd.h */ #define __LIBRARY__ /* 有它,编译器才能获知自定义的系统调用的编号 */ #include "unistd.h" /* iam()在用户空间的接口函数 */ _syscall1(int, iam, const char*, name); /* whoami()在用户空间的接口函数 */ _syscall2(int, whoami,char*,name,unsigned int,size);
从“int 0x80”进入内核函数
int 0x80
触发后,接下来就是内核的中断处理了。先了解一下 0.11 处理 0x80 号中断的过程。
在内核初始化时,主函数调用了 sched_init()
初始化函数:
void main(void) { // …… time_init(); sched_init(); buffer_init(buffer_memory_end); // …… }
sched_init()
在 kernel/sched.c
中定义为:
void sched_init(void) { // …… set_system_gate(0x80,&system_call); }
set_system_gate
是个宏,在 include/asm/system.h
中定义为:
#define set_system_gate(n,addr) \ _set_gate(&idt[n],15,3,addr)
_set_gate
的定义是:
该函数主要的作用是写 IDT(中断描述符表),将 system_call
函数地址写到 0x80
对应的中断描述符中,也就是在中断 0x80
发生后,自动调用函数 system_call
system_call
纯汇编函数,定义在 kernel/system_call.s
中:
!…… ! # 这是系统调用总数。如果增删了系统调用,必须做相应修改 nr_system_calls = 72 !…… .globl system_call .align 2 system_call: ! # 检查系统调用编号是否在合法范围内 cmpl \$nr_system_calls-1,%eax ja bad_sys_call push %ds push %es push %fs pushl %edx pushl %ecx ! # push %ebx,%ecx,%edx,是传递给系统调用的参数 pushl %ebx ! # 让ds, es指向GDT,内核地址空间 movl $0x10,%edx mov %dx,%ds mov %dx,%es movl $0x17,%edx ! # 让fs指向LDT,用户地址空间 mov %dx,%fs call sys_call_table(,%eax,4) pushl %eax movl current,%eax cmpl $0,state(%eax) jne reschedule cmpl $0,counter(%eax) je reschedule
我们对文件进行修改,将系统调用数量增加2
system_call
用 .globl
修饰为其他函数可见
call sys_call_table(,%eax,4)
之前是一些压栈保护,修改段选择子为内核段,call sys_call_table(,%eax,4)
之后是看看是否需要重新调度,只关心 call sys_call_table(,%eax,4)
这一句。
根据汇编寻址方法它实际上是:call sys_call_table + 4 * %eax
,其中 eax 中放的是系统调用号,即 __NR_xxxxxx
。
显然,sys_call_table
一定是一个函数指针数组的起始地址,它定义在 include/linux/sys.h
中:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,...
增加实验要求的系统调用,需要在这个函数表中增加两个函数引用 ——sys_iam
和 sys_whoami
。当然该函数在 sys_call_table
数组中的位置必须和 __NR_xxxxxx
的值对应上。
同时还要仿照此文件中前面各个系统调用的写法,加上:
extern int sys_whoami(); extern int sys_iam();
实现 sys_iam() 和 sys_whoami()
添加系统调用的最后一步,是在内核中实现函数 sys_iam()
和 sys_whoami()
。每个系统调用都有一个 sys_xxxxxx()
与之对应,所以只要自己创建一个文件:kernel/who.c
,然后实现两个函数
#include <asm/segment.h> #include <errno.h> #include <string.h> char _myname[24]; int sys_iam(const char *name) { char str[25]; int i = 0; do { // get char from user input str[i] = get_fs_byte(name + i); } while (i <= 25 && str[i++] != '\0'); if (i > 24) { errno = EINVAL; i = -1; } else { // copy from user mode to kernel mode strcpy(_myname, str); } return i; } int sys_whoami(char *name, unsigned int size) { int length = strlen(_myname); printk("%s\n", _myname); if (size < length) { errno = EINVAL; length = -1; } else { int i = 0; for (i = 0; i < length; i++) { // copy from kernel mode to user mode put_fs_byte(_myname[i], name + i); } } return length; }
修改 Makefile
想让我们添加的 kernel/who.c
可以和其它 Linux 代码编译链接到一起,必须要修改 Makefile 文件。
kernel/Makefile
在kernel
目录下make,在根目录下make
用 printk() 调试内核
printf() 是一个只能在用户模式下执行的函数,而系统调用是在内核模式中运行,所以 printf() 不可用,要用 printk()
printk()
和 printf()
的接口和功能基本相同,只是代码上有一点点不同。printk() 需要特别处理一下 fs
寄存器,它是专用于用户模式的段寄存器。
printk()首先 push %fs保存这个指向用户段的寄存器,在最后 pop %fs 将其恢复,printk() 的核心仍然是调用 tty_write()
编写测试程序
sys_iam()
中向终端 printk()
一些信息,让应用程序调用 iam()
,从结果可以看出系统调用是否被真的调用到了。
使用./mount-hdc
进行挂载
在hdc/usr/root
下编写相应的.c文件
/* iam.c */ #define __LIBRARY__ #include <unistd.h> #include <errno.h> #include <asm/segment.h> #include <linux/kernel.h> _syscall1(int, iam, const char*, name); int main(int argc, char *argv[]) { /*调用系统调用iam()*/ iam(argv[1]); return 0; }
/* whoami.c */ #define __LIBRARY__ #include <unistd.h> #include <errno.h> #include <asm/segment.h> #include <linux/kernel.h> #include <stdio.h> _syscall2(int, whoami,char *,name,unsigned int,size); int main(int argc, char *argv[]) { char username[64] = {0}; /*调用系统调用whoami()*/ whoami(username, 24); printf("%s\n", username); return 0; }
sudo umount hdc
取消挂载
最终使用gcc -o iam iam.c -Wall
在 Linux 0.11 下编译
编译时报错
说明之前修改的 unistd.h 没有加载到 linux 0.11 中,需要手动添加或直接拷贝
进入hdc/usr/include/unistd.h
,为新增的系统调用添加系统调用号
后可以正常运行
回答问题
1、从 Linux 0.11 现在的机制看,它的系统调用最多能传递几个参数?你能想出办法来扩大这个限制吗?
从定义的宏来看,目前最多只支持三个参数
要扩大系统调用参数数量的限制,通常需要修改内核的系统调用处理机制以支持更多的参数。
2、用文字简要描述向 Linux 0.11 添加一个系统调用 foo() 的步骤。
1、修改kernel/system_call.s
中的nr_system_calls,其值为增加后的系统调用数
2、修改include/linux/sys.h
,为其增添宏定义以及在调用表中创建函数指针
3、在kernel/foo.c
中增添相关实现
4、挂载后修改hdc/usr/include/unistd.h
,为新增的系统调用添加系统调用号
5、修改kernel以及根目录下的makefile文件,后重新编译内核