如何在缺少重定位信息的情况下完成链接
这个标题看起来有点没事找事的感觉, 没有重定位信息, 我们自然是不希望生成最终的可执行文件的, 不然不是挖坑给自己跳么? 但是问题是, 这似乎是能做到的! 最近遇到一项实验作业, 估计是来自CSAPP的, 第二项要求修复可重定位文件 b.o 的可重定位信息, 以方便和 a.o 一起链接后 a.o 中的 main 函数能成功调用 b.o 中的函数. 但是奇妙的情况出现了, 这个 a.o 在第一项要求是可以单独链接的. 明明需要调用 b.o 中的函数, 为什么能单独链接呢?
我考虑了一下如何从源码角度构造这个情景, 最后想到了弱符号链接. 一开始我准备了这样两份源文件:
// file: a.c
int foo;
int main() {
((void(*)())foo)();
}
// file: b.c
#include <stdio.h>
void foo() {
printf("Hello\n");
}
在 a.c 中, foo 由于没有初始化, 是弱符号, 而 b.c 中的 foo 由于有定义, 所以是强符号. a.c 既可以单独编译链接, 也可以和 b.c 一起编译链接(感谢 C 语言的强制类型转换). 与 b.c 一起编译链接的话, 由于 int 类型的 foo 是弱符号, 这个符号最终的地址被定位到了函数 foo 的起始地址. 但是在执行的时候却发生了段错误. 反汇编观察了一下发现传给 call 指令的操作数是通过寻址操作得来的, 最终 call 指令要跳转的地址不是 foo 首指令的地址, 而是 foo 指令串的前 4 个字节组成的值. 这是我忽略了变量名的含义造成的.
变量名作为符号的值是该变量的地址, 而在使用变量的地方, 编译器会自觉地生成寻址的代码. 函数名作为符号的值自然是首指令的地址. 结果在强符号代替弱符号的过程中, 变量 foo 的值变成了函数地址, 但是变量的语义没有变, 所以依然产生了寻址操作, 获取了错误的地址值. 所以 a.c 中正确的写法应该是:
// file: a.c
int foo;
int main() {
((void(*)())&foo)();
}
在一起链接时, ld 一直报错符号 foo 没有按 4 字节对齐, 大概变量都是要求 4 字节对齐的而函数首指令代码没有这个要求吧, 为了消除 b.c 导致的报警, 可以这么写:
// file: b.c
#include <stdio.h>
void __attribute__ ((aligned (4))) foo () {
printf("Hello\n");
}
至于要单独编译链接 a.c 造成它不需要 b.c 的函数的假象, 可以在调用之前先判断一下 foo 的值是否为 0. 在 a.c 这个编译单元中, int foo;
应该算是 tentative declaration.
tentative declaration 到最后都没有遇到 initializer 的话, 则会自动初始化为 0.
果然还是出题的人比较厉害…