资源描述
,单击此处编辑母版标题样式,单击此处编辑母版文本样式,第二级,第三级,第四级,第五级,*,Linux内核源代码导读,*,/37,Linux,内核源代码导读,中国科学技术大学计算机系,陈香兰(,0551,3606864,),Spring 2009,一个典型的,Linux,操作系统的结构,用户应用程序,System call,对硬件资源的管理,Shell,,,lib,Kernel implementation,10/2/2024,2,Linux内核源代码导读,系统调用的意义,操作系统为用户态进程与硬件设备进行交互提供了一组接口,系统调用,把用户从底层的硬件编程中解放出来,极大的提高了系统的安全性,使用户程序具有可移植性,10/2/2024,3,Linux内核源代码导读,API,和系统调用,应用编程接口,(application program interface,API),和系统调用是不同的,API,只是一个函数定义,系统调用通过“软”中断向内核发出一个明确的请求,在,x86,中为,int,指令,Libc,库定义的一些,API,引用了封装例程,(wrapper routine,,唯一目的就是发布系统调用,),一般每个系统调用对应一个封装例程,库再用这些封装例程定义出给用户的,API,10/2/2024,4,Linux内核源代码导读,不是每个,API,都对应一个特定的系统调用。,API,可能直接提供用户态的服务,如,一些数学函数,一个单独的,API,可能调用几个系统调用,不同的,API,可能调用了同一个系统调用,返回值,大部分封装例程返回一个整数,其值的含义依赖于相应的系统调用,-1,在多数情况下表示内核不能满足进程的请求,Libc,中定义的,errno,变量,包含特定的出错码,10/2/2024,5,Linux内核源代码导读,系统调用程序及服务例程,当用户态进程调用一个系统调用时,,CPU,切换到内核态并开始执行一个内核函数。,在,Linux,中是通过执行,int,$0 x80,来执行系统调用的,这条汇编指令产生向量为,128,的编程异常(回忆,,trap_init,中系统调用入口的初始化),传参,:内核实现了很多不同的系统调用,进程必须指明需要哪个系统调用,这需要传递一个名为,系统调用号,的参数,使用,eax,寄存器,10/2/2024,6,Linux内核源代码导读,系统调用的返回值,所有的系统调用返回一个整数值。,正数或,0,表示系统调用成功结束,负数表示一个出错条件,这里的返回值与封装例程返回值的约定不同,内核没有设置或使用,errno,变量,封装例程在系统调用返回取得返回值之后设置,errno,当系统调用出错时,返回的那个负值将要存放在,errno,变量中返回给应用程序,10/2/2024,7,Linux内核源代码导读,系统调用处理程序也和其他异常处理程序的结构类似,在进程的内核态堆栈中保存大多数寄存器的内容,(,即,保存,恢复进程到用户态执行所需要的,上下文,),调用,相应的,系统调用服务例程,处理系统调用,sys_xxx,通过,syscall_exit,从系统调用,返回,10/2/2024,8,Linux内核源代码导读,应用程序、封装例程、系统调用处理程序及系统调用服务例程之间的关系,10/2/2024,9,Linux内核源代码导读,为了把系统调用号与相应的服务例程关联起来,内核利用了一个系统调用分派表,(dispatch table),。,这个表存放在,sys_call_table,数组中,,有,nr_syscalls,个表项:第,n,个表项对应了系统调用号为,n,的服务例程的入口地址的指针,观察,sys_call_table,观察,nr_syscalls,的定义,10/2/2024,10,Linux内核源代码导读,初始化系统调用,内核初始化期间调用,trap_init,(),函数建立,IDT,表中向量,128,对应的表项,语句如下:,其中,,SYSCALL_VECTOR,被定义为,0 x80,该调用把下列值存入这个系统门描述符的相应字段,:,segment selector,:内核代码段,_KERNEL_CS,的段选择符,offset,:指向,system_call,(),异常处理程序的入口地址,type,:置为,15,。表示这个异常是一个陷阱,相应的处理程序不禁止可屏蔽中断,DPL(,描述符特权级,),:置为,3,。这就允许用户态进程访问这个门,即在用户程序中使用,int,$0 x80,是合法的,10/2/2024,11,Linux内核源代码导读,CPU,执行,int,0 x80,指令,过程类似中断,在进入,system_call,之前,堆栈情况为,10/2/2024,12,Linux内核源代码导读,system_call,(),函数,10/2/2024,13,Linux内核源代码导读,观察,SAVE_ALL,宏,SAVE_ALL,在堆栈上依次保存了,3,个段寄存器,fs,、,es,、,ds,的值,以及,ax,、,bp,、,di,、,si,、,dx,、,cx,、,bx,这,7,个寄存器的值。,并对段寄存器,ds,、,es,、,fs,赋予适当的值,10/2/2024,14,Linux内核源代码导读,系统调用的参数传递,系统调用也需要输入输出参数,例如,实际的值,用户态进程地址空间的变量的地址,甚至是包含指向用户态函数的指针的数据结构的地址,system_call,是,linux,中所有系统调用的入口点,每个系统调用至少有一个参数,即由,eax,传递的系统调用号,例如:一个应用程序调用,fork(),封装例程,那么在执行,int,$0 x80,之前就把,eax,寄存器的值置为,2(,即,_,NR_fork,),。,这个寄存器的设置是,libc,库中的封装例程进行的,因此,用户一般不关心系统调用号,10/2/2024,15,Linux内核源代码导读,很多系统调用需要不止一个参数,普通,C,函数的参数传递是通过把参数值写入堆栈,(,用户态堆栈或内核态堆栈,),来实现的。但因为系统调用是一种特殊函数,它由用户态进入了内核态,所以,既不能使用用户态的堆栈也不能直接使用内核态堆栈,用户态堆栈,用户态,C,函数,内核态堆栈,内核态,C,函数,?,10/2/2024,16,Linux内核源代码导读,在,int,$0 x80,汇编指令之前,系统调用的参数被写入,CPU,的,寄存器,。然后,在进入内核态调用系统调用服务例程之前,内核再把存放在,CPU,寄存器中的参数拷贝到内核态堆栈中。因为毕竟服务例程是,C,函数,它还是要到堆栈中去寻找参数的,用户态堆栈,用户态,C,函数,内核态堆栈,内核态,C,函数,寄存器,10/2/2024,17,Linux内核源代码导读,回想一下在进入中断和异常处理程序前,在内核态堆栈中保存的,pt_regs,结构,此时,pt_regs,结构中的一些寄存器被用来传递参数或者,pt_regs,结构本身就是参数,使用寄存器传参的限制,一个参数最长,32,位,根据,pt_regs,结构,最多可以传递,6,个参数,10/2/2024,18,Linux内核源代码导读,参数传递举例,处理,write,系统调用的,sys_write,服务例程声明如下,该函数期望在栈顶找到,fd,,,buf,和,count,参数,在封装,sys_write,(),的封装例程中,将会在,ebx,、,ecx,和,edx,寄存器中分别填入这些参数的值,然后在进入,system_call,时,,SAVE_ALL,会把这些寄存器保存在堆栈中,进入,sys_write,服务例程后,就可以在相应的位置找到这些参数,举例观察,asmlinkage,使得编译器不通过寄存器,(x=0),而,使用堆栈传递参数,10/2/2024,19,Linux内核源代码导读,SAVE_ALL,后的堆栈情况,10/2/2024,20,Linux内核源代码导读,在有些系统调用中,,C,库接口中尽管没有提供参数,但是在内核中却会依赖保存的,pt_regs,信息,例如,fork(),sys_clone()do_frok,(),10/2/2024,21,Linux内核源代码导读,传递返回值,服务例程的返回值是将会被写入,eax,寄存器中,这个是在执行“,return”,指令时,由编译器自动完成的,10/2/2024,22,Linux内核源代码导读,验证参数,在内核打算满足用户的请求之前,必须仔细的检查所有的系统调用参数,比如前面的,write(),系统调用,,fd,参数是一个文件描述符,,sys_write,(),必须检查这个,fd,是否确实是以前已打开文件的一个文件描述符,进程是否有向,fd,指向的文件的写权限,如果有条件不成立,那这个处理程序必须返回一个负数,10/2/2024,23,Linux内核源代码导读,只要一个参数指定的是地址,那么内核必须检查它是否在这个进程的地址空间之内,有两种验证方法:,验证这个线性地址是否属于进程的地址空间,并且是否有合理的访问权限,仅仅验证这个线性地址小于,PAGE_OFFSET,(用户态地址,粗粒度),对于第一种方法:,费时,大多数情况下,不必要,对于第二种方法:,高效,在后续的执行过程中,会自然的捕获到出错情况(配合缺页异常),从,linux2.2,开始执行第二种检查,10/2/2024,24,Linux内核源代码导读,对用户地址参数的粗略验证,在内核中,可以访问到所有的内存,要防止用户将一个内核地址作为参数传递给内核,这将导致它借用内核代码来读写任意内存,检查方法:,Addr,size,是否超出当前进程的地址边界,对于用户进程:不大于3G,对于内核线程:可以使用整个4G,10/2/2024,25,Linux内核源代码导读,访问进程的地址空间,系统调用服务例程需要非常频繁的读写进程地址空间的数据,10/2/2024,26,Linux内核源代码导读,举例,get_user,get_user,_,get_user_x,10/2/2024,27,Linux内核源代码导读,访问进程地址空间时的缺页,内核对进程传递的地址参数只进行粗略的检查,访问进程地址空间时的缺页,可以有多种情况,合理的缺页:来自虚存技术,由于错误引起的缺页,由于非法引起的缺页,10/2/2024,28,Linux内核源代码导读,非法缺页的判定,内核中,只有少数几个函数,/,宏会访问用户地址空间,对于一次非法缺页,一定来自于这些函数,/,宏,可以将访问用户地址空间的指令的地址一一列举出来,当发生非法缺页时,根据引起出错的指令地址来定位,若定位到:则说明系统调用参数有问题,否则,可能是更严重的,bug,引起的,Linux,使用了异常表的概念,_,ex_table,_,start_ex_table,_,stop_ex_table,10/2/2024,29,Linux内核源代码导读,_,ex_table,的表项,哪条指令访问了用户地址空间,如果这条指令引起了非法缺页,该怎么处理,Fixup,所指向的代码,称为修正代码通常为汇编代码,10/2/2024,30,Linux内核源代码导读,举例,get_user,相关,文件,getuser_32.S,10/2/2024,31,Linux内核源代码导读,缺页异常对非法缺页的处理,在缺页异常,do_page_fault,中,若最后发现是非法缺页,就会执行下面的操作,假设找到了修正代码,会发生什么事情?,该操作使用引起出错的代码地址在异常表中进行查找,若找到,就返回,相应的修正代码地址,填写在,regs,-,eip,中,10/2/2024,32,Linux内核源代码导读,IDT,表,,进入异,常处理,某内核函数,缺页,缺页处理,作为一次故障,要重新执行引起出错的代码,正常情况下,这个,eip,在发生异常时,,由硬件保存到堆栈中,因此,正常情况下,返回此处,非法异常,修改堆栈中的,eip,,指向修正代码,因此,非法缺页时,返回此处,修正代
展开阅读全文