保护机制:将内核程序与用户隔离
特权级
处理器有硬件设计的保护机制,共4个特权级(0级到3级)。数值越大,权限越低。 可以用圆环表示为:
为了检测特权级,有三种值:
- 当前特权级 CPL(Current Privilege Level)。这是CPU当前执行程序或任务的特权级。存放在 CS 和 SS 寄存器的最低2位。
- 描述符特权级 DPL(或称为目标段特权级)(Descriptor Privilege Level) DPL 是一个段或门的特权级,存放在段或门描述符的DPL字段中(GDT表中)
- 请求特权级 RPL(Request Privilege Level)RPL是一种赋予段选择符的特权级,存放在选择符的最低2位
特权级检查
DPL >= CPL
DPL >= RPL
只有同时成立时,才不会触发 一般保护异常
系统调用
由操作系统实现提供的所有系统调用所构成的集合即程序接口或应用编程接口(API)。是应用程序同系统之间的接口。
系统调用,如同电源插头一样,方便了使用,隐藏内部细节。
进入内核的方法
这是用户程序调用内核代码的唯一方式:
用 int 指令中断,将 CPL 改成 0
系统调用的核心
- 用户程序中包含 int 指令的代码
- 操作系统写中断处理,获取调用程序的编号
- 操作系统根据编号执行相应代码
实现
从write() 开始:
/*
* linux/lib/write.c
*
* (C) 1991 Linus Torvalds
*/
#define __LIBRARY__
#include <unistd.h>
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
这是一个宏,在unistd.h中有定义:
//...
#define _syscall3(type,name,atype,a,btype,b,ctype,c) \
type name(atype a,btype b,ctype c) \
{ \
long __res; \
__asm__ volatile ("int $0x80" \
: "=a" (__res) \
: "0" (__NR_##name),"b" ((long)(a)),"c" ((long)(b)),"d" ((long)(c))); \
if (__res>=0) \
return (type) __res; \
errno=-__res; \
return -1; \
}
//...
观察发现,其中的name
参数,在宏中替换为__NR_##name
,所以当name = write
时,被替换为__NR_write
。
在unistd.h文件中同样可找到出处:
//...
#define __NR_setup 0 /* used only by init, to get system going */
#define __NR_exit 1
#define __NR_fork 2
#define __NR_read 3
#define __NR_write 4
#define __NR_open 5
#define __NR_close 6
//...
可以发现__NR_write
是系统调用号,并保存在eax
中。
同时eax
也存放返回值,ebx,ecx,edx
存放3个参数。
int 0x80中断:
在 init / main.c 中,存在一个函数调用sched_init();
在 kernel / sched.c中有定义,并在最后调用set_system_gate(0x80,&system_call)
:
void sched_init(void)
{
//以上略.........
set_system_gate(0x80,&system_call);
}
又是一个宏,定义在system.h中:
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
_set_gate
也定义在该文件中,在处理IDT:
#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))
综上我们可以发现,set_system_gate(0x80,&system_call);
这是用来设置0x80
的中断。
开始研究system_call
,在文件kernel / system_call.s中,从文件名看出,这是纯汇编程序。
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 as parameters
pushl %ebx # to the system call
movl $0x10,%edx # set up ds,es to kernel space
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx # fs points to local data space
mov %dx,%fs
call sys_call_table(,%eax,4)
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
call sys_call_table(,%eax,4)
,这是比例变址寻址,地址值为 sys_call_table +4*%eax。
在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};
此时发现,原来__NR_write
就是在这个数组中的下标。
而call sys_call_table(,%eax,4)
,这个寻址是因为地址长度4个字节,此时eax
中存放的是__NR_write
,所以基址sys_call_table
+%eax
* 4个字节 从而跳转到 内核函数sys_write
。
最后,内核函数sys_write
在fs / read_write.c中。
实验
实验内容
此次实验的基本内容是:在 Linux 0.11 上添加两个系统调用,并编写两个简单的应用程序测试它们。
(1)iam()
第一个系统调用是 iam()
,其原型为:
int iam(const char * name);
完成的功能是将字符串参数name
的内容拷贝到内核中保存下来。要求name
的长度不能超过 23 个字符。返回值是拷贝的字符数。如果name
的字符个数超过了 23,则返回 “-1”,并置errno
为EINVAL
。
在 kernal/who.c
中实现此系统调用。
(2)whoami()
第二个系统调用是 whoami()
,其原型为:
int whoami(char* name, unsigned int size);
它将内核中由iam()
保存的名字拷贝到 name
指向的用户地址空间中,同时确保不会对 name
越界访存(name
的大小由 size 说明)。返回值是拷贝的字符数。如果 size 小于需要的空间,则返回“-1”,并置 errno
为EINVAL
。
也是在 kernal/who.c 中实现。
实验过程
要添加系统调用,思考前文对write()
的探究,只需要在kernel /who.c
中实现两个sys_iam
和sys_whoami
,并将两个函数添加进函数表中即可。
值得注意的是,在实现的过程中,需要用户态和内核态传递数据:
get_fs_byte()
获得一个字节的用户空间中的数据。
put_fs_byte()
可以将一个字节的数据拷贝到用户空间。
kernel / who.c
#include <errno.h>
#include <fcntl.h>
#include <sys/types.h>
#include <utime.h>
#include <sys/stat.h>
#include <string.h>
#include <linux/sched.h>
#include <linux/tty.h>
#include <linux/kernel.h>
#include <asm/segment.h>
char str[24]={'\0'};
int sys_iam(const char * name){
int i=0;
char c=NULL;
while (i < 23){
c = get_fs_byte(name+i);
str[i] = c;
if(c =='\0'){
break;
}
i++;
}
if(i== 23 && get_fs_byte(name+i)!='\0'){
errno = EINVAL;
return -1;
}
return i;
}
int sys_whoami(char* name, unsigned int size){
int i=0;
while(i < size){
put_fs_byte(str[i],name+i);
if(str[i] == '\0'){
break;
}
i++;
}
if(str[i] !='\0'){
errno = EINVAL;
return -1;
}
return i;
}
include / unistd.h
在这里将系统调用号添加进去
#define __NR_whoami 72
#define __NR_iam 73
include / linux / sys.h
按照文件中的格式,添加 extern 声明:
extern int sys_whoami();
extern int sys_iam();
向数组中添加两个函数:
fn_ptr sys_call_table[] = { sys_setup, sys_exit, sys_fork, sys_read,sys_write, sys_open, sys_close, .........
,sys_whoami,sys_iam };
更改Makefile
(1)第一处
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.o。
(2)第二处
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
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
。
用 testlab2.c 测试内核
在Bochs中可以进系统用 vi 修改或直接挂载磁盘修改/usr / include / unitstd.h
挂载磁盘将testlab2.c拷贝进去并编译运行。
存在个问题:errno
在who.c中已经设为EINVAL
,但在测试中errno
为1,还没找到问题原因。