调用门堆栈切换与调用过程返回

Posted rtoax

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了调用门堆栈切换与调用过程返回相关的知识,希望对你有一定的参考价值。

论天下大势,合久必分分久必合。上回书我们说了《CPL,RPL和DPL:这三个级别你搞懂了吗?》,这回书我们讲调用门以及堆栈切换。

门描述符


为了提供对具有不同特权级别的代码段的受控访问,处理器提供了一组特殊的描述符,称为门描述符。有四种门描述符:


  • 调用门

  • 陷阱门

  • 中断门

  • 任务门

任务门用于任务切换,陷阱和中断门是用于调用异常和中断处理程序的特殊类型的调用门。本文仅涉及调用门。

调用门


调用门促进了不同权限级别之间程序控制的受控转移。它们通常仅用于使用特权级保护机制的操作系统或执行程序。调用门也可用于在 16 位和 32 位代码段之间传输程序控制。

下图显示了调用门描述符的格式。调用门描述符可以驻留在 GDT 或 LDT 中,但不能驻留在中断描述符表 (IDT) 中。 

Call-Gate Descriptor

Call-Gate Descriptor in IA-32e Mode

调用门执行六项功能:


  1. 它指定要访问的代码段。

  2. 它为指定代码段中的过程定义了一个入口点。

  3. 它指定尝试访问过程的调用者所需的权限级别。

  4. 如果发生堆栈切换,则指定要在堆栈之间复制的可选参数的数量。

  5. 它定义了要推送到目标堆栈上的值的大小:16 位门强制 16 位推送,32 位门强制 32 位推送。

  6. 它指定调用门描述符是否有效。

描述符中的字段含义:


  • 调用门中的段选择子字段指定要访问的代码段。

  • offset 字段指定代码段中的入口点。

    这个入口点通常是特定过程的第一条指令。

  • DPL 字段指示调用门的权限级别,而后者又是通过门访问所选过程所需的权限级别。

  • P 标志指示调用门描述符是否有效。(门所指向的代码段的存在由代码段描述符中的 P 标志指示。) 

  • 参数计数字段指示从调用过程堆栈复制到新堆栈的参数数量,如果一个堆栈 发生切换。

    参数 count 指定 16 位调用门的字数和 32 位调用门的双字数。


请注意,门描述符中的 P 标志通常始终设置为 1。如果设置为 0,则当程序尝试访问该描述符时会生成不存在 (#NP) 异常。操作系统可以将 P 标志用于特殊目的。例如,它可用于跟踪使用门的次数。此处,P 标志最初设置为 0,导致不存在的异常处理程序陷入陷阱。异常处理程序然后递增一个计数器并将 P 标志设置为 1,以便从处理程序返回时,门描述符将有效。

通过调用门访问代码段


为了访问调用门,在 CALL 或 JMP 指令中提供了一个指向该门的远指针作为目标操作数。来自该指针的段选择器标识了调用门(见下图);指针的偏移量是必需的,但不被处理器使用或检查。(偏移量可以设置为任何值。)

当处理器访问调用门时,它使用调用门中的段选择子来定位目标代码段的段描述符。(这个段描述符可以在 GDT 或 LDT 中。)然后它将来自代码段描述符的基地址与来自调用门的偏移量组合起来,形成代码段中过程入口点的线性地址。

如下图所示,四种不同的权限级别用于通过调用门检查程序控制传输的有效性:

  • CPL(当前特权级别)。

  • 调用门的选择器的 RPL(请求者的特权级别)。

  • 调用门描述符的 DPL(描述符特权级别)。

  • 目标代码段的段描述符的 DPL。

权限检查规则根据控制传输是使用 CALL 还是 JMP 指令启动而有所不同,如下表所示。

指令特权检查规则
CALL

CPL ≤ 调用门 DPL;RPL ≤ 调用门 DPL

目标一致代码段 DPL ≤ CPL 

目标非一致代码段 DPL ≤ CPL

JMP

CPL ≤ 调用门 DPL;RPL ≤ 调用门 DPL

目标一致代码段 DPL ≤ CPL 

目标非一致代码段 DPL = CPL

调用门描述符的 DPL 字段指定了调用过程可以访问调用门的数字最高特权级别;也就是说,要访问调用门,调用过程的 CPL 必须等于或小于调用门的 DPL。例如,在下图中(在Intel手册中该图引用错误),调用门 A 的 DPL 为 3。因此所有 CPL(0 到 3)的调用过程都可以访问这个调用门,其中包括代码段 A、B 和 C 中的调用过程。门 B 的 DPL 为 2,因此只有在 CPL 或 0、1 或 2 处的调用过程才能访问调用门 B,其中包括代码段 B 和 C 中的调用过程。虚线表示代码段中的调用过程 A 无法访问调用门 B。

调用门的段选择器的 RPL 必须满足与调用过程的 CPL 相同的测试;也就是说,RPL 必须小于或等于调用门的 DPL。在上图的示例中(在Intel手册中该图引用错误),代码段 C 中的调用过程可以使用门选择器 B2 或 B1 访问调用门 B,但不能使用门选择器 B3 访问调用门 B。

如果调用过程和调用门之间的特权检查成功,则处理器会根据调用过程的 CPL 检查代码段描述符的 DPL。这里,权限检查规则在 CALL 和 JMP 指令之间有所不同。只有 CALL 指令可以使用调用门将程序控制转移到更高特权(数字特权级别更低)的非一致性代码段;也就是说,对于 DPL 小于 CPL 的非一致性代码段只有CALL可以使用。JMP 指令只能使用调用门将程序控制转移到 DPL 等于 CPL 的非一致性代码段。CALL 和 JMP 指令都可以将程序控制转移到更高特权的符合代码段;也就是说,符合 DPL 小于或等于 CPL 的代码段。

如果调用更高特权(数字特权级别更低)的非一致目标代码段,则 CPL 将降低到目标代码段的 DPL 并发生堆栈切换。如果调用或跳转到更高特权的符合目标代码段,则 CPL 不会更改,也不会发生堆栈切换。

调用门允许单个代码段具有可以在不同权限级别访问的过程。例如,位于代码段中的操作系统可能具有一些旨在供操作系统和应用程序软件使用的服务(例如处理字符 I/O 的过程,最重要的要数系统调用)。可以设置这些过程的调用门以允许在所有权限级别(0 到 3)进行访问。然后可以为仅由操作系统使用的其他操作系统服务(例如初始化设备驱动程序的过程)设置更多特权调用门(DPL 为 0 或 1)。

堆栈切换


每当使用调用门将程序控制转移到更高特权的非一致性代码段时(即当非一致性目标代码段的 DPL 小于 CPL 时),处理器自动切换到目标代码段特权的堆栈 等级。执行此堆栈切换是为了防止更多特权程序因堆栈空间不足而崩溃。它还可以防止特权较低的过程通过共享堆栈干扰(偶然或有意地)特权较高的过程。

每个任务定义最多 4 个堆栈:

  • 一个用于应用程序代码(以特权级别 3 运行),

  • 一个用于使用的特权级别 2、1 和 0 中的每一个。 

(如果只使用两个特权级别 [3 和 0],那么只需要定义两个堆栈,Linux就是这么做的。)这些堆栈中的每一个都位于一个单独的段中,并用段选择器和堆栈段中的偏移量(一个堆栈 指针)。

特权级 3 堆栈的段选择器和堆栈指针分别位于 SS 和 ESP 寄存器中,当执行特权级 3 代码时,并在发生堆栈切换时自动存储在被调用过程的堆栈中。

指向特权级别 0、1 和 2 堆栈的指针存储在当前运行任务的 TSS 中(见下图)。这些指针中的每一个都包含一个段选择器和一个堆栈指针(加载到 ESP 寄存器中)。这些初始指针只读,任务运行时CPU不会更改它们。它们仅用于在调用更多特权级别(数字较低的特权级别)时创建新堆栈。当从被调用的过程返回时,这些堆栈被处理掉。下次调用该过程时,将使用初始堆栈指针创建一个新堆栈。(TSS 没有为特权级别 3 指定堆栈,因为处理器不允许将程序控制从以 0、1 或 2 CPL 运行的过程转移到以 3 CPL 运行的过程)

操作系统负责为要使用的所有特权级别创建堆栈和堆栈段描述符,并将这些堆栈的初始指针加载到 TSS 中。每个堆栈都必须是可读/写访问的(在其段描述符的类型字段中指定),并且必须包含足够的空间(在限制字段中指定)以容纳以下项目:


  • SS、ESP、CS 和 EIP 寄存器的内容用于调用过程。

  • 被调用过程所需的参数和临时变量。

  • EFLAGS 寄存器和错误代码,当隐式调用异常或中断处理程序时。


堆栈将需要足够的空间来包含这些项的许多帧,因为程序经常调用其他程序,并且操作系统可能支持多个中断的嵌套。每个堆栈都应该足够大,以允许在其特权级别出现最坏的嵌套情况。

(如果操作系统不使用处理器的多任务机制,它仍然必须为此堆栈相关的目的创建至少一个 TSS。)

当通过调用门的过程调用导致特权级别发生变化时,处理器执行以下步骤来切换堆栈并在新的特权级别开始执行被调用的过程:


  1. 使用目标代码段的 DPL(新 CPL)从 TSS 中选择一个指向新堆栈的指针(段选择器和堆栈指针)。

  2. 从当前 TSS 读取要切换到的堆栈的段选择器和堆栈指针。在读取堆栈段选择器、堆栈指针或堆栈段描述符时检测到的任何限制违规都会导致生成无效的 TSS (#TS) 异常。

  3. 检查堆栈段描述符的正确权限和类型,如果检测到违规,则生成无效的 TSS (#TS) 异常。

  4. 暂时保存 SS 和 ESP 寄存器的当前值。

  5. 为 SS 和 ESP 寄存器中的新堆栈加载段选择器和堆栈指针。

  6. 将临时保存的 SS 和 ESP 寄存器(用于调用过程)的值压入新堆栈(见下图)。

  7. 将调用门的参数计数字段中指定的参数个数从调用过程栈复制到新栈中。如果计数为 0,则不复制任何参数。

  8. 将返回指令指针(CS 和 EIP 寄存器的当前内容)压入新堆栈。

  9. 将新代码段的段选择器和来自调用门的新指令指针分别加载到 CS 和 EIP 寄存器中,并开始执行被调用的过程。



调用门中的参数计数字段指定处理器应该从调用过程的堆栈复制到被调用过程的堆栈的数据项的数量(最多 31 个)。如果需要向被调用过程传递超过 31 个数据项,参数之一可以是指向数据结构的指针,或者可以使用 SS 和 ESP 寄存器中保存的内容来访问旧堆栈空间中的参数。传递给被调用过程的数据项的大小取决于调用门大小,如上一节中所述。

从调用过程返回


RET 指令可用于执行近返回、同一权限级别的远返回以及不同权限级别的远返回。此指令旨在执行从使用 CALL 指令调用的过程的返回。它不支持从 JMP 指令返回,因为 JMP 指令不会在堆栈上保存返回指令指针。

近返回只转移当前代码段内的程序控制;因此,处理器只执行限制检查。当处理器将返回指令指针从堆栈中弹出到 EIP 寄存器中时,它会检查该指针是否超出了当前代码段的限制。

在相同特权级别的远返回中,处理器弹出要返回的代码段的段选择器和堆栈中的返回指令指针。在正常情况下,这些指针应该是有效的,因为它们是由 CALL 指令压入堆栈的。但是,处理器执行特权检查以检测当前过程可能已更改指针或未能正确维护堆栈的情况。

只有在返回到较低特权级别(即返回代码段的 DPL 在数字上大于 CPL)时,才允许需要更改权限级别的远返回。处理器使用为调用过程保存的 CS 寄存器值中的 RPL 字段(见下图)来确定是否需要返回到数字更高的特权级别。如果 RPL 在数值上比 CPL 大(特权较低),则会发生跨特权级别的返回。



处理器在执行远返回调用过程时执行以下步骤:


  1. 检查保存的 CS 寄存器值的 RPL 字段以确定返回时是否需要更改特权级别。

  2. 使用被调用过程堆栈上的值加载 CS 和 EIP 寄存器。(类型和权限级别检查是在代码段选择器的代码段描述符和 RPL 上执行的。)

  3. (如果 RET 指令包含参数计数操作数并且返回需要更改权限级别。)将参数计数(以从 RET 指令获得的字节数)添加到当前 ESP 寄存器值(在弹出 CS 和 EIP 值之后),以跳过被调用过程堆栈上的参数。ESP 寄存器中的结果值指向为调用过程的堆栈保存的 SS 和 ESP 值。(请注意,必须选择 RET 指令中的字节数以匹配调用过程在进行原始调用时引用的调用门中的参数数乘以参数的大小。)

  4. (如果返回需要更改权限级别。)使用保存的 SS 和 ESP 值加载 SS 和 ESP 寄存器,并切换回调用过程的堆栈。被调用过程的堆栈的 SS 和 ESP 值被丢弃。加载堆栈段选择器或堆栈指针时检测到的任何限制违规都会导致生成一般保护异常 (#GP)。还会检查新的堆栈段描述符是否存在类型和特权冲突。

  5. (如果 RET 指令包含参数计数操作数。)将参数计数(从 RET 指令获得的字节数)添加到当前 ESP 寄存器值,以跳过调用过程堆栈上的参数。不会根据堆栈段的限制检查生成的 ESP 值。如果 ESP 值超出限制,则直到下一次堆栈操作才会识别该事实。

  6. (如果返回需要更改特权级别。)检查 DS、ES、FS 和 GS 段寄存器的内容。如果这些寄存器中的任何一个引用 DPL 小于新 CPL 的段(不包括符合的代码段),则段寄存器将加载一个空段选择器。

参考


《Intel® 64 and IA-32 Architectures Software Developer’s Manual》

CPL,RPL和DPL:这三个级别你搞懂了吗?

以上是关于调用门堆栈切换与调用过程返回的主要内容,如果未能解决你的问题,请参考以下文章

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

切换 C++ 函数的调用堆栈

返回时dll挂钩未正确更新调用堆栈

Linux从头学13:想彻底搞懂“系统调用”的底层原理?建议您别错过这篇调用门

结合中断上下文切换和进程上下文切换分析Linux内核的一般执行过程

IA-32汇编语言笔记——堆栈的作用