更广泛地说,问题实际上是--当在v8086模式下生成一个异常并将其传播到保护模式中断/陷阱门时,在为这些带有错误代码的异常推送返回地址之后,是否会将错误代码推送到堆栈上?
例如,我在V8086模式下运行(CPL=3,VM=1,PE=1),IOPL为0。我希望特权指令HLT
应该引发#GP异常。NASM代码可能看起来像这样:
bits 32
xor ebx, ebx ; EBX=0
push ebx ; Real mode GS=0
push ebx ; Real mode FS=0
push ebx ; Real mode DS=0
push ebx ; Real mode ES=0
push V86_STACK_SEG
push V86_STACK_OFS ; v8086 stack SS:SP (grows down from SS:SP)
push dword 1<<EFLAGS_VM_BIT | 1<<EFLAGS_BIT1
; Set VM Bit, IF bit is off, DF=0(forward direction),
; IOPL=0, Reserved bit (bit 1) always 1. Everything
; else 0. These flags will be loaded in the v8086 mode
; during the IRET. We don't want interrupts enabled
; because we don't have a proper v86 monitor
; GPF handler to process them.
push V86_CS_SEG ; Real Mode CS (segment)
push v86_mode_entry ; Entry point (offset)
iret ; Transfer control to v8086 mode and our real mode code
bits 16
v86_mode_entry:
hlt ; This should raise a #GP exception
当保护模式#GP异常处理程序开始运行时,我想知道是否在 CS:EIP 之后将错误代码推送到堆栈上。
有人可能会说RTFM,但英特尔文档是混淆的来源。
提问原因
英特尔将异常和错误代码记录在Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 3A * 表6-2* 中:
从表中可以看出,#DF、#TS、#NP、#SS、#GP、#PF和#AC都有错误代码。英特尔的文档中指出,在实地址模式下,错误代码不会被压入堆栈,但似乎建议在所有其他传统模式(16/32位保护模式和v8086模式)和长模式(64位和16/32位兼容模式)下,错误代码会被压入堆栈。
在第2A卷中,在INT n/INTO/INT3/INT1—Call to Interrupt Procedure
的指令集参考中,它在这些指令的伪代码中说,状态REAL_ADDRESS_MODE
推送了以下项:
Push(CS);
Push(IP);
(* No error codes are pushed in real-address mode*)
CS ← IDT(Descriptor (vector_number « 2), selector));
EIP ← IDT(Descriptor (vector_number « 2), offset)); (* 16 bit offset AND 0000FFFFH *)
英特尔已经走出了他们的方式,使它在实地址模式相当清楚-错误代码不适用。
- INT n/INTO/INT 3/INT 1-Call to Interrupt Procedure* 的指令集参考,伪代码定义了 INTER-PRIVILEGE-LEVEL-INTERRUPT 或 INTRA-PRIVILEGE-LEVEL-INTERRUPT 状态的机制。尽管门的大小(16/32/64位)决定了数据的宽度(包括错误代码的宽度),但错误代码会被推送(如果适用),并特别记录在:
Push(ErrorCode); (* If needed, #-bytes *)
其中#
为2(16位门)、4(32位门)或8(64位门)。
异常:错误代码没有被记录为推送的一个地方是状态 INTERRUPT-FROM-VIRTUAL-8086-MODE。相关伪代码的片段:
IF IDT gate is 32-bit
THEN
IF new stack does not have room for 40 bytes (error code pushed)
or 36 bytes (no error code pushed)
THEN #SS(error_code(NewSS,0,EXT)); FI;
(* idt operand to error_code is 0 because selector is used *)
ELSE (* IDT gate is 16-bit)
IF new stack does not have room for 20 bytes (error code pushed)
or 18 bytes (no error code pushed)
THEN #SS(error_code(NewSS,0,EXT)); FI;
(* idt operand to error_code is 0 because selector is used *)
FI;
IF instruction pointer from IDT gate is not within new code-segment limits
THEN #GP(EXT); FI; (* Error code contains NULL selector *)
tempEFLAGS ← EFLAGS;
VM ← 0;
TF ← 0;
RF ← 0;
NT ← 0;
IF service through interrupt gate
THEN IF = 0; FI;
TempSS ← SS;
TempESP ← ESP;
SS ← NewSS;
ESP ← NewESP;
(* Following pushes are 16 bits for 16-bit IDT gates and 32 bits for 32-bit IDT gates;
Segment selector pushes in 32-bit mode are padded to two words *)
Push(GS);
Push(FS);
Push(DS);
Push(ES);
Push(TempSS);
Push(TempESP);
Push(TempEFlags);
Push(CS);
Push(EIP);
GS ← 0; (* Segment registers made NULL, invalid for use in protected mode *)
FS ← 0;
DS ← 0;
ES ← 0;
CS ← Gate(CS); (* Segment descriptor information also loaded *)
CS(RPL) ← 0;
CPL ← 0;
IF IDT gate is 32-bit
THEN
EIP ← Gate(instruction pointer);
ELSE (* IDT gate is 16-bit *)
EIP ← Gate(instruction pointer) AND 0000FFFFH;
FI;
(* Start execution of new routine in Protected Mode *)
在Push(EIP);
之后,在保护模式下开始执行之前,明显没有提到error code
。有趣的是,在有错误代码和没有错误代码的情况下,检查是否有足够的堆栈空间。对于32位中断/陷阱门,大小要么是40,要么是36,没有错误代码。这就是问题1的原因。
脚注
- 1多年来,我从未密切关注过较新的Intel文档,也不知道文档中关于v8086模式的内容。我的v8086监视器和保护模式中断处理程序总是考虑到有错误代码的异常和没有错误代码的异常。我没有我没有注意到文档中的问题,直到上周有人来找我讨论,顺便提到了这个问题(但没有解释)。
2条答案
按热度按时间but5z9lq1#
TL;DR:英特尔指令集参考中的伪代码不正确。如果v8086模式下的异常导致保护模式调用/中断门执行异常处理程序,则如果异常是具有错误代码的异常之一,则将推送错误代码。#GP具有错误代码,并且在将控制转移到#GP处理程序之前,将其推送到环0堆栈上。您必须在执行
IRET
之前手动删除它。答案是虚拟8086模式(v8086或v86)中的异常由受保护模式处理程序(通过中断或陷阱门)处理,将为使用一个(包括#GP)的异常推送错误代码。伪代码应该是:
在Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1中,第6.4.1节 * 中断或异常处理过程的调用和返回操作 * 将inter(特权级别更改)和intra(特权级别保持不变)转换记录为应用此规则:
将错误代码推送到新堆栈上(如果适用)。
IMHO可能会更好地措辞为:
将错误代码推送到新堆栈上(如果适用于异常)。
v8086模式是在特权级3运行的保护模式的一种特殊模式。这些规则仍然适用,因为异常会将处理器从环3转换到环0(特权级间更改),以通过中断/陷阱门处理中断。
相关实地址模式文档不一致
在最初的8086处理器上,唯一的例外是到4包括#DE、#DB、NMI中断、#BP、和#OF。其余的都被英特尔记录为reserved 1,直到并包括异常31。8086上的异常都没有错误代码,所以这从来不是问题。这在286和更高版本的处理器上发生了变化,其中引入了带有错误代码的异常。
在Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1第6.4.3节中,英特尔提到了更高版本处理器(286+)上的实地址模式
6.4.3实地址模式下的中断和异常处理
在实地址模式下工作时,处理器对中断或异常的响应是隐式地调用中断或异常处理程序。处理器使用中断或异常向量作为中断表的索引。中断表包含指向中断和异常处理程序的指令指针。
处理器在切换到处理程序过程之前,将EFLAGS寄存器、EIP寄存器、CS寄存器的状态和可选错误码**保存在堆栈上。
中断或异常处理程序的返回是使用IRET指令执行的。
有关在实地址模式下处理中断和异常的详细信息,请参阅英特尔® 64和IA-32体系结构软件开发人员手册第3B卷第20章“8086仿真”。
我已经强调了文档中声称***“可选错误代码”*被推送的重要部分。这实际上不是真的。对于在其他操作模式下通常会推送错误代码的异常,在实地址模式下不会推送错误代码。本节确实说要参见第20章, “8086仿真” 卷3B。在第20章中,我们发现第20.1.4节中断和异常处理说:
处理器执行以下操作以隐式调用选定的处理程序:
1.将CS和EIP寄存器的当前值压入堆栈。(仅压入EIP寄存器的16个最低有效位。)
1.将EFLAGS寄存器的低16位压入堆栈。
1.清除EFLAGS寄存器中的IF标志以禁用中断。
1.清除EFLAGS寄存器中的TF、RF和AC标志。Vol. 3B 20-5 8086 EMULATION
1.将程序控制转移到中断向量表中指定的位置。处理程序过程结束时的IRET指令将这些步骤反转,将程序控制返回给被中断的程序。在实地址模式下,异常不返回错误代码。
文档的这一部分是正确的。5个步骤不包括推送错误代码。这与
INT n/INTO/INT3/INT1—Call to Interrupt Procedure
的指令集参考中的伪代码一致,其中记录了状态REAL_ADDRESS_MODE
:脚注
16-位和32位保护模式OS通常将主PIC基地址从中断8移动到保留中断之外的大于中断31的位置,以避免这个问题。
qoefvg9y2#
这是另一个答案的延续,因为超过了帖子限制。
v8086模式下生成#GP和#UD示例
下面的代码并不是进入v8086模式或编写正确的v8086监视器的入门代码。(#GP handler)。关于进入v8086模式的信息可以在我的另一个Stackoverflow answers中找到。那个答案讨论了进入v8086模式的机制。下面的代码是基于那个答案的,但是包含了一个TSS,和一个只处理#UD(异常6)和#GP(异常13)的中断描述符表。我选择#UD是因为它是一个没有错误代码的异常,我选择#GP是因为它是一个有错误代码的异常。
大多数代码都是支持在真实和保护模式下打印到显示器的代码。这个例子背后的想法只是在v8086模式下执行指令
UD2
并发出特权HLT
指令。我进入的是IOPL为0的v8086模式,因此HLT
导致#GP异常,由保护模式GPF处理程序处理。#GP有一个错误代码,#UD没有。为了确定是否推送了错误代码,异常处理程序只需要从堆栈底部的地址中减去当前ESP。我使用32位门,所以有错误代码的异常堆栈帧应该是40字节(0x28),没有错误代码的异常堆栈帧应该是36(0x24)。v8086模式下的代码在测试中执行以下操作:
有两个保护模式异常处理程序通过一个32位中断门到达。虽然很长,但它们最终会做一件事-在控制到达异常处理程序后立即打印出异常堆栈帧的大小(十六进制)。因为异常处理程序使用
pusha
来保存所有通用寄存器,所以从总量中减去32字节(8 * 4)。当返回到v8086模式时,有一些硬编码的技巧来调整CS:IP,这样我们就不会在同一个异常上重复地陷入无限循环。一个
UD2
指令是2个字节,所以我们添加2个字节。在HLT
的情况下,我们在返回之前向v8086 CS:IP添加1。这些异常处理程序只在来自v8086模式时才有用,否则如果异常发生在v8086模式以外的地方,它们会打印一个错误。不要认为这段代码是一种创建自己的异常和中断处理程序的方法,它们是专门为这个测试编写的,不是通用的。以下代码可以在模拟器中运行,也可以使用Stackoverflow answer中的bootloader测试工具在真实硬件上引导:
从test harness获取
bpb.inc
文件和boot.asm
。使用以下命令组装到磁盘映像:stage2.bin
必须首先组装,因为它是由boot.asm
嵌入的二进制文件。结果应该是一个1.44MiB的软盘映像,称为disk.img
。如果在QEMU中运行:结果应类似于: