最近在
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-neede
d
导致的
。
基本上,链接器所做的是仅在紧随其后的文件中查找给定文件(目标文件,静态归档或库)中缺少的符号。当使用普通链接时,如果不使用
--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
》
这四种方法的优先顺序:四
->
二
->
三
->
一