linux系统编译链接总结--高级c/c++编译技术读后总结(下)

本篇开始总结动态库设计的进阶篇。
一 : 动态库的设计,进阶篇
动态链接的重要原则就是不同进程共享同一个动态库的代码段,但不共享数据段。每个加载了动态库的进程都会提供一份自己的数据副本给动态库代码段使用。同时利用内存映射,将同一个动态库的代码段映射到不同的进程空间上。
1.由于动态库是运行的时候加载到进程的内存地址空间的,所以只有当运行时将该库加载到内存的时候,库中各个函数等的地址才能确定,所以要实现动态库的动态加载,就需要装载器提供一定的符号解析功能。只有对外提供的ABI接口才需要绝对地址,而库内部的符号则可以利用与ABI符号的offset来找到,所以,在编译链接生成动态库的时候,就会将那些需要在加载的时候重新确定地址的符号写在.rel.dyn节中,加载器会读取该节的内容,将需要重新确定地址的符号根据本进程的情况来确定其地址。其他内部符号则根据offset访问。为了能对内部符号通过offset访问,每个库提供了一个.got节,就是全局偏移表,每个进程会维护自己的got节,就像维护自己的数据节一样,只共用动态库的代码节。全局偏移表提供了内部函数的偏移offset。这也是编译的时候需要提供-fPIC选项的原因,该选项会告诉编译器生成got节。
二:动态链接时的重复符号问题
1.处理不同的库之间重复符号问题,对于c语言来说,只要函数名,或者结构体,数据类型名相同,就算重复符号,但对于c++来说,函数可以重载,所以,函数名相同,参数列表不同的,也不算重复,另外,c++还支持命名空间。
对于c和c++来说,static声明的局部函数不认为是重复符号。
2.对于静态库,链接进同一个二进制文件的静态库不能有重复的符号,否则会报链接错误。
3对于动态库,对于不同的库之间的相同符号,则有不同的优先级处理策略,优先级如下:
a)客户二进制文件符号
b)动态库可见符号
c)不参与链接的符号,即内部符号。
所以,动态库链接的时候,会根据上面的优先级,将所有重复的符号全部解析成最高优先级的符号。
具体例子可以参考本书P156 singleton的例子。
三:动态库的版本控制
动态库的版本号一般分为三部分,主版本号(M),次版本号(m),修订版本号(p)。
Linux动态库版本控制方案:
1.基于soname的版本控制方案
一般soname就是库名称加主版本号。所以,二进制可执行文件链接该动态库时,可以指定所需要的动态库的soname,然后,在动态库放置的目录下,对要使用的动态库建立一个软链接,软链接的名称就是soname,这样当动态加载时,会找到这个软链接,然后将该软链接指定的具体的动态库加载到内存中,这样的好处是在动态库的目录中可以同时存在不同次版本或者补丁版本的动态库,只需要利用软链接的指向,就可以指定所要使用的库,不需要重新链接可执行文件。这种方法一般适用于同一个主版本的各个库的升级。因为同一个主版本的不同此版本的库的soname是相同的。
gcc -shared input_file -l:libxyz.so.1 -o obj_binary //注意-l后面的冒号。
如果该目录中只有一个版本的libxyz,也可以直接-lxyz. 这种情况就不用冒号了。
当然,创建该动态库时也要指定其soname:

gcc -shared inputs_file -wl,-soname,<soname> -o lib_file_name

采用这种方法的优势为:
a.不需要重新构建客户二进制程序
b.不需要删除或者覆盖当前版本的动态库文件,新旧文件可以同时存放在相同的目录下。
c.可以简单优雅的实时配置客户二进制程序使用更新版本的动态库
d.如果升级新版本动态库后出现问题,可以优雅的将客户二进制程序使用的动态库恢复到老的版本。
ldconfig工具可以显示所有依赖的动态库文件,解析每个动态库文件的soname信息,并为每个解析的soname创建相应的软链接。
2.基于符号的版本控制方案
优点:
动态库二进制文件可以携带多个不同版本的相同符号,不同的客户二进制文件可能需要不同版本的动态库文件,那么只需加载同一份二进制文件,并连接特定的版本即可。也就是说编译动态库的时候,通过一个版本控制脚本来控制编译器,指定接口的版本属性,所以一个动态库中可以包含不同版本的接口。
比如某个linux系统上有些可执行文件需要链接libx1.0, 有些需要用到libx2.0,如果用soname版本控制,该系统上需要同时部署1.0和2.0的库,如果用基于符号的版本控制,则可以只部署一个库,该库中同时包含了1.0和2.0的内容和接口。
a. 基于符号的版本控制方法通过链接器版本控制脚本和》symver汇编器指令来实现。
实例:

simple.c :
int first_function(int x)
{
    return (x+1);
}
int second_function(int x)
{
    return (x+2);
}
int third_function(int x)
{
    return (x+2);
}
simpleVersionScript:
LIBSIMPLE_1.0 {
    globle:
        first_function; second_function;
    local:
        *;
}
gcc -fPIC -c simple.c
gcc -shared simple.o -wl,--version-script,simpleVersionScript -o libsimple.so.1.0.0

链接器会解析脚本文件中的信息,并将这些信息写入elf文件版本控制字段中。
下面介绍.symber汇编指令:
我们假设一种情况,动态库版本间的函数签名没有改变,但是其功能实现发生了很大的改变,比如说,某个函数原本用于返回链表元素的个数,但在最新版本中该函数被重新设计,用于返回该链表占用的总字节数,代码如下:

//VERSION 1.0
unsigned long list_occupancy(struct List* pStart)
{
    return nElements;
}
//VERSION 2.0
unsigned long list_occupancy(struct List* pStart)
{
    return nElements * sizeof(struct List);
}

正如我们前面提到的那样,该版本控制技术会在二进制文件中提供多个不同版本的符号,但问题时,如果直接编译两个不同版本的函数, 链接器则会提示“符号重复”, 幸运的是,gcc编译器支持自定义的.symver汇编指令,代码如下:

__asm__(".symver list_occupancy_1_0, list_occupancy@MYLIBVERSION_1.0");
unsigned long list_occupancy_1_0(struct List* pStart)
{
    return nElements;
}
__asm__(".symver list_occupancy_2_0, list_occupancy@@MYLIBVERSION_2.0")//多个@是表明默认版本
unsigned long list_occupancy_2_0(struct List* pStart)
{
    return nElements * sizeof(struct List);
}

内部函数名称不同避免了符号重复,但外部看到的函数名称依然是list_occupancy(). 所以使用1.0版本的二进制会自动链接1.0的函数版本,使用2.0的二进制文件会自动链接2.0版本的函数。两个@@表明是默认版本,所以在上面的例子中,默认版本为2.0版本。

下面介绍一个具体的列子:
第一阶段:

simple.h :
int first_function(int x);
int second_function(int x);
int third_function(int x);
simple.c:
#include <stdio.h>
#include "simple.h"
int first_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 1);
}
int second_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 2);
}
int third_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 3);
}
simpleVersionScript :
LIBSIMPLE_1.0 {
    globle:
        first_function; second_function;
    local:
        *;
}
build.sh :
gcc -Wall -g -O0 -fPIC -c simple.c
gcc -shared simple.o -wl,--version-script,simpleVersionScript -o libsimple.so.1.0.0

main.c :
#include <stdio.h>
#include "simple.h"
int main(int argc, char* argv[])
{
    int nFirst = first_function(1);
    int nSecond = second_function(2);
    int nRetValue = nFirtst + nSecond;
    printf("first(1) + second(2) = %d\n", nRetValue);
    return nRetValue;
}
gcc -g -O0 -c -I../sharedLib main.c
gcc main.o -Wl,-L../sharedLib -lsimple \
-Wl,-R../shardLib -o firstDemoApp

用readelf工具可以分析二进制可执行程序,可以发现里面的版本信息是从动态库继承来的。通过这种方式,客户二进制文件和动态库中的版本控制信息就建立起了联系。动态库中的代码可能会经过多次修改,这其中会包含次版本的升级和主版本的升级。无论动态库进行了何种修改,其使用者—客户二进制程序仍然保留了原先在链接时使用的版本控制信息,如果对应的版本找不到了,那么就会立即检测出向后兼容性问题。

第二阶段:(增改次版本号)

simple.h:
int first_function(int x);
int second_function(int x);
int third_function(int x);

int fourth_function(int x);
int fifth_function(int y);
simple.c :
#include <stdio.h>
#include "simple.h"
int first_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 1);
}
int second_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 2);
}
int third_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 3);
}
int forth_function(int x)  //exported in version 1.1
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 4);
}
int fifth_function(int x)
{
    printf("lib %s\n", __FUNCTION__);
    return (x + 5);
}

simpleVersionScript :
LIBSIMPLE_1.0 {
    global:
        first_function;second_function;
    local:
        *;
};
LIBSIMPLE_1.1 {
    global:
        fourth_function;
    local:
        *;
}
main.c :
#include <stdio.h>
#include "simple.h"
int main(int argc, char* argv[])
{
    int nFirst = first_function(1);
    int nSecond = second_function(2);
    int nFourth = fourth_function(4);
    int nRetValue = nFirtst + nSecond + nFourth;
    printf("first(1) + second(2) + fourth(4) = %d\n", nRetValue);
    return nRetValue;
}
gcc -g -O0 -c -I../sharedLib main.c
gcc main.o -Wl,-L../sharedLib -lsimple \
-Wl,-R../shardLib -o newerApp

用readelf工具分析库文件和newerApp,就会发现,里面的版本信息就包含1.0和1.1

第三阶段:增改主版本号
1.修改ABI函数的内容:
某个ABI接口没变,但是返回值的内容变了,新的函数如下:

int first_function(int x)
{
    printf("lib: %s\n", __FUNCTION__);
    return 1000*(x+1);
}

这种修改必然造成之前链接该库的二进制文件运行出错,现在介绍方案来实现之前的二进制文件正确运行,想要使用该first_function函数的二进制也能链接到最新的first_function函数。

首先:对版本控制脚本进行修改,增加新的主版本信息。

LIBSIMPLE_1.0{
    global:
        first_function; second_function;
    local:
        *;
};
LIBSIMPLE_1.1{
global:
    fourth_function;
local:
    *;
};
LIBSIMPLE_2.0{
    global :
        first_function;
    local:
        *;
};
//simple.c,只列出修改部分
__asm__(".symver first_function_1_0,first_function@LIBSIMPLE_1.0");
int first_function_1_0(int x)
{
    printf("lib: %s\n", __FUNCTION__);
    return (x+1);
}
__asm__(".symver first_function_2_0,first_function@@LIBSIMPLE_2.0");
int first_function_2_0(int x)
{
    printf("lib: %s\n", __FUNCTION__);
    return 1000*(x+1);
}

编译以上代码,.symver汇编器指令帮助我们导出了两个不同版本的first_function()符号:first_function_1_0()和first_function_2_0().

//main.c
#include <stdio.h>
#incldue "simple.h"
int main(int argc, char* argv[])
{
    int nFirst = first_function(1);
    int nSecond = second_function(2);
    int nFourth = fourth_function(4);
    int nRetValue = nFirst + nSecond + nFourth;
    printf("first(1) + seocnd(2) + fourth(4) = %d\n", nRetValue);
    return nRetValue;
}
gcc -g -o0 -c -I../shardLib main.c
gcc main.o -Wl,-L../sharedLib -lsimple -Wl,-R../sharedLib -o ver2PeerApp

我们会发现最新的可执行文件使用了最新的2.0版本的函数,而如果继续运行以前编译好的二进制文件,会自动使用1.0版本的函数。这是因为elf文件在链接的时候,会将所链接的库的版本信息写入elf文件,所以elf文件知道自己该使用哪个版本的库函数。

2.修改ABI函数原型:

int first_function(intx, int normfactor);
//simpleVersionScript
LIBSIMPLE_1.0{
    global:
        first_function;second_function;
    local:
        *;
};
LIBSIMPLE_1.1{
    global:
        fourth_function;
    local:
        *;
};
LIBSIMPLE_2.0{
    global:
        first_function;
    local:
        *;
};
LIBSIMPLE_3.0{
    global:
        first_function;
    local:
        *;
};
//simple.c : 只列出修改部分
__asm__(".symver first_function_1_0,first_function@LIBSIMPLE_1.0");
int first_function_1_0(int x)
{
    printf("lib: %s\n", __FUNCTION__);
    return (x+1);
}
__asm__(".symver first_function_2_0,first_function@LIBSIMPLE_2.0");
int first_function_1_0(int x)
{
    printf("lib: %s\n", __FUNCTION__);
    return 1000*(x+1);
}
__asm__(".symver first_function_3_0,first_function@@LIBSIMPLE_3.0");
int first_function_3_0(int x, int normfactor)
{
    printf("lib: %s\n", __FUNCTION__);
    return normfactor*(x+1);
}
//simple.h
#ifdef SIMPLELIB_VERSION_3_0
int first_function(int x, int normfactor);
#else
int first_function(int x);
#endif
int second_function(int x);
int third_function(int x);
int fourth_function(int x);
int fifth_function(int x);

gcc -g -o0 -c -DSIMPLELIB_VERSION_3_0 -I../sharedLib main.c
gcc main.o -Wl,-L../sharedLib -lsimple -Wl,-R../sharedLib -o ver3PeerApp

控制符号可见性:
利用global和local修饰符可以对符号可见性进行控制,还可以使用通配符,链接说明符,命名空间等

LIBXYZ_1.0.6{
    global:
        first*; second*;
    local: 
        *;
}

LIBXYZ_1.0.6{
    global:
        extern "C" {
            first_function;
        }
    local: 
        *;
}


LIBXYZ_1.0.6{
    global:
        extern "C" {
            libxyz_namespace::*;
        }
    local: 
        *;
}

链接过程调试
在链接阶段,最好的调试方法莫过于LD_DEBUG环境变量。该方法不仅适用于构建过程,还可以调试运行时动态库加载的过程。操作系统支持一组预设的值,在执行操作之前(构建或者执行之前),只需将LD_DEBUG设置为这些值即可。列出这些预设值的命令为:
LD_DEBUG=help cat
可以通过export 来设置该环境变量,可以使用unset LD_DEBUG来撤销操作。
LD_DEBUG=libs ./PS_AppServer_64bit
就可以打印出该可执行函数加载动态库的过程中搜索动态库的过程,可以看出去哪里搜索各个动态库。
如果想将结果放到一个文件里:LD_DEBUG=libs LD_DEBUG_OUTPUT=log ./PS_AppServer_64bit
就会将输出打印到log.xx文件里。
LD_DEBUG=files 可以显示出大量携带丰富信息的输出(库名称、运行时路径和入口点地址)

获取动态库的入口P240

利用cat /proc//maps 命令就可以得到该进程加载库的信息。

lsof实用程序:
lsof程序可以对正在运行中的进程进行分析,并将进程打开的所有文件的列表输出到标准输出流中。打开的文件列表包括普通文件,目录,块设备文件,字符设备文件,执行中的文本引用,库文件,流文件和网络文件(internet socket, NFS,unix domain socket)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值