当前位置: 首页 > 科技观察

从零开始学Linux:想吃透“系统调用”的底层原理?建议大家不要错过这个【召唤门】

时间:2023-03-13 18:46:23 科技观察

上一篇从零开始学习Linux10:三级跳转过程详解——从bootloader到操作系统,再到应用程序。由于当时没有引入特权级的概念,用户程序和操作系统都工作在同一个特权级,所以属于操作系统代码段的函数可以直接通过[段选择器:offset]调用,如下图:用户程序头橙色部分的信息表示操作系统提供的两个系统函数位于操作系统的哪个段描述符,偏移地址是多少。一旦引入了权限级别,上面的调用方式就不行了。因为用户程序的特权级必须低于操作系统,即使用户程序能够知道函数的段选择符和偏移地址,操作系统也会禁止用户程序跳入其中。例如:应用程序的CPL和RPL都是3,而函数在操作系统中的段DPL是0,就不能通过特权级检查。看过上一篇文章的朋友一定知道,如果在目标代码段的描述符中将TYPE.C标志设置为1,则表示这是一个兼容(或称为一致)的代码段,允许低权限的调用用户程序。除了这种方法,处理器还提供了另一种更“正式”的方式,将低权限代码转移到高权限代码,这就是:调用门。在这篇文章中,我们将一起学习调用门的机制,顺便介绍一下所有的门描述符。门描述符的所谓门就是一个通道。通过这个通道,可以进入另一个代码段执行。在x86中,有以下几种门:调用门:用于将低权限代码转移到高权限代码;任务门:用于不同任务之间的调度;中断门:用于异步执行中断处理程序;trapgate:也是用来执行中断处理程序的,但是这里的中断是在处理器内部产生的;门描述符与之前介绍的段描述符本质上是一样的,都是用来描述一个代码段的信息,只是门描述符增加了一层间接性。下面是4个门描述符的结构(32位系统):从上面4个门描述符的结构可以看出,它们并不是直接记录目标代码段的起始地址和边界,而是记录目标代码段选择器。也就是说:首先通过门描述符找到代码段选择器,然后利用这个选择器在GDT中找到真正的目标代码段描述符,最后找到目标代码段的起始地址、边界、属性等信息.它是以下结构:所以,这些门增加了一个间接层。这一间接层为操作系统提供了许多好处。首先,对于中断处理,把所有的中断描述符放在一个表中,可以解耦中断处理程序的地址。其次,对于执行代码段的转移,可以利用门来提供更灵活的特权级控制,实现更复杂的操作。关于taskgate中的TSSselector:所谓taskgate可以简单理解为用于任务切换。因为在一个TSS段中,保存的是一个任务上下文信息的快照。只要处理器发现选择器指向的描述符是一个任务门(通过TYPE域),就会进行任务切换:将当前CPU中的上下文保存到当前任务的TSS段;b.将指向的TSS段中的上下文内容加载到CPU寄存器中,从而实现任务切换。CallGatePrivilegeLevel检查规则从callgate的名字可以看出它是为系统调用服务的。我们再来看看它的描述符结构:Numberofparameters:调用者向目标代码传递了多少个参数(通过栈空间传递参数);DPL:表示调用门本身的特权级别;Targetcodesegmentselector:最终调用的目标代码段的选择器,需要用这个选择器在GDT中找到目标代码段的基地址;Offset:被调用代码距离目标代码段起始地址的偏移字节数;从上面的字段来看,这简直就是为低权限的用户代码调用高权限的操作系统代码量身定做的,只要处理器让用户程序走在特权级别就行。其实正是如此:当用户请求调用门时,操作系统会进行如下特权级检查:当前特权级CPL(用户程序)和请求的特权级RPL必须[高于或等于to]调用门中的DPL;也就是说,在数值上:CPL<=DPL,RPL<=DPL。(注意:这是调用门描述符中的DPL)当前特权级CPL(用户程序),必须[低于或等于]目标代码段中的DPL;也就是说,在value:CPL>=目标代码段描述符中的DPL。从上面的规则又可以看出:即使通过调用门,目标代码段也只允许相同或更低权限级别的代码进入,这也验证了前面所说的:高权限级别的代码不会主动转移到低权限级别代码中间。如果特权级检查通过,进入目标代码段后,当前的特权级CPL会不会改变?这取决于目标代码段描述符中TYPE字段中C标志的值:TYPE.C=1:CPL保持不变,在用户程序中仍然是特权级3;TYPE.C=0:CPL发生变化,成为目标代码段的特权级;安装调用门的使用过程。所谓安装调用门就是在GDT中构造一个调用门描述符,使其目标代码段选择器指向真正的代码段。假设:下图是安装调用门之前的状态:操作系统为用户程序调用提供了2个系统函数,它们的代码位于一个独立的代码段(GDT中有代码段描述符)。然后在GDT中,添加一个门描述符(index=8),描述符中“目标代码段选择器”中的索引号等于7:注意:根据上面提到的特权级检查规则,为了允许用户程序要正确进入调用门,需要将调用门描述符的DPL设置为3(与用户程序的CPL相同)。告诉用户程序调用门的选择器按照前面的做法,操作系统可以在用户程序头部约定的位置填写调用者的选择器和函数偏移地址:选择器的值为:0x0043(二进制:0000_0000_0100_0011):RPL=3;去GDT搜索;索引号index=8;用户程序通过调用门进入系统函数。当用户程序请求调用系统函数时,处理器开始扩展三方的特权级Check:用户程序的CPL=3,RPL=3;调用门本身的DPL=3;调用门中目标代码段选择器指向的描述符(index=7)中的DPL=0;该值满足调用门的特权级规则要求,因此进入系统函数所在代码执行。堆栈切换x86处理器要求:当前特权级别CPL必须与目标堆栈段的DPL相同。因此,用户程序在操作系统中进入系统函数后:1、如果特权CPL没有发生变化,那么在执行系统函数时,使用的栈还是之前用户程序使用的栈空间。如果用户程序通过栈传递参数,系统函数可以直接在同一个栈空间中获取这些参数。2、如果特权级CPL发生了变化,则在执行系统函数时,需要切换到0特权级下的用户程序的栈空间(操作系统在加载用户程序时已经提前做好准备)。同时,处理器会将用户程序在3特权级下使用的栈空间中的所有参数复制到0特权级下的栈空间中,以便系统函数能够正确获取这些参数。本文转载自微信公众号《IOT物联网小镇》