x86 32位保护模式下段寄存器值不符合预期的原因排查
我已进入32位保护模式,通过C语言与汇编代码加载全局描述符表(GDT),并编写check_gdt()函数打印各段寄存器的值,但实际输出与预期不符。相关代码如下:
check_gdt()函数代码
void check_gdt() { uint16_t cs, ds, es, fs, gs, ss; // Inline assembly to get the values of segment registers asm volatile("mov %%cs, %0" : "=r"(cs)); asm volatile("mov %%ds, %0" : "=r"(ds)); asm volatile("mov %%es, %0" : "=r"(es)); asm volatile("mov %%fs, %0" : "=r"(fs)); asm volatile("mov %%gs, %0" : "=r"(gs)); asm volatile("mov %%ss, %0" : "=r"(ss)); // Print the segment register values printf("CS: 0x%x\n", cs); printf("DS: 0x%x\n", ds); printf("ES: 0x%x\n", es); printf("FS: 0x%x\n", fs); printf("GS: 0x%x\n", gs); printf("SS: 0x%x\n", ss); }
GDT初始化代码
void initGdt(){ gdt_ptr.limit = (sizeof(struct gdt_entry_struct) * 5) - 1; gdt_ptr.base = (uint32_t)&gdt_entries; setGdtGate(0,0,0,0,0); // Null segment _________________ _______________ // Explanation: |P|DPL|S|E|DC|RW|A| |G|DB|L|Res| | setGdtGate(1,0,0xFFFFFFFF, 0x9A, 0xCF); // Kernel code segment : access = 0x9A =>|1|00 |1|1|0 |1 |0|, gran = 0xCF => |1|1 |0|0 |1111| setGdtGate(2,0,0xFFFFFFFF, 0x92, 0xCF); // Kernel data segment : access = 0x92 =>|1|00 |1|0|0 |1 |0|, gran = 0xCF => |1|1 |0|0 |1111| // _________________ _______________ setGdtGate(3,0,0xFFFFFFFF, 0xFA, 0xCF); // User code segment : access = 0xFA =>|1|11 |1|1|0 |1 |0|, gran = 0xCF => |1|1 |0|0 |1111| setGdtGate(4,0,0xFFFFFFFF, 0xF2, 0xCF); // User data segment : access = 0x9A =>|1|11 |1|0|0 |1 |0|, gran = 0xCF => |1|1 |0|0 |1111| // ----------------- --------------- gdt_flush((uint32_t) &gdt_ptr); } void setGdtGate(uint32_t num, uint32_t base, uint32_t limit, uint8_t access, uint8_t gran){ gdt_entries[num].base_low = (base & 0xFFFF); // Assign lower base 16 bit gdt_entries[num].base_middle = (base >> 16) & 0xFF; // Assign middle base 8 bit gdt_entries[num].base_high = (base >> 24) & 0xFF; // Assign higher base 8 bit gdt_entries[num].limit_low = (limit & 0xFFFF); // Assign lower limit 16 bit gdt_entries[num].flags = (limit >> 16) & 0x0F; // Assign flags lower 4 bit gdt_entries[num].flags |= (gran & 0xF0); // Assign flags upper 4 bit gdt_entries[num].access = access; // Assign access 8 bit }
gdt_flush汇编函数
gdt_flush: ; [esp] Return address (from CALL) ; Load the address of the GDT descriptor MOV eax, [esp + 4] ; To load the content of [esp+4] address which is the first argument of gdt_flush((uint32_t) &gdt_ptr) LGDT [eax] ; Load the GDT into the CPU MOV ax, 0x10 ; Load kernel data segment selector (0x10) MOV ds, ax ; Set data segment register to kernel data segment MOV es, ax ; Set extra segment register to kernel data segment MOV fs, ax ; Set general-purpose segment to kernel data segment MOV gs, ax ; Set general-purpose segment to kernel data segment MOV ss, ax ; Set stack segment to kernel data segment JMP 0x08:.flush ;Far jump to selector 0x08 (code segment) and the address of flush ; Entry 1: Kernel code segment (selector = 0x08) .flush: RET
预期结果
CS : 0x08 DS : 0x10 ES : 0X10 FS : 0X10 GS : 0X10 SS : 0X10
但实际得到错误结果,请问原因是什么?
问题排查与解决方案
1. 内联汇编的寄存器覆盖问题
你当前的内联汇编是分6个独立块执行的,编译器可能会复用寄存器,导致前面读取的段寄存器值被后面的赋值覆盖。比如第一次读取CS到某个寄存器,第二次读取DS时又用同一个寄存器,就会把CS的值冲掉。
修复方案:将所有段寄存器读取合并到一个内联汇编块中,一次性完成所有赋值,避免寄存器复用:
void check_gdt() { uint16_t cs, ds, es, fs, gs, ss; asm volatile( "mov %%cs, %0\n" "mov %%ds, %1\n" "mov %%es, %2\n" "mov %%fs, %3\n" "mov %%gs, %4\n" "mov %%ss, %5\n" : "=r"(cs), "=r"(ds), "=r"(es), "=r"(fs), "=r"(gs), "=r"(ss) ); printf("CS: 0x%x\n", cs); printf("DS: 0x%x\n", ds); printf("ES: 0x%x\n", es); printf("FS: 0x%x\n", fs); printf("GS: 0x%x\n", gs); printf("SS: 0x%x\n", ss); }
2. GDT结构体的内存对齐问题
如果struct gdt_entry_struct和struct gdt_ptr_struct没有添加__attribute__((packed))属性,编译器会自动添加填充字节,导致GDT条目或GDT指针的内存结构不符合CPU要求的格式,CPU无法正确解析GDT。
检查点:确认结构体定义是否包含打包属性,比如:
struct gdt_entry_struct { uint16_t base_low; uint16_t limit_low; uint8_t base_middle; uint8_t access; uint8_t flags; // 包含4位limit高和4位flags uint8_t base_high; } __attribute__((packed)); struct gdt_ptr_struct { uint16_t limit; uint32_t base; } __attribute__((packed));
3. GDT条目成员顺序错误
GDT条目的标准内存布局是:[16位limit低] → [16位base低] → [8位base中] → [8位access] → [4位limit高+4位flags] → [8位base高]
如果你的struct gdt_entry_struct成员顺序和上面不一致(比如把limit_low和base_low放反,或者flags和access顺序错误),会导致CPU加载的段描述符完全错误,段寄存器的值自然异常。
4. printf的干扰
如果你的printf依赖的C库没有适配32位保护模式,可能会在内部修改段寄存器(比如切换到用户段、或者使用错误的段选择符)。建议先用底层方式(比如直接写VGA显存)打印段寄存器值,排除printf的影响:
void print_hex(uint16_t val) { // 简化的VGA显存打印十六进制值,假设VGA显存基地址是0xB8000 char *vga = (char*)0xB8000; char hex[] = "0123456789ABCDEF"; vga[0] = '0'; vga[1] = 0x07; vga[2] = 'x'; vga[3] = 0x07; vga[4] = hex[(val >> 12) & 0xF]; vga[5] = 0x07; vga[6] = hex[(val >> 8) & 0xF]; vga[7] = 0x07; vga[8] = hex[(val >> 4) & 0xF]; vga[9] = 0x07; vga[10] = hex[val & 0xF]; vga[11] = 0x07; } void check_gdt() { uint16_t cs, ds; asm volatile("mov %%cs, %0\nmov %%ds, %1" : "=r"(cs), "=r"(ds)); print_hex(cs); }
5. GDT加载后的上下文干扰
确保调用initGdt()后立即调用check_gdt(),中间不要插入其他可能修改段寄存器的代码(比如异常处理、库函数调用)。
内容的提问来源于stack exchange,提问作者baponkar

