目录
前言
本文采用自顶向下的方式完成实验,也即从自己编写的用户应用程序开始一步一步完成最终的功能,这样可以更好地把握主线。
零、总览框图
一、编写应用程序
该应用程序iam.c和whoami.c将会调用iam()和whoami()这两个系统调用。
虽然实验讲义中提供了testlab2.sh用于测试我们的系统调用能否成功运行,但是编写自己的应用程序查看效果会更直观。以下仅以iam.c为例。
iam.c
建议放在oslab/hdc/usr/root目录下
这个hdc在哪?
首先在oslab’下执行sudo ./mount就会出现了
注意1:在取消挂载前记得执行sync来将数据写入硬盘
取消挂载:在oslab下,执行sudo umount hdc
注意2:一定要取消挂载后才可以打开linux-0.11
#define __LIBRARY__
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
_syscall1(int,iam,const char*,name)
int main(int argc,char* argv[])
{
iam(argv[1]); /*这里使用了iam()这个系统调用*/
return 0;
}
可以看到main函数上有一个接口
_syscall1(int,iam,const char*,name)
这个接口就是用来定位iam()这个系统调用的。这个syscall1在include/unistd.h下面。
补充:whoami.c
#define __LIBRARY__
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
_syscall2(int, whoami, char*, name, unsigned int, size);
int main(int argc, char ** argv)
{
char t[30];
whoami(t, 30);
printf("%s\n", t);
return 0;
}
二、追溯include/unistd.h的_syscall1(type,name,atype,a)
我们先来看看_syscall1的内容是什么
#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; \
}
1、先看第7行,这里是输入参数行,意思是将__NR_##name送到%eax,将a送到%ebx。
由于我们刚刚是这样调用的_syscall1(int,iam,const char*,name),所以输入参数行的意思就是将__NR_iam送到%eax,将argv[1]送到%ebx
2、再看第5行,这里是开始执行的第一行,调用了$0x80中断。
由上面可知,我们的系统里面肯定是没有__NR_iam这个宏的,所以我们需要在unistd.h添加__NR_iam这个宏。如下:
#define __NR_sigaction 67
#define __NR_sgetmask 68
#define __NR_ssetmask 69
#define __NR_setreuid 70
#define __NR_setregid 71 /*Linux system_call total 72*/
#define __NR_iam 72 /*new system_call 72 and 73*/
#define __NR_whoami 73
注意!!!
修改unistd.h需要修改oslab/hdc/usr/include/unistd.h才会生效。
最后两行是我们添加的。(不仅添加了__NR_iam,还添加了__NR_whoami)
好,现在的下一步就是去研究0x80中断了。
三、研究0x80中断
这部分在实验讲义6.2里面描述的非常详细,下面仅给出有关框图总结。
内核初始化时,
重点就在_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))
这段代码就是设置中断描述符表项
- 这里面的gate_addr就是段选择符。
- addr就是段描述符在段描述符表的偏移地址,也就时图中的过程入口点地址。
由上,对于set_system_gate(0x80,&system_call) ------->_set_gate(&idt[n],15,3,addr)这个调用来说,就是设置了0x80最终会找到一个叫system_call的函数(过程)
好的,我们接下来看看system_all这个函数(过程)
四、追溯system_call
该system_call过程在oslab/linux-0.11/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
- 第3行,由于我们需要添加两个系统调用(iam和whoami),所以要+2,即改为nr_system_calls = 74
- 第29行,是重点,call sys_call_table(,%eax,4)
对于call sys_call_table(,%eax,4),从汇编寻址方面来考虑,它是call sys_call_table + 4 * %eax,即sys_call_table是数组基址,实际上是找到sys_call_table数组的第%eax个元素(是个函数),然后执行这个函数。
这个时候的%eax是什么呢?可以回头看看上面的syscall1(),它里面设置了%eax等于__NR_iam的值啊!
接下来,我们追溯 sys_call_table这个数组。
五、追溯sys_call_table数组
该数组在os/linux-0.11/include/linux/sys.h
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 };//在中断向量表的最后填上系统调用
在最后加上我们的系统调用,分别叫sys_iam和sys_whoami
当然,还要像其它系统调用一样注册为extern。
extern int sys_sigaction();
extern int sys_sgetmask();
extern int sys_ssetmask();
extern int sys_setreuid();
extern int sys_setregid();
extern int sys_iam();//需要新增的地方
extern int sys_whoami();
六、编写who.c
who.c里面包含系统调用函数sys_iam()和sys_whoami()
把该文件放在oslab/linux-0.01/kernel下
#include <string.h>
#include <errno.h>
#include <asm/segment.h>
char msg[24];
/*sys_iam就是把iam.c输入的一个命令行参数从用户空间放到内核空间中*/
int sys_iam(const char * name)
{
char tep[26];
int i = 0;
for(; i < 26; i++)
{
tep[i] = get_fs_byte(name+i);
if(tep[i] == '\0') break;
}
if (i > 23) return -(EINVAL);
strcpy(msg, tep);
return i;
}
/*sys_whoami则是将此参数从内核空间取到用户空间,再由whoami.c打印出来*/
int sys_whoami(char * name, unsigned int size)
{
int len = 0;
for (;msg[len] != '\0'; len++);
if (len > size)
{
return -(EINVAL);
}
int i = 0;
for(i = 0; i < size; i++)
{
put_fs_byte(msg[i], name+i);
if(msg[i] == '\0') break;
}
return i;
}
关于如何在用户态和内核态传递消息,讲义已经叙述得非常清楚,此处不再赘述。
这结束了吗?还没有,我们仅仅是编写了who.c放在kernel/下。一个.c文件是对于系统来说是没有任何用的,我们需要编译链接它,让它成为可执行文件。这时候就要改makefile了,为什么?因为整个linux都是都是由它来编译的。
七、修改Makefile
Makefile在linux-0.11/kernel/下,
第一处
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
就是添加了一个who.c
第二处
### 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
就是添加了一行who.s who.o: who.c …/include/linux/kernel.h …/include/unistd.h
大功告成!!
取消挂载后,就可以开机了!
八、开机
开机后,在/usr/root下执行一下程序来编译
gcc -o iam iam.c
gcc -o whoami whoami.c
sync
编译应用程序成功后,可以尝试执行他们了
成功了!!
如果想测试讲义的testlab2.sh,可以按一下步骤进行
1、关闭linux0.11机子
2、chmod +x testlab2.sh
3、挂载hdc
4、复制testlab2.sh到/usr/root
5、取消挂载
6、打开linux-0.11机子
7、运行./testlab2.sh