zoukankan      html  css  js  c++  java
  • 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) 对于一个库,最好只引用一个版本。

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

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

  • 相关阅读:
    go 字符串拼接
    go中字符串的切片和索引使用
    golang 日志输出到指定位置代码
    go命令手动加载所有的安装包
    gin框架入门前后端gin-admin开源项目学习
    go container/list双向链使用实例
    使用 container/list 包 手写实现一个双向链表,将 101、102 和 103 放入其中并打印出来
    Hibernate基础增删改查语法
    Eclipse集成Hibernate操作Sqlserver实例
    sqlserver存储过程批量插入数据
  • 原文地址:https://www.cnblogs.com/lidabo/p/13862672.html
Copyright © 2011-2022 走看看