目前在科锐的学习已经进入了安卓阶段。温故而知新,故把在学习Windows内核阶段时比较感兴趣的调用门进行了整理。在这里与大家分享一下。
由于论坛的版面问题,很多图片看似不清晰。建议大家下载附件中的Word文档。Word文档中的图片是我对角拉伸缩小过的。可以对角拉伸放大,防止图片变形。
纲要:
CPU手册中调用门相关章节的解释以及名词解释(因为还涉及到多方面的知识结构,这里就只包含了和调用门紧密相关的部分资料)。
使用内核调试器向GDT中安装一个调用门描述符。
利用调用门来切换特权级,从Ring3到Ring0的切换。
在调用门中使用参数、全局变量和测试特权指令。
调试调用门时的要注意的事项。
调用门可以理解为一个可以安全的切换特权级的使用起来稍微麻烦些的工具。调用门通过CALL FAR和RET FAR来调用和返回。
强调:在Windows XP中并没有使用调用门;听说在Windows 7 64位版本中操作系统利用调用门进行32位向64位的转换,也就是当一个运行在64位操作系统上的32位程序通过调用门转换之后最终调用了64位的内核代码。调用门内部进行了对参数的转换。
以下内容摘自CPU手册综合卷
名词解释:
Privilege Levels(特权级) - Protection Rings(保护环):
在卷一的6.3.5节和卷三的5.5节都有相关解释。大意:提供4种特权级是为了处理器的段保护机制,编号从0到3。编号越大,特权级越小,相对的可以使用的指令也就越少。保护环就是特权级,特权级就是保护环。手册中规定Ring0用于操作系统内核非常重要的代码和数据,一般都被操作系统控制。Ring1、2用于操作系统的服务、设备、驱动等等。Ring3用于普通的应用程序。Windows操作系统只使用到了4种特权级中的两种,Ring0和Ring3。CPU手册中也说明,是可以只使用4种特权级中的两种,Ring0和Ring3。
Procedure:
该词汇在手册卷三的2-4页底部有注解:procedure是对逻辑单元或代码块的总称。可以理解为程序、过程、函数或者是例程都可以。本文解释为例程,比较符合我的个人观点。
Current privilege level (CPL): 当前特权级,表示当前正在被执行的任务或程序所在的特权级。存储在CS和SS段寄存器的第0位和第1位,当程序流程切换到不同的特权级时,处理器会改变CPL。
Descriptor privilege level (DPL):描述符特权级,表示段或门的特权级。存储在描述符的DPL字段。
Requested privilege level (RPL):请求的特权级,存储在段选择子的第0位和第1位。
GDT:全局描述符表,存储了所有段描述符、数据描述符或者是系统描述符。GDT的地址存放在GDTR寄存器中。
LDT:局部描述符表,和GDT一样可以存储各种描述符。LDT的地址存放在LDTR寄存器中。Windows没有使用LDT。
IDT:中断描述符表。IDT的地址存放在IDTR寄存器中。
每当使用一个调用门转移程序控制到一个高特权级的非一致代码段(也就是非一致目标代码段的DPL比CPL小)时,处理器自动切换到目标代码段特权级的栈。栈切换是为了预防高特权级的例程由于栈空间不足而导致的崩溃。它也阻止了低特权级例程通过共享栈有意或无意的干扰高特权级例程(例如通过修改高特权级例程的返回地址来提权等等…)。
每个任务必须定义最多4个栈:一个给运行在特权级3的应用程序代码使用并且特权级2、1和0也都有一个栈。也就是每个特权级都有自己的栈。如果只使用了特权级3和0,那么必须定义两个栈。每个栈的位于单独的段并且用一个段选择子(栈段SS)和偏移(栈指针ESP)来确定栈。
Windows操作系统只使用了特权级3和0。
特权级0、1和2的栈指针存储在当前正在运行任务的TSS中。图7-2。这些指针由一个段选择子和栈指针(ESP)组成。这些指针初始化为完全只读的值(也就是静态的)。当任务运行时处理器不会改变他们。他们只是用来在调用到更高特权级时创建新栈。当从被调用例程返回时将处理这些栈。在之后例程被调用使用初始化栈指针创建一个新栈。TSS没有指定特权级3的栈,因为处理器不允许程序控制从运行在CPL0、1或2的例程转移到一个运行在CPL3的例程,除了返回(ret)之外。
TSS中的SSx、ESPx(x=0,1,2)字段是静态的,除非软件修改了它们。假设操作系统为用户任务的TSS填写了ESP0其值为0x800;当通过调用门进入特权级0的代码段时,会切换到特权级0的栈。栈指针ESP初始值就是0x800;返回时假设ESP变成了0x808,处理器并不会把0x808写回到ESP0字段;下次再使用调用门进入0特权级的代码段时,使用的还是0x800来初始化特权级0的栈指针。
操作系统负责为所有被使用的特权级创建栈和创建栈段描述符。并且保存这些栈指针的初始值到TSS中。每个栈必须是可读写访问(在段描述符的type字段指定)并且必须有足够的空间(在段描述符的limit字段指定)来保存以下信息:
1. 调用者例程的SS、ESP、CS和EIP寄存器内容。
2. 被调用例程所需要的参数和临时变量(也就是局部变量)需要的空间。
3. 当对异常或中断处理例程进行隐式调用时,还需要EFLAGS寄存器和错误码的空间。
由于一个例程可以调用其他例程,因此每个栈必须有足够的空间来保存很多帧的数据(每一帧都表示上述的三个信息)。
如果操作系统没有使用处理器的多任务机制,它还是必须为栈的相关操作创建至少一个TSS。
当通过调用门调用一个例程导致特权级改变时,处理器执行以下步骤去切换栈并且在新特权级上开始执行被调用的例程:
1. 使用目标代码段的DPL(也就是新的CPL)从TSS选择一个指向新栈指针。也就是说使用调用门的Segment Selector字段作为目标代码段,如果目标代码段的DPL为0那么在TSS中选择SS0和ESP0来作为被调用例程栈的栈段选择子和栈指针。
2. 从当前TSS中为将要被切换的栈读取栈段选择子和栈指针。当读取栈段选择子、栈指针或栈段描述符时发现任何违规都会产生一个无效TSS(#TS)异常。
3. 检查目标特权的栈段描述符和类型如果检测到违例将产生无效TSS(#TS)异常。
4. 临时保存当前(调用者例程)的SS和ESP寄存器的值。保存在处理器内部。
5. 为新栈(被调用例程)加载段选择子和栈指针到SS和ESP寄存器。
6. push临时保存(调用者例程)的SS和ESP寄存器的值到新栈(被调用例程)中。图5-13
7. 拷贝调用门Param.Count字段指定的参数数量到新栈中。也就是说从调用者的栈中拷贝调用门的Param.Count字段指定个数的参数到被调用例程的栈中。
8. push返回指令指针(调用者例程的CS和下一条指令的指针)到新栈中。
9. 从调用门分别加载新代码段的段选择子和新指令指针到CS和EIP寄存器中。并且被调用的例程开始执行。代码段的段选择子在Segment Selector字段指定,新指令指针在Offset in Segment字段指定。
在调用门的Param.Count字段指定了处理器应当从调用者的栈拷贝到被调用者的栈数据的数量(最多31个,因为Param.Count字段只有5个2进制位)。如果需要传递给被调用者的数据超过31个,那么参数可以是指向数据结构的指针或使用已经保存的SS和ESP寄存器来访问调用者的栈空间。(这里我并不能理解如何通过已经保存的SS和ESP来访问调用者的栈空间,是通过段超越前缀的这种方式吗???好像又不对。。我是直接通过DS段寄存器来访问调用者的栈空间的。)
可以使用RET指令在相同的特权级上执行近返回、远返回;在不同的特权级上执行远返回。预期是为了从一个使用CALL指令从被调用的例程中返回的指令。不支持从一个JMP指令返回,因为JMP指令不在栈中保存要返回的指令指针。
近返回只在当前代码段中进行程序控制转移;因此处理器只做界限检查。当处理器从栈中pop指令指针到EIP中时,处理器检查指针是否超出了当前代码段的界限。
在相同的特权级远返回,处理器从栈中pop要返回的代码段的段选择子和返回指令指针。正常情况下,这些指针应当是有效的,因为是由CALL指令push到栈上的。然而,处理器依然会进行特权级检查来检测是否改变了指针或者对栈维护的不当。
只有当返回到低特权级(要返回的代码段的DPL在数值上大于CPL)时才允许改变特权级。处理器使用调用者例程保存的CS寄存器中的RPL字段决定是否可以返回到数值上较高的特权级。如果RPL的特权比CPL低。触发一个return acorss privilege levels
当远返回到调用者例程时,处理器执行以下步骤:
1. 检查保存的CS寄存器值(调用者的CS寄存器)的RPL字段决定是否需要再返回时改变特权级。
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!