编译中的各种undefined reference解决方式和坑
最近在Linux下编程发现一个诡异的现象,就是在链接一个静态/动态库的时候总是报错,类似下面这样的错误:
(.text+0x13): undefined reference to `func'
关于undefined reference这样的问题,大家其实经常会遇到,在此,详细地示例给出常见错误的各种原因以及解决方法。
1. 链接时缺失了相关目标文件(.o)
测试代码如下:
test.c 中:
int test(){return 0;}
Main.c中:
Int main(){return test();}
然后编译。
gcc -c test.c
gcc –c main.c
得到两个 .o 文件,一个是 main.o,一个是 test.o ,然后我们链接 .o 得到可执行程序:
gcc -o main main.o
这时,你会发现,报错了:
main.o: In function `main':
main.c:(.text+0x7): undefined reference to `test'
collect2: ld returned 1 exit status
这就是最典型的undefined reference错误,因为在链接时发现找不到某个函数的实现文件,本例中test.o文件中包含了test()函数的实现,所以如果按下面这种方式链接就没事了。
gcc -o main main.o test.o
【扩展】:其实上面为了让大家更加清楚底层原因,把编译链接分开了,下面这样编译也会报undefined reference错,其实底层原因与上面是一样的。
gcc -o main main.c //缺少test()的实现文件
需要改成如下形式才能成功,将test()函数的实现文件一起编译。
gcc -o main main.c test.c //ok,没问题了
2. 链接时缺少相关的库文件(.a/.so)
在此,只举个静态库的例子,假设源码如下。
test.c 中:
int test(){return 0;}
先把test.c编译成静态库(.a)文件
gcc -c test.c
ar -rc test.a test.o
至此,我们得到了test.a文件。我们开始编译main.c
gcc -c main.c
这时,则生成了main.o文件,然后我们再通过如下命令进行链接希望得到可执行程序。
gcc -o main main.o
你会发现,编译器报错了:
/tmp/ccCPA13l.o: In function `main':
main.c:(.text+0x7): undefined reference to `test'
collect2: ld returned 1 exit status
其根本原因也是找不到test()函数的实现文件,由于该test()函数的实现在test.a这个静态库中的,故在链接的时候需要在其后加入test.a这个库,链接命令修改为如下形式即可。
gcc -o main main.o ./test.a //注:./ 是给出了test.a的路径
【扩展】:同样,为了把问题说清楚,上面我们把代码的编译链接分开了,如果希望一次性生成可执行程序,则可以对main.c和test.a执行如下命令。
gcc -o main main.c ./test.a //同样,如果不加test.a也会报错
3. 链接的库文件中又使用了另一个库文件
这种问题比较隐蔽,也是我最近遇到的与网上大家讨论的不同的问题,举例说明如下,首先,还是看看测试代码。
Fun.c中:
Int fun(){return 0;}
Test.c 中
Int test(){return fun();}
Main.c中:
Int main(){return test();}
从上可以看出,main.c调用了test.c的函数,test.c中又调用了fun.c的函数。
首先,我们先对fun.c,test.c,main.c进行编译,生成 .o文件。
gcc -c func.c
gcc -c test.c
gcc -c main.c
然后,将test.c和func.c各自打包成为静态库文件。
ar –rc func.a func.o
ar –rc test.a test.o
这时,我们准备将main.o链接为可执行程序,由于我们的main.c中包含了对test()的调用,因此,应该在链接时将test.a作为我们的库文件,链接命令如下。
gcc -o main main.o test.a
这时,编译器仍然会报错,如下:
test.a(test.o): In function `test':
test.c:(.text+0x13): undefined reference to `func'
collect2: ld returned 1 exit status
就是说,链接的时候,发现我们的test.a调用了func()函数,找不到对应的实现。由此我们发现,原来我们还需要将test.a所引用到的库文件也加进来才能成功链接,因此命令如下。
gcc -o main main.o test.a func.a
ok,这样就可以成功得到最终的程序了。同样,如果我们的库或者程序中引用了第三方库(如pthread.a)则同样在链接的时候需要给出第三方库的路径和库文件,否则就会得到undefined reference的错误。
4 多个库文件链接顺序问题
这种问题也非常的隐蔽,不仔细研究你可能会感到非常地莫名其妙。我们依然回到第3小节所讨论的问题中,在最后,如果我们把链接的库的顺序换一下,看看会发生什么结果?
gcc -o main main.o func.a test.a
我们会得到如下报错.
test.a(test.o): In function `test':
test.c:(.text+0x13): undefined reference to `func'
collect2: ld returned 1 exit status
因此,我们需要注意,在链接命令中给出所依赖的库时,需要注意库之间的依赖顺序,依赖其他库的库一定要放到被依赖库的前面,这样才能真正避免undefined reference的错误,完成编译链接。
5. 在c++代码中链接c语言的库
如果你的库文件由c代码生成的,则在c++代码中链接库中的函数时,也会碰到undefined reference的问题。下面举例说明。
首先,编写c语言版库文件:
Test.c 中
Int test(){return 0;}
编译,打包为静态库:test.a
gcc -c test.c
ar -rc test.a test.o
至此,我们得到了test.a文件。下面我们开始编写c++文件main.cpp
Main.cc中:
Int main(){return test();}
然后编译main.cpp生成可执行程序:
g++ -o main main.cpp test.a
会发现报错:
/tmp/ccJjiCoS.o: In function `main':
main.cpp:(.text+0x7): undefined reference to `test()'
collect2: ld returned 1 exit status
原因就是main.cpp为c++代码,调用了c语言库的函数,因此链接的时候找不到,解决方法:即在main.cpp中,把与c语言库test.a相关的头文件包含添加一个extern "C"的声明即可。例如,修改后的main.cpp如下:
extern “C”
{
#include “test.h”
}
Int main(){
return test();
}
g++ -o main main.cpp test.a
再编译会发现,问题已经成功解决。
或者直接在test.h中加入:
ifdef __cplusplus
extern "C"
{
#endif
int cadd(int x, int y);
#ifdef __cplusplus
}
#endif
一样也可以解决
6. 编译参数加入-Wl, --as-needed的好处和注意事项
用--as-needed标志可使链接程序避免以二进制形式链接额外的库。这不仅缩短了启动时间(因为加载器不必每一步都加载所有库),更重要的是,使用--as-needed避免将依赖项添加到二进制文件中,这是其直接或间接依赖项之一的先决条件。
6.1 最终链接失败,未定义符号
这是使用时发生的最常见错误--as-needed。它发生在可执行文件的最后链接阶段(库不会造成问题,因为允许它们具有未定义的符号)。可执行链接阶段之所以消失,是因为在馈送到命令行的库中存在未定义的符号。但是,可执行文件本身未使用该库,因此该库将被删除--as-needed。这通常意味着一个库没有链接到另一个库,而是在使用它,然后依靠最终的可执行文件将它们链接在一起。对于使用该库的开发人员来说,这种行为也是一种额外的负担,因为他们必须检查需求。
解决这类问题的方法通常很简单:只需找到哪个库提供了符号,哪个库就需要它们(来自链接器的错误消息应包含后者的名称)。然后确保从源文件链接库时,它也链接到第一个库。
6.2 执行失败,未定义符号
有时,未定义的符号错误不会在链接时发生,而是在使用--as-need生成的应用程序执行时发生。但是,原因与链接中未定义符号的原因相同:直接链接的库未链接其依赖项之一。它还具有相同的解决方案:查找哪个库包含未定义的符号,并确保将其链接到提供它们的库。
6.3 链接顺序的重要性
尽管所有库都出现在链接行中,但它们只是被忽略而不是完全链接。这导致了与上述相同的问题;在最终链接或执行期间缺少符号。这是因为强制实施了GNU链接程序的行为--as-needed导致的。
基本上,链接器所做的是仅在紧随其后的文件中查找给定文件(目标文件,静态归档或库)中缺少的符号。当使用普通链接时,如果不使用--as-needed,则这不是问题,尽管链接阶段可能存在一些内部缺陷,但是文件链接在一起却没有考虑顺序。但是使用该标志时,不用于解析符号的库将被丢弃,因此不会链接。
6.4错误和正确的链接顺序的编码示例
(这种情况下,libm在对象文件之前被考虑,并且独立于两者的内容而被丢弃,即不会被编译到pro)
$ gcc -Wl,--as-needed -lm someunit1.o someunit2.o -o pro
(这是仅在需要时才能链接libm的正确链接顺序。)
$ gcc -Wl,--as-needed someunit1.o someunit2.o -lm -o程序
通常这种情况下,解决方法是简单地修复链接顺序,以使提供给链接器的库都位于目标文件和静态档案之后。
6.5 实例用法
1. linux下查看一个可执行文件或动态库依赖哪些动态库的办法
readelf -d PyGalaxy.so
ldd PyGalaxy.so
load 动态库过程:基本的说就是符号重定位,然后合并到全局符号表。
- 在编译动态库时:关键的看–as-needed,意思是说:只给用到的动态库设置DT_NEEDED。比如:
g++ -shared PyGalaxy.o -lGalaxyParser -lxxx -lrt -o PyGalaxy.so
像这样链接一个PyGalaxy.so的时候,假设PyGalaxy.so里面用到了libGalaxyParser.so但是没 有用到libxxx.so。查看依赖关系如下:(不加不管什么指定了就加进来)
ocaml@ocaml:~$ readelf -d PyGalaxy.so
0x0000000000000001 (NEEDED) Shared library: [libGalaxyParser.so]
0x0000000000000001 (NEEDED) Shared library: [libxxx.so]
当开启–as-needed的时候,像
g++ -shared -Wl,--as-needed PyGalaxy.o -lGalaxyParser -lxxx -lrt -o PyGalaxy.so
这样链接PyGalaxy.so的时候,查看依赖关系如下:
ocaml@ocaml:~$ readelf -d PyGalaxy.so
0x0000000000000001 (NEEDED) Shared library: [libGalaxyParser.so]
–as-needed就是忽略链接时没有用到的动态库,只将用到的动态库set NEEDED。
3 开启–as-needed的一些常见的问题:
一)链接主程序模块(可执行程序bin)或者是静态库的时的‘undefined reference to: xxx’
如:
g++ -Wl,--as-needed -lGalaxyRT -lc -lm -ldl -lpthread -L/home/ocaml/lib/ -lrt -o mutex mutex.o
假设可执行程序mutex依赖libGalaxyRT.so中的东西。因为gcc对库的顺序要求和–as-needed(因为libGalaxyRT.so在mutex.o的左边,所以gcc认为没有用到它,–as-needed会将其忽略),ld忽略libGalaxyRT.so,定位mutex.o的符号的时候当然会找不到符号的定义,所以‘undefined reference to’这个错误是正常地!
正确的链接方式是:
g++ -Wl,--as-needed mutex.o -lGalaxyRT -lc -lm -ldl -lpthread -L/home/ocaml/lib/ -lrt -o mutex
二) 编译动态库(shared library)的时候会导致一个比较隐晦的错误
编译出来的动态库的时候没有问题,但是加载(link)的时候有“undefined symbol: xxx”这样的错误。
假如像这也链接PyGalaxy.so
g++ -shared -Wl,--as-needed -lGalaxyParser -lc -lm -ldl -lpthread -L/home/ocaml/lib/ -lrt -o PyGalaxy.so PyGalaxy.o
load PyGalaxy.so的时候会有上面的运行时错误!
简单分析原因:因为libGalaxyParser.so在PyGalaxy.o的左边,所以gcc认为没有用到–as-needed将其忽略。但是前面说的动态库符号解析的特点导致ld认为某些符号是加载(link)的时候才去地址重定位的。但是 libGalaxyParser.so已经被忽略了。所以就算你写上了依赖的库,load的时候也会找不到符号,因为编译库时已经被忽略啦,一般链接PyGalaxy.so库时会报未定义错误。但是为什么没有-Wl,–as-needed的时候是正确的呢?没有的话,ld会set NEEDED libGalaxyParser.so(用前面提到的查看动态库 依赖关系的办法可以验证)。load的时候还是可以找到符号的,所以正确,因为没有会将所以的库都编译进去,不管是否需要,只要被列出。
正确的链接方式是:
g++ -shared -Wl,--as-needed PyGalaxy.o -lGalaxyParser -lc -lm -ldl -lpthread -L/home/ocaml/lib/ -lrt -o PyGalaxy.so
三) 对链接顺序导致问题的解决方案
在项目开发过层中尽量让lib是垂直关系,避免循环依赖;越是底层的库,越是往后面写!
例如:
g++ ... obj($?) -l(上层逻辑lib) -l(中间封装lib) -l(基础lib) -l(系统lib) -o $@
这样写可以避免很多问题,这个是在搭建项目的构建环境的过程中需要考虑 清楚地,在编译和链接上浪费太多的生命不值得!
四)通过-(和-)强制repeat
-(和-),它能够强制"The specified archives are searched repeatedly", 这就是我们要找的啦。比如:
g++ -shared -Wl,--as-needed PyGalaxy.o Xlinker "-("-lGalaxyParser -lxxx -lrt"-)" -o PyGalaxy.so
简单解释一下,Xlinker是将后面的一个参数传给ld(这里就是 "-("-lGalaxyParser -lxxx -lrt"-)"),然后-(和-)强制repeat,当然就可以找到了,可以没有顺序。但是这样的repeat需要浪费一些时间。
7. 编译指定库路径
在链接时语句后面添加如下命令:
-Wl,-rpath=《my_thirdparty_lib_path》
对比一下添加前后的Makefile语句。not found时的语句:
更改之后的语句:
来看看更改之后的编译结果:
可以看到,我的libpaho-mqtt3cs.so.1从我在文章开头时的【not found】变成了有来源了,而绿色部分的路径就是我刚刚Makefile中的-Wl,-rpath=之后的路径。
通常设置库的方式有四种:
第一种方法:找到缺少的动态库(由于编译和链接时候的使用到了这个动态库,所以很容易找得到),将其加到/lib,/usr/lib中的一个文件夹下,这几个文件夹是系统默认的搜索路径。将库文件放置在其中,运行时就可以搜索到了。
第二种方法:设置临时增加链接动态库的路径;使用
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:《your_lib_path》
比如我的libpaho-mqtt3cs.so.1在/home/mqtt/MQTT-c/lib目录下,那我使用的是:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/mqtt/MQTT-c/lib
这种方法设置的是临时的,系统重启之后就没了。当然也可以设置为持久的,这里就不过多讲述。
还有一种方法是不常用的,更改配置文件:
第三种方法:/etc/ld.so.cache中缓存了动态库路径,可以通过修改配置文件/etc/ld.so.conf中指定的动态库搜索路径,然后执行ldconfig命令来改变。
第四种就是-Wl,-rpath=《my_thirdparty_lib_path》
这四种方法的优先顺序:四->二->三->一