【问题标题】:Problem switching to v8086 mode from 32-bit protected mode by setting EFLAGS.VM to 1通过将 EFLAGS.VM 设置为 1 从 32 位保护模式切换到 v8086 模式时出现问题
【发布时间】:2019-07-17 15:18:18
【问题描述】:

我处于 32 位保护模式,以当前特权级别 (CPL=0) 运行。我试图通过将 EFLAGS.VM(位 17)标志设置为 1(并将 IOPL 设置为 0)并对我的 16 位实模式代码执行 FAR JMP 来进入 v8086 模式。我使用PUSHF 获取当前标志;将 EFLAGS.VM(第 17 位)设置为 1;将 EFLAGS.IOPL(位 22 和位 23)设置为 0;使用POPF 设置新的EFLAGS。代码如下:

    bits 32
    cli
    [snip]
    pushf                       ; Get current EFLAGS
    pop eax
    or eax, 1<<EFLAGS_VM_BIT    ; Set VM flag to enter v8086 mode
    and eax, ~(3<<EFLAGS_IOPL_BITS)
                                ; Set IOPL to 0
                                ; IF flag already 0 because of earlier CLI
    push eax
    popf                        ; Reload new flags
    jmp CODE32_SEL:v86_mode_entry
                                ; Far JMP to v8086 entry point

    ; v8086 code entry point
    bits 16
    v86_mode_entry:
        hlt                         ; Halt should double fault
    [snip]

对于这些测试,我特意运行:

  • 始终在 CPL=0 时中断。
  • 在 v8086 模式下运行时中断关闭。
  • 我没有 IDT。
  • 我没有 TSS,因为我没有通过中断、门和异常在特权级别之间转换。

为了测试我是否进入了 v8086 模式,我执行了 HLT 指令。由于我没有适当的中断机制,我预计会发生双重故障。 hlt 似乎执行正确,系统就在那里。在 BOCH 中,当我到达 hlt 时,我注意到标志是:

eflags 0x00000046: id vip vif ac vm rf nt IOPL=0 of df if tf sf ZF af PF cf

EFLAGS.VM 标志被标记为关闭 (0),因为它被列为 vm 而不是 VM。这不是我所期望的。

问题:

  • 我的代码有什么问题,如何纠正它以便进入 v8086 模式并出现hlt 双重错误?
  • 在 64 位模式或 32 位兼容模式(长模式的子模式)下是否可以进入 v8086 模式?

此代码的最小完整可验证示例是进入保护模式并执行上述任务的引导加载程序:

VIDEO_TEXT_ADDR        EQU 0xb8000 ; Hard code beginning of text video memory
ATTR_BWHITE_ON_GREEN   EQU 0x2f    ; Bright white on green attribute
ATTR_BWHITE_ON_MAGENTA EQU 0x5f    ; Bright White on magenta attribute

PM_MODE_STACK          EQU 0x80000 ; Protected mode stack below EBDA
EFLAGS_VM_BIT          EQU 17      ; EFLAGS VM bit
EFLAGS_IOPL_BITS       EQU 12      ; EFLAGS IOPL bits (bit 12 and bit 13)

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

bits 16
ORG 0x7c00

; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
; %include "bpb.inc"

boot_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

    ; Fast method of enabling A20 may not work on all x86 BIOSes
    ; It is good enough for emulators and most modern BIOSes
    ; See: https://wiki.osdev.org/A20_Line
    cli                         ; Disable interrupts for rest of code as we don't
                                ; want A20 code to be interrupted. In protected mode
                                ; we have no IDT so any interrupt that does occur will
                                ; double fault and reboot.

    in al, 0x92
    or al, 2
    out 0x92, al                ; Enable A20 using Fast Method

    lgdt [gdtr]                 ; Load our GDT

    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:
    hlt                         ; Halt

; 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 ah, ATTR_BWHITE_ON_GREEN; Attribute to print with
    mov al, ah                  ; Attribute to clear last line when scrolling
    mov esi, in_pm_msg          ; Print message that we are in protected mode
    call print_string_pm

    pushf                       ; Get current EFLAGS
    pop eax
    or eax, 1<<EFLAGS_VM_BIT    ; Set VM flag to enter v8086 mode
    and eax, ~(3<<EFLAGS_IOPL_BITS)
                                ; Set IOPL to 0
                                ; IF flag already 0 because of earlier CLI
    push eax
    popf                        ; Reload new flags
    jmp CODE32_SEL:v86_mode_entry
                                ; Far JMP to v8086 entry point

; 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

align 4
vidmem_ptr: dd VIDEO_TEXT_ADDR  ; Start console output in upper left of display

in_pm_msg:
    db "In 32-bit protected mode!", 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
end_of_gdt:

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

CODE32_SEL equ gdt32_code - gdt_start
DATA32_SEL equ gdt32_data - gdt_start

; Pad boot sector to 510 bytes and add 2 byte boot signature
TIMES 510-($-$$) db  0
dw 0xaa55

引导加载程序可以通过以下方式生成:

nasm -f bin v86.asm -o v86.bin

它可以在 QEMU 中运行:

qemu-system-i386 -fda v86.bin

【问题讨论】:

    标签: assembly x86 nasm x86-64 osdev


    【解决方案1】:

    TL;DR

    问题 #1

    POPF 实际上不允许您根据指令集架构参考更改 VM 标志:

    当在 受保护、兼容性或 64 位模式下以 特权级别 0(或实地址模式,相当于特权级别 0)运行时,所有除了 RF1、VIP、VIF 和 VM 之外,EFLAGS 寄存器中的非保留标志可以被修改。 VIP、VIF 和 VM 不受影响

    有两种通用机制可用于设置 EFLAGS.VM 和enter v8086 mode

    • 任务切换到 80386 任务会从新的 TSS 加载 EFLAGS 的图像。新任务的TSS必须是80386 TSS,而不是80286 TSS,因为80286 TSS不存储EFLAGS的高位字,其中包含VM标志。新 EFLAGS 的 VM 位值为 1 表示新任务正在执行 8086 条指令;因此,在从 TSS 加载段寄存器时,处理器会像 8086 一样形成基地址。

    • 来自 80386 任务过程的 IRET 从堆栈加载 EFLAGS 的图像。在这种情况下,VM 中的值 1 表示将控制返回到的过程是 8086 过程。执行 IRET 时的 CPL 必须为零,否则处理器不会更改 VM。

    问题 #2

    v8086 模式仅在 32 位保护模式(传统模式)下的 x86-64 处理器上可用。您不能在 64 位模式或 32 位(或 16 位)兼容模式下使用它。您必须将处理器从长模式切换并进入以 CPL=0 运行的 32 位保护模式(传统模式)并执行上述两种方法之一。这是一项昂贵的(性能方面的)工作,并且充满了问题。然后,您必须在完成后切换回长模式。

    如果有一些这样做的用例,并且您在具有多个内核的系统上 - 您可以在引导处理器 (BSP) 时以 32 位保护模式启动其中一个内核以长模式运行。


    方法一:使用IRET进入v8086模式

    这是最简单的解决方案。如果您从 32 位保护模式(CPL=0)执行IRET 并且堆栈上的 EFLAGS.VM 寄存器已设置,CPU 将尝试返回到 v8086 模式并假定堆栈帧包含所需的信息进行过渡:

    PROTECTED-MODE:
    [snip]
        EIP ← Pop();
        CS ← Pop(); (* 32-bit pop, high-order 16 bits discarded *)
        tempEFLAGS ← Pop();
    [snip]
    
     RETURN-TO-VIRTUAL-8086-MODE:
        (* Interrupted procedure was in virtual-8086 mode: PE = 1, CPL=0, VM = 1 in flag image *)
        IF EIP not within CS limit
            THEN #GP(0); FI;
        EFLAGS ← tempEFLAGS;
        ESP ← Pop();
        SS ← Pop(); (* Pop 2 words; throw away high-order word *)
        ES ← Pop(); (* Pop 2 words; throw away high-order word *)
        DS ← Pop(); (* Pop 2 words; throw away high-order word *)
        FS ← Pop(); (* Pop 2 words; throw away high-order word *)
        GS ← Pop(); (* Pop 2 words; throw away high-order word *)
        CPL ← 3;
        (* Resume execution in Virtual-8086 mode *)
    END;
    

    如果您以相反的顺序将这些项目压入堆栈并执行iret,您应该能够进入 v8086 模式。

    V86_STACK_SEG          EQU 0x0000  ; v8086 stack SS
    V86_STACK_OFS          EQU 0x0000  ; 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)
    
    [snip]
    
        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 have no v86 monitor via protected mode
                                    ; GPF handler
        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
    

    我在 V86_STACK_SEG:V86_STACK_OFS 设置了 ES=DS=CS=FS=GS=0 和实模式堆栈(根据您的需要定义这些)。 IP 设置为v86_mode_entry 标签的偏移量。在上面的代码 sn-p 中,我只将 2 位设置为 1(位 1 和 VM)。第 1 位是EFLAGS 中的保留位,始终假定设置为 1。EFLAGS 中的所有其他标志均为 0,因此 IOPL=0。

    所有其他寄存器将包含与进入 v8086 模式之前相同的值。您可能希望将它们归零以避免将信息从 32 位保护模式(即:内核)泄漏到 v8086 任务中。

    使用此代码的最小完整可验证示例是:

    VIDEO_TEXT_ADDR        EQU 0xb8000 ; Hard code beginning of text video memory
    ATTR_BWHITE_ON_GREEN   EQU 0x2f    ; Bright white on green attribute
    ATTR_BWHITE_ON_MAGENTA EQU 0x5f    ; Bright White on magenta attribute
    
    PM_MODE_STACK          EQU 0x80000 ; Protected mode stack below EBDA
    V86_STACK_SEG          EQU 0x0000  ; v8086 stack SS
    V86_STACK_OFS          EQU 0x0000  ; 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
    
    ; 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))
    
    bits 16
    ORG 0x7c00
    
    ; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
    ; %include "bpb.inc"
    
    boot_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
    
        ; Fast method of enabling A20 may not work on all x86 BIOSes
        ; It is good enough for emulators and most modern BIOSes
        ; See: https://wiki.osdev.org/A20_Line
        cli                         ; Disable interrupts for rest of code as we don't
                                    ; want A20 code to be interrupted. In protected mode
                                    ; we have no IDT so any interrupt that does occur will
                                    ; double fault and reboot.
    
        in al, 0x92
        or al, 2
        out 0x92, al                ; Enable A20 using Fast Method
    
        lgdt [gdtr]                 ; Load our GDT
    
        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:
        sub dword [vidmem_ptr], VIDEO_TEXT_ADDR
                                    ; Adjust video pointer to be relative to beginning of
                                    ;     segment 0xb800
    
        mov si, in_v86_msg          ; Print in v86 message
        mov ah, ATTR_BWHITE_ON_MAGENTA
                                    ; Attribute to print with
        call print_string_rm_nobios
    
    .endloop:
        jmp $                       ; Infinite loop since we did code a solution to exit VM
    
    ; Function: print_string_rm_nobios
    ;           Display a string to the console on display page 0 in real/v8086 mode
    ;           without using the BIOS. We don't have a proper v8086 monitor so can't
    ;           use BIOS to display.
    ;
    ;           Very basic. Doesn't update hardware cursor, doesn't handle scrolling,
    ;           LF, CR, TAB.
    ;
    ; Inputs:   SI  = Offset of address to print
    ;           AH  = Attribute of string to print
    ; Clobbers: None
    ; Returns:  None
    
    print_string_rm_nobios:
        push di
        push si
        push ax
        push es
    
        mov di, VIDEO_TEXT_ADDR>>4  ; ES=0xb800 (text video mode segment)
        mov es, di
    
        mov di, [vidmem_ptr]        ; Start from video address stored at vidmem_ptr
        jmp .getchar
    .outchar:
        stosw                       ; Output character to display
    .getchar:
        lodsb                       ; Load next character from string
        test al, al                 ; Is character NUL?
        jne .outchar                ; If not, go output character
    
        mov [vidmem_ptr], di        ; Update global video pointer
    
        pop es
        pop ax
        pop si
        pop di
        ret
    
    ; 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 ah, ATTR_BWHITE_ON_GREEN; Attribute to print with
        mov al, ah                  ; Attribute to clear last line when scrolling
        mov esi, in_pm_msg          ; Print message that we are in protected mode
        call print_string_pm
    
        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 have no v86 monitor via protected mode
                                    ; GPF handler
        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
    
    align 4
    vidmem_ptr: dd VIDEO_TEXT_ADDR  ; Start console output in upper left of display
    
    in_pm_msg:
        db "In 32-bit protected mode!", 0
    in_v86_msg:
        db "In v8086 mode!", 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
    end_of_gdt:
    
    gdtr:
        dw end_of_gdt - gdt_start - 1
                                    ; limit (Size of GDT - 1)
        dd gdt_start                ; base of GDT
    
    CODE32_SEL equ gdt32_code - gdt_start
    DATA32_SEL equ gdt32_data - gdt_start
    
    ; Pad boot sector to 510 bytes and add 2 byte boot signature
    TIMES 510-($-$$) db  0
    dw 0xaa55
    

    这个示例代码可以修改为hlt,它会出现双重错误。它确实正确进入了 v8086 模式。我在它处于 32 位保护模式时打印一个字符串,在它进入 v8086 模式后打印一个字符串。由于 IOPL=0,实模式代码不使用任何特权指令,也不使用任何对中断标志 (IF) 敏感的指令,也不使用端口 IO。如果没有 VM 监视器(支持 v8086 模式的 GPF 处理程序),您将受限于非特权和非中断标志敏感指令。由于 INT 指令是 IF 敏感的,因此无法使用 BIOS。我直接把字符写到显示器上。


    方法二:使用硬件任务开关进入v8086模式

    如果你没有在你的操作系统中使用硬件任务切换,我不推荐使用这种机制。如果您已选择使用硬件任务切换,那么使用此方法是有意义的。1

    如果使用硬件任务切换进入 v8086 模式,则需要 TSS 结构和 GDT 中的 TSS 条目。 GDT 中的 TSS 条目用于指定包含 TSS 的段的基础和限制。一个 GDT 条目一般定义为:

    最初标记为可用的 32 位 TSS descriptor 的类型为 0x09; S 位(系统段)设置为 0; P 位为 1; G 位设置为 0(字节粒度);其余标志位设置为 0。对于 v8086 任务,我们希望描述符特权级别 (DPL) 为 0。这导致访问字节为 0x89,标志字节为 0x00。

    TSS 结构本身可以遵循相关Stackoverflow answer 中建议的结构类型。对于下面的示例,我们将不使用 IO 端口位图,因此我将 TSS_IO_BITMAP_SIZE 设置为 0。

    一旦创建了适当的结构,就可以用 v8086 任务所需的寄存器状态填充 TSS。这将包括将在 v8086 任务中开始执行的 CS:IP。要进入 v8086 任务,只需通过 TSS 选择器进行 FAR JMP:

    jmp TSS32_SEL:0             ; Transfer control to v8086 mode and our real mode code
    

    通过 TSS 选择器跳转时忽略偏移量。我使用 0 值作为偏移量,但它可以设置为任何值。此 FAR JMP 将使用 TSS 选择器加载任务寄存器并将任务标记为 busy;根据 TSS 结构设置 CPU 状态;将控制权转移到任务。一个最小的完整示例如下:

    VIDEO_TEXT_ADDR        EQU 0xb8000 ; Hard code beginning of text video memory
    ATTR_BWHITE_ON_GREEN   EQU 0x2f    ; Bright white on green attribute
    ATTR_BWHITE_ON_MAGENTA EQU 0x5f    ; Bright White on magenta attribute
    
    PM_MODE_STACK          EQU 0x80000 ; Protected mode stack below EBDA
    
    V86_STACK_SEG          EQU 0x0000  ; v8086 stack SS
    V86_STACK_OFS          EQU 0x0000  ; 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 0       ; Size 0 disables IO port bitmap (no permission)
    
    ; 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))
    
    bits 16
    ORG 0x7c00
    
    ; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
    ; %include "bpb.inc"
    
    boot_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
    
        ; Fast method of enabling A20 may not work on all x86 BIOSes
        ; It is good enough for emulators and most modern BIOSes
        ; See: https://wiki.osdev.org/A20_Line
        cli                         ; Disable interrupts for rest of code as we don't
                                    ; want A20 code to be interrupted. In protected mode
                                    ; we have no IDT so any interrupt that does occur will
                                    ; double fault and reboot.
    
        in al, 0x92
        or al, 2
        out 0x92, al                ; Enable A20 using Fast Method
    
        lgdt [gdtr]                 ; Load our GDT
    
        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:
        sub dword [vidmem_ptr], VIDEO_TEXT_ADDR
                                    ; Adjust video pointer to be relative to beginning of
                                    ;     segment 0xb800
    
        mov si, in_v86_msg          ; Print in v86 message
        mov ah, ATTR_BWHITE_ON_MAGENTA
                                    ; Attribute to print with
        call print_string_rm_nobios
    
    .endloop:
        jmp $                       ; Infinite loop since we did code a solution to exit VM
    
    ; Function: print_string_rm_nobios
    ;           Display a string to the console on display page 0 in real/v8086 mode
    ;           without using the BIOS. We don't have a proper v8086 monitor so can't
    ;           use BIOS to display.
    ;
    ;           Very basic. Doesn't update hardware cursor, doesn't handle scrolling,
    ;           LF, CR, TAB.
    ;
    ; Inputs:   SI  = Offset of address to print
    ;           AH  = Attribute of string to print
    ; Clobbers: None
    ; Returns:  None
    
    print_string_rm_nobios:
        push di
        push si
        push ax
        push es
    
        mov di, VIDEO_TEXT_ADDR>>4  ; ES=0xb800 (text video mode segment)
        mov es, di
    
        mov di, [vidmem_ptr]        ; Start from video address stored at vidmem_ptr
        jmp .getchar
    .outchar:
        stosw                       ; Output character to display
    .getchar:
        lodsb                       ; Load next character from string
        test al, al                 ; Is character NUL?
        jne .outchar                ; If not, go output character
    
        mov [vidmem_ptr], di        ; Update global video pointer
    
        pop es
        pop ax
        pop si
        pop di
        ret
    
    ; 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 ah, ATTR_BWHITE_ON_GREEN; Attribute to print with
        mov al, ah                  ; Attribute to clear last line when scrolling
        mov esi, in_pm_msg          ; Print message that we are in protected mode
        call print_string_pm
    
        mov ecx, TSS_SIZE           ; Zero out entire TSS structure
        mov edi, tss_entry
        xor eax, eax
        rep stosb
    
        ; v8086 stack SS:SP (grows down from SS:SP)
        mov dword [tss_entry.ss], V86_STACK_SEG
        mov dword [tss_entry.esp], V86_STACK_OFS
    
        mov dword [tss_entry.eflags], 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. We don't want interrupts enabled upon entry to
                                    ; v8086 because we have no v8086 monitor (a protected mode
                                    ; GPF handler)
    
        ; Set Real Mode CS:EIP to start execution at
        mov dword [tss_entry.cs], V86_CS_SEG
        mov dword [tss_entry.eip], v86_mode_entry
    
        ; 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
    %if TSS_IO_BITMAP_SIZE > 0
        ; If using an IO Bitmap then a padding byte has to be set to 0xff at end of bitmap
        mov byte [tss_entry.iomap_pad], 0xff
    %endif
    
        jmp TSS32_SEL:0             ; 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
    
    align 4
    vidmem_ptr: dd VIDEO_TEXT_ADDR  ; Start console output in upper left of display
    
    in_pm_msg:
        db "In 32-bit protected mode!", 0
    in_v86_msg:
        db "In v8086 mode!", 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
    
    ; Pad boot sector to 510 bytes and add 2 byte boot signature
    TIMES 510-($-$$) db  0
    dw 0xaa55
    
    ; Data section above bootloader @ 0x7c00. Acts like a BSS section
    ABSOLUTE 0x7e00
    
    ; Store the TSS just beyond the boot signature read into memory
    ; at 0x0000:0x7e00
    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
    
    ;.cetssp:    resd 1             ; Need this if CET is enabled
    
    ; Insert any kernel defined task instance data here
    ; ...
    
    ; If using VME (Virtual Mode extensions) there need to bean additional 32 bytes
    ; available immediately preceding iomap. If using VME uncomment next 2 lines
    ;.vmeintmap:                     ; If VME enabled uncomment this line and the next
    ;    resb 32                     ;     32*8 bits = 256 bits (one bit for each interrupt)
    
    .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
    

    备注

    • 1依靠硬件任务切换很难移植到其他CPU; x86 CPU 没有针对硬件任务切换进行优化;不保留 FPU 和 SIMD 状态;性能可能比通过软件编写任务切换要慢。 x86-64 处理器上的长模式甚至不支持硬件任务切换。在 x86 上运行的现代操作系统通常不使用 CPU 的硬件任务切换。

    【讨论】:

      【解决方案2】:

      这个答案必须与第一个答案分开,因为超出了帖子限制。


      方法 3:使用 IRET 和 TSS 结构

      这个方法其实和方法#1一样。使用 IRET 进入 v8086 模式,但我们在 GDT 中创建一个 TSS 结构和一个 32 位 TSS 条目,如 方法 #2。在没有硬件任务切换的情况下创建 TSS 允许我们在运行 IOPL

      当中断/调用/陷阱门将控制权从 CPL=1,2,3 转移到 CPL=0 时,CPU 将使用 .esp0.ss0 字段作为内核堆栈。在没有 TSS 的情况下以 CPL>0 运行代码时,您无法处理中断。 LTR 指令用于指定初始 TSS,而不进行实际的任务切换。 TSS 被 LTR 标记为忙。

      以下最小的完整示例演示了这个概念。在本例中,IOPB 设置为允许端口访问前 0x400 端口并拒绝其余端口访问:

      VIDEO_TEXT_ADDR        EQU 0xb8000 ; Hard code beginning of text video memory
      ATTR_BWHITE_ON_GREEN   EQU 0x2f    ; Bright white on green attribute
      ATTR_BWHITE_ON_MAGENTA EQU 0x5f    ; Bright White on magenta attribute
      
      PM_MODE_STACK          EQU 0x80000 ; Protected mode stack below EBDA
      
      V86_STACK_SEG          EQU 0x0000  ; v8086 stack SS
      V86_STACK_OFS          EQU 0x0000  ; 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)
      
      ; 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))
      
      bits 16
      ORG 0x7c00
      
      ; Include a BPB (1.44MB floppy with FAT12) to be more compatible with USB floppy media
      ; %include "bpb.inc"
      
      boot_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
      
          ; Fast method of enabling A20 may not work on all x86 BIOSes
          ; It is good enough for emulators and most modern BIOSes
          ; See: https://wiki.osdev.org/A20_Line
          cli                         ; Disable interrupts for rest of code as we don't
                                      ; want A20 code to be interrupted. In protected mode
                                      ; we have no IDT so any interrupt that does occur will
                                      ; double fault and reboot.
      
          in al, 0x92
          or al, 2
          out 0x92, al                ; Enable A20 using Fast Method
      
          lgdt [gdtr]                 ; Load our GDT
      
          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:
          sub dword [vidmem_ptr], VIDEO_TEXT_ADDR
                                      ; Adjust video pointer to be relative to beginning of
                                      ;     segment 0xb800
      
          mov si, in_v86_msg          ; Print in v86 message
          mov ah, ATTR_BWHITE_ON_MAGENTA
                                      ; Attribute to print with
          call print_string_rm_nobios
      
      .endloop:
          jmp $                       ; Infinite loop since we did code a solution to exit VM
      
      ; Function: print_string_rm_nobios
      ;           Display a string to the console on display page 0 in real/v8086 mode
      ;           without using the BIOS. We don't have a proper v8086 monitor so can't
      ;           use BIOS to display.
      ;
      ;           Very basic. Doesn't update hardware cursor, doesn't handle scrolling,
      ;           LF, CR, TAB.
      ;
      ; Inputs:   SI  = Offset of address to print
      ;           AH  = Attribute of string to print
      ; Clobbers: None
      ; Returns:  None
      
      print_string_rm_nobios:
          push di
          push si
          push ax
          push es
      
          mov di, VIDEO_TEXT_ADDR>>4  ; ES=0xb800 (text video mode segment)
          mov es, di
      
          mov di, [vidmem_ptr]        ; Start from video address stored at vidmem_ptr
          jmp .getchar
      .outchar:
          stosw                       ; Output character to display
      .getchar:
          lodsb                       ; Load next character from string
          test al, al                 ; Is character NUL?
          jne .outchar                ; If not, go output character
      
          mov [vidmem_ptr], di        ; Update global video pointer
      
          pop es
          pop ax
          pop si
          pop di
          ret
      
      ; 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 ah, ATTR_BWHITE_ON_GREEN; Attribute to print with
          mov al, ah                  ; Attribute to clear last line when scrolling
          mov esi, in_pm_msg          ; Print message that we are in protected mode
          call print_string_pm
      
          mov ecx, TSS_SIZE           ; Zero out entire TSS structure
          mov edi, tss_entry
          xor eax, eax
          rep stosb
      
          ; 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 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 have no v86 monitor via protected mode
                                      ; GPF handler
          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
      
      align 4
      vidmem_ptr: dd VIDEO_TEXT_ADDR  ; Start console output in upper left of display
      
      in_pm_msg:
          db "In 32-bit protected mode!", 0
      in_v86_msg:
          db "In v8086 mode!", 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
      
      ; Pad boot sector to 510 bytes and add 2 byte boot signature
      TIMES 510-($-$$) db  0
      dw 0xaa55
      
      ; Data section above bootloader @ 0x7c00. Acts like a BSS section
      ABSOLUTE 0x7e00
      
      ; Store the TSS just beyond the boot signature read into memory
      ; at 0x0000:0x7e00
      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
      
      ;.cetssp:    resd 1             ; Need this if CET is enabled
      
      ; Insert any kernel defined task instance data here
      ; ...
      
      ; If using VME (Virtual Mode extensions) there need to bean additional 32 bytes
      ; available immediately preceding iomap. If using VME uncomment next 2 lines
      ;.vmeintmap:                     ; If VME enabled uncomment this line and the next
      ;    resb 32                     ;     32*8 bits = 256 bits (one bit for each interrupt)
      
      .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
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-04-22
        • 2018-08-07
        • 1970-01-01
        • 2011-10-14
        • 2018-11-27
        • 1970-01-01
        • 1970-01-01
        • 2016-03-29
        相关资源
        最近更新 更多