assembly 从v8086模式引发#GP时,处理器是否会将错误代码推送到ring0堆栈上?

vuktfyat  于 2023-04-06  发布在  其他
关注(0)|答案(2)|浏览(112)

更广泛地说,问题实际上是--当在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-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。有趣的是,在有错误代码和没有错误代码的情况下,检查是否有足够的堆栈空间。对于32位中断/陷阱门,大小要么是40,要么是36,没有错误代码。这就是问题1的原因。

脚注

  • 1多年来,我从未密切关注过较新的Intel文档,也不知道文档中关于v8086模式的内容。我的v8086监视器和保护模式中断处理程序总是考虑到有错误代码的异常和没有错误代码的异常。我没有我没有注意到文档中的问题,直到上周有人来找我讨论,顺便提到了这个问题(但没有解释)。
but5z9lq

but5z9lq1#

TL;DR:英特尔指令集参考中的伪代码不正确。如果v8086模式下的异常导致保护模式调用/中断门执行异常处理程序,则如果异常是具有错误代码的异常之一,则将推送错误代码。#GP具有错误代码,并且在将控制转移到#GP处理程序之前,将其推送到环0堆栈上。您必须在执行IRET之前手动删除它。

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

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

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

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尽管英特尔在原始8086上保留了中断32之前未使用的异常,IBM在MapPIC的外部中断处理程序时做出了一个糟糕的设计决策(中断控制器)中断8到15(包括)并将BIOS调用也放在保留的空间中。主PIC外部中断与Intel添加的异常重叠的处理器。例如,#GP和IRQ 5在实地址模式下共享相同的中断编号13(0x 0 d)。

16-位和32位保护模式OS通常将主PIC基地址从中断8移动到保留中断之外的大于中断31的位置,以避免这个问题。

qoefvg9y

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)。

  • 错误码为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,这样我们就不会在同一个异常上重复地陷入无限循环。一个UD2指令是2个字节,所以我们添加2个字节。在HLT的情况下,我们在返回之前向v8086 CS:IP添加1。这些异常处理程序只在来自v8086模式时才有用,否则如果异常发生在v8086模式以外的地方,它们会打印一个错误。不要认为这段代码是一种创建自己的异常和中断处理程序的方法,它们是专门为这个测试编写的,不是通用的。
以下代码可以在模拟器中运行,也可以使用Stackoverflow answer中的bootloader测试工具在真实硬件上引导:

    • 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 9       ; 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

test harness获取bpb.inc文件和boot.asm。使用以下命令组装到磁盘映像:

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

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

qemu-system-i386 -fda disk.img

结果应类似于:

  • UD帧大小应为0x00000024(36 =无错误代码的异常帧大小)
  • GP帧大小应为0x00000028(40 =包含错误代码的异常帧的大小)

相关问题