我们由刚开始接触编程的一个程序说起。
上述一段代码,通过gcc编译,生成一个.o文件,运行,就能输出printf函数要输出的文字。那么,对于printf()函数这个调用,操作系统到底发生了什么?
内核态与用户态
为了方便管理,我们将计算机系统资源分为用户态和内核态,对于内核部分,一般是对进程资源进行管理,而这些东西是不允许直接操作的,所有对这些资源的访问都必须有操作系统控制。在Linux中,系统调用(System call)是用户空间访问内核的唯一手段,除了异常和陷入外,它是内核唯一的合法入口。
那么怎么样才能引起系统调用呢?即怎么从用户态来到内核态?一般来说,系统调用都是通过软件中断实现的,在X86系统上的软件中断是由int 0x80指令引起的,而128号异常处理程序就是系统调用处理程序system_call()。
有了以上知识,我们再做以下几点说明。
1、将内核程序和用户程序隔离,即用户态和内核态,这是由硬件来实现的,这个我们在讲内存的时候经常会提到,比如某段内存不能随便访问,某段内存仅仅只让操作系统访问。
2、那么怎么知道当前程序执行在什么态呢?由于CS:IP是指向需要执行的指令的地址的,那么我们用CS的低两位来表示,其中0是内核态,3是用户态。
3、内核态可以访问任何数据,用户态不能访问内核态数据。
4、这里还介绍两个重要的参数,DPL即描述目标内存段的特权级,CPL(CS的低两位)表示当前的特权级,只有当CPL<=DPL时,才能执行当前指令。注意,初始化的时候DPL=0。
printf函数的展开
对于我们文章开始提到的printf()函数的调用过程大致如下所示。下面我们将分析整个调用过程。
对于任何用户程序,它最终都有一段包含int指令的代码;操作系统通过写中断处理,获取想要调用程序的编号;操作系统根据编号执行相应的代码。
对于printf()来说,最终展开成包含int指令的代码,如下。
Linux/lib/write.c
_syscall3(int,write,int,fd,const,char*,buf,off_t,count) //3表示参数为3个,此次调用是库函数调用,注意第一个是函数名,展开就是int write(int fd,const char * buf,off_t count)
Linux/include/unistd.h
#define _syscall3(type,name,atype,a,btype,b,ctype,c)
type name(atype a,btype b,ctype c)
{
long _res;
_asm_ volitile(“int 0x80”:”=a”(_res):””(_NR_##name),”b”((long)(a)),”c”((long)(b)),”d”((long)(c)));
if(__res>=0) return (type)__res;
errno=-__res;
return -1;
}//此段代码为内嵌汇编,就是将_NR_write(即系统调用号)放在eax中。
而_NR_write的系统调用号定义下面的函数中。
上面说的就是将系统调用号赋值给eax,然后调用int 0x80。那么int 0x80做了什么?
Int 0x80中断的处理
前面说过中断是通过itd表来找处理函数,然后执行,ITD表如下所示。
void sched_init(void)
{set_system_gate(0x80,&system_call);} //该函数用来设置0x80中断处理
在linux/include/asm/system.h中
#define set_system_gate(n,addr) //n为中断号,addr为中断地址
_set_gate(&idt[n],15,3,addr); //idt是中断向量表基址
#define _set_gate(gate_addr,type,dpl,addr) //DPL=3,当DPL置3之后,CPL就会满足小于等于DPL了
__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”(*(4+(char *)(gate_addr))),
”d”((char *)(addr),
”a”(0c00080000))
当上面DPL=3之后,就能进入80中断了,此时CS=8,IP=system_call。当CS等于8的时候,CPL=0,那么就完全满足CPL<=DPL了,那么就能进入内核了。说白了int 0x80就是让CS:IP进入到内核,可以在内核中执行。
中断处理程序system_call
在linux/kernel/system_call.s中
Nr_system_calls=72
.globl _system_call
_system_call:cmpl $nr_system_calls-1,%eax //eax存放的是系统调用号
ja bad_sys_call
push %ds
push %es
push %fs
pushl %eax
pushl %ecx
pushl %ebx
movl $0x10,%edx
mov %dx,%ds
mov %dx,%es
movl $0x17,%edx
mov %dx,%fs
call _sys_call_table(,%eax,4) //a(,%eax,4)=a+4*eax,
pushl %eax //返回值压栈,留给ret_from_sys_call返回时使用
......
ret_from_sys_call:
popl %eax
......
iret
_sys_call_table+4*%eax就是相应的系统调用处理函数入口,由于每个系统调用对应的函数占用4个字节,所以*4。其中_sys_call_table就是sys_call_table的基地址,也就是指向那个表的第0个系统调用,而%eax中存放的是4,那么最终就指向sys_write了。所以整个调用过程如下。