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

汇编代码在QEMU中崩溃但Bochs中正常运行的问题排查

问题:32位保护模式返回实模式的QEMU崩溃故障

背景

我正在学习OS开发,写了一个基础引导加载程序:将软盘镜像的部分扇区加载到RAM的0x10000地址,切换到32位保护模式后跳转到该地址执行。0x10000处的代码通过BIOS视频缓冲区显示"Hello, World!",随后尝试跳回16位实模式并进入无限循环。

加载到0x10000的代码

; file is loaded at address 0x10000
org 0x10000

; Entry point is here
; 32-bit protected mode

bits 32
pm32:
    ; print "Hello World!" using the BIOS video buffer
    VideoBuffer equ 0xb8200

    ; Set up data segments so we can access the "Hello World!" string
    mov ax, 10h
    mov ds, ax
    mov ss, ax

    sti

    mov edi, VideoBuffer
    mov esi, msg

    print:
        lodsb

        ; check if we reached the null char
        test al, al
        jz print_done

        ; print text and move destination pointer
        mov [edi], al
        inc edi
        inc edi

        jmp print

    print_done:
        ; jump to "pmdone" in 16-bit protected mode
        jmp 18h:pmdone

bits 16
pmdone:
    ; switch to real mode
    mov eax, cr0
    xor al, 1
    mov cr0, eax

halt:
    ; halt the CPU using an infinite loop
    ; (gotta use 1000h: segment because the address of halt is cut to 16 bits)
    jmp 1000h:halt

msg: db "Hello World!", 0

GDT配置

  • 0x8: 32位代码段
  • 0x10: 32位数据段
  • 0x18: 16位代码段
  • 0x20: 16位数据段

现象

Bochs调试下代码运行完全正常,能显示消息并进入循环;但在QEMU中频繁崩溃重启,仅偶尔能成功显示消息。怀疑是寻址或跳转操作存在错误,同时想了解退出32位模式到停机的最优实现方式。

QEMU最后中断日志

2: v=08 e=0000 i=0 cpl=0 IP=0018:0000002b pc=0000002b SP=0010:00000000 env->regs[R_EAX]=00000011
EAX=00000011 EBX=00000000 ECX=00000002 EDX=00000000
ESI=0001003f EDI=000b8218 EBP=00000000 ESP=00000000
EIP=0000002b EFL=00000293 [--S-A-C] CPL=0 II=0 A20=1 SMM=0 HLT=0
ES =1000 00010000 0000ffff 00009300 DPL=0 DS16 [-WA]
CS =0018 00000000 000fffff 000f9a00 DPL=0 CS16 [-R-]
SS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
DS =0010 00000000 ffffffff 00cf9300 DPL=0 DS   [-WA]
FS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
GS =0000 00000000 0000ffff 00009300 DPL=0 DS16 [-WA]
LDT=0000 00000000 0000ffff 00008200 DPL=0 LDT
TR =0000 00000000 0000ffff 00008b00 DPL=0 TSS32-busy
GDT=     00007c7a 00000027
IDT=     00000000 000003ff
CR0=00000011 CR2=00000000 CR3=00000000 CR4=00000000
DR0=00000000 DR1=00000000 DR2=00000000 DR3=00000000
DR6=ffff0ff0 DR7=00000400
CCS=000000d4 CCD=ffffff3d CCO=SUBW
EFER=0000000000000000
check_exception old: 0x8 new 0xd

故障分析与解决

1. 崩溃原因定位

看QEMU日志,触发了0xd(一般保护异常),之前还出现0x8(双重故障),说明回退实模式的流程有核心错误:

  • 修改CR0的PE位关闭保护模式后,未立即执行远跳转刷新CS寄存器。CPU修改PE位后仍停留在保护模式解码逻辑中,必须通过远跳转到实模式兼容段,强制CPU重新加载CS描述符,彻底切换到实模式地址解析方式。
  • 原代码跳到16位保护模式的pmdone后直接修改CR0,修改后未做远跳转,导致CS仍为16位保护模式段、CR0的PE位却已清零,CPU状态冲突触发异常。
  • jmp 1000h:halt寻址逻辑错误:halt标签的实际物理地址是0x10000 + pmdone偏移 + halt偏移,用1000h做段基址时,1000h*16 + halt偏移不等于实际地址,跳错位置引发崩溃。

2. 修正步骤

步骤1:实模式切换的正确流程

清除CR0的PE位后必须立即远跳转,示例代码:

bits 16
pmdone:
    ; 先切换到16位数据段(实模式兼容)
    mov ax, 20h
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov sp, 0xFFFF  ; 设置安全栈指针

    ; 关闭保护模式
    mov eax, cr0
    and al, ~1
    mov cr0, eax

    ; 远跳转刷新CS,彻底进入实模式
    jmp 0x1000:real_mode_halt

real_mode_halt:
    ; 实模式下重置段寄存器
    mov ax, 0x1000
    mov ds, ax
    mov ss, ax

    cli
    hlt
    jmp real_mode_halt  ; 被NMI唤醒后重新暂停

步骤2:修正pmdone的跳转地址

原代码中jmp 18h:pmdone的偏移错误——pmdone是相对于0x10000的偏移,而16位保护模式段0x18的基址如果是0,线性地址0x18:pmdone不等于实际物理地址。需调整为:

jmp 18h:(pmdone - 0x10000)

或修改0x18段的基址为0x1000,让线性地址偏移直接对应物理地址。

步骤3:最优停机方式

实模式下最节省资源的停机流程是:

halt:
    cli  ; 禁用中断,避免被唤醒
    hlt  ; 让CPU进入低功耗暂停状态
    jmp halt  ; 被NMI唤醒后重新暂停

3. 完整修正后的代码片段

; file is loaded at address 0x10000
org 0x10000

bits 32
pm32:
    VideoBuffer equ 0xb8200

    mov ax, 10h
    mov ds, ax
    mov ss, ax
    mov esp, 0x10000  ; 设置32位栈指针

    mov edi, VideoBuffer
    mov esi, msg

print:
    lodsb
    test al, al
    jz print_done

    mov [edi], al
    inc edi
    mov byte [edi], 0x07  ; 增加黑底白字的文本属性
    inc edi
    jmp print

print_done:
    ; 跳转到16位保护模式的pmdone(偏移相对于段基址)
    jmp 18h:(pmdone - 0x10000)

bits 16
pmdone:
    ; 切换到16位数据段
    mov ax, 20h
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0xFFFF

    ; 关闭保护模式
    mov eax, cr0
    and al, ~1
    mov cr0, eax

    ; 远跳转到实模式,段基址0x1000对应物理地址0x10000
    jmp 0x1000:real_mode_halt

real_mode_halt:
    ; 实模式段寄存器重置
    mov ax, 0x1000
    mov ds, ax
    mov ss, ax

    cli
    hlt
    jmp real_mode_halt

msg: db "Hello World!", 0

4. GDT配置注意事项

确保16位代码段(0x18)的描述符覆盖代码加载区域:

; 16位代码段描述符(基址0x1000,覆盖0x10000-0x1FFFF)
dw 0xFFFF       ; 段限长(16位)
dw 0x1000       ; 段基址低16位
db 0x00         ; 段基址中8位
db 0x9A         ; 访问权限(代码段、可读、DPL0)
db 0x00         ; 粒度位(16位模式)+ 段基址高8位
db 0x00         ; 段基址最高8位

内容的提问来源于stack exchange,提问作者axolotlKing0722

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 01:10:55