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

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损坏,本质是分页启用后的地址映射错误或内存越界写入:

  1. 分页映射错位:分页启用前,虚拟地址直接对应物理地址,GDT的物理地址与虚拟地址一致;启用分页后,如果页表未正确映射GDT所在的物理页,或者映射的虚拟地址偏移,后续访问GDT时会指向错误的物理内存,此时任何对该错误地址的写入都会覆盖真实的GDT。
  2. 栈溢出覆盖:init_pm将栈顶设为0x90000,而GDT起始地址为0x7CCA,两者距离较近;若kernel_main执行时栈溢出,会直接覆盖GDT区域。
  3. 段寄存器验证的隐式访问:mov ax, DATA_SEG后的mov ds, ax等操作会触发CPU对GDT项的验证读取,若此时分页映射错误,CPU读取的是错误物理地址,同时若有其他代码(如野指针)写入该地址,就会损坏真实GDT。

解决方案

  1. 校验分页映射表:确保GDT所在物理页被正确映射到虚拟地址空间,验证CR3指向的页目录、页表中对应项的物理地址、权限标记(如只读)正确。
  2. 调整栈空间位置:将栈移至远离GDT的内存区域,比如0x100000(1MB边界),避免栈溢出覆盖GDT。
  3. 设置GDT内存只读:在分页表中标记GDT所在页为只读,意外写入会触发页错误,便于定位写入来源。
  4. 精准跟踪内存写入:用Bochs的内存写入断点,捕捉修改GDT的具体指令,确认是野指针、中断处理还是其他操作导致的写入。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 07:55:09