x86-64裸机内核:GCC -fPIC下常量未用RIP相对寻址的问题与解决
问题核心原因
你遇到的是const全局符号重定位项缺失的问题:
- GCC对
extern "C" const全局变量,默认会将其标记为非重定位符号(const被视为编译期固定值),即便开启了-fPIE/-fPIC也不会生成重定位项。 - 链接为
ET_EXEC格式时,链接器会直接将该符号的地址绑定到链接时的固定地址(如0x4072D0),不会生成R_X86_64_RELATIVE或R_X86_64_PC32这类重定位项。 - 而普通全局变量(非const)和函数符号,编译器默认会生成RIP相对寻址代码,链接器也会生成对应重定位项,因此加载器重定位后地址正确。
强制生成重定位项的方法
1. 给常量添加重定位属性
修改Memory.cc中的定义,将常量放入可重定位的只读数据段,强制链接器生成重定位项:
extern "C" const unsigned long kMemHighAddrDirectMappingBase __attribute__((section(".data.rel.ro"))) = 0xFFFF880000000000;
.data.rel.ro是专为可重定位只读数据设计的段,链接器会为该段内的符号生成R_X86_64_RELATIVE重定位项,加载器重定位时会自动修正符号地址。
2. 用包装函数获取地址
编写一个extern "C"的包装函数,返回常量的地址——编译器会为函数生成RIP相对寻址的代码:
extern "C" const unsigned long* get_kMemHighAddrBase() { return &kMemHighAddrDirectMappingBase; }
汇编代码中调用这个函数,就能得到重定位后的正确地址:
call get_kMemHighAddrBase ; rax寄存器中存储的就是重定位后的&kMemHighAddrDirectMappingBase
3. 切换为ET_DYN格式链接
将编译链接选项调整为生成位置无关的ET_DYN格式ELF:
- 在
CFLAGS中保留-fPIE,链接时添加-pie参数:LDFLAGS += -pie - 调整UEFI加载器支持加载
ET_DYN文件,这类文件本身就是位置无关的,链接器会为所有符号生成重定位项,重定位后地址自然正确。
4. 自定义链接脚本强制重定位
编写链接脚本,明确标记.rodata段需要参与重定位:
SECTIONS { . = 0x400000; /* 链接基地址 */ .text : { *(.text) } .rodata : { *(.rodata) KEEP(*(.rodata.kMemHighAddrDirectMappingBase)) } :reloc /* 标记该段需要重定位 */ .data : { *(.data) } .bss : { *(.bss) } }
链接时指定该脚本:LDFLAGS += -T your_kernel.ld,链接器会为.rodata段内的符号生成重定位项。
其他可行解决方案
1. 改为非const变量放入只读段
去掉const修饰,用属性强制放入.rodata段保证只读特性:
extern "C" unsigned long kMemHighAddrDirectMappingBase __attribute__((section(".rodata"))) = 0xFFFF880000000000;
这样编译器会将其当作普通全局变量处理,生成RIP相对寻址代码,链接器也会生成重定位项,同时.rodata的只读属性保证变量无法被修改。
2. 汇编中直接使用RIP相对寻址
如果汇编代码需要访问该常量的地址,直接用lea指令的RIP相对形式:
lea rax, [rip + kMemHighAddrDirectMappingBase] ; rax寄存器中存储的就是重定位后的常量地址
链接器会自动为该指令生成R_X86_64_PC32重定位项,加载器重定位后即可得到正确地址。
内容的提问来源于stack exchange,提问作者Jack Zhao
相关产品推荐
相关产品推荐

