通过将 EFLAGS.VM 设置为 1 从 32 位保护模式切换到 v8086 模式时出现问题

Problem switching to v8086 mode from 32-bit protected mode by setting EFLAGS.VM to 1

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

    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]

为了这些测试我特意 运行:

为了测试我是否进入了 v8086 模式,我执行了 HLT 指令。由于我没有适当的中断机制,我预计会发生双重故障。 hlt 似乎正确执行并且系统就在那里。在 BOCHs 中,当我到达 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。这不是我所期望的。

问题:


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

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

TL;DR :

问题 #1

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

When operating in protected, compatibility, or 64-bit mode at privilege level 0 (or in real-address mode, the equivalent to privilege level 0), all non-reserved flags in the EFLAGS register except RF1, VIP, VIF, and VM may be modified. VIP, VIF and VM remain unaffected.

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

  • A task switch to an 80386 task loads the image of EFLAGS from the new TSS. The TSS of the new task must be an 80386 TSS, not an 80286 TSS, because the 80286 TSS does not store the high-order word of EFLAGS, which contains the VM flag. A value of one in the VM bit of the new EFLAGS indicates that the new task is executing 8086 instructions; therefore, while loading the segment registers from the TSS, - the processor forms base addresses as the 8086 would.

  • An IRET from a procedure of an 80386 task loads the image of EFLAGS from the stack. A value of one in VM in this case indicates that the procedure to which control is being returned is an 8086 procedure. The CPL at the time the IRET is executed must be zero, else the processor does not change VM.

问题 #2:

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

如果有一些用例可以执行此操作,并且您在具有多个内核的系统上 - 您可以在 32 位保护模式下启动其中一个内核,而Bootstrap 处理器 (BSP) 以长模式运行。


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

这是最简单的解决方案。如果您从 32 位保护模式(在 CPL=0 中)执行 IRET 并且设置了堆栈上的 EFLAGS.VM 寄存器,则 CPU 将尝试 return 到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 标签的偏移量。在上面的代码片段中,我只将 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 Monitor(支持 v8086 模式的 GPF 处理程序),您将只能使用非特权和非中断标志敏感指令。由于INT指令是IF敏感的,BIOS不能使用。我直接把字符写到显示器上。


方法二:使用硬件任务切换进入v8086模式

如果您没有在 OS 中使用硬件任务切换,我不建议您使用此机制。如果您已选择使用硬件任务切换,那么使用此方法很有意义。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 结构本身可以遵循此相关 中建议的结构类型。对于下面的示例,我们不会使用 IO 端口位图,因此我将 TSS_IO_BITMAP_SIZE 设置为 0。

一旦创建了适当的结构,就可以用 v8086 任务所需的寄存器状态填充 TSS。这将包括 CS:IP,其中将在 v8086 任务中开始执行。要进入 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 上的现代 OSes 运行 通常不使用 CPU 的硬件任务切换。

这个答案必须与第一个答案分开,因为超过了 post 限制。


方法 3:使用 IRET 和 TSS 结构

此方法实际上与方法#1相同。使用 IRET 进入 v8086 模式,但我们在 GDT 中创建了一个 TSS 结构和一个 32 位的 TSS 条目,如 Method #2。在没有硬件任务切换的情况下创建 TSS 允许我们在 运行 非特权 (CPL=1,2,3) 代码时指定 IO 端口位图,其中 IOPL < CPL。在多核系统上,内核通常会为每个处理器创建一个 TSS。

当 interrupt/call/trap 门将控制权从 CPL=1,2 转移到 CPL=0 时,CPU 将使用 .esp0.ss0 字段作为内核堆栈,3.当 运行 代码在 CPL>0 且没有 TSS 时,您无法处理中断。 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