这个标题看起来有点没事找事的感觉, 没有重定位信息, 我们自然是不希望生成最终的可执行文件的, 不然不是挖坑给自己跳么? 但是问题是, 这似乎是能做到的! 最近遇到一项实验作业, 估计是来自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.

果然还是出题的人比较厉害…