32位内核开发中ld生成的call指令跳转地址错误问题
自定义内核32位保护模式下call指令跳转错误问题
问题背景
我正在开发一款小型自定义内核,目前已完成启用A20地址线、加载GDT、进入32位保护模式、从磁盘加载内核的流程。在编写C代码测试函数调用功能时,发现链接器ld生成的call指令存在跳转目标错误的问题,相关代码与配置如下:
相关代码与配置
测试用C代码
asm(".code32"); int a(int *d); int main() { int c = 0; c = a(&c); c = a(&c); c = a(&c); while(1); } int a(int *d) { int b = 1; b *= 5; b *= 7; b -= 6; b *= 9; *d = b; return b; }
精简版Makefile配置
KERNEL-PARTFILE = int/parts/detailed-boot.prt KERNEL-OBJECTFILE = int/detailed-boot.o KERNEL-SOURCEFILE = src/detailed-boot.c GCC = ~/opt/cross/bin/i686-elf-gcc LD = ~/opt/cross/bin/i686-elf-ld VM = qemu-system-x86_64 SYSFILE = lizard.bin kernel: $(GCC) -ffunction-sections -ffreestanding $(KERNEL-SOURCEFILE) -o $(KERNEL-OBJECTFILE) -nostdlib -Wall -Wextra -O0 $(LD) -o $(KERNEL-PARTFILE) -Ttext 0x7e00 --oformat binary $(KERNEL-OBJECTFILE) -e main --script=LDfile -O 0 -Ttext-segment 0x7e00 --verbose run: $(VM) $(SYSFILE) debug: $(VM) $(SYSFILE) -gdb tcp:localhost:6000 -S
链接脚本LDfile内容
ENTRY(main) SECTIONS { . = 0x7e00; .text . : { *(.text) } .data . : { *(.data) } .bss . : { *(.bss ) } }
已做排查
- 对编译生成的目标文件
int/detailed-boot.o使用交叉版objdump反汇编,结果无异常,call指令均正确指向函数a的入口地址,反汇编结果与预期一致。 - 对链接生成的纯二进制内核文件
int/parts/detailed-boot.prt使用ndisasm默认16位模式反汇编,可见三处call指令均跳转到0x4F地址(即main函数末尾的死循环位置),而非预期的函数a入口0x51地址,对应反汇编片段如下:
0000001F E82D00 call 0x4f 00000031 E81B00 call 0x4f 00000043 E80900 call 0x4f 0000004E 90 nop 0000004F EBFD jmp short 0x4e
- 后续补充测试:使用
ndisasm -b 32指定32位模式反汇编二进制文件时结果显示正确,但完成GDT完整配置后通过make debug启动QEMU+GDB调试,运行时CPU仍然跳转到0x4F地址,未正确进入函数a。
本次使用的工具链为按照osdev官方GCC交叉编译器教程构建的i686-elf交叉编译工具链,安装路径为~/opt/cross。
问题根因
核心问题出在保护模式切换流程缺失关键步骤,其次是编译链接参数存在冗余冲突:
- 32位x86下的近调用
call指令总长度为5字节(1字节操作码+4字节相对偏移),16位模式下同指令长度为3字节(1字节操作码+2字节相对偏移)。16位模式反汇编看到的3字节call、跳转到0x4f的现象,本质是把32位指令按16位规则截断译码的结果——运行时CPU实际就是在16位默认操作数的状态下执行了这段32位代码,才会出现和16位反汇编完全一致的跳转错误。 - 触发运行时16位译码的原因是:设置CR0寄存器的PE位启用保护模式后,没有立刻执行跨段远跳转(长跳转)刷新CPU的指令预取队列、加载正确的32位代码段选择子到CS寄存器。x86架构在切换CR0的模式位后,CS寄存器的隐藏缓存部分仍然保留着实模式下的16位默认操作数配置,只有执行远跳转强制加载CS段描述符,才会把代码段的D位(32位段需设为1,标识默认操作数为32位)加载到CPU的指令译码逻辑中,否则CPU会一直按照16位规则译码后续所有指令。
- 编译链接配置存在两个隐患:
- ld命令行同时传了
-Ttext 0x7e00、-Ttext-segment 0x7e00两个加载基址参数,又使用自定义链接脚本指定基址,重复配置可能导致ld段布局计算异常。 - 开启了
-ffunction-sections编译选项,该选项会将每个函数放到独立的.text.<函数名>段中,但链接脚本只收集了*(.text)段,没有收集*(.text.*)段,会导致部分函数代码被ld按默认规则放置,布局不符合预期。
- ld命令行同时传了
修复方案
- 修正保护模式切换流程:在设置完CR0的PE位后,立刻执行远跳转加载32位代码段,示例如下:
mov eax, cr0 or eax, 1 mov cr0, eax ; 必须添加下面这行远跳转,CODE_SEG替换为你GDT中32位代码段对应的选择子 jmp CODE_SEG:protected_mode_entry [BITS 32] protected_mode_entry: ; 后续再初始化DS、ES、SS等其他段寄存器,完成后再跳转到C内核入口
- 检查GDT中32位代码段的描述符配置,确保D位(段描述符第二个双字的第22位)设置为1,标识这是32位代码段。
- 清理冗余编译链接参数:
- 删除ld命令行中的
-Ttext 0x7e00和-Ttext-segment 0x7e00参数,仅保留链接脚本中的地址配置,避免地址冲突。 - 修改链接脚本,增加对独立函数、数据段的收集,修改后内容如下:
- 删除ld命令行中的
ENTRY(main) SECTIONS { . = 0x7e00; .text : { *(.text .text.*) } .data : { *(.data .data.*) } .bss : { *(.bss .bss.*) } }
- 如果不需要做函数级别的段裁剪(比如删除未引用的函数),可以直接去掉GCC参数中的
-ffunction-sections选项,简化段配置。
内容的提问来源于stack exchange,提问作者saltq。
相关产品推荐
相关产品推荐

