汇编代码在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
相关产品推荐
相关产品推荐

