当#GP 从 v8086 模式启动时,处理器是否将错误代码推送到 ring0 堆栈?

When #GP is raised from v8086 mode does the processor push an error code on the ring0 stack?

更广泛的问题是 - 当在 v8086 模式下生成异常并传播到保护模式 interrupt/trap 门时,错误代码是否会在 return 为那些带有错误代码的异常推送地址?

例如我运行处于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 Table 6-2:

中记录异常和错误代码

来自table #DF、#TS、#NP、#SS、#GP、#PF 和#AC 有错误代码。 Intel 文档在实地址模式下错误代码不会压入堆栈,但似乎建议在所有其他遗留模式(16/32 位保护模式和 v8086 模式)和长模式(64 位和 16 /32 位兼容模式)推送错误代码。

INT n/INTO/INT3/INT1—Call to Interrupt Procedure 的指令集参考的第 2A 卷中,它在这些指令的伪代码中说状态 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/INT3/INT1 的指令集参考——调用中断程序 伪代码定义了 INTER-PRIVILEGE-LEVEL 的机制-INTERRUPTINTRA-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。有趣的是,在有错误代码和没有错误代码的情况下,检查是否有足够的堆栈 space。对于 32 位 interrupt/trap 门,大小为 40 有错误代码或 36 没有错误代码。这就是问题的原因1.


脚注

TL;DR:英特尔指令集参考中的伪代码不正确。如果 v8086 模式中的异常导致保护模式 call/interrupt 门执行异常处理程序,那么如果异常是具有错误代码的异常之一,则将推送错误代码。 #GP 有一个错误代码,在将控制权转移到您的#GP 处理程序之前,它将被压入环 0 堆栈。您必须在执行 IRET.

之前手动删除它

答案是虚拟 8086 模式(v8086 或 v86)中由保护模式处理程序(通过中断或陷阱门)处理的异常将为使用一个(包括#GP)。伪代码应该是:

Push(CS);
Push(EIP);
Push(ErrorCode); (* If needed *)

在第 6.4.1 节的Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1中断或异常处理过程的调用和Return操作文档间(特权级别更改)和内部(权限级别保持不变)应用此规则后的转换:

Pushes an error code on the new stack (if appropriate).

恕我直言,措辞可能更好:

Pushes an error code on the new stack (if applicable to the exception).

v8086 模式是特权级别 3 的保护模式 运行 的一种特殊模式。这些规则仍然适用,因为异常会将处理器从 ring 3 转换到 ring 0(内部特权级别更改)以通过以下方式处理中断interrupt/trap 门。


相关实地址模式文档不一致

在最初的 8086 处理器上,唯一的例外是 0 到 4(含)。其中包括#DE、#DB、NMI 中断、#BP 和#OF。其余的被英特尔记录为 reserved1 直到并包括异常 31。8086 上的异常 None 有错误代码,所以这从来都不是问题。这在 286 和更高版本的处理器上发生了变化,其中引入了带有错误代码的异常。

Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 1 第 6.4.3 节中,Intel 对后续处理器 (286+) 上的实地址模式进行了说明

6.4.3 Interrupt and Exception Handling in Real-Address Mode

When operating in real-address mode, the processor responds to an interrupt or exception with an implicit far call to an interrupt or exception handler. The processor uses the interrupt or exception vector as an index into an interrupt table. The interrupt table contains instruction pointers to the interrupt and exception handler procedures.

The processor saves the state of the EFLAGS register, the EIP register, the CS register, and an optional error code on the stack before switching to the handler procedure.

A return from the interrupt or exception handler is carried out with the IRET instruction.

See Chapter 20, “8086 Emulation,” in the Intel® 64 and IA-32 Architectures Software Developer’s Manual, Volume 3B, for more information on handling interrupts and exceptions in real-address mode.

我强调了文档声称 "an optional error code" 被推送的重要部分。 事实并非如此。对于通常会在其他操作模式下推送的异常,在实地址模式下不会推送错误代码。本节确实要看第 3B 卷的第 20 章,“8086 仿真”。在第 20 章中,我们发现第 20.1.4 节 中断和异常处理 说:

The processor performs the following actions to make an implicit call to the selected handler:

  1. Pushes the current values of the CS and EIP registers onto the stack. (Only the 16 least-significant bits of the EIP register are pushed.)
  2. Pushes the low-order 16 bits of the EFLAGS register onto the stack.
  3. Clears the IF flag in the EFLAGS register to disable interrupts.
  4. Clears the TF, RF, and AC flags, in the EFLAGS register. Vol. 3B 20-5 8086 EMULATION
  5. Transfers program control to the location specified in the interrupt vector table. An IRET instruction at the end of the handler procedure reverses these steps to return program control to the interrupted program. Exceptions do not return error codes in real-address mode.

这部分文档是正确的。这 5 个步骤不包括推送错误代码。这与 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 *)

脚注

  • 1尽管 Intel 在原始 8086 上保留了直到中断 32 的未使用异常,IBM 做出了一个糟糕的设计决策,映射其 PIC 的外部中断处理程序(中断控制器)中断 8 到 15(含)并将 BIOS 调用也置于保留 space 中。这在 286+ 处理器的 IBM 系统上引起了问题,其中主 PIC 外部中断与英特尔添加的异常重叠。例如#GP 和 IRQ5 在实地址模式下共享相同的中断号 13 (0x0d)。

    16位和32位保护模式操作系统一般会将主控PIC基地址从中断8移到保留中断之外大于中断31的位置来避免这个问题。

这是另一个答案的延续,因为超过了 post 限制。


示例演示在 v8086 模式下生成#GP 和#UD

以下代码并不意味着作为进入 v8086 模式或编写适当的 v8086 监视器(#GP 处理程序)的入门。有关进入 v8086 模式的信息可以在我的另一个 Whosebug . That answer discusses the mechanisms of getting into v8086 mode. The following code is based on that answer, but includes a 和一个仅处理#UD(异常 6)和#GP(异常 13)的中断描述符 table 中找到。我选择#UD是因为它是没有错误码的异常,我选择#GP是因为它是有错误码的异常

大部分代码都支持在实模式和保护模式下打印到显示器的代码。这个例子背后的想法只是在 v8086 模式下执行指令 UD2 并发出特权 HLT 指令。我正在进入 IOPL 为 0 的 v8086 模式,因此 HLT 导致由保护模式 GPF 处理程序处理的 #GP 异常。 #GP 有错误代码,而#UD 没有。要查明错误代码是否被推送,异常处理程序只需要从堆栈底部的地址中减去当前的 ESP。我使用 32 位门,所以有错误代码的异常堆栈帧应该是 40 字节 (0x28),没有它应该是 36 (0x24)。

  • 带有错误代码 GS、FS、DS、ES、USER_SS、USER_ESP、EFLAGS、CS、EIP、错误代码被推送。每个都是 32 位宽(4 字节)。 10*4=40.
  • 如果没有错误代码 GS、FS、DS、ES,USER_SS、USER_ESP,将推送 EFLAGS、CS、EIP。每个都是 32 位宽(4 字节)。 9*4=36.

v8086 模式下的代码用于测试:

; v8086 code entry point
v86_mode_entry:

    ud2                         ; Cause a #UD exception (no error code pushed)
    mov dword [vidmem_ptr], 0xb8000+80*2
                                ; Advance current video ptr to second line
    hlt                         ; Cause a #GP exception (error code pushed)

    ; End of the test - enter infinite loop sice we didn't provide a way for
    ; the v8086 process to be terminated. We can't do a HLT at ring 3.
.endloop:
    jmp $

有两个通过 32 位中断门到达的保护模式异常处理程序。尽管他们最终做了一件事——打印出(十六进制)异常堆栈帧的大小,因为它在控制到达异常处理程序后立即出现。因为异常处理程序使用pusha来保存所有的通用寄存器,所以从总量中减去32字节(8*4)

; #UD Invalid Opcode v8086 exception handler
exc_invopcode:
    pusha                       ; Save all general purpose registers
    mov eax, DATA32_SEL         ; Setup the segment registers with kernel data selector
    mov ds, eax
    mov es, eax
    cld                         ; DF=0 forward string movement

    test dword [esp+efrm_noerr.user_flags], 1<<EFLAGS_VM_BIT
                                ; Is the VM (v8086) set in the EFLAGS of the code
                                ;     that was interrupted?
    jnz .isvm                   ; If set then proceed with processing the exception

    mov esi, exc_not_vm         ; Otherwise print msg we weren't interrupting v8086 code
    mov ah, ATTR_BWHITE_ON_RED
    call print_string_pm        ; Print message to console
.endloop:
    hlt
    jmp .endloop                ; Infinite HLT loop

.isvm:
    mov esi, exc_msg_ud
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print that we are a #UD exception

    ; The difference between the bottom of the kernel stack and the ESP
    ; value (accounting for the extra 8 pushes by PUSHA) is the original
    ; exception stack frame size. Without an error code this should print 0x24.
    mov eax, EXC_STACK-8*4
    sub eax, esp                ; EAX = size of exception stack frame without
                                ;       registers pushed by PUSHA
    mov edi, tmp_hex_str        ; EDI = address of buffer to store converted integer
    mov esi, edi                ; ESI = copy of address for call to print_string_pm
    call dword_to_hex_pm        ; Convert EAX to HEX string
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print size of frame in HEX

    add word [esp+efrm_noerr.user_eip], 2
                                ; A UD2 instruction is encoded as 2 bytes so update
                                ;     the real mode instruction pointer to point to
                                ;     next instruction so that the test can continue
                                ;     rather than repeatedly throwing #UD exceptions

    popa                        ; Restore all general purpose registers
    iret

; #GP v8086 General Protection Fault handler
exc_gpf:
    pusha                       ; Save all general purpose registers
    mov eax, DATA32_SEL         ; Setup the segment registers with kernel data selector
    mov ds, eax
    mov es, eax
    cld                         ; DF=0 forward string movement

    test dword [esp+efrm_err.user_flags], 1<<EFLAGS_VM_BIT
                                ; Is the VM (v8086) set in the EFLAGS of the code
                                ;     that was interrupted?
    jnz .isvm                   ; If set then proceed with processing the exception

    mov esi, exc_not_vm         ; Otherwise print msg we weren't interrupting v8086 code
    mov ah, ATTR_BWHITE_ON_RED
    call print_string_pm        ; Print message to console
.endloop:
    hlt
    jmp .endloop                ; Infinite HLT loop

.isvm:
    mov esi, exc_msg_gp
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print that we are a #UD exception

    ; The difference between the bottom of the kernel stack and the ESP
    ; value (accounting for the extra 8 pushes by PUSHA) is the original
    ; exception stack frame size. With an error code this should print 0x28.
    mov eax, EXC_STACK-8*4
    sub eax, esp                ; EAX = size of exception stack frame without
                                ;       registers pushed by PUSHA

    mov edi, tmp_hex_str        ; EDI = address of buffer to store converted integer
    mov esi, edi                ; ESI = copy of address for call to print_string_pm
    call dword_to_hex_pm        ; Convert EAX to HEX string
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print size of frame in HEX

    inc word [esp+efrm_err.user_eip]
                                ; A HLT instruction is encoded as 1 bytes so update
                                ;     the real mode instruction pointer to point to
                                ;     next instruction so that the test can continue
                                ;     rather than repeatedly throwing #GP exceptions

    popa                        ; Restore all general purpose registers

    add esp, 4                  ; Remove the error code
    iret

当返回到 v8086 模式时,有一些硬编码技巧可以调整 CS:IP,这样我们就不会陷入无限循环,反复出现相同的异常。 A UD2 instruction is 2 bytes so we add 2 bytes. In the case of HLT 我们在返回前对 v8086 CS:IP 加 1。这些异常处理程序仅在来自 v8086 模式时才做一些有用的事情,否则如果异常发生在 v8086 模式以外的地方,它们会打印错误。不要将此代码视为创建您自己的异常和中断处理程序的方法,它们是专门为此测试编写的,并不通用。

以下代码可以在模拟器中 运行 或使用此 Whosebug 中的引导加载程序测试工具在真实硬件上引导 :

stage2.asm:

VIDEO_TEXT_ADDR        EQU 0xb8000 ; Hard code beginning of text video memory
ATTR_BWHITE_ON_MAGENTA EQU 0x5f    ; Bright White on magenta attribute
ATTR_BWHITE_ON_RED     EQU 0x4f    ; Bright White on red attribute

PM_MODE_STACK          EQU 0x80000 ; Protected mode stack below EBDA
EXC_STACK              EQU 0x70000 ; Kernel Stack for interrupt/exception handling

V86_STACK_SEG          EQU 0x0000  ; v8086 stack SS
V86_STACK_OFS          EQU 0x7c00  ; v8086 stack SP
V86_CS_SEG             EQU 0x0000  ; v8086 code segment CS

EFLAGS_VM_BIT          EQU 17      ; EFLAGS VM bit
EFLAGS_BIT1            EQU 1       ; EFLAGS bit 1 (reserved, always 1)
EFLAGS_IF_BIT          EQU 6       ; EFLAGS IF bit

TSS_IO_BITMAP_SIZE     EQU 0x400/8 ; IO Bitmap for 0x400 IO ports
                                   ; Size 0 disables IO port bitmap (no permission)
ORG_ADDR               EQU 0x7e00  ; Origin point of stage2 (test code)

; Macro to build a GDT descriptor entry
%define MAKE_GDT_DESC(base, limit, access, flags) \
    (((base & 0x00FFFFFF) << 16) | \
    ((base & 0xFF000000) << 32) | \
    (limit & 0x0000FFFF) | \
    ((limit & 0x000F0000) << 32) | \
    ((access & 0xFF) << 40) | \
    ((flags & 0x0F) << 52))

; Macro to build a IDT descriptor entry
%define MAKE_IDT_DESC(offset, selector, access) \
    ((offset & 0x0000FFFF) | \
    ((offset & 0xFFFF0000) << 32) | \
    ((selector & 0x0000FFFF) << 16) | \
    ((access & 0xFF) << 40))

; Macro to convert an address to an absolute offset
%define ABS_ADDR(label) \
    (ORG_ADDR + (label - $$))

; Structure representing exception frame WITH an error code
; including registers pushed by a PUSHA
struc efrm_err
; General purpose registers pushed by PUSHA
.edi:        resd 1
.esi:        resd 1
.ebp:        resd 1
.esp:        resd 1
.ebx:        resd 1
.edx:        resd 1
.ecx:        resd 1
.eax:        resd 1

; Items pushed by the CPU when an exception occurred
.errno:      resd 1
.user_eip:   resd 1
.user_cs:    resd 1
.user_flags: resd 1
.user_esp:   resd 1
.user_ss:    resd 1
.vm_es:      resd 1
.vm_ds:      resd 1
.vm_fs:      resd 1
.vm_gs:      resd 1
EFRAME_ERROR_SIZE equ $-$$
endstruc

; Structure representing exception frame WITHOUT an error code
; including registers pushed by a PUSHA
struc efrm_noerr
; General purpose registers pushed by PUSHA
.edi:        resd 1
.esi:        resd 1
.ebp:        resd 1
.esp:        resd 1
.ebx:        resd 1
.edx:        resd 1
.ecx:        resd 1
.eax:        resd 1
; Items pushed by the CPU when an exception occurred
.user_eip:   resd 1
.user_cs:    resd 1
.user_flags: resd 1
.user_esp:   resd 1
.user_ss:    resd 1
.vm_es:      resd 1
.vm_ds:      resd 1
.vm_fs:      resd 1
.vm_gs:      resd 1
EFRAME_NOERROR_SIZE equ $-$$
endstruc

bits 16
ORG ORG_ADDR

start:
    xor ax, ax                  ; DS=SS=ES=0
    mov ds, ax
    mov ss, ax                  ; Stack at 0x0000:0x7c00
    mov sp, 0x7c00
    cld                         ; Set string instructions to use forward movement

    ; No enabling A20 as we don't require it
    lgdt [gdtr]                 ; Load our GDT
    lidt [idtr]                 ; Install interrupt table

    mov eax, cr0
    or eax, 1
    mov cr0, eax                ; Set protected mode flag
    jmp CODE32_SEL:start32      ; FAR JMP to set CS

; v8086 code entry point
v86_mode_entry:

    ud2                         ; Cause a #UD exception (no error code pushed)
    mov dword [vidmem_ptr], 0xb8000+80*2
                                ; Advance current video ptr to second line
    hlt                         ; Cause a #GP exception (error code pushed)

    ; End of the test - enter infinite loop sice we didn't provide a way for
    ; the v8086 process to be terminated. We can't do a HLT at ring 3.
.endloop:
    jmp $

; 32-bit protected mode entry point
bits 32
start32:
    mov ax, DATA32_SEL          ; Setup the segment registers with data selector
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov esp, PM_MODE_STACK      ; Set protected mode stack pointer

    mov fs, ax                  ; Not currently using FS and GS
    mov gs, ax

    mov ecx, BSS_SIZE_D         ; Zero out BSS section a DWORD at a time
    mov edi, bss_start
    xor eax, eax
    rep stosd

    ; Set iomap_base in tss with the offset of the iomap relative to beginning of the tss
    mov word [tss_entry.iomap_base], tss_entry.iomap-tss_entry

    mov dword [tss_entry.esp0], EXC_STACK
    mov dword [tss_entry.ss0], DATA32_SEL

    mov eax, TSS32_SEL
    ltr ax                      ; Load default TSS (used for exceptions, interrupts, etc)

    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

; Function: print_string_pm
;           Display a string to the console on display page 0 in protected mode.
;           Very basic. Doesn't update hardware cursor, doesn't handle scrolling,
;           LF, CR, TAB.
;
; Inputs:   ESI = Offset of address to print
;           AH  = Attribute of string to print
; Clobbers: None
; Returns:  None

print_string_pm:
    push edi
    push esi
    push eax

    mov edi, [vidmem_ptr]       ; Start from video address stored at vidmem_ptr
    jmp .getchar
.outchar:
    stosw                       ; Output character to video display
.getchar:
    lodsb                       ; Load next character from string
    test al, al                 ; Is character NUL?
    jne .outchar                ;     If not, go back and output character

    mov [vidmem_ptr], edi       ; Update global video pointer
    pop eax
    pop esi
    pop edi
    ret

; Function: dword_to_hex_pm
;           Convert a 32-bit value to its equivalent HEXadecimal string
;
; Inputs:   EDI = Offset of buffer for converted string (at least 8 bytes)
;           EAX = 32-bit value to convert to HEX
; Clobbers: None
; Returns:  None

dword_to_hex_pm:
    push edx                    ; Save all registers we use
    push ecx
    push edi

    mov ecx, 8                  ; Process 8 nibbles (4 bits each)
.nibble_loop:
    rol eax, 4                  ; Rotate the high nibble to the low nibble of EAX

    mov edx, eax                ; Save copy of rotated value to continue conversion
    and edx, 0x0f               ; Mask off eveything but the lower nibble

    movzx edx, byte [.hex_lookup_tbl+edx]
    mov [edi], dl               ; Convert nibble to HEX character using lookup table
    inc edi                     ; Continue with the next nibble

    dec ecx
    jnz .nibble_loop            ; Continue with next nibble if we haven't processed all

    pop edi                     ; Retsore all the registers we clobbered
    pop ecx
    pop edx
    ret
.hex_lookup_tbl:  db  "0123456789abcdef"

; #UD Invalid Opcode v8086 exception handler
exc_invopcode:
    pusha                       ; Save all general purpose registers
    mov eax, DATA32_SEL         ; Setup the segment registers with kernel data selector
    mov ds, eax
    mov es, eax
    cld                         ; DF=0 forward string movement

    test dword [esp+efrm_noerr.user_flags], 1<<EFLAGS_VM_BIT
                                ; Is the VM (v8086) set in the EFLAGS of the code
                                ;     that was interrupted?
    jnz .isvm                   ; If set then proceed with processing the exception

    mov esi, exc_not_vm         ; Otherwise print msg we weren't interrupting v8086 code
    mov ah, ATTR_BWHITE_ON_RED
    call print_string_pm        ; Print message to console
.endloop:
    hlt
    jmp .endloop                ; Infinite HLT loop

.isvm:
    mov esi, exc_msg_ud
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print that we are a #UD exception

    ; The difference between the bottom of the kernel stack and the ESP
    ; value (accounting for the extra 8 pushes by PUSHA) is the original
    ; exception stack frame size. Without an error code this should print 0x24.
    mov eax, EXC_STACK-8*4
    sub eax, esp                ; EAX = size of exception stack frame without
                                ;       registers pushed by PUSHA
    mov edi, tmp_hex_str        ; EDI = address of buffer to store converted integer
    mov esi, edi                ; ESI = copy of address for call to print_string_pm
    call dword_to_hex_pm        ; Convert EAX to HEX string
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print size of frame in HEX

    add word [esp+efrm_noerr.user_eip], 2
                                ; A UD2 instruction is encoded as 2 bytes so update
                                ;     the real mode instruction pointer to point to
                                ;     next instruction so that the test can continue
                                ;     rather than repeatedly throwing #UD exceptions

    popa                        ; Restore all general purpose registers
    iret

; #GP v8086 General Protection Fault handler
exc_gpf:
    pusha                       ; Save all general purpose registers
    mov eax, DATA32_SEL         ; Setup the segment registers with kernel data selector
    mov ds, eax
    mov es, eax
    cld                         ; DF=0 forward string movement

    test dword [esp+efrm_err.user_flags], 1<<EFLAGS_VM_BIT
                                ; Is the VM (v8086) set in the EFLAGS of the code
                                ;     that was interrupted?
    jnz .isvm                   ; If set then proceed with processing the exception

    mov esi, exc_not_vm         ; Otherwise print msg we weren't interrupting v8086 code
    mov ah, ATTR_BWHITE_ON_RED
    call print_string_pm        ; Print message to console
.endloop:
    hlt
    jmp .endloop                ; Infinite HLT loop

.isvm:
    mov esi, exc_msg_gp
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print that we are a #UD exception

    ; The difference between the bottom of the kernel stack and the ESP
    ; value (accounting for the extra 8 pushes by PUSHA) is the original
    ; exception stack frame size. With an error code this should print 0x28.
    mov eax, EXC_STACK-8*4
    sub eax, esp                ; EAX = size of exception stack frame without
                                ;       registers pushed by PUSHA

    mov edi, tmp_hex_str        ; EDI = address of buffer to store converted integer
    mov esi, edi                ; ESI = copy of address for call to print_string_pm
    call dword_to_hex_pm        ; Convert EAX to HEX string
    mov ah, ATTR_BWHITE_ON_MAGENTA
    call print_string_pm        ; Print size of frame in HEX

    inc word [esp+efrm_err.user_eip]
                                ; A HLT instruction is encoded as 1 bytes so update
                                ;     the real mode instruction pointer to point to
                                ;     next instruction so that the test can continue
                                ;     rather than repeatedly throwing #GP exceptions

    popa                        ; Restore all general purpose registers

    add esp, 4                  ; Remove the error code
    iret

; Data section
align 4
vidmem_ptr: dd VIDEO_TEXT_ADDR  ; Start console output in upper left of display
tmp_hex_str: TIMES 9 db 0       ; String to store 32-bit value converted HEX + NUL byte

exc_msg_ud:
    db "#UD frame size: 0x", 0
exc_msg_gp:
    db "#GP frame size: 0x", 0
exc_not_vm:
    db "Not a v8086 exception", 0

align 4
gdt_start:
    dq MAKE_GDT_DESC(0, 0, 0, 0)   ; null descriptor
gdt32_code:
    dq MAKE_GDT_DESC(0, 0x000fffff, 10011010b, 1100b)
                                ; 32-bit code, 4kb gran, limit 0xffffffff bytes, base=0
gdt32_data:
    dq MAKE_GDT_DESC(0, 0x000fffff, 10010010b, 1100b)
                                ; 32-bit data, 4kb gran, limit 0xffffffff bytes, base=0
gdt32_tss:
    dq MAKE_GDT_DESC(tss_entry, TSS_SIZE-1, 10001001b, 0000b)
                                ; 32-bit TSS, 1b gran, available, IOPL=0
end_of_gdt:

CODE32_SEL equ gdt32_code - gdt_start
DATA32_SEL equ gdt32_data - gdt_start
TSS32_SEL  equ gdt32_tss  - gdt_start

gdtr:
    dw end_of_gdt - gdt_start - 1
                                ; limit (Size of GDT - 1)
    dd gdt_start                ; base of GDT

align 4
; Create an IDT which handles #UD and #GPF. All other exceptions set to 0
; so that they triple fault. No external interrupts supported.
idt_start:
    TIMES 6 dq 0
    dq MAKE_IDT_DESC(ABS_ADDR(exc_invopcode), CODE32_SEL, 10001110b) ; 6
    TIMES 6 dq 0
    dq MAKE_IDT_DESC(ABS_ADDR(exc_gpf), CODE32_SEL, 10001110b) ; D
    TIMES 18 dq 0
end_of_idt:

align 4
idtr:
    dw end_of_idt - idt_start - 1
                                ; limit (Size of IDT - 1)
    dd idt_start                ; base of IDT

; Data section above bootloader acts like a BSS section
align 4
ABSOLUTE ABS_ADDR($)            ; Convert location counter to absolute address
bss_start:

; Task State Structure (TSS)
tss_entry:
.back_link: resd 1
.esp0:      resd 1              ; Kernel stack pointer used on ring transitions
.ss0:       resd 1              ; Kernel stack segment used on ring transitions
.esp1:      resd 1
.ss1:       resd 1
.esp2:      resd 1
.ss2:       resd 1
.cr3:       resd 1
.eip:       resd 1
.eflags:    resd 1
.eax:       resd 1
.ecx:       resd 1
.edx:       resd 1
.ebx:       resd 1
.esp:       resd 1
.ebp:       resd 1
.esi:       resd 1
.edi:       resd 1
.es:        resd 1
.cs:        resd 1
.ss:        resd 1
.ds:        resd 1
.fs:        resd 1
.gs:        resd 1
.ldt:       resd 1
.trap:      resw 1
.iomap_base:resw 1              ; IOPB offset
.iomap: resb TSS_IO_BITMAP_SIZE ; IO bitmap (IOPB) size 8192 (8*8192=65536) representing
                                ; all ports. An IO bitmap size of 0 would fault all IO
                                ; port access if IOPL < CPL (CPL=3 with v8086)
%if TSS_IO_BITMAP_SIZE > 0
.iomap_pad: resb 1              ; Padding byte that has to be filled with 0xff
                                ; To deal with issues on some CPUs when using an IOPB
%endif
TSS_SIZE EQU $-tss_entry

bss_end:
BSS_SIZE_B EQU bss_end-bss_start; BSS size in bytes
BSS_SIZE_D EQU (BSS_SIZE_B+3)/4 ; BSS size in dwords

获取 bpb.inc 文件和 boot.asm。 Assemble 到磁盘映像:

nasm -f bin stage2.asm -o stage2.bin
nasm -f bin boot.asm -o disk.img

stage2.bin 必须首先被汇编,因为它被 boot.asm 嵌入为二进制文件。结果应该是一个名为 disk.img 的 1.44MiB 软盘映像。如果 运行 在 QEMU 中有:

qemu-system-i386 -fda disk.img

结果应类似于:

  • UD 帧大小应为 0x00000024(36 = 没有错误代码的异常帧大小)
  • GP 帧大小应为 0x00000028(40 = 带有错误代码的异常帧的大小)