1.用户态和内核态
用户程序是如何调用内核程序的呢?考虑实现下面的一个whoami的系统调用:
在内核中100地址处有一个用户“lizhijun”,whoami函数的功能是要打印出这个用户名,那可以直接打印出100地址处的内容吗?答案当然是否定的,因为用户程序不能随意的访问内核程序,内核态可以访问任何数据,用户态却不能访问内核数据,这是一种处理器的硬件设计所决定的,如下图:
判断一段程序能不能访问另一段是根据程序的特权级来判断的,DPL 是指目标程序的特权级(内核态代码DPL存在于GDT表中,在head.s已经初始化好),CPL指当前程序的特权级,当前程序执行在什么态,使用CS的最低两位来表示的,0表示内核态,3表示用户态,当DPL大于CPL时,当前程序可访问目标程序。
2.主动进入内核的方法
首先硬件提供了主动进入内核的方法,对于intel x86而言,就是中断指令,通过中断指令将CS的CPL改为0,从而进入内核,这是用户程序调用内核代码的唯一方法。
2.1系统调用的实现
以c语言中printf函数为例,printf函数实际上是调用了内核的write函数,库函数write最终通过宏展开成包含指令int 0x80的代码,从而进入内核执行内核的write函数:
那么具体流程是怎样的呢?当调用库函数write时,执行下面的代码:
展开后的代码如下:
int write(int fd, const char *buf, off_t count)
{ long _res;\
_ _asm__ volatile("int 0x80":"=a"(_res):" "(__NR_write), "b"(long(fd)),"c"((long)(*buf)), "d"((long)(count))); if(_res>=0) return (int)_res; error = -_res; return -1;}
代码是内嵌汇编代码,一共3条语句,分别用:隔开,第一条是中断指令,第二条是输出,是指把_res的内容付给eax寄存器,第三条是输入,是指把三个参数和_NR_write的值分别付给ebx 、ecx、edx和eax寄存器,首先执行第三条语句,然后执行中断指令,最后执行输出指令,然后再执行c语言的return语句。总的来说就是将系统调用号(NR_write)置给eax,然后调用中断指令进入内核。
那么调用中断又是怎么进入内核呢?首先调用中断时需要查询idt表,早在系统初始化时就已经初始化好了idt表,初始化函数(set_system_gate(0x80,&system_call);)做了如下操作:
将0x80号中断表的表项赋值:DPL赋值为3,&system_call赋给处理函数入口偏移,段选择符赋为0x0008,即CS=8,IP = &system_call,(通过CS=8找到gdt表中的内核代码段,同jmpi 0,8的那个用法,而且8的最后一位为0,所以CPL被置为0),然后跳转到内核区域执行。
接下来就调用system_call函数:
该函数首先做了一些铺设操作,然后将数据段寄存器赋值为0x10,现在数据指针和代码指针都指向了内核区域,因此接下来就真正进入内核操作,执行call _sys_call_table(,%eax,4)指令,_sys_call_table是一个系统调用表的起始地址,通过地址和eax的值找到要调用的系统调用的真正地址,即_sys_call_table+4*%eax,eax中是之前存入的NR_write(4),找到表中的第4个元素:
确实为sys_write,因此接下来就调用sys_write,此函数就是真正要做写操作的函数了。
总结一下,就是如下图:
当从第一个框图到第二框图时,此时CPL=3,在初始化时,又将int 0x80指令的DPL做成了3,因此可以调到执行int 0x80指令。