您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > C/C++/C#

C++程序同时使用同一个库的不同版本,为什么运行时崩溃了?

时间:2021-05-14 14:06:20  来源:  作者:IT刘小虎

最近在编写C++工程时,使用了 MNN 库(libMNN.a,1.0 版本)。是这么使用的:我首先把 MNN 封装了下,打包成一个新的库,暂且称之为 wrApper.so 吧,提供给客户使用。不久后客户反馈问题,使用我提供的 wrapper.so 后,程序编译正常,但是运行时崩溃了。。。分析原因后,知道客户除了使用我提供的 wrapper.so 之外,也使用了 MNN 库,只不过版本是 1.1 版本。

C++程序同时使用同一个库的不同版本,为什么运行时崩溃了?

为什么运行时崩溃了?

把这个问题抽象概括一下,就是一个程序链接不同版本的同一个库,可能会崩溃,这是为什么呢?要如何解决呢?回答这两个问题之前,正好趁机对静态库和动态库做深入一点的理解。

不想看理解,只想看解决方案的直接翻到最后。

静态库(static libraries)

linux 系统中,静态库通常以 .a 结尾,例如 libg++.a。使用 ar 命令可以将一系列目标文件(.o 文件)打包成静态库,因此,静态库的本质其实就是 .o 文件的集合,所以它的基础表现与 .o文件没有区别——在链接时(link time),链接器从静态库中搜索所有的可见全局函数/变量符号,并且把这些符号复制到二进制文件(通常是可执行程序)中。

动态库/共享库(shared libraries)

大多数现代操作系统(Linux、windows 等)都支持动态链接库,也即支持在程序运行时(runtime)链接动态库。动态链接库的文件名在 Linux 中通常以 .so 结尾,在 Windows 中则通常以 .dll 结尾。

使用编译器/链接器可以将 .o 文件打包成动态库,通常来说,要制作动态库,编译器需将 .o 文件编译为 PIC(Position Independent Code,位置独立代码),例如使用 gcc/g++ 编译时指定 -fPIC选项。

$ g++ test.cpp -fPIC ...

动态库允许多个应用程序共享同一个库,并不把动态库的代码复制到二进制文件中,因此相比于链接静态库,同等条件下,链接动态库的的程序具备更小的 size。在运行时,动态库允许二进制文件访问库内的所有符号,即使在链接时没有用到这些符号。

C++程序同时使用同一个库的不同版本,为什么运行时崩溃了?

 

因为动态库通常是 PIC,所以就算多个应用程序链接的是一个动态库,在这些程序中,动态库函数也可以是不同的地址。

动态库的名称中还可以包含版本控制信息,例如 libg++.so.2.7.1,这个版本控制一般依赖于体系架构。动态库的版本信息可以在 SONAME 域中编码。一般来说,动态库的 SONAME 和它的文件名是相同的,例如/usr/lib/libgxx.so.2.1.0 的 SONAME 是 libgxx.so.2.1.0。

值得注意的是,如果我们不更改动态库的 SONAME,更改共享库的文件名,然后指定给链接器更改文件名后的动态库,那么在运行时,二进制文件可能会报错:找不到指定的库。

和静态库不同,动态库程序需要在运行时链接,因此必须保证程序能够找到动态库,通常程序会从一些特殊的目录、环境变量里搜索需要链接的动态库,例如在 Linux 中,程序会从 LD_LIBRARY_PATH环境变量中搜索需要的动态库。

二进制文件本身也可以在其内部编码存储要搜索的动态库所在路径列表(RPATH),这样做更好一点,因为不需要用户再手动指定库的搜索环境变量。

动态库和静态库都是 ELF 文件吗?

C++程序同时使用同一个库的不同版本,为什么运行时崩溃了?

 

静态库

正如前文所述,静态库不是可执行的文件,它只是一系列 .o 文件的集合。鉴于 .o 文件是 ELF 文件,我们可以说静态库是 .o 文件的集合

所谓的“链接静态库到程序”,并不是指静态库本身链接到程序。静态库被传递给链接器后,链接器从静态库中提取出 .o 文件,然后从这些 .o 文件中挑选出自己需要的使用。

这里再强调一下,静态库是 ELF 文件的集合,它本身并不是 ELF 文件。虽说典型的 ELF 解析工具(例如 objdump,readelf,nm)能够解析静态库,但这是因为它们知道静态库的本质,所以解析输出的信息其实是静态库中 .o 文件的信息列表。

例如,我们有目标文件 test.o,执行下面的命令将其打包为静态库 libtest.a:

$ ar cr libtest.a test.o

此时,通过 readelf 命令读取 test.o 和 libtest.a,输出是一致的。

$ readelf -a test.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          680 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10
...
$
$
$ readelf -a libtest.a

File: libtest.a(test.o)
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          680 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 10
...

动态库

动态库是 ELF 文件。假设有 test.cpp 文件,执行下面的命令将其编译为动态库 libtest.so 和静态库 libtest.a:

$ g++ -fPIC test.cpp -shared -o libtest.so
$
$ g++ test.cpp -c -o test.o 
$ ar cr libtest.a test.o

然后通过 readelf 命令读取其 ELF 信息,不难发现相较于静态库 libtest.a,动态库 libtest.so 的 ELF 信息多出一些 program headers。而我们知道,program headers 提供的信息是程序运行时需要的,这一点和动态库在程序运行时被链接相印证

$ readelf -a libtest.so
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              DYN (Shared object file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x5a0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6264 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         7
  Size of section headers:           64 (bytes)
  Number of section headers:         29
  Section header string table index: 26
...
Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  LOAD           0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000754 0x0000000000000754  R E    200000
  LOAD           0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000228 0x0000000000000230  RW     200000
  DYNAMIC        0x0000000000000e18 0x0000000000200e18 0x0000000000200e18
                 0x00000000000001c0 0x00000000000001c0  RW     8
  NOTE           0x00000000000001c8 0x00000000000001c8 0x00000000000001c8
                 0x0000000000000024 0x0000000000000024  R      4
  GNU_EH_FRAME   0x00000000000006d0 0x00000000000006d0 0x00000000000006d0
                 0x000000000000001c 0x000000000000001c  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e00 0x0000000000200e00 0x0000000000200e00
                 0x0000000000000200 0x0000000000000200  R      1
...

就 ELF 文件结构而言,动态库和可执行程序并无太多区别。

unresolved 符号

有了前文的分析,我们现在应该明白动态库和静态库关于 unresolved 符号的区别了。在链接时,静态库中可以有 unresolved(未解决)的符号,只要程序不引用这些 unresolved 符号——不引用含有 unresolved 符号的 .o 文件里的所有符号

因为“静态库被传递给链接器后,链接器从静态库中提取出 .o 文件,然后从这些 .o 文件中挑选出自己需要的使用。”

而动态库是独立的 ELF 文件,如果程序链接的是动态库,那么我们必须 resolve(解决)库里的所有的 unresolved 符号。因此就本例而言,要链接 libpigi.so 库,我们必须同时也引用 Octtools,即使程序没有使用 Octtolls。

动态库不能包含 unresolved 符号,意味着动态库中的所有符号都是可用的。

符号可见性(symbols visibility)

在 Linux 系统中,所有非静态的全局符号对外默认都是可见的,“默认”一词意味着有手段改变符号的可见性。

编译器的 -fvisibility 选项

编写函数或者类时,指定 __attribute__((visibility("default"))) 属性,如此一来,在编译时便可通过 -fvisibility 选项限制相应符号的可见性。详情可参考这里。

链接器的 --version-script 选项

该选项可以为链接器指定版本控制脚本,支持动态库的 ELF 平台都可以使用,通常在创建动态库时使用,以指定所创建库的版本层次结构附加信息。详情可参考这里。下面是一个实例:

/* foo.c */
int foo() { return 42; }
int bar() { return foo() + 1; }
int baz() { return bar() - 1; }

编译上述文件,并且查看符号:

$ gcc -fPIC -shared -o libfoo.so foo.c && nm -D libfoo.so | grep ' T '
0000000000000718 T _fini
00000000000005b8 T _init
00000000000006b7 T bar
00000000000006c9 T baz
00000000000006ac T foo

可见在默认情况下,所有的符号都被导出了。现在我们创建 version 脚本:libfoo.version,限制一些符号的可见性,内容如下所示:

FOO {
  global: bar; baz; # 只导出 bar 和 baz
  local: *;         # 隐藏其他的符号
};

然后把它传递给链接器,重新编译链接,再查看相应的符号:

$ gcc -fPIC -shared -o libfoo.so foo.c -Wl,--version-script=libfoo.version
$ nm -D libfoo.so | grep ' T '
00000000000005f7 T bar
0000000000000609 T baz

与预期一致。

链接器的 --exclude-libs 选项

链接器的这个选项可以不导出(exclude)指定静态库的符号,该选项可以接收多个参数,各个参数用逗号或者冒号分开:

$ ... --exclude-libs lib,lib,lib

--exclude-libs 在 i386 PE 平台和 ELF 平台可用,对于 ELF 平台来说,该选项将会把指定库里的符号改为本地隐藏状态,具体可参考这里。稍后将看到实例。

几个方法的特点

  • 要使用编译器的 -fvisibility 需要从代码层面修改,工作量略大;
  • 编写链接器的版本脚本需要明确知道每一个符号,处理复杂库比较痛苦;
  • 使用链接器的 --exclude-libs,虽然简单,但是要求传递的参数是静态库。

综合考虑,就解决本文开头提出的问题而言,使用链接器的 --exclude-libs 最方便。

实验

实验现象

现在编写简易代码模拟本文开头遇到的问题。首先编写 lib_v1.0.cpp,表示版本 1.0 的库:

// lib_v1.0.cpp
float foo() {
    return 1.0;
}

然后编写 lib_v1.1.cpp,表示版本 1.1 的库:

// lib_v1.1.cpp
float foo() {
    return 1.1;
}

接着编写 wrapper.cpp,调用库函数 foo():

// wrapper.cpp
float foo();

float wfoo() {
    return foo();
}

我们首先将两个版本的库编译出来:

$ g++ -c lib_v1.0.cpp -o lib_v1.0.o
$ ar cr libv1.0.a lib_v1.0.o
$
$ g++ -c lib_v1.1.cpp -o lib_v1.1.o
$ ar cr libv1.1.a lib_v1.1.o

我们还有封装了版本 1.0 的库的 libwrapper.so ,编译之:

$ g++ -fPIC wrapper.cpp -L./ -lv1.0 -shared -o libwrapper.so

此时我们得到了三个库:

  • libv1.0.a
  • libv1.1.a
  • libwrapper.so(封装了 libv1.0)

按照文章开头的问题:程序同时链接 libwrapper.so 和 libv1.1.a 运行时崩溃。对应到本小节的试验,我们编写 main() 函数生成可执行程序:

// test.cpp
#include <IOStream>

float foo();
float wfoo();

int main() {
    float f = foo();
    float wf = wfoo();

    std::cout << f << ", " << wf << std::endl;
    return 0;
}

编译 test.cpp,并同时链接 libv1.1.a 和 libwrapper.so,然后执行之,得到如下输出:

$ g++ test.cpp -L./ -lv1.1 -lwrapper -o test
$ ./test
1.1, 1.1

可以看出,此处的输出与直觉(1.1, 1)并不一致。现在我们交换 libv1.1.a 和 libwrapper.so 的链接顺序:

$ g++ test.cpp -L./ -lwrapper -lv1.1 -o test
$ ./test
1, 1

分析和解决

同样,输出还是与直觉(1, 1.1)不一致,怎么回事呢?结合前文的分析思考下,其实是不难理解的,这个现象背后隐含的原理也可以解释和解决文章开头遇到问题:一个程序链接不同版本的同一个库,可能会崩溃。

隐藏内容,请点击文章末尾的“了解更多”查看。

此时,无论我们如何交换链接顺序,都能得到预期结果:

$ g++ test.cpp -L./ -lwrapper -lv1.1 -o test
$ ./test 
1.1, 1
$
$ g++ test.cpp -L./ -lv1.1 -lwrapper -o test
$ ./test 
1.1, 1

我们的实验虽然简单,但是原理是通用的。将上述方法应用到 MNN 不同版本库的冲突问题解决上,确实解决了问题。

小结

应用程序的编译链接过程,很多时候是处理符号的过程。程序同时链接不同版本的同一个库时,只要解决好符号问题,就能避免冲突崩溃。这方面的知识需要继续提升啊,不然再遇到类似的问题,就要连猜带蒙了。



Tags:C++   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,如有任何标注错误或版权侵犯请与我们联系(Email:2595517585@qq.com),我们将及时更正、删除,谢谢。
▌相关推荐
一、编程语言1.根据熟悉的语言,谈谈两种语言的区别?主要浅谈下C/C++和PHP语言的区别:1)PHP弱类型语言,一种脚本语言,对数据的类型不要求过多,较多的应用于Web应用开发,现在好多互...【详细内容】
2021-12-15  Tags: C++  点击:(17)  评论:(0)  加入收藏
函数调用约定(Calling Convention),是一个重要的基础概念,用来规定调用者和被调用者是如何传递参数的,既调用者如何将参数按照什么样的规范传递给被调用者。在参数传递中,有两个很...【详细内容】
2021-11-30  Tags: C++  点击:(19)  评论:(0)  加入收藏
一、为什么需要使用内存池在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;另一...【详细内容】
2021-11-17  Tags: C++  点击:(37)  评论:(0)  加入收藏
C++编程中,你是否有为 我到底该写个struct还是class 而苦恼过?如果你到现在还不知道该如何选择,那么请求继续阅读,下文或许能给你些建议。问题的产生C++语言继承了 C语言的 stru...【详细内容】
2021-10-18  Tags: C++  点击:(61)  评论:(0)  加入收藏
C++在C的面向过程概念的基础上提供了面向对象和模板(泛型编程)的语法功能。下面以一个简单实例(动态数组的简单封装,包括下标的值可以是任意正数值,并提供边界检查)来说明C++是如...【详细内容】
2021-10-18  Tags: C++  点击:(49)  评论:(0)  加入收藏
0 前言Hello,大家好,欢迎来到『自由技艺』的 C++ 系列专题。代码重用,尽可能避免冗余代码是程序员的一项必备技能,今天就来给大家介绍其中一种:函数装饰器。在设计模式中,与它对应...【详细内容】
2021-09-28  Tags: C++  点击:(75)  评论:(0)  加入收藏
今天我们就来聊一聊 C++ 中的异常机制吧。在学校期间,我们很少会用得上异常机制。然而,工作之后,很多时候却不得不引入异常机制。因为一般情况下,使用函数的返回值来确定函数的...【详细内容】
2021-09-26  Tags: C++  点击:(181)  评论:(0)  加入收藏
一、内存泄漏(memory leak)内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存...【详细内容】
2021-09-03  Tags: C++  点击:(104)  评论:(0)  加入收藏
stack容器#include <iostream>using namespace std;#include <stack>//容器头文件void test(){stack<int>p;p.push(100);p.push(1000);p.push(100);while(!p.empty()){cout<...【详细内容】
2021-08-17  Tags: C++  点击:(81)  评论:(0)  加入收藏
stl 常用遍历算法(for_each transform)示例代码(结论在结尾!!!!)#include<iostream>using namespace std;#include"vector"#include"map"#include"string"#include"list"#in...【详细内容】
2021-08-13  Tags: C++  点击:(89)  评论:(0)  加入收藏
▌简易百科推荐
一、简介很多时候我们都需要用到一些验证的方法,有时候需要用正则表达式校验数据时,往往需要到网上找很久,结果找到的还不是很符合自己想要的。所以我把自己整理的校验帮助类分...【详细内容】
2021-12-27  中年农码工    Tags:C#   点击:(1)  评论:(0)  加入收藏
引言在学习C语言或者其他编程语言的时候,我们编写的一个程序代码,基本都是在屏幕上打印出 hello world ,开始步入编程世(深)界(坑)的。C 语言版本的 hello world 代码:#include <std...【详细内容】
2021-12-21  一起学嵌入式    Tags:C 语言   点击:(10)  评论:(0)  加入收藏
读取SQLite数据库,就是读取一个路径\\192.168.100.**\position\db.sqlite下的文件<startup useLegacyV2RuntimeActivationPolicy="true"> <supportedRuntime version="v4.0"/...【详细内容】
2021-12-16  今朝我的奋斗    Tags:c#   点击:(21)  评论:(0)  加入收藏
什么是shellshell是c语言编写的程序,它在用户和操作系统之间架起了一座桥梁,用户可以通过这个桥梁访问操作系统内核服务。 它既是一种命令语言,同时也是一种程序设计语言,你可以...【详细内容】
2021-12-16  梦回故里归来    Tags:shell脚本   点击:(16)  评论:(0)  加入收藏
一、编程语言1.根据熟悉的语言,谈谈两种语言的区别?主要浅谈下C/C++和PHP语言的区别:1)PHP弱类型语言,一种脚本语言,对数据的类型不要求过多,较多的应用于Web应用开发,现在好多互...【详细内容】
2021-12-15  linux上的码农    Tags:c/c++   点击:(17)  评论:(0)  加入收藏
1.字符串数组+初始化char s1[]="array"; //字符数组char s2[6]="array"; //数组长度=字符串长度+1,因为字符串末尾会自动添&lsquo;\0&lsquo;printf("%s,%c\n",s1,s2[2]);...【详细内容】
2021-12-08  灯-灯灯    Tags:C语言   点击:(46)  评论:(0)  加入收藏
函数调用约定(Calling Convention),是一个重要的基础概念,用来规定调用者和被调用者是如何传递参数的,既调用者如何将参数按照什么样的规范传递给被调用者。在参数传递中,有两个很...【详细内容】
2021-11-30  小智雅汇    Tags:函数   点击:(19)  评论:(0)  加入收藏
一、问题提出问题:把m个苹果放入n个盘子中,允许有的盘子为空,共有多少种方法?注:5,1,1和1 5 1属同一种方法m,n均小于10二、算法分析设f(m,n) 为m个苹果,n个盘子的放法数目,则先对...【详细内容】
2021-11-17  C语言编程    Tags:C语言   点击:(46)  评论:(0)  加入收藏
一、为什么需要使用内存池在C/C++中我们通常使用malloc,free或new,delete来动态分配内存。一方面,因为这些函数涉及到了系统调用,所以频繁的调用必然会导致程序性能的损耗;另一...【详细内容】
2021-11-17  深度Linux    Tags:C++   点击:(37)  评论:(0)  加入收藏
OpenCV(Open Source Computer Vision Library)是一个(开源免费)发行的跨平台计算机视觉库,可以运行在Linux、Windows、Android、ios等操作系统上,它轻量级而且高效---由一系列...【详细内容】
2021-11-11  zls315    Tags:C#   点击:(50)  评论:(0)  加入收藏
最新更新
栏目热门
栏目头条