Linux动态链接库so版本兼容

1 Linux下so的特性

1.1 So的内容

nm可以看so的导出符号表

nm -C libsayhello.so 
...
00000000000006a0 T sayhello
...

可看到该so导出了一个函数,sayhello

 

1.2 App运行时加载的so名字

app链接时用到的so库,它在运行的时候就会去找同样名字的so库。比如app链接了libsayhello.so,运行时就会去找libsayhello.so。

我们也可以让app运行时去找另外的名字的so,方法是在编译libsayhello.so的时候,指定编译项soname,比如

-Wl,-soname,libwhatever.so.1

那么当app链接了libsayhello.so之后,运行时会去找libwhatever.so.1。

 

2 不同so符号覆盖问题

2.1 动态库so同名函数覆盖

如果app链接了两个so,两个so里面存在同样签名的函数,但是实现不一样,那么只会保留一个实现。

比如假设libsayhello.so和libsayworld.so里面都有一个函数,叫void say(),

libsayhello.so

void say() { printf("hello
"); }

libsayworld.so

void say() { printf("world
"); }

当app里面调用了say的时候,要么输出hello,要么输出world,不可能即输出hello,又输出world。

 

假设是两个so分别引用了libsayhello.so和libsayworld.so,app再链接这四个so。

这种情况和app直接链接libsayhello.so和libsayworld.so是一样的。

 

当然如果是用dlopen,dlclose,dlsym这种动态加载so的方法,还是可以做到既输出hello,又输出world的。

 

2.2 动态库so同名变量覆盖

同名变量和同名函数也一样存在覆盖的问题。

而且更隐晦的是,变量和函数即使不在.h里定义,直接在.c/.cpp里面定义,也会导出到符号表,造成覆盖。

举个例子:

p1.so

//p1.h
void setInt_1(int i);
void sayInt_1();

//p1.cpp
#include <stdio.h>
#include "p1.h"

int myInt = 1;
void setInt_1(int i) { myInt = i; }
void sayInt_1() { printif("p1 myInt=%d
", myInt); }

p2.so

//p2.h
void setInt_2(int i);
void sayInt_2();

//p2.cpp
#include <stdio.h>
#include "p2.h"

int myInt = 2;
void setInt_2(int i) { myInt = i; }
void sayInt_2() { printif("p2 myInt=%d
", myInt); }

main.c

#include "p1.h"
#include "p2.h"

int main(int argc, char** argv)
{
    setInt_1(100);
    sayInt_1();
    sayInt_2();
    return 0;
}

结果为:

p1 myInit=100
p2 myInit=100

p1和p2都使用了一个同名的全局变量myInt,并且只在.c/.cpp文件里面定义,但是链接到so之后,就会只剩下一个全局变量myInt。所以,调用了p1.so里面的setInit_1函数之后,同时修改了p2.so里面的myInt值。

 

2.3 动态库so类静态变量覆盖

这个问题就更隐晦了!

如果两个so库的cpp文件里都包含了一个类A的定义,类里有一个静态变量s_a:

// p1.cpp & p2.cpp 新加入以下代码
class A
{
public:
    A() 
    {
        printf("A() this=%lld
", (long long)this);
        m_int = new int();
        *m_int = 1;
    }
    ~A() { printf("~A()
"); /*delete m_int;*/ }
private:
    static A s_a;
    int* m_int;
};

A A::s_a;

main.c保持不变。

输出:

A() this=140279519260760
A() this=140279519260760
p1 myInit=100
p2 myInit=100
~A()
~A()

可以看出,同一个对象先被构造了两次,再被析构了两次!

如果去掉注释,delete m_int的话将会crash。

 

2.4 静态库同名函数和同名变量覆盖

静态函数库的情况和动态库的情况类似。

 

2.5 导出脚本对符号覆盖的影响

前面之所以函数和变量会互相覆盖,是因为两个so都导出了相同的符号。

可以使用导出脚本,指定要导出的符号,未指定的就不会被导出。

如果我们指定了导出脚本为:

p1.map

{
  global:
  extern "C++"
  {
    "setInt_1(int)";
    "sayInt_1()";
  };

local:
    *;
};

p2.map

{
  global:
  extern "C++"
  {
    "setInt_2(int)";
    "sayInt_2()";
  };

local:
    *;
};

编译选项如:

g++ -shared -Wl,--version-script=p1.map -o libp1.so p1.o
g++ -shared -Wl,--version-script=p2.map -o libp2.so p2.o

输出为:

A() this=139883050766416
A() this=139883052871760
p1 myInt=100
p2 myInt=2
~A()
~A()

查看一下导出表,可知未导出变量:

 nm libp2.so | grep " D "

查看一下导出表,可知导出了两个函数

nm libp2.so | grep " T "
0000000000000756 T _Z8sayInt_2v
0000000000000740 T _Z8setInt_2i

 

分析整个流程,可知道两个so都分别只导出了两个函数, 各自里面的myInt变量和静态变量A::s_a都保留着,没有互相覆盖。

 

2.6 so之间符号覆盖的解决方案

简单的说就是不允许so之间出现符号覆盖,如果有符号覆盖基本可以肯定是出问题了。

 

那么万一用到的两个不同功能的so,比如是两个不同的开源项目的代码,由于是各自开发,出现了函数或变量名字相同的情况,应该怎么办呢?

答案简单粗暴,也最可靠,那就是改名。

话说回来,没考虑到符号冲突的so,质量要打个问号,能不用还是不要用。。。

 

如果是我们自己开发的so库,要注意

(1) 函数/变量/类加名字空间,如果是c函数就需要加前缀

(2) 不导出不需要的函数/变量/类

 

3 相同so版本兼容问题

3.1 新旧版本的兼容问题

动态库可能有新旧多个版本,并且新旧版本也可能不兼容。

可能有多个app依赖于这些不同版本的so库。

因此当一个so库被覆盖的时候,就可能出问题。

(1) 旧so覆盖新so,可能导致找不到新函数,

(2) 新so覆盖旧so,可能导致找不到旧的函数,

(3) 而更加隐蔽的问题是:新旧so里的同一个函数,语义已经不一样,即前置条件和效果不一样。

 

3.2 新旧版本的兼容关系

(1) 新版本完全兼容旧版本,只是新增了函数。

这种情况只需要新版本即可。

(2) 新版本删除了一些旧版函数,并且保持签名相同的语义相同(可能新增了函数)。

这种情况需要新旧版本同时存在。

(3) 新旧两个版本有一些相同签名但是语义不一样的函数。

这种情况是不予许的。

因为可能出现一个app必须同时依赖新旧两个版本,由于同一签名函数只能有一个实现,也就说另一个实现会被覆盖,就会出错。

 

3.3 新旧版本兼容的解决方法

由此我们知道,有两个解决方案:

(1) 新版本完全兼容旧版本,并保证新版本覆盖旧版本或者新旧版本共存。

这种方法太理想化。

实际情况下,新版本完全兼容旧版本比较难以做到,这要求函数一旦发布就不能改不能删,并且永远必须兼容。

(2) 新版本可以删除一些旧版函数,需保持签名相同的函数语义相同,并保证新旧版本共存。

这是可行的解决方法。

 

3.4 Linux的版本兼容解决方法

首先加版本号保证新旧版本可以共存,不会互相覆盖。版本号形如openssl.so.1.0.0。

其次新版本需保持和旧版本签名相同的函数语义相同。

 

这样已经可以解决问题了,但是还可以优化。

因为版本号分的太细,导致有很多的版本同时存在,其实不需要这么多版本。

仔细考虑一下:

(1) 如果新版本和旧版本的函数完全相同,只是fix bug:那么新版本需要替换掉旧版本,旧版本不需要保留。

(2) 如果新版本新增了函数:那么新版本可以替换掉旧版本,旧版本不需要保留。

(3) 如果新版本删除了函数:那么旧版本就需要保留。

 

如果linux系统下有新旧两个so,它怎么知道可不可以需不需要替换掉旧版本?

答案是通过版本号:

linux规定对于大版本号相同的一系列so,可以选出里面最新的so,用它替换掉其它的so。

这里所谓的替换,其实是建立了一个软链接,型如openssl.so.1,把它指向openssl.so.1.x.x.x系列so里面最新的那一个so。

 

4 Linux下的so规则

总结一下:

 

4.1 so导出规则

(1) 函数/变量/类加名字空间,如果是c函数就需要加前缀

(2) 不导出不需要的函数/变量/类

 

4.2 so版本号规则

版本号升级规则:

(1) 如果新旧版本函数完全相同,那么大版本号不变。

(2) 如果新版本新增了函数,那么大版本号不变。

(3) 如果新版本删除了函数,那么大版本号需要变。

(4) build号每次都变,小版本号按特性或需求变

 

此外还有两个版本号相关规则:

(1) 新版本不允许和旧版本函数签名相同语义不同。

(2) 建立软链接(形如openssl.so.1),指向大版本号相同的一系列so里面最新的so。app应当依赖于so的大版本号,不依赖于更细致的小版本号和build号。

 

4.3 App引用so的规则

(1) 不同库之间不能有同名全局函数/变量,静态函数/变量等。

否则会造成符号覆盖,基本可以肯定会出问题。

(2) 对于一个库,最好只引用一个版本。

如果需要引用同一库的多个版本,那么该库必须保证同名函数/变量的语义一致,除非是动态加载。

但是引用同一库的多个版本即使编译链接通过了,运行时依然可能会有潜在的问题,比如使用了同名的全局文件,信号量等,所以最好就是只引用一个版本。

原文地址:https://www.cnblogs.com/lidabo/p/13862672.html