32PM→16RM模式切换异常:QEMU中16位代码被当作32位执行
32位保护模式→16位实模式切换流程及问题排查
正确的32PM→16RM切换步骤(x86_64平台兼容)
要让CPU正确进入16位实模式并执行对应指令,必须严格遵循以下流程:
- 配置GDT中的16位兼容段:在全局描述符表中添加一个基址为0、限长0xFFFF的段描述符,属性设为
0x92(表示存在、特权级0、可读写、16位模式),确保切换后能映射实模式的1MB地址空间。 - 禁用中断与分页:先执行
cli关闭中断;如果当前开启了分页(哪怕是32位分页),必须清除CR0寄存器的PG位(位31),再通过远跳转刷新TLB,避免地址翻译混乱。 - 远跳转到16位段:通过
jmp 选择子:实模式入口地址的远跳转指令,强制CPU将CS寄存器切换到之前定义的16位段,这一步是触发CPU切换指令译码模式的关键——仅修改CR0的PE位不足以切换指令模式。 - 恢复实模式环境:
- 加载实模式IDT:用
lidt加载一个限长为0x3FF的IDT指针(对应实模式下的256个中断向量)。 - 退出保护模式:清除CR0的PE位(位0),彻底回到实模式。
- 刷新段寄存器:将DS、ES、SS、FS、GS全部设置为0,确保所有段的基址都是0,符合实模式的内存模型。
- 按需开启中断:执行
sti,之后就可以调用BIOS中断了。
- 加载实模式IDT:用
你的问题定位
结合你描述的现象(16位代码被当作32位执行),问题大概率出在代码逻辑上,核心可能是以下几点:
- 未执行远跳转切换CS到16位段:如果只是直接清除CR0的PE位,CPU会继续以32位模式译码指令,导致16位指令被错误解析。
- GDT中的16位段属性错误:比如误将段的D位(位5)设为1(表示32位段),这样即使切换到该段,CPU仍会按32位模式执行。
- 分页未完全关闭:如果CR0的PG位仍为1,关闭PE位后CPU的地址翻译机制会异常,间接导致指令执行混乱。
- 段寄存器未刷新:切换到实模式后,DS/ES等寄存器仍保留保护模式下的段基址,访问内存时会出现地址偏移,引发未定义行为。
构建环节(编译、链接)的可能性很低:你已经尝试了flat binary格式和调整链接脚本,只要编译时用-m16生成16位指令、链接脚本将16位代码定位到正确的物理地址,就不会导致指令被错误解析。
QEMU的问题可以排除:QEMU对x86模式切换的模拟非常成熟,x86_64 CPU本身也完全兼容实模式,除非你错误配置了CPU型号(比如指定了不兼容x86的架构),否则不会出现这类模拟错误。
修复建议
- 强制添加远跳转步骤:在清除CR0的PE位之前,先跳转到GDT中的16位段,示例代码:
; 假设16位段的选择子是0x10 jmp 0x10:real_mode_entry real_mode_entry: ; 后续执行清除PE位等操作 - 验证GDT段描述符:确保16位段的属性正确,示例定义:
real_mode_segment: dw 0xFFFF ; 限长低16位 dw 0x0000 ; 基址低16位 db 0x00 ; 基址中8位 db 0x92 ; 16位可读写段属性 db 0x00 ; 限长高4位 + 基址高8位 db 0x00 ; 基址最高8位 - 彻底关闭分页:切换前必须清除CR0的PG位并刷新TLB:
mov eax, cr0 and eax, ~(1 << 31) ; 清除PG位 mov cr0, eax jmp 0x8:flush_tlb ; 远跳转刷新TLB flush_tlb: - 切换到实模式后立即刷新所有段寄存器:
mov ax, 0 mov ds, ax mov es, ax mov ss, ax mov fs, ax mov gs, ax
内容的提问来源于stack exchange,提问作者Self learning student
相关产品推荐
相关产品推荐

