操作系统笔记(1)------printf()的故事
一.引例
-
1.为什么用户程序不能直接调用内核程序中的whoami()函数呢?
当我们实现一个whoami()的系统调用时,不能随意的调用数据,不能随意jump。如果随意调用,就可以看到root密码,可以修改它;可以通过显存看到别人world里的内容。这是十分不安全的。
-
2.凭什么不让jump呢?是如何做到的呢?
一种处理器"硬件设计"可以区分内核态和用户态。
计算机对内存的使用是一段一段的的。由于CS:IP是当前指令,所以CS的最低两位来表示:0是内核态,3是用户态。内核态可以访问任何数据,用户态则不能访问内核数据。特权级检查:DPL>=CPL
DPL表示目标特权级,CPL为当前特权级。
DPL初始状态为0硬件检查当前程序是否满足特权级检查,如果满足条件,才可以调到内核中进行函数调用和执行程序。
简单总结一下:
在系统初始化时,会针对内核态的代码和内核态的数据建立GDT表,它对应的DPL为0。在系统初始化好后,当用户态执行时,会启动一个用户程序。用户程序的CPL=3,这时无法进入内核;而当系统调用时,会将CPL变为0,这样便可以进入内核执行。内核处理完毕后又会将程序的CPL变为3 -
3.如何"主动进入内核呢?"
硬件提供了"主动进入内核的方法"
。
对于Intel x86来说,中断指令int 0x80可以机内内核中。
二.printf()如何执行的故事
1.简单的图解
2.详细解释
- (1)
调用write()变成一段包含"int 0x80"的中断代码
这一过程是如何进行的呢?
在linux/lib/write.c中
#include <unistd.h>
_syscall3(int,write
,int,fs,const char *buf,off_t,count)
在linux/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):""(_NR_##name
),“b”((long)(a)),“c”((long)(b)),“d”((long)(c
)));if(_res>=0) return (type)_res;…}(汇编代码)
这两段代码其实就相当于
在linux/lib/write.c中
#include <unistd.h>
int write(int fd,const char *buff…){
把_NR_write置给eax
把第一个参数置给ebx
把第二个参数置给ecx
把第三个参数置给edx
执行"int 0x80"
把"=a"(也就是eax)赋给_res
返回_res
}
简单的总结一下,当调用write()时,在指向这个write()函数的过程中,把一个系统调用号置给了eax,然后调用"int 0x80"就到了内核中。
- (2)
"int 0x80"
做了什么事呢?它为什么就能进入内核中呢?
void sched_init(void){
set_system_gate(0x80
,&system_call);
}
在linux/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) …
这段代码的执行就相当于
<1>.在IDT表中,将DPL设置成3,从而使它可以跳进80号中断里。
<2>.跳进该中断后,在IDT表中用段选择符(CS变成了8,CPL就变成了0)和处理函数入口点偏移(IP)设置成新的PC指针(CS+IP)。
<3>.CS为8就相当于汇编代码jumpi 0,8
。就会通过GDP表找到内核的代码段,处理system_call这一函数。
简单总结一下:"int 0x80"这段代码将80号中断的DPL由0变成3,使CPL=3的用户程序能够跳进80号中断中。然后在80号中断中将CPL由3变成0,在IDT表中取出中断处理函数,跳到内核(内核中的DPL为0)中的那个函数去执行。中断处理完毕后也就代表着内核处理已经完成。
- (3)
system_call
又做了什么呢?
system_call函数中有这样的一句代码call _sys_call_table(,%eax,4)
会进入到_sys_call_table函数中执行。 - (4)
_sys_call_table
又做了什么呢?
它会找到 _sys_call_table的起始地址+4*eax 这一地址所对应的函数,也就是sys_write
函数。然后执行call sys_write
这样就真正调用了sys_write进入到内核中。 - (5)接下来的事就真正交给了内核来办。到这里printf的故事就结束了。
三.小结
通过whoami无法直接进入内核程序的故事和printf的故事,我们可以总结一下系统调用的核心:
(1)用户程序包含一段含有int的代码
(2)操作系统写中断处理,获取要调用程序的编号。
(3)操作系统根据编号执行相应的代码