最近在 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

    这四种方法的优先顺序:四 -> -> ->