为什么用户程序不能直接访问内核态的内存?
- 因为为了安全起见,强制规定用户程序只能通过系统调用来访问内核态的内存。
- 区分内核和用户段:一种处理器“硬件设计”
- DPL在GDT表中会有初始化
- CPL:当前特权级,处于CS的低2位
- DPL:目标特权级
- DPL >= CPL
用户程序主动进入访问内核的方法
- 通过使用中断"int"指令可以主动进入内核
系统调用核心
- 用户程序中包含一段int指令的代码
- 操作系统写中断处理,获取想调程序的编号
- 操作系统根据编号执行代码
系统调用实例实现(以printf为例)
- 前提知识点:
- 1、系统有内核(用户)态、内核(用户)段之分。内核态可以访问任何数据,用户态不能访问内核数据。内核段包括所有pc指针中CPL段为0的内存段,用户段包括所有pc指针中CPL段为3的内存段。
- 2、pc指针每次跳转的前都会判断当前系统状态下的CPL与DPL的大小,满足DPL大于等于CPL则跳转。
- 3、以printf函数为例,在执行printf函数后,pc指针会有从用户段到内核段的过程,在进入内核段的时候会照常判断CPL(此时为3)与DPL(此时为0)的大小,这个时候就有一个问题了,既然能进入内核态,那什么时候满足DPL大于等于CPL呢?
- 4、系统调用接口核心是①用户程序中包含一段int指令的代码;②操作系统写中断处理,获取想调程序的编号;③操作系统根据编号执行相应代码。
printf详细分析
printf执行流程:
1、调用printf;2、调用库函数printf;3、调用库函数write;4、系统调用write。
系统调用write进一步是这样一句宏定义
// linux-0.11/lib/write.c
#include <unistd.h>
_syscall3(int,write,int,fd,const char *,buf,off_t,count)
// linux-0.11/include/unistd.h
#define __NR_write 4
#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; \
}
上述宏定义的整体解释:
1、初始化输入寄存器eax(4),ebx(fd),ecx(buf),edx(count)
2、执行指令int 0x80,执行完成后并返回值到寄存器eax中
3、输出eax的值到__res中,并退出系统调用write函数
- 从上述代码可以看出:在系统调用write中会调用int 0x80中断。系统如何执行int 0x80的指令的呢?
- 在思考上个问题之前我们先看看有关系统调用的初始化过程,也就是下面列出代码的解释:
1、初始化输入部分:得到立即数0xee00,得到IDT表中下标为0x80对应的那块内存A,得到IDT表中下标为0x80再加偏移地址4对应的那块内存B,edx(&system_call),eax(0x00080000)
2、movw %%dx,%%ax\n\t – 此时ax = &system_call
3、movw %0,%%dx\n\t – 此时dx = 0xee00
4、movl %%eax,%1\n\t – 此时A = 0x80000 + &system_call
5、movl %%edx,%2 – 此时B = &system_call << 16 + 0xee00
上述的A和B两块内存区共8个字节构成一个idt表,代码目的初始化这个IDT表,将DPL设置为3,段描述符置为8,调用接口设置为system_call
// linux-0.11/init/main.c
void main(void)
{
sched_init(); // 系统初始化时会调用这个函数
}
// linux-0.11/kernel/sched.c
void sched_init(void)
{
// 调度函数初始化时会设置系统门,设定
// 系统调用的中断接口
set_system_gate(0x80,&system_call);
}
// linux-0.11/include/asm/system.h
#define set_system_gate(n,addr) \
_set_gate(&idt[n],15,3,addr)
#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))
- 所以pc指针在从用户段到内核段的过程,在进入内核段的时候会照常判断CPL(此时为3)与DPL(此时为0)的大小,这个时候就有一个问题了,既然能进入内核态,那什么时候满足DPL大于等于CPL呢?就在进入系统调用接口中的中断指令执行初始化IDT表时设置DPL为3这个时候满足上述满足DPL大于等于CPL的条件的。
- 随后由于段描述符(CS)为8,CPL此时为0.进入内核态,完成从用户态到内核态的流程。
- 这个时候我就有一个问题了,system_call怎样执行调用write的?
system_call代码分析
// linux-0.11/kernel/system_call.s
nr_system_calls = 72
system_call:
cmpl $nr_system_calls-1,%eax #此时eax = __NR_write = 4
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) #相当于往后偏移4*eax个字节
pushl %eax
movl current,%eax
cmpl $0,state(%eax) # state
jne reschedule
cmpl $0,counter(%eax) # counter
je reschedule
ret_from_sys_call:
movl current,%eax # task[0] cannot have signals
cmpl task,%eax
je 3f
cmpw $0x0f,CS(%esp) # was old code segment supervisor ?
jne 3f
cmpw $0x17,OLDSS(%esp) # was stack segment = 0x17 ?
jne 3f
movl signal(%eax),%ebx
movl blocked(%eax),%ecx
notl %ecx
andl %ebx,%ecx
bsfl %ecx,%ecx
je 3f
btrl %ecx,%ebx
movl %ebx,signal(%eax)
incl %ecx
pushl %ecx
call do_signal
popl %eax
3: popl %eax
popl %ebx
popl %ecx
popl %edx
pop %fs
pop %es
pop %ds
iret
// 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_write正好处于系统调用表中下标为4的位置,所以上述执行int 0x80时确实调用了sys_write接口。
那sys_write又是怎样把字符写入到显示屏中去的呢?这个问题,我们日后再说。