代码之家  ›  专栏  ›  技术社区  ›  Michael Petch

当GP从v8086模式提升时,处理器是否在ring0堆栈上推送错误代码?

  •  0
  • Michael Petch  · 技术社区  · 5 年前

    更广泛地说,真正的问题是-当在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
    

    当protected mode#GP异常处理程序开始运行时,我想知道在 CS:EIP系统 .

    可以说 RTFM 但英特尔的文档是造成混乱的根源。


    问题的原因

    英特尔在 Intel® 64 and IA-32 Architectures Software Developer’s Manual Vol 3A 表6-2 :

    enter image description here

    从表#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/INT3/INT1–调用中断过程 伪代码定义了 特权级中断 特权级中断 国家。虽然门大小(16/32/64位)决定了数据的宽度(包括错误代码的宽度),但错误代码是被推送的(如果适用),并具体记录在:

    Push(ErrorCode); (* If needed, #-bytes *)
    

    在哪里? # 是2(16位门)、4(32位门)或8(64位门)。

    例外情况 :错误代码未被记录为被推送的地方是 中断自虚拟8086模式 . 相关伪代码的片段:

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

    是什么 明显缺席 有没有提到 error code 之后 Push(EIP); 在以保护模式开始执行之前。感兴趣的是,在错误代码和没有错误代码的情况下,检查是否有足够的堆栈空间。对于32位中断/陷阱门,大小为40(带错误代码)或36(不带错误代码)。这就是问题的原因 1个 .


    脚注

    • 1个 这些年来,我从未密切关注过最新的英特尔文档,也不知道文档中关于v8086模式的内容。我的v8086监视器和保护模式中断处理程序的编写总是考虑到有错误代码的异常和没有错误代码的异常。我没有注意到文档中的问题,直到上个星期,有人找我讨论了一个顺便提到(但没有解释)的问题。
    1 回复  |  直到 5 年前
        1
  •  2
  •   Michael Petch    5 年前

    TL;博士 :Intel instruction set reference中的伪代码是 不正确的 . 如果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节中 中断或异常处理过程的调用和返回操作 文档内部(权限级别更改)和内部(权限级别保持不变)转换与应用此规则的转换:

    在新堆栈上推送错误代码(如果合适)。

    我想,它可能会被更好地表述为:

    在新堆栈上推送错误代码(如果适用于异常)。

    v8086模式是在3级权限下运行的一种特殊的保护模式。这些规则仍然适用,因为异常将处理器从环3转换到环0(特权级别变化),以通过中断/陷阱门处理中断。


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

    在最初的8086处理器上,唯一的异常是0到4(包括0到4)。包括DE,DB,NMI中断,BP和OF。其余的都被记录为保留 1个 由英特尔提供,包括例外情况31。8086上的所有异常都没有错误代码,因此这从来不是问题。在286和更高版本的处理器上,这种情况发生了变化,其中引入了错误代码异常。

    英特尔64与IA-32体系结构软件开发人员手册第一卷 在第6.4.3节中,英特尔说这是关于以后处理器(286+)上的实数地址模式

    6.4.3实际地址模式下的中断和异常处理

    在实地址模式下运行时,处理器对中断做出响应 或带有对中断或异常的隐式远调用的异常 处理程序。处理器使用中断或异常向量作为 索引到中断表中。中断表包含 指向中断和异常处理程序的指令指针 程序。

    这个 处理器保存状态 EFLAGS公司 弹性公网IP 注册,注册 反恐精英 注册, 以及可选的错误代码 在堆栈上 在切换到处理程序之前。

    从中断中返回 或者使用IRET指令执行异常处理程序。

    参见第20章“8086仿真”,英特尔64和IA-32体系结构 软件开发人员手册,第3B卷,了解有关 在实地址模式下处理中断和异常。

    我已经强调了文档声称 “可选错误代码” 被推了。 事实上这不是真的 . 对于通常在其他操作模式下推送错误代码的异常,在实际地址模式下不会推送错误代码。这一节确实说要看第20章, “8086仿真” 在第20章中,我们发现第20.1.4节 中断和异常处理 这么说:

    处理器执行以下操作来进行隐式调用 到所选处理程序:

    1. 将CS和EIP寄存器的当前值推送到堆栈上。(只有EIP寄存器的16个最低有效位 推动)
    2. 将EFLAGS寄存器的低阶16位推入堆栈。
    3. 清除EFLAGS寄存器中的IF标志以禁用中断。
    4. 清除EFLAGS寄存器中的TF、RF和AC标志。第3B卷20-5 8086仿真
    5. 将程序控制传输到中断向量表中指定的位置。处理程序末尾的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做出了一个糟糕的设计决策,将其PIC(中断控制器)的外部中断处理程序映射到中断8到15(含),并将BIOS调用也放在保留空间中。这在IBM系统上造成了问题,该系统有286+处理器,其中主pic的外部中断与Intel添加的异常重叠。例如,GP和IRQ5在实地址模式下共享相同的中断号13(0x0d)。

      16位和32位保护模式操作系统通常将主PICs基址从中断8移动到保留中断之外大于中断31的位置,以避免此问题。

        2
  •  1
  •   Michael Petch    5 年前

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


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

    以下代码不是进入v8086模式或编写正确的v8086监视器(#GP handler)的入门代码。进入v8086模式的信息可以在我的另一个Stackoverflow中找到 answers . 这个答案讨论了进入v8086模式的机制。下面的代码基于这个答案,但是包括 TSS ,以及只处理#UD(异常6)和#GP(异常13)的中断描述符表。我选择UD是因为它是一个没有错误代码的异常,我选择GP是因为它是一个有错误代码的异常。

    大部分代码都是支持以真实和受保护模式打印到显示器上的代码。这个例子背后的思想只是执行指令 UD2 在v8086模式下并发出特权 HLT 指示。我正在以0的IOPL进入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,这样我们就不会在同一个异常上重复出现无限循环错误。一个 UD2 指令是2个字节,所以我们加上2个字节。在这种情况下 HLT 在返回之前,我们将1添加到v8086 CS:IP。这些异常处理程序仅在来自v8086模式时才执行有用的操作,否则,如果异常是从v8086模式以外的其他地方发生的,则它们会打印错误。不要将此代码视为创建自己的异常和中断处理程序的方法,它们是专门为此测试编写的,不是通用的。

    以下代码可以在模拟器中运行,也可以使用此Stackoverflow中的bootloader测试工具在实际硬件上引导 answer :

    阶段2.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 test harness . 组装到磁盘映像:

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

    stage2.bin 必须先组装,因为它是以二进制形式嵌入的 启动.asm . 结果应该是一个名为 disk.img . 如果在QEMU中运行:

    qemu-system-i386 -fda disk.img
    

    结果应类似于:

    enter image description here

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