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

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。


问题根因

核心问题出在保护模式切换流程缺失关键步骤,其次是编译链接参数存在冗余冲突:

  1. 32位x86下的近调用call指令总长度为5字节(1字节操作码+4字节相对偏移),16位模式下同指令长度为3字节(1字节操作码+2字节相对偏移)。16位模式反汇编看到的3字节call、跳转到0x4f的现象,本质是把32位指令按16位规则截断译码的结果——运行时CPU实际就是在16位默认操作数的状态下执行了这段32位代码,才会出现和16位反汇编完全一致的跳转错误。
  2. 触发运行时16位译码的原因是:设置CR0寄存器的PE位启用保护模式后,没有立刻执行跨段远跳转(长跳转)刷新CPU的指令预取队列、加载正确的32位代码段选择子到CS寄存器。x86架构在切换CR0的模式位后,CS寄存器的隐藏缓存部分仍然保留着实模式下的16位默认操作数配置,只有执行远跳转强制加载CS段描述符,才会把代码段的D位(32位段需设为1,标识默认操作数为32位)加载到CPU的指令译码逻辑中,否则CPU会一直按照16位规则译码后续所有指令。
  3. 编译链接配置存在两个隐患:
    • ld命令行同时传了-Ttext 0x7e00、-Ttext-segment 0x7e00两个加载基址参数,又使用自定义链接脚本指定基址,重复配置可能导致ld段布局计算异常。
    • 开启了-ffunction-sections编译选项,该选项会将每个函数放到独立的.text.<函数名>段中,但链接脚本只收集了*(.text)段,没有收集*(.text.*)段,会导致部分函数代码被ld按默认规则放置,布局不符合预期。

修复方案

  1. 修正保护模式切换流程:在设置完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内核入口
  1. 检查GDT中32位代码段的描述符配置,确保D位(段描述符第二个双字的第22位)设置为1,标识这是32位代码段。
  2. 清理冗余编译链接参数:
    • 删除ld命令行中的-Ttext 0x7e00和-Ttext-segment 0x7e00参数,仅保留链接脚本中的地址配置,避免地址冲突。
    • 修改链接脚本,增加对独立函数、数据段的收集,修改后内容如下:
ENTRY(main)
SECTIONS {
        . = 0x7e00;
        .text : { *(.text .text.*) }
        .data : { *(.data .data.*) }
        .bss  : { *(.bss .bss.*) }
}
  1. 如果不需要做函数级别的段裁剪(比如删除未引用的函数),可以直接去掉GCC参数中的-ffunction-sections选项,简化段配置。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 07:06:09