32位OS开发:设置寄存器时GDT代码段损坏引发三重故障求助
32位OS分页启用后GDT代码段损坏导致GPF故障
问题现象
开发32位操作系统时,启用分页后运行正常,但kernel_main返回至汇编代码的jmp $指令时触发通用保护错误(GPF),最终导致三重故障。Bochs报错显示cs=0x0008不可访问或非代码段,说明GDT中的代码段已损坏。
Bochs输出信息:
interrupt(): not accessible or not code segment cs=0x0008 interrupt(): not accessible or not code segment cs=0x0008 interrupt(): not accessible or not code segment cs=0x0008 CPU is in protected mode (active) CS.mode = 32 bit SS.mode = 32 bit EFER = 0x00000000 | EAX=00000001 EBX=00007d76 ECX=00000014 EDX=00000000 | ESP=0008fff8 EBP=00090000 ESI=000e0000 EDI=0000ffac | IOPL=0 id vip vif ac vm RF nt of df IF tf sf zf af PF cf | SEG sltr(index|ti|rpl) base limit G D | CS:0008( 0001| 0| 0) 00000000 ffffffff 1 1 | DS:00010 0002| 0| 0) 00000000 ffffffff 1 1 | SS:00010 0002| 0| 0) 00000000 ffffffff 1 1 | ES:00010 0002| 0| 0) 00000000 ffffffff 1 1 | FS:00010 0002| 0| 0) 00000000 ffffffff 1 1 | GS:00010 0002| 0| 0) 00000000 ffffffff 1 1 | EIP=00001021 (00001021) | CR0=0xe0000011 CR2=0x00000000 | CR3=0x00006000 CR4=0x00000000 0008:0000000000001021 (unk. ctxt): jmp .-2 (0x00001021) ; ebfe exception(): 3rd (13) exception with no resolution, shutdown status is 00h, resetting bx_pc_system_c::Reset(HARDWARE) called cpu hardware reset
通过Bochs调试器设置内存观察点,发现GDT代码段损坏的触发关联指令是mov ax, DATA_SEG(DATA_SEG为GDT数据段选择子,值0x10),但该指令本身仅为寄存器赋值,无法直接修改内存,需定位深层原因。
相关代码片段:
切换到保护模式的汇编代码
[bits 16] switch_to_pm: cli ; 1. 禁用中断 lgdt [gdt_descriptor] ; 2. 加载GDT描述符 mov eax, cr0 or eax, 0x1 ; 3. 设置CR0的PE位,启用保护模式 mov cr0, eax jmp CODE_SEG:init_pm ; 4. 远跳刷新流水线,进入32位模式 [bits 32] init_pm: ; 现在执行32位指令 mov ax, DATA_SEG ; 5. 更新段寄存器 mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. 将栈顶设置在空闲内存顶部 mov esp, ebp call BEGIN_PM ; 7. 调用保护模式初始化代码
GDT定义
gdt_start: ; 保留标签用于计算大小和跳转 0x7CCA ; GDT以8字节空描述符开头 dd 0x0 ; 4字节 dd 0x0 ; 4字节 ; 代码段GDT:基地址0x00000000,长度0xfffff ; 标志位参考os-dev.pdf第36页 gdt_code: dw 0xffff ; 段长度,位0-15 dw 0x0 ; 段基地址,位0-15 db 0x0 ; 段基地址,位16-23 db 10011010b ; 标志位(8位) db 11001111b ; 标志位(4位)+ 段长度,位16-19 db 0x0 ; 段基地址,位24-31 ; 数据段GDT:基地址和长度与代码段相同,仅标志位修改 gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end: ; 0x7CE2 ; GDT描述符 gdt_descriptor: dw gdt_end - gdt_start - 1 ; 段大小(16位),必须比实际大小小1 dd gdt_start ; GDT起始地址(32位) ; 定义常量供后续使用 CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start
问题分析
看似mov ax, DATA_SEG导致GDT损坏,本质是分页启用后的地址映射错误或内存越界写入:
- 分页映射错位:分页启用前,虚拟地址直接对应物理地址,GDT的物理地址与虚拟地址一致;启用分页后,如果页表未正确映射GDT所在的物理页,或者映射的虚拟地址偏移,后续访问GDT时会指向错误的物理内存,此时任何对该错误地址的写入都会覆盖真实的GDT。
- 栈溢出覆盖:
init_pm将栈顶设为0x90000,而GDT起始地址为0x7CCA,两者距离较近;若kernel_main执行时栈溢出,会直接覆盖GDT区域。 - 段寄存器验证的隐式访问:
mov ax, DATA_SEG后的mov ds, ax等操作会触发CPU对GDT项的验证读取,若此时分页映射错误,CPU读取的是错误物理地址,同时若有其他代码(如野指针)写入该地址,就会损坏真实GDT。
解决方案
- 校验分页映射表:确保GDT所在物理页被正确映射到虚拟地址空间,验证CR3指向的页目录、页表中对应项的物理地址、权限标记(如只读)正确。
- 调整栈空间位置:将栈移至远离GDT的内存区域,比如
0x100000(1MB边界),避免栈溢出覆盖GDT。 - 设置GDT内存只读:在分页表中标记GDT所在页为只读,意外写入会触发页错误,便于定位写入来源。
- 精准跟踪内存写入:用Bochs的内存写入断点,捕捉修改GDT的具体指令,确认是野指针、中断处理还是其他操作导致的写入。
内容的提问来源于stack exchange,提问作者modlegend
相关产品推荐
相关产品推荐

