实验3 系统调用
提醒
这次实验涉及的宏过于复杂,加上本人能力有限,我也没有花大量时间去研究每一段代码,只是理解到每一段代码做了什么这一程度。
实验目的
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
1.iam()
第一个系统调用是 iam(),其原型为:
int iam(const char * name);
完成的功能是将字符串参数 name 的内容拷贝到内核中保存下来。要求 name 的长度不能超过 23 个字符。返回值是拷贝的字符数。如果 name 的字符个数超过了 23,则返回 “-1”,并置 errno 为 EINVAL。
2.whoami()
第二个系统调用是 whoami(),其原型为:
int whoami(char* name, unsigned int size);
它将内核中由 iam() 保存的名字拷贝到 name 指向的用户地址空间中,同时确保不会对 name 越界访存(name 的大小由 size 说明)。返回值是拷贝的字符数。如果 size 小于需要的空间,则返回“-1”,并置 errno 为 EINVAL。
应用程序如何调用系统调用
在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。
调用自定义函数是通过 call 指令直接跳转到该函数的地址,继续运行。
而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫 API(Application Programming Interface)。API 并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
把系统调用的编号存入 EAX;
把函数参数存入其它通用寄存器;
触发 0x80 号中断(int 0x80)。
linux-0.11 的 lib 目录下有一些已经实现的 API。Linus 编写它们的原因是在内核加载完毕后,会切换到用户模式下,做一些初始化工作,然后启动 shell。而用户模式下的很多工作需要依赖一些系统调用才能完成,因此在内核中实现了这些系统调用的 API。
我们不妨看看 lib/close.c,研究一下 close() 的 API:
#define __LIBRARY__ #include <unistd.h> _syscall1(int, close, int, fd)
其中 _syscall1 是一个宏,在 include/unistd.h 中定义。
#define _syscall1(type,name,atype,a) \ type name(atype a) \ { \ long __res; \ __asm__ volatile ("int $0x80" \ : "=a" (__res) \ : "0" (__NR_##name),"b" ((long)(a))); \ if (__res >= 0) \ return (type) __res; \ errno = -__res; \ return -1; \ }
将 _syscall1(int,close,int,fd) 进行宏展开,可以得到:
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; }
这就是 API 的定义。它先将宏 __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);
在 0.11 环境下编译 C 程序,包含的头文件都在 /usr/include 目录下。
该目录下的 unistd.h 是标准头文件(它和 0.11 源码树中的 unistd.h 并不是同一个文件,虽然内容可能相同),没有 __NR_whoami 和 __NR_iam 两个宏,需要手工加上它们,也可以直接从修改过的 0.11 源码树中拷贝新的 unistd.h 过来。
从“int 0x80”进入内核函数
int 0x80 触发后,接下来就是内核的中断处理了。先了解一下 0.11 处理 0x80 号中断的过程。
在内核初始化时,主函数在 init/main.c 中,调用了 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 的定义是:
#define _set_gate(gate_addr,type,dpl,addr) \ __asm__ ("movw %%dx,%%ax\n\t" \ "movw %0,%%dx\n\t" \ "movl %%eax,%1\n\t" \ "movl %%edx,%2" \ : \ : "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \ "o" (*((char *) (gate_addr))), \ "o" (*(4+(char *) (gate_addr))), \ "d" ((char *) (addr)),"a" (0x00080000))
虽然看起来挺麻烦,但实际上很简单,就是填写 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
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();
不然,编译会出错的。