assembly - 从引导加载程序进入保护模式时出现问题
问题描述
此汇编代码从引导加载程序进入保护模式,但在调用远跳转并重新启动后无法重置 CS 段(或执行远跳转)。如果我删除远跳,它会在保护模式下进入无限循环(0x66,jmp $)就好了,无需重新启动。
[bits 16]
[org 0x7c00]
xor ax,ax
xor eax,eax
add eax,ENTRY_POINT_32 ;address to plug to far jmp
mov [ENTRY_OFF],eax
xor eax,eax
mov eax,GDT ;load GDT label address
mov [GDTR+2],eax ; load it into address space in GDTR
lgdt [GDTR] ;load GDTR
cli ;turn off masked interrupts
in al,0x70
or al,0x80
out 0x70,al ;turn off nonmasked interrupts
in al,0x92
or al,2
out 0x92, al ;open line A20 (change address 20 to 32 bits)
mov eax,cr0
or al,1
mov cr0,eax ;switch to protected mode
db 0x66 ;prefix of opcode to change bitness
db 0xEA ;opcode of jmp far
ENTRY_OFF dd 0x0 ;32 bit offset of 32 bit instructions
dw 00001000b ; selector 1st descriptor CODE_descr,=1
ENTRY_POINT_32:
db 0x66 ;prefix of opcode to change bitness
jmp $ ;infinite jump to the same location
GDT:
NULL_descr dd 0x0,0x0 ; must be present in GDT
CODE_descr db 0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0
;descriptor of 32 bit code segment, base 0, size ffffffff
DATA_descr db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0
;descriptor of 32 bit data segment, base 0, size ffffffff
VIDEO_descr 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
;descriptor of video buffer, base 0x000B8000, size ffff
GDT_size db $-GDT ;size of GDT table
GDTR dw GDT_size-1 ;next 3 words are size &
dd 0x0 ;address of beginning of GDT, loaded in code
times 510 - ($ - $$) db 0
dw 0xaa55
来自 wasm.in 的原始代码,稍作修改。
解决方案
在实模式下,所有内存操作数上都有一个隐含的段。如果内存操作数不包含BP作为基数,则隐含段为DS。如果内存操作数确实包含 BP,则隐含的基数是 SS。您的内存操作数不使用BP所以隐含的段是DS。带有如下内存操作数的指令:
mov [ENTRY_POINT_32],eax
相当于:
mov [ds:ENTRY_POINT_32],eax
实模式使用段:偏移地址来到达物理内存地址。如果DS错误,您将写入错误的内存位置。20 位物理地址 = (segment<<4)+偏移量。
话虽如此,当您的引导加载程序启动时,您不能依赖段和通用寄存器作为您期望的值,但包含 BIOS 传递的引导驱动器的DL除外。您可以阅读我的引导加载程序提示,了解有关引导加载程序开发的更多信息。
您需要明确设置DS寄存器。由于您的代码正在使用org 0x7c00
,您需要将DS段设置为零。(0x0000<<4)+0x7c00 = 0x07c00(物理地址)。引导加载程序始终由 BIOS 加载到物理地址 0x07c00。
你也有这两行:
xor ax,ax
xor eax,eax
第一个是不必要的,因为您使用后者将所有EAX设置为零。如果在 32 位代码之前使用bits 32
NASM指令,则不需要以下行:
db 0x66 ;prefix of opcode to change bitness
GDTR 也设置不正确。您不正确地计算大小。你有这个代码:
GDT_size db $-GDT ;size of GDT table
GDTR dw GDT_size-1 ;next 3 words are size &
您创建一个包含 GDT 大小的字节的内存位置。GDTR dw GDT_size-1
获取标签的偏移量GDT_size
并从中减去一个。这仅适用于标签的偏移GDT_size
量大于 GDT 的大小。您可以执行以下操作:
GDT:
NULL_descr: dd 0x0,0x0 ; must be first entry in GDT
; descriptor of 32 bit code segment, base 0, size ffffffff
CODE_descr: db 0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0
; descriptor of 32 bit data segment, base 0, size ffffffff
DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0
; descriptor of video buffer, base 0x000B8000, size ffff
VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
GDT_END:
GDTR dw GDT_END-GDT-1 ; Size of GDT (minus 1)
dd 0x0 ; address of beginning of GDT, loaded in code
在创建自修改代码时,您还需要关注清除指令预取队列以确保处理器看到代码的更改。处理器可能已经预读了您正在修改的 FAR JMP 指令,并且不知道您对代码所做的更改。这可以通过在修改指令后简单地在代码中插入 JMP 来纠正。使用计算出的地址更新指令后,您可以执行以下操作:
mov [ENTRY_OFF],eax
jmp clear_prefetch ; Clear the instruction prefetch queue
; by jumping to next instruction
clear_prefetch:
工作代码(我已经清理了一些格式)可能如下所示:
bits 16
org 0x7c00
start:
xor eax,eax
mov ds, ax ; Explicitly set DS to zero
add eax,ENTRY_POINT_32 ; address to plug to far jmp
mov [ENTRY_OFF],eax
jmp clear_prefetch ; Clear the instruction prefetch queue
; by jumping to next instruction
clear_prefetch:
xor eax,eax
mov eax,GDT ; load GDT label address
mov [GDTR+2],eax ; load it into address space in GDTR
lgdt [GDTR] ; load GDTR
cli ; turn off masked interrupts
in al,0x70
or al,0x80
out 0x70,al ; turn off nonmasked interrupts
in al,0x92
or al,2
out 0x92, al ; enable A20 line
mov eax,cr0
or al,1
mov cr0,eax ; switch to protected mode
db 0x66 ; prefix of opcode to change bitness
db 0xEA ; opcode of jmp far
ENTRY_OFF:
dd 0x0 ; 32 bit offset of 32 bit instructions
dw 00001000b ; selector 1st descriptor CODE_descr,=1
bits 32
ENTRY_POINT_32:
jmp $ ; infinite jump to the same location
GDT:
NULL_descr: dd 0x0,0x0 ; must be first entry in GDT
; descriptor of 32 bit code segment, base 0, size ffffffff
CODE_descr: db 0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0
; descriptor of 32 bit data segment, base 0, size ffffffff
DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0
; descriptor of video buffer, base 0x000B8000, size ffff
VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
GDT_END:
GDTR dw GDT_END-GDT-1 ; Size of GDT (minus 1)
dd 0x0 ; address of beginning of GDT, loaded in code
times 510 - ($ - $$) db 0
dw 0xaa55
无需在引导加载程序中进行 FAR JMP 的运行时计算
对于这种情况,您的代码过于复杂。x86 上的旧版 BIOS 始终将引导加载程序加载到物理地址 0x07c00。使用 anORG 0x7c00
并将段设置为 0x0000 的一个优点是 0x0000:0x7c00 和线性地址(与实模式下的物理地址相同)与内存开头的 0x07c00 偏移量相同。您可以利用这一点并避免在运行时进行不必要的计算。代码可能如下所示:
bits 16
org 0x7c00
start:
xor ax,ax
mov ds,ax ; Explicitly set DS to zero
lgdt [GDTR] ; load GDTR
cli ; turn off masked interrupts
in al,0x70
or al,0x80
out 0x70,al ; turn off nonmasked interrupts
in al,0x92
or al,2
out 0x92, al ; enable A20 line
; Enter protected mode
mov eax,cr0
or al,1
mov cr0,eax ; switch to protected mode
jmp CODE32_SEL:ENTRY_POINT_32
bits 32
ENTRY_POINT_32:
mov eax, DATA32_SEL ; Set the protected mode selector
mov ds, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x9C000 ; Set protected mode stack below EBDA
mov eax, VIDEO32_SEL ; Set the video memory selector
mov es, ax
; Print some characters to top left of the screen in white on magenta
xor ebx, ebx
mov word [es:ebx], 0x57 << 8 | 'M'
mov word [es:ebx+2], 0x57 << 8 | 'D'
mov word [es:ebx+4], 0x57 << 8 | 'P'
jmp $ ; infinite jump to the same location
GDT:
NULL_descr: dd 0x0,0x0 ; must be first entry in GDT
; descriptor of 32 bit code segment, base 0, size ffffffff
CODE_descr: db 0xFF,0xFF,0x0,0x0,0x0,10011010b,11001111b,0x0
; descriptor of 32 bit data segment, base 0, size ffffffff
DATA_descr: db 0xFF,0xFF,0x0,0x0,0x0,10010010b,11001111b,0x0
VIDEO_descr: db 0xFF,0xFF,0x0,0x80,0x0B,10010010b,01000000b,0x0
; descriptor of video buffer, base 0x000B8000, size ffff
GDT_END:
CODE32_SEL equ CODE_descr-GDT
DATA32_SEL equ DATA_descr-GDT
VIDEO32_SEL equ VIDEO_descr-GDT
GDTR dw GDT_END-GDT-1 ; Size of GDT (minus 1)
dd GDT ; address of beginning of GDT
times 510 - ($ - $$) db 0
dw 0xaa55
此代码在汇编时计算 CODE 和 DATA 选择器。它还在装配时计算 GDTR 并对 FAR JMP 进行硬编码。应该注意的是,由于引导加载程序和 32 位入口点完全位于内存的前 64KiB 内,因此您可以在 FAR JMP 中使用 16 位偏移而不是 32 位来保护模式。无需自行修改代码。
注意:没有必要为视频内存创建选择器。您始终可以使用 32 位 4GiB 平面数据选择器来寻址该内存。
何时使用在运行时计算地址的代码?
构建 FAR JMP 并在运行时生成 GDTR 记录的概念并非完全没有价值。在代码可能放置在内存中不同段的环境中,您需要在运行时计算 GDTR 的 FAR JMP 和 GDT 线性地址。如果您试图通过 COM 或 EXE 程序从 DOS 进入保护模式,就会出现这种情况。DOS 加载程序决定将内容放入哪个段。在这种情况下,您必须在运行时计算地址。几年前,我为 IRC 上的某个人写了一些代码。我的代码没有禁用 NMI(它应该禁用),也没有修改 FAR JMP。我所做的是在堆栈上构建 FAR JMP 地址,然后通过堆栈上的地址执行间接 FAR JMP。原理和做自修改代码一样。
一个示例 DOS COM 程序在运行时生成堆栈上的 FAR JMP 的地址并在 GDTR 中生成 GDT 地址,如下所示:
; Assemble with NASM as
; nasm -f bin enterpm.asm -o enterpm.com
STACK32_TOP EQU 0x200000
CODE32_REL EQU 0x110000
VIDEOMEM EQU 0x0b8000
use16
; COM program CS=DS=SS
org 100h
call check_pmode ; Check if we are already in protected mode
; This may be the case if we are in a VM8086 task.
; EMM386 and other expanded memory manager often
; run DOS in a VM8086 task. DOS extenders will have
; the same effect
jz not_prot_mode ; If not in protected mode proceed to switch
mov dx, in_pmode_str; otherwise print an error and exit back to DOS
mov ah, 0x9
int 0x21 ; Print Error
ret
not_prot_mode:
call a20_on ; Enable A20 gate (uses Fast method as proof of concept)
cli
; Compute linear address of label gdt_start
; Using (segment << 4) + offset
mov eax,cs ; EAX = CS
shl eax,4 ; EAX = (CS << 4)
mov ebx,eax ; Make a copy of (CS << 4)
add [gdtr+2],eax ; Add base linear address to gdt_start address
; in the gdtr
lgdt [gdtr] ; Load gdt
; Compute linear address of label code_32bit
; Using (segment << 4) + offset
add ebx,code_32bit ; EBX = (CS << 4) + code_32bit
push dword 0x08 ; CS Selector
push ebx ; Linear offset of code_32bit
mov bp, sp ; m16:32 address on top of stack, point BP to it
mov eax,cr0
or eax,1
mov cr0,eax ; Set protected mode flag
jmp dword far [bp] ; Indirect m16:32 FAR jmp with
; m16:32 constructed at top of stack
; DWORD allows us to use a 32-bit offset in 16-bit code
; 16-bit functions that run in real mode
; Check if protected mode is enabled, effectively checking if we are
; in in a VM8086 task. Set ZF to 1 if in protected mode
check_pmode:
smsw ax
test ax, 0x1
ret
; Enable a20 (fast method). This may not work on all hardware
a20_on:
cli
in al, 0x92 ; Read System Control Port A
test al, 0x02 ; Test current a20 value (bit 1)
jnz .skipfa20 ; If already 1 skip a20 enable
or al, 0x02 ; Set a20 bit (bit 1) to 1
and al, 0xfe ; Always write a zero to bit 0 to avoid
; a fast reset into real mode
out 0x92, al ; Enable a20
.skipfa20:
sti
ret
in_pmode_str: db "Processor already in protected mode - exiting",0x0a,0x0d,"$"
align 4
gdtr:
dw gdt_end-gdt_start-1
dd gdt_start
gdt_start:
; First entry is always the Null Descriptor
dd 0
dd 0
gdt_code:
; 4gb flat r/w/executable code descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 0b10011010 ; access
db 0b11001111 ; granularity
db 0 ; base high
gdt_data:
; 4gb flat r/w data descriptor
dw 0xFFFF ; limit low
dw 0 ; base low
db 0 ; base middle
db 0b10010010 ; access
db 0b11001111 ; granularity
db 0 ; base high
gdt_end:
; Code that will run in 32-bit protected mode
; Align code to 4 byte boundary. code_32bit label is
; relative to the origin point 100h
align 4
code_32bit:
use32
; Set virtual memory address of pm code/data to CODE32_REL
; We will be relocating this section from low memory where DOS
; originally loaded it.
section protectedmode vstart=CODE32_REL, valign=4
start_32:
cld ; Direction flag forward
mov eax,0x10 ; 0x10 is flat selector for data
mov ds,eax
mov es,eax
mov fs,eax
mov gs,eax
mov ss,eax
mov esp,STACK32_TOP ; Should set ESP to a usable memory location
; Stack will be grow down from this location
mov edi,start_32 ; EDI = linear address where PM code will be copied
mov esi,ebx ; ESI = linear address of code_32bit
mov ecx,PMSIZE_LONG ; ECX = number of DWORDs to copy
rep movsd ; Copy all code/data from code_32bit to CODE32_REL
jmp 0x08:.relentry ; Absolute jump to relocated code
.relentry:
mov ah, 0x57 ; Attribute white on magenta
; Print a string to display
mov esi,str ; ESI = address of string to print
mov edi,VIDEOMEM ; EDI = base address of video memory
call print_string_attr
cli
endloop:
hlt ; Halt CPU with infinite loop
jmp endloop
print_string_attr:
push ecx
xor ecx,ecx ; ECX = 0 current video offset
jmp .loopentry
.printloop:
mov [edi+ecx*2],ax ; Copy attr and character to display
inc ecx ; Next word position
.loopentry:
mov al,[esi+ecx] ; Get next character to print
test al,al
jnz .printloop ; If it's not NUL continue
.endprint:
pop ecx
ret
str: db "Protected Mode",0
PMSIZE_LONG equ ($-$$+3)>>2
; Number of DWORDS that the protected mode
; code and data takes up (rounded up)
这段代码比我认为的要复杂一些。感兴趣的部分是指针计算,not_prot_mode
其中类似于您的代码正在执行的计算类型。进入保护模式后,代码将自身重新定位到 DOS 上方的 0x00110000。这是最初问我关于切换到保护模式的人的要求。
注意:此代码仅在尚未启用保护模式的环境中运行。如果在 VM8086 任务中运行,它将显示错误并退出。
推荐阅读
- excel - Excel Sumifs 索引匹配仅返回值
- mysql - MYSQL使用FIELD函数和多列自定义顺序
- python - 如何在 Python 中使用 kafka 客户端描述一个主题
- python - 如何同步一些 Flask-RESTful 资源?
- python - Python没有关闭文件,Windows 10
- html - 使用 Angular Material 表在特定索引处显示隐藏的 Div 元素
- c# - 如何附加到“dotnet”正在使用的文件
- go - 在 Go 的多个地方收听一个频道
- python - 如何在从 python selenium 脚本打开私人浏览器时禁用“不安全连接”?
- bash - 将两个连续的行合并为一个(寻求适用于 Windows 文件的解决方案)