1、栈
堆栈(见图 6-1)是一个连续的内存位置数组。它包含在一个段中,并由 SS 寄存器中的段选择器标识。使用平面内存模型时,堆栈可以位于程序线性地址空间中的任何位置。堆栈最长可达 4 GB,这是段的最大大小。
使用 PUSH 指令将项目放入堆栈,使用 POP 指令将项目从堆栈中删除。当一个项目被压入堆栈时,处理器会递减 ESP 寄存器,然后将该项目写入新的堆栈顶部。当一个项目从堆栈中弹出时,处理器从堆栈顶部读取该项目,然后递增 ESP 寄存器。以这种方式,当项目被压入堆栈时,堆栈在内存中向下增长(朝向较小的地址),并且当项目从堆栈弹出时,堆栈向上收缩(朝向更大的地址)。
程序或操作系统/执行程序可以设置许多堆栈。例如,在多任务系统中,每个任务都可以有自己的堆栈。系统中堆栈的数量受到最大段数和可用物理内存的限制。当系统设置多个堆栈时,一次只有一个堆栈(当前堆栈)可用。当前堆栈是 SS 寄存器引用的段中包含的堆栈。
处理器会自动引用 SS 寄存器来执行所有堆栈操作。例如,当 ESP 寄存器用作内存地址时,它会自动指向当前堆栈中的地址。此外,CALL、RET、PUSH、POP、ENTER 和 LEAVE 指令都对当前堆栈执行操作。
要设置堆栈并将其建立为当前堆栈,程序或操作系统/执行程序必须执行以下操作:
1) 建立堆栈段。
2.)使用 MOV、POP 或 LSS 指令将堆栈段的段选择器加载到 SS 寄存器中。
3.)使用 MOV、POP 或 LSS 指令将堆栈的堆栈指针加载到 ESP 寄存器中。 LSS 指令可用于在一次操作中加载 SS 和 ESP 寄存器。
PUSH 和 POP 指令使用 D 标志来确定在压入或弹出操作中分别减少或增加堆栈指针的量。当栈宽为16位时,栈指针以16位为增量递增或递减;当宽度为 32 位时,堆栈指针以 32 位增量递增或递减。用作堆栈的数据段的默认地址大小属性由段描述符的 B 标志控制。当该标志被清除时,默认地址大小属性为 16;当设置该标志时,地址大小属性为 32。
堆栈通常分为帧。然后,每个堆栈帧可以包含局部变量、要传递给被调函数的参数以及过程链接信息。堆栈帧基指针(包含在 EBP 寄存器中)标识被调用过程的堆栈帧内的固定参考点。为了使用堆栈帧基指针,被调用的过程通常在将任何局部变量压入堆栈之前将 ESP 寄存器的内容复制到 EBP 寄存器中。然后,堆栈帧基指针EBP允许轻松访问堆栈上传递的数据结构、返回指令指针以及由被调用过程添加到堆栈的局部变量。
在进入被调函数之前,CALL指令将EIP寄存器中的地址压入当前堆栈。这个地址被称为返回指令指针,它指向在被调函数返回后当前函数应该继续执行的指令。当从被调用的过程返回时,RET指令将返回指令指针从堆栈中弹出到EIP寄存器中。然后继续执行调用函数。
2、影子堆栈
影子堆栈是专门用于控制传输操作的第二个堆栈。该堆栈与过程堆栈是分开的。影子堆栈不用于存储数据,因此软件不能显式写入。对影子堆栈的写入仅限于控制传输指令和影子堆栈管理指令。可以为特权级别 3(用户模式)或特权级别小于 3(管理员模式)单独启用影子堆栈。
CALL 指令允许控制转移到当前代码段(近调用)和不同代码段(远调用)中的过程。近调用通常提供对当前正在运行的程序或任务中的本地过程的访问。远调用通常用于访问操作系统过程或不同任务中的过程。RET 指令还允许近端和远端返回,以匹配 CALL 指令的近端和远端版本。此外,RET 指令允许程序在返回时递增堆栈指针,以从堆栈中释放参数。从堆栈中释放的字节数由 RET 指令的可选参数 (n) 确定。
2、近调用:当执行近调用时,处理器执行以下操作:
1)将 EIP 寄存器的当前值压入堆栈。如果启用了影子堆栈且位移值不为 0,则将 EIP 寄存器的当前值压入影子堆栈。
2)在 EIP 寄存器中加载被调用过程的偏移量。
3)开始执行被调函数(callee)。
当近调用返回时,处理器执行以下操作:
1)将栈顶值(返回指令指针)弹出到 EIP 寄存器中。如果启用影子堆栈,则从影子堆栈中弹出堆栈顶部(返回指令指针)值,如果它与从堆栈中弹出的返回指令指针不同,则处理器会导致控制保护异常并出现错误代码 NEAR-RET (#CP(NEAR-RET))。
2.)如果 RET 指令具有可选的 n 参数,则将堆栈指针增加 n 操作数指定的字节数,以从堆栈中释放参数。
3) 恢复调用函数(caller)。、
3、远调用
执行远调用时,处理器执行以下操作(参见图 6-2):
1) 将 CS 寄存器的当前值压入堆栈。如果启用了影子堆栈:
a.在内部临时保存 SSP 寄存器的当前值,并将 SSP 与下一个 8 字节边界对齐。
b.将 CS 寄存器的当前值压入影子堆栈。
c.将 LIP (CS.base + EIP) 的当前值推送到影子堆栈上。
d.将 SSP 寄存器内部保存的值推送到影子堆栈上。
2)将EIP 寄存器的当前值压入堆栈。
3)将包含被调用过程的段的段选择器加载到CS 寄存器中。
4)加载被调用过程在EIP 寄存器中的偏移量。
5)开始执行被调用函数。
远调用返回时,处理器执行以下操作:
1)将栈顶值(返回指令指针)弹出到 EIP 寄存器中。
2)将栈顶值(要返回的代码段的段选择器)弹出到 CS 寄存器中。
a.如果启用了影子堆栈:如果 SSP 未与 8 字节对齐,则会导致控制保护异常 (#CP(FAR-RET/IRET))。
b.将地址 SSP+8(LIP)和 SSP+16(CS)处的影子堆栈上的值与从堆栈中弹出的 CS 和(CS.base + EIP)进行比较,并引发控制保护异常(#CP( FAR-RET/IRET)) 如果它们不匹配。
C.将栈顶值(正在返回的过程的 SSP)从影子堆栈弹出到 SSP 寄存器中。
3)如果 RET 指令具有可选的 n 参数,则将堆栈指针增加 n 操作数指定的字节数,以从堆栈中释放参数。
4)恢复执行调用函数。
处理器不会在函数调用时保存通用寄存器、段寄存器或 EFLAGS 寄存器的值。函数调用之前,应明确保存在被调函数返回后调用函数恢复执行时需要的通用寄存器的值,这些值可以保存在堆栈中,也可以保存在某个数据段指向的内存中。
PUSHA 和 POPA 指令有助于保存和恢复通用寄存器的内容。PUSHA 按以下顺序将所有通用寄存器中的值压入堆栈:EAX、ECX、EDX、EBX、ESP(执行 PUSHA 指令之前的值)、EBP、ESI 和 EDI。 POPA指令将PUSHA指令保存的所有寄存器值(ESP值除外)从堆栈弹出到各自的寄存器。如果被调函数显式更改任何段寄存器的状态,则应在执行返回到调用过程之前将它们恢复到以前的值。如果调用程序需要维护 EFLAGS 寄存器的状态,则可以使用 PUSHF/PUSHFD 和 POPF/POPFD 指令保存和恢复全部或部分寄存器。 PUSHF 指令将 EFLAGS 寄存器的低位字压入堆栈,而 PUSHFD 指令则压入整个寄存器。 POPF 指令将一个字从堆栈弹出到 EFLAGS 寄存器的低位字中,而 POPFD 指令将一个双字从堆栈弹出到寄存器中。
IA-32体系结构的保护机制识别从0到3的四个特权级别,其中数字越大意味着特权越小。使用特权级别的原因是为了提高操作系统的可靠性。较低特权段中的代码模块只能通过称为门的严格控制和保护的接口访问在较高特权段运行的模块。尝试在不通过保护门且没有足够访问权限的情况下访问更高权限段会导致生成一般保护异常(#GP)。
3、跨级别调用:如果操作系统或执行程序使用这种多级保护机制,则调用具有更高特权保护级别的函数的处理细节类似于远调用方式。当然二者也有一些细微的不同之处:·
1)CALL指令中提供的段选择器引用一种称为调用门描述符的特殊数据结构。其中,调用门描述符提供以下内容:
——访问权限信息。
——被调用过程的代码段的段选择器。
——代码段的偏移量(即被调用过程的指令指针)。
2)处理器切换到新的堆栈以执行被调用的过程。每个特权级别都有自己的堆栈。特权级别3堆栈的段选择器和堆栈指针分别存储在SS和ESP寄存器中,并在发生对更高特权级别的调用时自动保存。特权级别2、1和0堆栈的段选择器和堆栈指针存储在称为任务状态段(TSS)的系统段中。堆栈切换期间调用门和TSS的使用对调用函数是透明的,除非引发一般保护异常。
当调用更高特权的保护级别时,处理器执行以下操作(参见图 6-5):
1)执行访问权限检查(特权检查)。
2) 临时保存(内部)SS、ESP、CS 和EIP 寄存器的当前内容到栈中
3)将新堆栈(即被调用权限级别的堆栈)的段选择器和堆栈指针从 TSS 加载到 SS 和 ESP 寄存器,并切换到新堆栈。
4)将临时保存的调用函数caller堆栈的 SS 和 ESP 值推入新堆栈。
5)将参数从调用函数caller堆栈复制到新堆栈。调用门描述符中的一个值决定了向新堆栈复制多少个参数。
6)将临时保存的调用存储过程的 CS 值和 EIP 值推送到新堆栈。
如果在调用存储过程的权限级别启用了影子堆栈,那么处理器会在内部临时保存调用函数的 SSP。如果调用存储过程的权限级别为 3,则调用存储过程的 SSP 也会保存到 IA32_PL3_SSP MSR 中。
7)将新代码段的段选择器和新指令指针从调用门分别装入 CS 和 EIP 寄存器。
8)以新的权限级别开始执行被调用函数。
当从特权函数执行返回时,处理器执行以下操作:
1)执行特权检查。
2)将 CS 和 EIP 寄存器恢复到调用之前的值。如果在当前权限级别启用影子堆栈:
— 如果 SSP 未与 8 字节对齐,则导致控制保护异常 (#CP(FAR-RET/IRET))。
— 如果要返回的过程的特权级别小于 3(返回管理程序模式): a .将地址 SSP+8(LIP)和 SSP+16(CS)处影子堆栈上的值与 CS 进行比较, (CS.base + EIP) 从堆栈中弹出,如果它们不匹配,则会导致控制保护异常 (#CP(FARRET/IRET))。b.在内部临时保存栈顶值(正在返回的过程的SSP)。
— 如果返回的过程的权限级别小于 3(返回管理模式),则从内部保存的值恢复 SSP 寄存器。
— 如果要返回的过程的权限级别为 3(返回用户模式)并且在权限级别 3 处启用了影子堆栈,则使用 IA32_PL3_SSP MSR 的值恢复 SSP 寄存器。
3)如果 RET 指令具有可选的 n 参数,则将堆栈指针增加 n 操作数指定的字节数,以从堆栈中释放参数。如果调用门描述符指定将一个或多个参数从一个堆栈复制到另一个堆栈,则必须使用 RET n 指令从两个堆栈释放参数。这里,操作数n是参数在每个堆栈上占用的字节数。返回时,处理器将每个堆栈的 ESP 递增 n,以从堆栈中跳过(有效地删除)这些参数。
4)将 SS 和 ESP 寄存器恢复到调用之前的值,这会导致切换回调用过程的堆栈。
5) 如果 RET 指令具有可选的 n 参数,则将堆栈指针增加 n 操作数指定的字节数,以从堆栈中释放参数(请参阅步骤 3 中的说明)。
6)恢复执行调用函数。
4、中断和异常
处理器提供两种中断程序执行的机制:中断和异常:
—— 中断是一种异步事件,通常由 I/O 设备触发。
——异常是一种同步事件,当处理器在执行一条指令时检测到一个或多个预定义条件时产生。IA-32 体系结构规定了三类异常:故障、陷阱和中止。
中断和异常是表明系统、处理器中或当前正在执行的程序或任务中存在需要处理器注意的条件的事件。它们通常会导致执行从当前运行的程序或任务强制转移到称为中断处理程序或异常处理程序的特殊软件例程或任务。处理器响应中断或异常而采取的操作称为服务或处理中断或异常。中断在程序执行期间随机发生,以响应来自硬件的信号。系统硬件使用中断来处理处理器外部的事件,例如服务外围设备的请求。软件还可以通过执行 INT n 指令来生成中断。当处理器在执行指令时检测到错误情况(例如被零除)时,就会发生异常。处理器检测各种错误情况,包括保护违规、页面错误和内部机器故障。 当接收到中断或检测到异常时,当前运行的过程或任务被挂起,同时处理器执行中断或异常处理程序。当处理程序的执行完成时,处理器恢复执行被中断的过程或任务。被中断的过程或任务的恢复不会丢失程序连续性,除非无法从异常中恢复或者中断导致当前正在运行的程序终止。
处理器响应中断和异常的方式基本相同。当中断或异常发出信号时,处理器会停止执行当前程序或任务,并切换到专门为处理中断或异常情况而编写的处理程序。处理器通过中断描述符表(IDT)中的条目访问处理程序。当处理程序完成对中断或异常的处理后,程序控制返回到被中断的程序或任务。操作系统、执行器和/或设备驱动程序通常独立于应用程序或任务处理中断和异常。不过,应用程序可以通过汇编语言调用访问操作系统或执行程序中的中断和异常处理程序。IA-32 体系结构定义了 18 个预定义中断和异常以及 224 个用户定义中断,它们与 IDT 中的条目相关联。IDT 中的每个中断和异常都用一个数字标识,称为向量(为了帮助处理异常和中断,需要处理器进行特殊处理的每个体系结构定义的异常和每个中断条件都被分配了一个唯一的标识号,称为向量号。处理器使用分配给异常或中断的向量号作为中断描述符表 (IDT) 的索引。该表提供了异常或中断处理程序的入口点。)。 向量 0 至 8、10 至 14 和 16 至 19 为预定义中断和异常(即 0到 31 范围内的向量编号由 Intel 64 和 IA-32 体系结构保留,用于体系结构定义的异常和中断。并非该范围内的所有向量编号都具有当前定义的函数);向量 32 至 255 为软件定义中断,可用于软件中断或可屏蔽硬件中断。请注意,处理器还定义了几个不指向 IDT 中条目的额外中断;其中最著名的是 SMI 中断。
异常被分类为故障、陷阱或中止,具体取决于异常的报告方式以及导致异常的指令是否可以在不丢失程序或任务连续性的情况下重新启动。
1)故障— 故障是一种通常可以纠正的异常,并且一旦纠正,就可以重新启动程序而不会失去连续性。当报告错误时,处理器将机器状态恢复到开始执行错误指令之前的状态。故障处理程序的返回地址(CS 和 EIP 寄存器的保存内容)指向故障指令,而不是指向故障指令之后的指令。
2)陷阱— 陷阱是在执行陷阱指令后立即报告的异常。陷阱允许继续执行程序或任务,而不会丢失程序的连续性。陷阱处理程序的返回地址指向陷阱指令之后要执行的指令。
3) 中止— 中止是一种异常,它并不总是报告导致异常的指令的精确位置,并且不允许重新启动导致异常的程序或任务。中止用于报告严重错误,例如硬件错误以及系统表中的不一致或非法值。
当处理器检测到中断或异常时,它会执行以下操作之一:
——执行中断/异常处理函数(或者处理例程)的隐式调用。
——执行中断处理处任务的隐式调用。
5、调用中断和异常的处理例程
对中断或异常处理程序的调用类似于对另一保护级别的程序调用。在这里,向量引用了 IDT 中的两种门之一:中断门或陷阱门。中断门和陷阱门与调用门类似,都提供以下信息:
1)访问权限信息
2)包含处理程序的代码段选择器
3)到处理程序第一条指令的代码段偏移量
中断门和陷阱门的区别如下:如果通过中断门调用中断或异常处理程序,处理器会清除 EFLAGS 寄存器中的中断使能(IF)标志,以防止后续中断干扰处理程序的执行。通过陷阱门调用处理程序时,IF 标志的状态不会改变。
如果中断/异常处理程序的代码段与当前执行的程序或任务具有相同的权限级别,则中断/异常处理程序使用当前堆栈;如果中断/异常处理程序的执行权限级别更高,则处理器切换到 属于当前任务的、与处理程序权限等级相同的堆栈。如果没有进行堆栈切换,处理器在调用中断或异常处理程序时会执行以下操作: 1) 将 EFLAGS、CS 和 EIP 寄存器的当前内容(按此顺序)推送到堆栈。
a. 在内部临时保存 SSP 寄存器的当前值。
b. 将 CS 寄存器的当前值推入影子堆栈。
c. 将 LIP 的当前值(CS.base + EIP)推入影子堆栈。
d. 将临时保存的 SSP 值推入影子堆栈。
2)向堆栈推送错误代码(如适用)。
3)将新代码段的段选择器和新指令指针(来自中断门或陷阱门)分别加载到 CS 和 EIP 寄存器中。
4)如果调用是通过中断门进行的,则清除 EFLAGS 寄存器中的 IF 标志。
5)开始执行处理程序。
如果发生了堆栈切换,处理器将执行以下操作:
1)临时保存(内部)SS、ESP、EFLAGS、CS 和 EIP 寄存器的当前内容到当前堆栈。
2) 将新堆栈(即当前进程或者任务的特权级别的堆栈)的段选择器和堆栈指针从 TSS 加载到 SS 和 ESP 寄存器中,并切换到新堆栈。
3)将中断过程堆栈中临时保存的 SS、ESP、EFLAGS、CS 和 EIP 值推入新堆栈。如果在中断过程的特权级别启用了影子堆栈,则处理器会在内部临时保存中断过程的SSP。如果被中断的过程处于特权级3,则被中断的过程的SSP也被保存到IA32_PL3_SSP MSR中。
如果在被调用的权限级别启用了影子堆栈,则根据目标权限级别,将从下面列出的 MSR 之一获取被调用权限级别的 SSP。然后通过验证 SSP 指向的地址处的监管者影子堆栈令牌来验证获得的 SSP,以确保它指向当前未活动的有效监管者影子堆栈。
— IA32_PL2_SSP(如果转换到环 2)。
— IA32_PL1_SSP(如果转换到环 1)。
— IA32_PL0_SSP(如果转换到环 0)。
如果在被调用的特权级别启用了影子堆栈,并且被中断的过程不在特权级别3,则处理器将被中断过程临时保存的CS、LIP(CS.base + EIP)和SSP推送到新的影子堆。
4)将错误代码推送到新堆栈上(如果适用)。
5)将新代码段的段选择器和新指令指针(来自中断门或陷阱门)分别加载到 CS 和 EIP 寄存器中。
6)如果调用是通过中断门进行的,则清除 EFLAGS 寄存器中的 IF 标志。
7)在新的权限级别开始执行中断/异常处理例程。
6、中断和异常处理例程的返回
从中断或异常处理程序的返回是通过 IRET 指令启动的。 IRET 指令与远 RET 指令类似,不同之处在于它还为中断的过程恢复 EFLAGS 寄存器的内容。当从与被中断过程相同的特权级别执行中断或异常处理程序的返回时,处理器执行以下操作:
1)将 CS 和 EIP 寄存器恢复为中断或异常之前的值。 如果启用了影子堆栈:
a. 将影子堆栈上地址 SSP+8(LIP)和 SSP+16(CS)的值与从堆栈中弹出的 CS 和(CS.base + EIP)进行比较,如果不匹配,则引发控制保护异常 (#CP(FAR-RET/IRET))。
b. 将堆栈顶部值(中断或异常之前的 SSP)从影子堆栈弹出到 SSP 寄存器中。
2)恢复 EFLAGS 寄存器。
3)适当增加堆栈指针。
——如果在当前权限级别启用了影子堆栈:—如果SSP没有与8字节对齐,则导致控制保护异常(#CP(FAR-RET/IRET))。
——如果返回到的程序的特权级别小于3(返回到管理程序模式):将地址SSP+8(LIP)和SSP+16(CS)处的影子堆栈上的值与从堆栈中弹出的CS和(CS.base + EIP)进行比较,如果不匹配,则导致控制保护异常(#CP(FARRET/IRET))。
—— 在内部临时保存栈顶值(返回到的过程的SSP)。
—— 如果地址SSP+24处存在一个繁忙的监管程序影子堆栈令牌,则将该令牌标记为空闲(细节见英特尔64位和IA-32架构软件开发人员手册第1卷第17.2.3节中描述的操作)。
——如果返回到的程序的权限级别小于3(返回到管理员模式),则从内部保存的值恢复SSP寄存器。
——如果返回到的过程的权限级别为3(返回到用户模式),并且在权限级别3启用了影子堆栈,则使用IA32_PL3_SSP MSR的值恢复SSP寄存器。
4)恢复被中断的程序的执行。 当从与被中断程序不同的特权级别执行中断或异常处理程序的返回时,处理器执行以下操作:
a.执行特权检查。
b. 将 CS 和 EIP 寄存器恢复为中断或异常之前的值。
c.恢复 EFLAGS 寄存器。
4)将 SS 和 ESP 寄存器恢复到中断或异常之前的值,从而导致堆栈切换回中断过程的堆栈。
5) 恢复执行被中断的进程或者任务。
7、调用中断或异常处理程序任务
中断和异常处理例程也可以在单独的任务中执行。这里,中断或异常导致任务切换到处理程序任务。处理程序任务被赋予其自己的地址空间,并且(可选地)可以在比应用程序或任务更高的保护级别下执行。换到处理程序任务是通过引用任务门描述符的隐式任务调用来完成的。任务门为处理程序任务提供对地址空间的访问。作为任务切换的一部分,处理器保存被中断程序或任务的完整状态信息。从处理程序任务返回后,被中断的程序或任务的状态被恢复,执行继续进行。如需有关通过处理程序任务处理中断和异常的更多信息,请参阅英特尔64位和IA-32架构软件开发人员手册第3A卷第6章“中断和异常处理”。