自定义x86引导加载器在QEMU中运行时闪烁异常求助
自定义i386引导加载器QEMU闪烁问题排查
问题概述
使用NASM编写适用于i386的自定义引导加载器,编译运行后QEMU出现闪烁异常,无法执行预期功能。
编译命令:
nasm -f bin head.asm -o boot
运行命令:
qemu-system-i386 boot
原始代码
defs.asm
%define STACK_SEGMENT_REAL_MODE 0x9000 %define STACK_SEGMENT_PROT_MODE 0xFFFFFFFE
head.asm
[bits 16] [org 0x7C00] %include "defs.asm" start: cli xor ax, ax ; AX = 0 mov ds, ax ; DS = 0 mov es, ax ; ES = 0 mov ss, ax ; SS = 0 mov esp, STACK_SEGMENT_REAL_MODE mov ebp, esp sti call switch_real_to_prot ; at this point interrupts are disabled ; infinite loop jmp $ %include "protection.asm" times 510-($-$$) db 0 db 0x55 db 0xAA
protection.asm
; ----------------- Global Descriptor Table ----------------- ; ; Segment Descriptor ; 31 23 15 7 0 ; +---------------+-+-+-+----------+-+-----+-+-----+-+---------------+ ; | | | |A| | | | | | | | ; | BASE 31..24 |G|X|V| LIMIT |P| DPL |1| TYPE|A| BASE 21..16 | ; | | | |L| 19..16 | | | | | | | ; +---------------+-+-+-+----------+-+-----+-+-----+-+---------------+ ; | | | ; | SEGMENT BASE 15..0 | SEGMENT LIMIT 15..0 | ; | | | ; +--------------------------------+---------------------------------+ %include "defs.asm" %define GDT_CS_PROT 0x08 %define GDT_DS_PROT 0x10 gdt: ; NULL Segment Descriptor dq 0 ; Code Segment Descriptor ; ; BASE = 0x00000000 ; LIMIT = 0xFFFFF (4GB), 4KiB GRANULARITY, ; PRESENT TYPE = 32 bit, EXECUTE/READ, DPL = 0 dw 0xFFFF dw 0 db 0 db 0x9A db 0xCF db 0 ; Data Segment Descriptor ; ; BASE = 0x00000000 ; LIMIT = 0xFFFFF (4GB), 4KiB GRANULARITY, ; PRESENT TYPE = 32 bit, READ/WRITE, DPL = 0 dw 0xFFFF dw 0 db 0 db 0x92 db 0xCF db 0 gdt_end: gdt_desc: ; Func | Bits ; --------+------ db gdt_end - gdt ; Limit | 0-15 dw gdt ; Address | 16-47 switch_real_to_prot: [bits 16] cli xor ax, ax mov ds, ax lgdt [gdt_desc] ; switch to protected mode mov eax, cr0 or eax, 1 mov cr0, eax jmp GDT_CS_PROT:init_prot [bits 32] init_prot: ; Since this moment all data segment registers ; point to the same data segment in GDT:0x10 mov ax, GDT_DS_PROT mov es, ax mov fs, ax mov gs, ax mov ds, ax mov ss, ax ; move return address to the real stack mov eax, [esp] mov [STACK_SEGMENT_REAL_MODE], eax ; get protected mode stack mov eax, [STACK_SEGMENT_PROT_MODE] mov esp, eax mov ebp, eax ; put the return address to the correct stack mov eax, [STACK_SEGMENT_REAL_MODE] mov [esp], eax xor eax, eax ret
错误点分析
GDT描述符格式错误
LGDT指令要求6字节描述符结构:前2字节是GDT长度减1(Limit),后4字节是GDT的32位基地址。原始代码仅定义3字节(1字节Limit + 2字节基地址),格式完全错误,导致CPU无法正确加载GDT,进入保护模式后触发异常。实模式下错误使用ESP寄存器
实模式是16位寻址模式,栈指针应使用16位SP而非32位ESP。原始代码中mov esp, STACK_SEGMENT_REAL_MODE会导致ESP高16位值未定义,切换到保护模式后栈地址异常。保护模式栈地址无效
STACK_SEGMENT_PROT_MODE定义为0xFFFFFFFE,栈向下增长,第一次栈操作会访问0xFFFFFFFC,该地址超出常规内存范围(QEMU默认分配内存远小于4GB),引发内存访问错误。返回地址处理逻辑错误
用call switch_real_to_prot调用模式切换函数,实模式返回地址是16位,但32位保护模式下ret指令会读取32位地址,格式不匹配。且切换到保护模式后不应返回实模式,该逻辑完全不合理。
修复方案
修正后的代码
defs.asm
%define STACK_SEGMENT_REAL_MODE 0x9000 ; 实模式栈指针(16位) %define STACK_SEGMENT_PROT_MODE 0x90000 ; 保护模式栈指针(32位,避开实模式栈区域)
head.asm
[bits 16] [org 0x7C00] %include "defs.asm" start: cli xor ax, ax mov ds, ax mov es, ax mov ss, ax mov sp, STACK_SEGMENT_REAL_MODE ; 实模式用SP而非ESP mov bp, sp sti jmp switch_real_to_prot ; 直接跳转,无需返回 ; 永远不会执行到这里 jmp $ %include "protection.asm" times 510-($-$$) db 0 db 0x55 db 0xAA
protection.asm
%include "defs.asm" %define GDT_CS_PROT 0x08 %define GDT_DS_PROT 0x10 gdt: dq 0 ; NULL描述符 ; 代码段描述符 dw 0xFFFF ; Limit 0-15 dw 0 ; Base 0-15 db 0 ; Base 16-23 db 0x9A ; P=1, DPL=0, 代码段/可执行读 db 0xCF ; G=1, D=1, Limit 16-19 db 0 ; Base 24-31 ; 数据段描述符 dw 0xFFFF ; Limit 0-15 dw 0 ; Base 0-15 db 0 ; Base 16-23 db 0x92 ; P=1, DPL=0, 数据段/可读写 db 0xCF ; G=1, D=1, Limit 16-19 db 0 ; Base 24-31 gdt_end: gdt_desc: dw gdt_end - gdt - 1 ; GDT长度减1(LGDT要求) dd gdt ; GDT的32位基地址 switch_real_to_prot: [bits 16] cli xor ax, ax mov ds, ax lgdt [gdt_desc] ; 切换到保护模式 mov eax, cr0 or eax, 1 mov cr0, eax ; 远跳转刷新指令流水线 jmp GDT_CS_PROT:init_prot [bits 32] init_prot: ; 初始化所有数据段寄存器 mov ax, GDT_DS_PROT mov es, ax mov fs, ax mov gs, ax mov ds, ax mov ss, ax ; 设置保护模式栈 mov esp, STACK_SEGMENT_PROT_MODE mov ebp, esp ; 示例:在屏幕左上角打印'A'(验证保护模式功能) mov dword [0xB8000], 0x0F41 ; 0x0F是属性(白字黑底),0x41是'A'的ASCII码 ; 无限循环 jmp $
修复说明
- 修正GDT描述符格式,符合LGDT指令要求;
- 实模式下使用16位SP寄存器管理栈;
- 更换保护模式栈地址为合理的
0x90000,避免内存访问错误; - 移除错误的返回地址处理逻辑,直接跳转到模式切换函数,进入保护模式后执行无限循环(可添加自定义功能);
- 添加保护模式下屏幕输出示例,验证功能正常。
内容的提问来源于stack exchange,提问作者k1r1t0
相关产品推荐
相关产品推荐

