实验原理
1、为什么需要系统调用才可以访问内核
了保证系统的安全性以及稳定性, 当用户的程序直接访问OS内核, 就能够方便的修改内核数据, 造成系统的崩溃, 隐私泄露等等的问题, 因此我们的OS不允许用户程序直接访问内核空间和代码.
2、如何实现内核空间和用户空间的隔离
通过硬件实现, 在硬件上将内存空间分为内核态和用户态, 用户态的程序只能够自由的访问用户态的数据, 内核态下程序可以访问任意的空间.
我们使用CPL来表示当前内存段的特权级, 用DPL来表示目标内存段的特权级, 特权级越高数字越小. 我门每当执行mov, jmp等指令时, 都会检查CPL和DPL, 只有CPL<=DPL(即源内存段特权级要小于等于目标内存段的特权级)才允许操作, 否则硬件会拒绝该指令.
在OS初始化时, head.s建立GDT表时, 将内核端的DPL置为0, 对于用户程序DPL设置, 老师说在应用程序加载时, 会进行相应设置.
3、用户程序如何访问内核
通过且只能通过0x80中断进行内核的访问,在系统初始化时, OS会将0x80中断和system_call函数的地址装配到IDT表中, 并且在IDT的0x80表项中将DPL(即跳转到的内核段的特权级)置为3, 而其他的中断表项中DPL=0, 应用程序只有在0x80处满足XPL<=DPL, 因此应用程序只有通过0x80中断才能够进入内核.
4、系统调用流程
printf为例:当我们调用printf时, C库都会将printf通过一个预先定义好的宏(syscall1,2,3()) 展开为一段包含相应系统调用函数以及0x80中断的代码, 并且会将相应的中断服务号(__NR_xxx 保存在eax中)保存起来
系统调用的核心:
熟悉一下流程:
1、我们调用printf的时候会调用c库函数
2、c库函数会调用write.c函数
3、write.c函数会调用sys_call
4、展开sys_call会根据中断号,得到相应的程序。
再看一下close.c
我们不妨看看 lib/close.c
#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; \
}
//上面是嵌入式汇编,语法很简单一:分成四部分以:分割。第一部分是语句,第二部分输入,第三部分输出。。。。
https://blog.csdn.net/stone_kingnet/article/details/2910832
学习资料了解一下就可以
将 _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。
*/
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
。
接下来看 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
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
的值对应上。
具体步骤
1、解压文件
cd oslab
tar -zvxf hit-oslab-linux-20110823.tar.gz -C /home/shiyanlou
2、首先我们是需要先写一个系统调用的函数之后,我们再在虚拟机linux中写一个应用小程序来调用那我们先来写系统调用的函数系统调用函数 首先是有系统调用表的 我们先去把系统调用表中调用号给 增加了表项中的调用号是在一个是虚拟机中的/usr/include/unistd.h
在linux-0.11/include/linux/sys.h中添加函数声明extern int sys_iam()和extern int sys_whoami(); 然后在中断向量表fn_ptr sys_call_table[]的最后面填上系统调用sys_iam()和sys_whoami()。
cd ./linux-0.11/include/linux
gedit sys.h
需要注意的一点就是函数的顺序要一致:先添加函数声明:
extern int sys_iam();
extern int sys_whoami();
再在中断向量表最后面添加:sys_iam, sys_whoami.
3、在linux-0.11/kernel/system_call.s中修改系统调用的个数nr_system_calls,使其增加2。
cd ../../kernel
gedit system_call.s
4、挂载硬盘
回到oslab目录执行一下目录,修改文件如下
sudo ./mount-hdc
cd hdc/usr/include/
gedit unistd.h
5、创建一个who.c文件(在kernal目录下),然后在该文件中实现两个系统调用函数,
#include <unistd.h>
//#include <sys.h>
#include <errno.h>
#include <string>
char sys_name[24];//系统内核的内存区域
int sys_iam(const char * name)
{
//将字符串name中的内容,存放到系统内核中
int count = 0;
//while(*(name+count) != '\0')//要改成下面的形式
while(get_fs_byte(name+count) != '\0')//在内核态中从用户态取出数据
{
count++;
}
if(count > 23)
{
//如果字符串的长度大于23,则表示不能存放到内核中,返回-1,设置errno
errno = EINVAL;
return -1;
}
else
{
for(int i = 0; i < count; i++)
{
sys_name[i] = get_fs_byte(name+i);
}
sys_name[count] = '\0';
return count;
}
}
int sys_whoami(char * name,unsigned int size)
{
//将内核段中的字符串复制到name中,name能够容纳的最大空间为size
int count = 0;
while(*(sys_name+count) != '\0')
{
count++;
}
if(size < count)//name所能容纳的空间size小于字符串的长度
{
errno = EINVAL;
return -1;
}
else
{
for(int i = 0; i < count; i++)
{
put_fs_byte(sys_name[i],(name+i));//将内核态中的数据拷贝到用户态
}
name[count] = '\0';
return count;
}
}
修改linux-0.01/kernel目录下的Makefile文件,在OBJS中加入who.o,并添加生成who.s、who.o的依赖规则。
gedit Makefile
6、修改完后,退回linux-0.11目录下,运行make all指令编译内核,who.c会被加入到内核
cd ../
make all
7、新增iam.c 跟whoami.c文件以测试是否添加系统调用成功注意这两个文件是要在linux 0.11版本上编译的,所以我们应当先通过运行mount-hdc文件来把虚拟机的硬盘挂载在oslab/hdc 目录下,然后进入hdc/user/root目录中(这个目录就是虚拟机一开机的所在的目录)再创建iam.c和whoami.c。
cd ..
cd hdc/usr/root/
gedit iam.c
gedit whoami.c
#iam.c
#whoami.c
8、运行和测试
首先把/home/teacher 目录下的两个测试文件testlab2.c和testlab2.sh移动到虚拟机的硬盘中的开机目录里
cd ~/oslab
sudo ./mount-hdc
cd ./hdc/usr/root
cp /home/teacher/testlab2.c ./
cp /home/teacher/testlab2.sh ./
切换到oslab
cd ../../../
./run
在弹出的bochs虚拟机窗口中的命令行中,编译几个C语言文件
gcc -o iam iam.c
gcc -o whoami whoami.c
gcc -o testlab2 testlab2.c
编译好了以后,就可以最后的运行测试了:在虚拟机中通过iam将你的名字从用户态传入内核。然后通过whoami将传入内核的名字打印出来
./iam zhangyong
whoami