写点什么

C++ 静态链接符号冲突的几种处理方法

用户头像
ElvinYang
关注
发布于: 2021 年 01 月 24 日
C++静态链接符号冲突的几种处理方法

C++项目中第三方库越来越多,链接时符号冲突的可能性就越来越大。比如项目依赖 libA 和 libB,libA 和 libB 都使用了 libX,在链接项目的时候就很可能产生 libX 的符号冲突,导致链接报错。本文介绍 C++链接符号冲突时的几种应对方法。

(封面图片来自 Chris Sabor on Unsplash


allow-multiple-definition

--allow-multiple-definition-z muldefsNormally when a symbol is defined multiple times, the linker will report a fatal error. These options allow multiple definitions and the first definition will be used.
复制代码

链接器 ld 有个选项 allow-multiple-definition,当有符号重定义时,使用这个选项可以让链接器忽略错误,实际使用的是解析时遇到的第一个定义,后面的符号定义会被忽略。比如可以使用如下命令让项目顺利链接,实际使用的是 libA.a 中的符号定义。

g++ -Wl,--allow-multiple-definition main.cpp libA.a libB.a

这个方法有个缺点。如果 libA 和 libB 使用的是不同版本的 libX,或者说 libA 中的 libX 是经过修改的,那么实际运行的时候,libB 也是用的 libA 中的符号定义,这就可能会出问题。


objcopy --localize-symbol

C++可重定位目标模块(即.o 文件)中有个符号表,包含本模块所有定义和引用的符号信息。符号又分为全局符号和本地符号两种。全局符号指本模块定义的非静态函数和全局变量,其他模块可见,可以供其他模块使用。本地符号指静态函数和静态变量,只能供本模块使用,其他模块不可见。使用 readelf 命令可以查看一个符号是本地还是全局。比如 libB.cpp 内容如下:

int subfunc(int a, int b) {    return a - b;}
int funcB(int a, int b) { return subfunc(a, b);}
static void foo() {}
复制代码

执行下面命令:

# g++ -c libB.cpp# readelf -a libB.oSymbol table '.symtab' contains 11 entries:   Num:    Value          Size Type    Bind   Vis      Ndx Name     5: 0000000000000035     6 FUNC    LOCAL  DEFAULT    1 _ZL3foov     9: 0000000000000000    22 FUNC    GLOBAL DEFAULT    1 _Z7subfuncii    10: 0000000000000016    31 FUNC    GLOBAL DEFAULT    1 _Z5funcBii
复制代码

可以看到 foo 是LOCAL ,funcB 和 subfunc 都是GLOBAL

根据这个特性,可以考虑把库中的符号变成LOCAL符号,只保留一个库中的符号是GLOBAL,这样链接的时候就不会报错符号重定义了。

使用 objcopy 命令的 --localize-symbol 选项可以把一个符号变成LOCAL

--localize-symbol=symbolnameMake symbol symbolname local to the file, so that it is not visible externally. This option may be given more than once.
复制代码

比如使用 objcopy --localize-symbol=_Z7subfuncii 命令。可以看到 subfunc 变成LOCAL了,funcB 还是 GLOBAL

# readelf -a libB.a | grep fun     9: 0000000000000000    22 FUNC    LOCAL  DEFAULT    1 _Z7subfuncii    10: 0000000000000016    31 FUNC    GLOBAL DEFAULT    1 _Z5funcBii
复制代码

这个方法虽然可以消除链接时符号冲突问题,但有新的问题。如果冲突的符号很多,如何进行批量替换呢?可以使用--localize-symbols选项批量替换。

--localize-symbols=filenameApply --localize-symbol option to each symbol listed in the file filename. filename is simply a flat file, with one symbol name per line. Line comments may be introduced by the hash character. This option may be given more than once.
复制代码

--localize-symbols选项可以输入一个文件,文件中每行是一个符号名字。然而问题又来了,如何获取到所有冲突的符号名字?

一种方法是先编译,链接器打印出所有重定义符号错误后,使用 grep, awk 等工具提取出每个冲突的符号,导入到一个文件中。

libB.a(libB.o): In function `subfunc(int, int)':libB.cpp:(.text+0x0): multiple definition of `subfunc(int, int)'libA.a(libA.o):libA.cpp:(.text+0x0): first defined here
复制代码

这个方法对 C 语言的符号比较适用,但是 C++不能直接用,因为 C++中的符号经过了编译器的 name mangling,符号表中的名字不是代码和错误信息中的名字,比如subfunc在符号表中的名字是_Z7subfuncii,而传给 localize-symbol 选项的名字需要符号表中的名字。这就需要几次处理才能得到 mangling 之后的名字。

可以先用readelf命令导出.a 中所有 mangling 之后的名字,然后用c++filt命令把 mangling 之后的名字进行 demangle,得到代码中的变量名字(同时也是编译器报错的符号名字),然后和编译器报错的符号名字匹配一下,这样就可以把所有编译器报错的符号名字变成符号表中的名字,供obj --localize-symbols使用。


objcopy --weaken

C++可重定位目标模块中的全局符号分为强符号和弱符号两种。强符号指的是函数和已经初始化的全局变量。弱符号指的是未初始化的全局变量。链接器使用如下规则来处理重定义的强弱符号:

1. 不允许多个同名的强符号2. 有同名的一个强符号和多个弱符号,使用强符号的实现3. 有同名的多个弱符号,选择任意一个弱符号的实现
复制代码

根据这些信息,当有符号冲突时,肯定是多个强符号,这时可以把其中多个库的符号都变成弱符号,只保留一个强符号。

objcopy 命令的 weaken-symbol 选项可以把一个库的某个符号变成弱符号。weaken-symbol 选项说明如下:

--weaken-symbol=symbolnameMake symbol symbolname weak. This option may be given more than once.
--weaken-symbols=filenameApply --weaken-symbol option to each symbol listed in the file filename. filename is simply a flat file, with one symbol name per line. Line comments may be introduced by the hash character. This option may be given more than once.
--weakenChange all global symbols in the file to be weak. This can be useful when building an object which will be linked against other objects using the -R option to the linker. This option is only effective when using an object file format which supports weak symbols.
复制代码

比如使用 objcopy --weaken libB.a 把 libB.a 中的符号全部变成弱符号。

执行前使用 readelf 命令可以看到 subfunc 是全局函数属于强符号。

# readelf -a libB.a | grep subfunc     8: 0000000000000000    22 FUNC    GLOBAL DEFAULT    1 _Z7subfuncii
复制代码

执行objcopy --weaken libB.a后:

# readelf -a libB.a | grep subfunc     8: 0000000000000000    22 FUNC    WEAK   DEFAULT    1 _Z7subfuncii
复制代码

subfunc 已经变成了弱符号。

这个方法的缺点和 allow-multiple-definition 一样,如果不同库使用的是不同定义的符号,可能会出问题。


objcopy --redefine-sym

以上方法都可以解决链接时的符号冲突问题,但都有隐患,可能造成使用了错误的符号定义。其实即使链接阶段没有报符号冲突错误,也可能会产生符号的错误使用。举个例子:

libA.cpp 内容如下:

int subfunc_c(int a, int b) {    return a + b;}int funcAA(int a, int b) {    return subfunc_c(a, b);}
复制代码

libB.cpp 内容如下:

int subfunc_c(int a, int b);int funcBB(int a, int b) {    return subfunc_c(a, b);}
复制代码

libC.cpp 内容如下:


int subfunc_c(int a, int b) {    return a - b;}
复制代码

main.cpp 内容如下:

#include <cstdio>int funcAA(int, int);int funcBB(int, int);int main() {    printf("%d,", funcAA(2, 1));    printf("%d\n", funcBB(2, 1));    return 0;}
复制代码

libA.cpp 和 libC.cpp 都定义了 subfunc_c,libB.cpp 引用了 subfunc_c。我们把 libB.o 和 libC.o 打包成 libB.a,libA.o 打包成 libA.a,然后编译链接,居然没有报错说 subfunc_c 冲突,而且执行输出的是 3,3,也就是说 libB 使用了 libA 的 subfunc_c,而不是 libB.a 自己的 subfunc_c 定义。

# g++ -c libA.cpp# g++ -c libB.cpp# g++ -c libC.cpp# ar rvs libA.a libA.o# ar rvs libB.a libB.o libC.o# g++ main.cpp libA.a libB.a# ./a.out3,3
复制代码

解释这个问题,得从链接器的符号解析原理说起。

链接器从左往右扫描命令行传入的文件列表,包括.o 和.a 文件。扫描过程中,链接器维护三个集合,分别是:需要合并到可执行文件的符号集合 E,未定义的符号集合 U,已定义的符号集合 D。

1. 初始E,U,D都为空。2. 对于传入的每个文件,判断是.o文件还是.a文件。如果是.o文件,扫描.o文件中的符号,全部添加到E集合中,根据每个符号是否定义,更新对应的未定义集合U和已定义集合D。扫描过程如果发现某个符号已经在已定义集合D中,报错符号重定义。如果是.a文件,.a文件也是由一系列.o文件组成。链接器扫描每个.o文件,判断该.o文件中是否有未定义集合U中需要的符号,如果有,把该.o的符号全部添加到E集合中,更新未定义集合U和已定义集合D,如果没有,直接跳过该.o文件,不会把该.o文件合并到可执行文件中。3. 全部输入文件扫描完成后,如果未定义集合U不是空的,报错有未定义符号。
复制代码

根据这个原理,对于上面举的例子,libB.o 和 libC.o 虽然都打包进 libB.a,但是先扫描 libA.a,subfunc_c 已经在已定义符号集合 D 中了,后面扫描 libB.a,扫描其中的 libC.o,libC.o 只有 subfunc_c 一个符号,而且发现 subfunc_c 已经在已定义集合 D 中了,这时 libC.o 就不会被合并到可执行文件中,也不会报错说符号重定义。

解决这个问题的一种方法是代码里重命名变量和函数,但这种方法比较难实现,尤其是一些第三方库可能连源代码都没有。另外一种方法是使用objcopy --redefine-sym重命名.a 文件中的符号名字。

--redefine-sym old=newChange the name of a symbol old, to new. This can be useful when one is trying link two things together for which you have no source, and there are name collisions.--redefine-syms=filenameApply --redefine-sym to each symbol pair "old new" listed in the file filename. filename is simply a flat file, with one symbol pair per line. Line comments may be introduced by the hash character. This option may be given more than once.
复制代码

--redefine-syms可以传入文件批量替换符号名字,文件中每行是需要修改前的名字和修改后的名字,中间用空格分开。

这个方法可以彻底解决链接时的符号冲突问题,以及运行时多个符号定义导致使用了错误定义的问题。

当然使用这个方法也会遇到 C++ name mangling 的问题,解决方法参考上文objcopy --localize-symbol一节中的介绍。


GCC visibility

前面介绍的都是静态链接库的处理方法,对于动态链接库(.so 文件),编译链接过程并不会报错,但是多个 so 文件定义了同一个符号,导致使用了错误定义的问题还是会出现。可以使用 GCC visibility 特性解决这个问题,具体参考 GCC的符号可见性——解决多个库同名符号冲突问题 一文。


发布于: 2021 年 01 月 24 日阅读数: 34
用户头像

ElvinYang

关注

每个人的一生都是一次远行 2020.05.11 加入

喜欢天文历史滑雪潜水的程序员,个人博客地址 https://yang.observer

评论

发布
暂无评论
C++静态链接符号冲突的几种处理方法