L5系统调用的实现
实现一个whoami系统调用
为什么不能直接访问——操作系统安全
目标:用户程序调用whoami,一个字符串“lizhijun”放在内核中 (系统引导时载入),利用系统调用打印该字符串
首先不能随意jmp到内核代码或取用内核中的字符串。
因为要防止操作系统数据泄露。操作系统中的数据可能有:
- root用户密码
- word文档写入时数据也会经过操作系统,某段时间在操作系统中存储,如果其他用户程序能取得这个数据,那么会造成数据泄露
如何实现这种限制——内核(用户)态,内核(用户)段
那么如何实现这种限制呢?
通过区分程序执行在用户态还是内核态,隔离内核态和用户态的代码和数据。
这种限制通过为段设置特权级实现。
具体来说,访问时,通过硬件比较CPL(当前的特权级)和DPL(目标段的特权级)
DPL:标识一个段的特权级
CPL:标识正在执行的程序的特权级
用户程序(代码+数据)位于用户段,执行在用户态(CPL=3 , DPL=3)
内核程序位于内核段,执行在内核态(CPL=0 ,DPL=0)
用户程序位于用户段(CPL=3),调用系统调用进入内核(CPL=0),系统调用的具体代码位于内核段。
CS的最低两位(0,1,2,3)表示当前的特权级(0内核态/3用户态)
系统初始化时(即head.s执行的时候),会建立GDT表项,表项所有的DPL都是0,这些表项对应的段是内核段。
那如果想进入内核,如何进入?答:硬件提供了“主动进入内核的方法”——中断(int 0x80)
只有中断能进入内核,而且不是所有中断都能进入内核
以用户程序调用printf()的过程为例说明系统调用的过程
//1、应用程序调用的printf
printf(格式化输入){
//C库函数printf,负责把参数转换为库函数write()需要的格式
printf(write需要格式的参数);
}
//2、C库函数printf
printf(write需要格式的参数){
//writeC库函数
write(write需要格式的参数);
}
//3、C库函数write
write(write需要格式的参数){
//中断代码,int 0x80
int 0x80
}
//4、int 0x80调用特定中断处理程序,即系统调用write()
//系统调用write,位于内核区
write(){
...
}
关于库函数write的具体实现
- 宏展开
write.c中的_syscall3()按照unistd.h中定义的格式,将参数一次填入表示为
int write(int fd,const char *buf,off_t count)
但注意只有这个宏定义只适用于3个参数的。
- 内联汇编
“int 0x80”这一句表示嵌入的汇编代码
“=a”(__res) 这一句表示汇编向C的输出,其中a为eax,这句的意思为,将eax置给__res。由于eax存放的是返回值,所以表示返回值置给 _res
**""(_NR_##name) **这一句表示C向汇编的输入,“”如果里边没有东西,则表明默认和输出时选择的一样(eax)。_NR_##name中将name替换为write。这一句表示将__NR_write输入到eax,。
"b"((long)(a)) 同上,“b”"c"d"分别表示eax,ebx,ecx。((long)(a))中的a表示第一个参数。
总结:
-
“”内的东西是有关汇编的。()里的东西是有关C语言的
-
内联汇编的执行过程是,先输入,然后执行"int 0x80",执行结束后输出
-
__NR_write这个值代表的是系统调用号,用它找到系统调用write函数(作为中断处理函数),系统调用write()才算进入内核。之后返回int 0x80后的语句,从内核返回用户态。
- 后边的if语句以及前面的long __res;就是简单的C语言
关于int 0x80中断
![](https://img2018.cnblogs.com/blog/1735814/201910/1735814-20191029200013610-1343595035.png
-
sched_init(void)是系统初始化执行的函数
-
set_system_gate(0x80,&system_call);用于设置中断处理门(IDT中的每个表项就对应一个中断处理门),将中断0x80交给system_call()处理
具体实现分析:
-
_set_gate(&idt[n],15,3,addr)通过宏定义
#define _set_gate(gate_addr,type,dp1,addr)展开
-
gate_addr=&idt[n]表示idt是中断向量表基址(是个全局变量)
-
idt[n]找到IDT中0x80对应的表项
-
type=15表示
-
dpl=3表示中断向量表
-
“a”(0x0008 0000)的作用是最后截取0x0008 0000的高16位放入段选择符,即段选择符为8。
-
addr为中断处理函数入口的偏移地址
-
"movl %%eax,%1 "中%1表示C向汇编输入的第2个变量即*((char*)(gate_addr))。完成了将addr的低4位放入eax。
同理"movl %%edx,%2"将高4位放入edx。剩下的细节不讲,最终实现将addr组装至IDT表中
-
总结:
- DPL=3的作用: 中断描述符表中int0x80中断的DPL=3,使得用户态下的程序可以进入。根据段选择符和中断处理函数入口的偏移地址可以跳转到中断函数入口
- 这里注意段选择符为8,即0000 0000 0000 1000,末两位为00,DPL=0,跳转后的CPL=0,通过这种方式进入内核态,之后执行中断处理函数。
- 中断返回后CS最后两位又会变成3,回到用户态。后面再具体讲
-
-
中断处理函数system_call
-
movl $0x10,%edx mov %dx,%ds mov %dx,%es 用于将ds和es都置为0x10,将数据段的段选择符置为0x10,数据段也在内核态中
-
call _sys_call_table(,%eax,4) 这句进入系统调用处理函数。
这句中,%eax内是__NR_write。
_sys_call_table+4*%eax就是相应的系统调用处理函数(sys_wirte)的入口 。__NR_write相当于系统调用的编号。*4是因为每个系统调用占4个字节。
_sys_call_table是一个函数指针表。
-
总结:
- system_call调用系统调用处理函数sys_write,sys_write才是真正执行打印内核数据的函数。具体是怎么实现的设计到操作系统IO,后面再讲。
- 进入system_call后才算进入内核态
-
大总结
-
程序用户态不能直接访问内核态。
-
printf的过程
库函数:printf。包含int 0x80调用中断。
中断处理函数:system_call。调用系统调用处理函数sys_write。
系统调用处理函数:sys_write。执行真正的对内核数据的操作。
-
实现用户态进入内核态的关键
查IDT调用中断处理程序时,通过IDT中DPL=3,且跳转到段选择符为8的内核段,让程序得以从用户态进入内核态。