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 的硬件任务切换。