链接器的链接顺序问题

  在学习和使用CMake的时候,最容易遇到的问题就是链接库时报错未定义的符号。从以往的经验来看,一般情况下,如果是使用的第三方库,大概率是因为没有链接到对应的库文件,如果是自己编写的库有可能是因为自己忘记定义或者实现某个已经声明了的符号。不过,最近学习Modern CMake的过程中看到一则例子,发现链接的顺序也会导致未定义符号的问题。因此,记录下来以供将来某一天可以记起这种由于链接顺序导致的未定义符号错误。

示例程序

  同样的,从示例代码入手,如下为该场景中各个源文件的内容。
  main.cpp

#include <iostream>

extern int a;
int main()
{
    std::cout << a << std::endl;
    return 0;
}

  nested.cpp

int b = 123;

  outer.cpp

extern int b;
int a = b;

  CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(link_sequence
    LANGUAGES CXX
)

add_library(outer outer.cpp)
add_library(nested nested.cpp)

add_executable(main main.cpp)
target_link_libraries(main nested outer)

  上述代码使用CMake进行编译,其中编译器选择GNU

分析与说明

  不出意料地,你会得到类似于一下的报错(我使用的链接器版本是GNU ld (GNU Binutils for Ubuntu) 2.42)。

[build] /usr/bin/ld: libouter.a(outer.cpp.o): in function `__static_initialization_and_destruction_0()':
[build] /root/workspace/cmake_demo/outer.cpp:2:(.text+0x0): undefined reference to `b'
[build] /usr/bin/ld: /root/workspace/cmake_demo/outer.cpp:2:(.text+0x4): undefined reference to `b'

  很显然,变量b已经在nested.cpp中声明并定义了,CMakeLists.txt也的确链接了nested库。那么,对b的未定义是怎么来的呢?
  对于GNU的链接器来说,解析符号的工作方式可以大致理解为从左向右处理二进制文件,每当遍历到一个二进制文件时,处理过程可以简单理解如下:
  1.收集当前二进制文件导出的所有未定义符号,并记录下来(未来会匹配其在他处定义的符号)。
  2.根据从当前二进制文件中收集到的所有符号,解析以前记录的未定义符号。
  3.对下一个二进制文件重复此过程。
  根据这个规则,我们再来看上述示例代码的链接过程:
  1.先处理main.o,收集到对未定义符号a的引用。
  2.处理libnested.a,发现一个定义的符号b(未引用符号现在没有b,所以不做处理),当前依然有一个对符号a的引用。
  3.处理libouter.a,收集到对未定义符号b的引用,发现了a的定义,因此可以解析main中对符号a的引用了。
  这个过程中,对a符号的引用能正确处理,将其定义与其引用相匹配。但是,处理libouter.a的时候记录的对符号b的引用没有正确的解析到它的定义。因此链接器会报错。

解决方法

  解决方法其实也比较简单。
  其一,根据链接的规则调换一下nestedouter的链接顺序即可:

target_link_libraries(main outer nested)

  其二,再链接一遍nested即可,即循环引用:

target_link_libraries(main nested outer nested)

  当然,也可以使用链接器的特定标记来处理这种情况,具体可以参考链接器的说明文档。

补充

  需要补充的是,上述情况我只在GNU的链接器上可以复现,试过使用clang作为编译器,则不存在这种情况。这也意味着,链接顺序可能导致的未定义符号与编译器的具体实现相关。


当珍惜每一片时光~