You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义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

错误点分析

  1. GDT描述符格式错误
    LGDT指令要求6字节描述符结构:前2字节是GDT长度减1(Limit),后4字节是GDT的32位基地址。原始代码仅定义3字节(1字节Limit + 2字节基地址),格式完全错误,导致CPU无法正确加载GDT,进入保护模式后触发异常。

  2. 实模式下错误使用ESP寄存器
    实模式是16位寻址模式,栈指针应使用16位SP而非32位ESP。原始代码中mov esp, STACK_SEGMENT_REAL_MODE会导致ESP高16位值未定义,切换到保护模式后栈地址异常。

  3. 保护模式栈地址无效
    STACK_SEGMENT_PROT_MODE定义为0xFFFFFFFE,栈向下增长,第一次栈操作会访问0xFFFFFFFC,该地址超出常规内存范围(QEMU默认分配内存远小于4GB),引发内存访问错误。

  4. 返回地址处理逻辑错误
    用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 18:32:12