编译链接的时候静态库顺序导致符号未定义问题详解

本文探讨了在C++编译链接过程中,静态库顺序如何影响符号未定义问题。通过实验分析,揭示了静态库由.o文件组成,链接时按从左到右顺序处理,并不回溯的策略。同时,指出在Mac上使用clang++编译可能不会有此类问题。文章通过不同情况的示例,解释了符号未定义错误的原因,并提出将所有文件打包到一个静态库可以避免顺序问题。

前情概要

接入新库的时候遇到符号位定义问题。有人跟我说改一下库的顺序,就能解决问题。我试了下,嗯,问题是解决了。但是我总觉得库之间不应该有依赖关系,毕竟链接库就是个找符号的问题,如果有依赖,那a,b两个库,相互依赖,顺序怎么整呢?大型的软件,岂不是加一个库就要弄死人?带着这个疑问我做了一些小实验,来看一下一下库位置的顺序到底是如何影响符号未定义问题的。

问题根源

之所以链接顺序改变可以解决符号未定义问题,本质是因为以下两点导致的:

  1. 静态库是由.o文件拼出来的,链接静态库的时候,是以.o文件为单位进行的。

  2. 我们开发,编译机上,gcc/g++在进行链接的时候,使用的是从左到右,逐个文件处理,并且不走回头路的策略。

另外这个行为与编译器有关,在mac上用clang++测试,是没有这些问题的,顺序怎么打乱,都能链接上(确实没有符号定义的除外)。具体是上面哪个因素被打断,目前不确定,以后有需要的时候再研究吧。

我使用下面这个makefile和main.cpp,然后使用a,b,c,d四个cpp文件进行示例分析。注意,每个情况都是独立的a,b,c,d四个文件,不需要的时候你可以随意往文件里写个不相干的函数,不影响结果。不要弄混。

void hello();

int main(int argc, char *argv[])

{

    hello();

    return 0;

}
a.out: libab.a libcd.a

    - g++ main.o libab.a libcd.a

    - g++ main.o libcd.a libab.a





libab.a: a.o b.o

    ar csr libab.a a.o b.o





libcd.a: c.o d.o

    ar csr libcd.a c.o d.o





main.o: main.cpp

    g++ -c main.cpp

a.o: a.cpp

    g++ -c a.cpp

b.o: b.cpp

    g++ -c b.cpp

c.o: c.cpp

    g++ -c c.cpp

d.o: d.cpp

    g++ -c d.cpp

简单说明 这个makefile。

分别编译abcd四个cpp文件,然后把ab两个文件链接成libab.a,把cd两个文件链接成libcd.a,然后用不同的顺序将这两个库跟main.o链接。

下面分情况讨论

情况 1

这种是最常见的。下面展示四个文件的内容,只需要关心a,b,c三个文件即可,d.cpp存在即可。

➜  seq1 git:(master) ✗ cat a.cpp

void hello_in_a()

{

}

➜  seq1 git:(master) ✗ cat b.cpp

void hello_in_b()

{

}

➜  seq1 git:(master) ✗ cat c.cpp

void hello_in_a();

void hello()

{

    hello_in_a();

}

这个情况下,编译结果如下:

g++ main.o libab.a libcd.a

libcd.a(c.o): In function `hello()':

c.cpp:(.text+0x5): undefined reference to `hello_in_a()'

collect2: ld 返回 1

make: [a.out] 错误 1 (忽略)

g++ main.o libcd.a libab.a

也就是如果把ab放前面,cd放后面,就会有未定义符号。失败的那个case,链接器行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libab.a的符号表,没有hello()这个符号,跳过这个库的所有.o文件

  3. 查看libbc.a的符号表,有hello()这个符号,把c.o放进来,现在引入一个新的未定义符号hello_in_a()

  4. 结束,发现未定义符号表还有符号,报undefined reference to `hello_in_a()'

成功的case,行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libbc.a的符号表,有hello()这个符号,把c.o放进来,现在引入一个新的未定义符号hello_in_a()

  3. 查看libab.a的符号表,找到hello_in_a(),把a.o放进来

  4. 结束,未定义符号表为空,ok

情况2

这个情况相对少见,如果出现了还挺麻烦的。这里只关心ac两个cpp文件。内容如下:

➜  seq0 git:(master) ✗ cat a.cpp

void hello_unexist();

void hello()

{

    hello_unexist();

}

➜  seq0 git:(master) ✗ cat c.cpp

#include <stdio.h>





void hello()

{

    printf("hello");

}

运行结果如下:

libab.a(a.o): In function `hello()':

a.cpp:(.text+0x5): undefined reference to `hello_unexist()'

collect2: ld 返回 1

make: [a.out] 错误 1 (忽略)

g++ main.o libcd.a libab.a

可以看到,两个库分别定义了hello(),但是区别在于a.cpp的hello里面调用了一个不存在的hello_unexist方法。

失败的case链接器行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libab.a的符号表,找到hello(),把a.o放进来,引入了未定义符号hello_unexist()

  3. 查看libcd.a的符号表,没有新的符号

  4. 结束,报错

成功的case行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libcd.a的符号表,找到hello(),把c.o引进来。

  3. 查看libab.a,没有未定义符号

  4. 结束

情况3

这个也是比较少见的情况,ab相互依赖,并且他们的.o文件相互错过。使用abcd四个文件,内容如下:

➜  seq2 git:(master) ✗ cat a.cpp

void hello_in_d();

void hello_in_a()

{

    hello_in_d();

}

➜  seq2 git:(master) ✗ cat b.cpp

void hello_in_d();

void hello_in_b()

{

hello_in_d();

}

➜  seq2 git:(master) ✗ cat c.cpp

void hello_in_b();

void hello()

{

    hello_in_b();

}

➜  seq2 git:(master) ✗ cat d.cpp

void hello_in_b();

void hello_in_d()

{

    hello_in_b();

}

 

编译结果如下:

libcd.a(c.o): In function `hello()':

c.cpp:(.text+0x5): undefined reference to `hello_in_b()'

collect2: ld 返回 1

make: [a.out] 错误 1 (忽略)

g++ main.o libcd.a libab.a

libab.a(b.o): In function `hello_in_b()':

b.cpp:(.text+0x5): undefined reference to `hello_in_d()'

collect2: ld 返回 1

make: [a.out] 错误 1 (忽略)

两个case,第一个,先ab的,链接器行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libab.a的符号表,没找到hello(),什么都不放进来

  3. 查看libcd.a的符号表,有一个hello(),把c.o放进来,引入hello_in_b()未定义

  4. 结束,发现有未定义符号hello_in_b,报错

第二个,先cd的,链接器行为如下:

  1. main.o,直接放进来,现在有一个未定义符号void hello()

  2. 查看libcd.a的符号表,有一个hello(),把c.o放进来,引入hello_in_b()未定义

  3. 查看libab.a的符号表,有一个hello_in_b(),把b.o放进来,引入hello_in_d()未定义

  4. 结束,此时有未定义符号hello_in_d,报错

这种情况,不管如何调整顺序,都是不能解决问题的。需要在后面重复一次libab.a或者libcd.a

 

上面的代码可以从https://github.com/johnzeng/linkage_sample.git上面clone到(不过seq目录的命名跟文章的情况罗列顺序不同就是了)

拓展问题:如果所有文件在一个静态库会怎么样

我尝试找一种办法,避免上面的问题。考虑了一下编译连接的原理,我将abcd四个cpp用不同顺序打包到一个静态库。看看会发生什么。修改makefile(只写了修改部分,其他不变):

a.out: libab.a libcd.a libabcd.a libdcba.a

    - g++ main.o libab.a libcd.a

    - g++ main.o libcd.a libab.a

    - g++ main.o libabcd.a

    - g++ main.o libdcba.a





libabcd.a: a.o b.o c.o

    ar csr libabcd.a a.o b.o c.o d.o

libdcba.a: a.o b.o c.o

    ar csr libdcba.a d.o c.o b.o a.o

在上面几个例子里,只有情况2(两个文件分别定义hello()的情况)有报错,并且是使用libabcd.a编译的时候报错:

g++ main.o libabcd.a

libabcd.a(a.o): In function `hello()':

a.cpp:(.text+0x5): undefined reference to `hello_unexist()'

根据上面的分析,我们可以进一步得出结论:

  1. 当所有obj文件在一个静态库的时候,不会出现因为打包顺序导致的符号未定义

  2. 当一个符号有两个定义的时候,根据ar生成静态库的时候输入的文件顺序,搜索符号。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值