实验目的
- 建立对系统调用接口的深入认识
- 掌握系统调用的基本过程
- 能完成系统调用的全面控制
- 为后续实验做准备
实验内容
此次实验的基本内容是:在Linux 0.11上添加两个系统调用,并编写两个简单的应用程序测试它们。
iam()
第一个系统调用是iam(),其原型为:
int iam(const char * name);
完成的功能是将字符串参数name的内容拷贝到内核中保存下来。要求name的长度不能超过23个字符。返回值是拷贝的字符数。如果name的字符个数超过了23,则返回“-1”,并置errno为EINVAL。
在kernal/who.c中实现此系统调用。
whoami()
第二个系统调用是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。最终的运行结果是:
$ ./iam lizhijun
$ ./whoami
lizhijun
实验原理
操作系统实现系统调用的基本过程是:
- 应用程序调用库函数(API);
- API将系统调用号存入EAX,然后通过中断调用使系统进入内核态;
- 内核中的中断处理函数根据系统调用号,调用对应的内核函数(系统调用);
- 系统调用完成相应功能,将返回值存入EAX,返回到中断处理函数;
- 中断处理函数返回到API中;
- API将EAX返回给应用程序。
应用程序如何调用系统调用
在通常情况下,调用系统调用和调用一个普通的自定义函数在代码上并没有什么区别,但调用后发生的事情有很大不同。调用自定义函数是通过call指令直接跳转到该函数的地址,继续运行。而调用系统调用,是调用系统库中为该系统调用编写的一个接口函数,叫API(Application Programming Interface)。API并不能完成系统调用的真正功能,它要做的是去调用真正的系统调用,过程是:
- 把系统调用的编号存入EAX
- 把函数参数存入其它通用寄存器
- 触发0x80号中断(int 0x80)
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中定义。将_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。而在应用程序中,要有:
#define __LIBRARY__ /* 有它,_syscall1等才有效。详见unistd.h */
#include <unistd.h> /* 有它,编译器才能获知自定义的系统调用的编号 */
_syscall1(int, iam, const char*, name); /* iam()在用户空间的接口函数 */
_syscall2(int, whoami,char*,name,unsigned int,size); /* whoami()在用户空间的接口函数 */
在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中,Linux实验环境下是main(),Windows下因编译器兼容性问题被换名为start())调用了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。具体细节请参考《注释》的第4章。
接下来看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
pushl %ebx # push %ebx,%ecx,%edx,是传递给系统调用的参数
movl $0x10,%edx # 让ds,es指向GDT,内核地址空间
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修饰为其他函数可见。Windows实验环境下会看到它有一个下划线前缀,这是不同版本编译器的特质决定的,没有实质区别。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();
不然,编译会出错的。
实验过程
添加系统调用的流程
添加一个系统调用的流程如下:
-
修改
include/unistd.h
, 添加#define __NR_foo num
,num为接下来使用的系统调用号 -
修改
include/linux/sys.h
, 添加extern rettype sys_foo();
, 在sys_call_table
数组对应位置加入sys_foo
-
修改
kernel/system_call.s
,修改nr_system_calls = num
(num
为系统调用总数目) -
在
kernel
中添加foo.c
(若需要支持内核态与用户态数据交互,则包含include/asm/segment.h
,其中有put_fs_XXX
和get_fs_XXX
函数) -
在
foo.c
实现系统调用sys_foo() -
修改
kernel
的Makefile,将foo.c
与内核其它代码编译链接到一起 -
系统调用用户需要使用
#define __LIBRARY__
#include <unistd.h>
_syscallN宏展开系统调用,提供用户态的系统调用接口(参数数目确定具体宏)
添加whoami和iam两个系统调用:
- 修改
include/unistd.h
, 添加#define __NR_foo num
,num为接下来使用的系统调用号
#define __NR_iam 72
#define __NR_whoami 73
- 修改
include/linux/sys.h
, 添加extern rettype sys_foo();
, 在sys_call_table
数组对应位置加入sys_foo
extern int sys_iam();
extern int sys_whoami();
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,
sys_write, sys_open, sys_close, sys_waitpid, sys_creat, sys_link,
sys_unlink, sys_execve, sys_chdir, sys_time, sys_mknod, sys_chmod,
sys_chown, sys_break, sys_stat, sys_lseek, sys_getpid, sys_mount,
sys_umount, sys_setuid, sys_getuid, sys_stime, sys_ptrace, sys_alarm,
sys_fstat, sys_pause, sys_utime, sys_stty, sys_gtty, sys_access,
sys_nice, sys_ftime, sys_sync, sys_kill, sys_rename, sys_mkdir,
sys_rmdir, sys_dup, sys_pipe, sys_times, sys_prof, sys_brk, sys_setgid,
sys_getgid, sys_signal, sys_geteuid, sys_getegid, sys_acct, sys_phys,
sys_lock, sys_ioctl, sys_fcntl, sys_mpx, sys_setpgid, sys_ulimit,
sys_uname, sys_umask, sys_chroot, sys_ustat, sys_dup2, sys_getppid,
sys_getpgrp, sys_setsid, sys_sigaction, sys_sgetmask, sys_ssetmask,
sys_setreuid, sys_setregid, sys_iam, sys_whoami};
- 修改
kernel/system_call.s
,修改nr_system_calls = num
(num
为系统调用总数目)
nr_system_calls = 74
- 在
kernel
中添加foo.c
(若需要支持内核态与用户态数据交互,则包含include/asm/segment.h
,其中有put_fs_XXX
和get_fs_XXX
函数) - 在
foo.c
实现系统调用sys_foo()
who.c内容如下:
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
char username[24];
int sys_iam(const char * name) {
char tmp[26];
short break_flag = 0, i = 0;
for (i = 0; i < 26; ++i) {
tmp[i] = get_fs_byte(name + i);
if (tmp[i] == '\0') {
break_flag = 1;
break;
}
}
if (!break_flag || i > 23) {
return -(EINVAL);
}
char* dest = username;
strcpy(dest, tmp);
return i;
}
int sys_whoami(char* name, unsigned int size) {
short length = strlen(username);
if (length > size) {
return -(EINVAL);
}
short i = 0;
for (i; i < size; ++i) {
put_fs_byte(username[i], name + i);
if (username[i] == '\0') {
break;
}
}
return i;
}
- 修改
kernel
的Makefile,将foo.c
与内核其它代码编译链接到一起
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
- 用户调用系统调用:(在运行的linux0.11上编写编译运行)
whoami.c:
#include <errno.h>
#define __LIBRARY__
#include <unistd.h>
_syscall2(int, whoami,char*,name,unsigned int,size);
int main()
{
char s[30];
whoami(s,30);
printf("%s",s);
return 0;
}
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;
}
如编译错误,说__NR_whoami和__NR_iam未定义,则是下面的问题:
在0.11环境下编译C程序,包含的头文件都在/usr/include目录下。该目录下的unistd.h是标准头文件(它和0.11源码树中的unistd.h并不是同一个文件,虽然内容可能相同),没有__NR_whoami和__NR_iam两个宏,需要手工加上它们,也可以直接从修改过的0.11源码树中拷贝新的unistd.h过来。
实验结果
可以发现可以很好地执行!证明此次实验是成功的!
reference
[1] 实验指导书
[2] 现成代码