动态链接编程

编写一些大型应用程序时,如果将所有的源文件一起编译,则牵一发而动全身,任何文件微小的修改都需要重新编译整个项目,则显然是相当低效的。所以,将一个项目分为几个依赖库,运行的时候再查找对应库中的入口,能提高效率。这就是动态链接库,Linux下为so文件,Windows下则为DLL文件。

在一篇文章将介绍常用的开发动态链接库的技巧和方法。

动态链接介绍

众所周知,任何程序从源代码变成二进制文件,需要经过编译和连接两个过程。编译是将每个源代码文件转为对应的机器代码;连接是将每份机器代码整合成一个文件,将调用的函数用相应地址代替。

正如前面介绍,如果将所有调用的文件都打包起来,不利于高效开发,我们有个新的思路:不妨先做编译和初步的连接,等运行的时候再从内存等地方实时地调用相应函数。这就是动态链接。

下面我们来实现一个简单的例子:

main.c代码:

#include "libmewwoof_hello.h"

int main() {
    mewwoof_hello_print();
    return 0;
}

libmewwoof_hello.h代码:

#ifndef _LIB_MEWWOOF_HELLO_H_

#define _LIB_MEWWOOF_HELLO_H_

void mewwoof_hello_print();

#endif

libmewwoof_hello.c代码:

#include <stdio.h>
#include "libmewwoof_hello.h"

void mewwoof_hello_print() {
    printf("Hello, mewwoof!\n");
}

然后分别编译成动态库和主程序。-f PIC 表示生成位置无关代码 (Position-Independent Code),-L . 指定在当前目录下查找链接库。

gcc libmewwoof_hello.c -fPIC -shared -o libmewwoof_hello.so
gcc main.c -L. -lmewwoof_hello -o mewwoof-hello

得到可执行文件mewwoof-hello和动态库文件libmewwoof_hello.so。执行:

$ LD_LIBRARY_PATH=. ./mewwoof-hello
Hello, mewwoof!

可以使用ldd命令查看动态链接的内容:

$ LD_LIBRARY_PATH=. ldd ./mewwoof-hello 
        linux-vdso.so.1 (0x00007ffd1c5f6000)
        libmewwoof_hello.so => ./libmewwoof_hello.so (0x00007f318d3d8000)
        libc.so.6 => /usr/lib/libc.so.6 (0x00007f318d17e000)
        /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2 (0x00007f318d3e4000)

库命名规范

一般使用libfoo.solibfoo_bar.so这样的命名方式。gcc也是通过这种方式查找动态库的。

但动态链接库一般还附带有版本信息:如libfoo.so.1.2.1。这个命名也有规范。对于libname.so.x.y.z

  • x表示主版本号,当这个值变动表示API可能发生较大变化,不兼容之前版本的应用
  • y表示次版本号,当这个值变动表示API功能只增不减,向后兼容
  • z表示向后兼容的版本数。比如说版本1.5是版本1.4,1.3的超集,但不兼容版本1.2,则应该命名为1.5.2。(实际上有争议,比如Apache使用z表示patch数,仅仅表示bug修复:https://apr.apache.org/versioning.html

除了版本命名有讲究,我们在查看Linux库文件时,也常常会看到这种情形:

$ ls -la
...
lrwxrwxrwx   1 root    root            18 12月 24 01:43 libvtkverdict.so -> libvtkverdict.so.1
lrwxrwxrwx   1 root    root            22 12月 24 01:43 libvtkverdict.so.1 -> libvtkverdict.so.9.1.0
-rwxr-xr-x   1 root    root        214712 12月 24 01:43 libvtkverdict.so.9.1.0
...

同样一个库却被“复制”了多次,事实上,libname.so.x.y.z为realname,libname.so.x为soname,libname.so为linker name。realname对应文件是真正用来存储二进制库代码的,soname是动态链接时查找的,linker name是编译时查找的。

https://www.cnblogs.com/lidabo/p/13862630.html?ivk_sa=1024320u

例如:gcc -o test test.o -lz , 链接时就会找libz.so 。若没有这个文件,链接器就报错。

库版本管理

既然库是相对于主程序独立的,有自己的版本,那么一个问题就出现了:不同版本的库提供的功能也不一样,就算是不同次版本号的库,也会有功能的增加。如果一个应用使用了高版本库的功能,而运行时却只安装了低版本库,那么动态链接时就会出错。如何避免这些错误,让程序早点知道这个问题呢?这是就需要使用符号版本管理了。可以参考前面的一篇文章,介绍了符号版本:

现在我们来为自己编写的库添加上符号版本。

现在我们的主程序main.c是这样的:

#include "libmewwoof_hello.h"

int main() {
    mewwoof_hello_print();
    return 0;
}

编译,查看符号列表:

$ objdump -T ./mewwoof-hello 

./mewwoof-hello:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*  0000000000000000  Base        mewwoof_hello_print
0000000000000000  w   D  *UND*  0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize

$ objdump -T ./libmewwoof_hello.so             

./libmewwoof_hello.so:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) puts
0000000000000000  w   D  *UND*  0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000001109 g    DF .text  0000000000000016  Base        mewwoof_hello_print

可以看到mewwoof_hello_print版本都是Base,也就相当于没有版本。

编写版本文件libmewwoof_hello.ver:

MWF_HE_0.1.0 {
    global:
        mewwoof_hello_print;
};

重新编译库:

$ gcc libmewwoof_hello.c -fPIC -shared -Xlinker --version-script libmewwoof_hello.ver -o libmewwoof_hello.so

再次使用objdump,可以看见打上了版本信息:

0000000000001109 g    DF .text  0000000000000016  MWF_HE_0.1.0 mewwoof_hello_print

此时直接运行之前的主程序,可以发现运行成功,这是因为原来的主程序并没有包含版本符号。现在我们重新编译主程序:

$ gcc main.c -L. -lmewwoof_hello -o mewwoof-hello

再次使用objdump,可以看见主程序调用的mewwoof_hello_print也标记了版本符号。

将库命名为libmewwoof_hello.so.0.1.0,修改ver文件,版本号改为0.1.1,再次编译库文件,查看库文件的符号,确认修改成功:

0000000000001109 g    DF .text  0000000000000016  MWF_HE_0.1.1 mewwoof_hello_print

此时直接运行主程序,就会报错了:

$ LD_LIBRARY_PATH=. ./mewwoof-hello
./mewwoof-hello: ./libmewwoof_hello.so: version `MWF_HE_0.1.0' not found (required by ./mewwoof-hello)

指定库版本编译

现在我们有了两个库,0.1.0和0.1.1的。可以在编译时指定使用那个具体的库:

gcc main.c -L. -l:libmewwoof_hello.so.0.1.0 -o mewwoof-hello

在运行时,就会专门查找libmewwoof_hello.0.1.0来动态链接。

同时提供两个版本的函数

一个库还可以同时提供两个版本的符号:

libmewwoof_hello.c

#include <stdio.h>
#include "libmewwoof_hello.h"

asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");
asm(".symver mewwoof_hello_print_new, mewwoof_hello_print@MWF_HE_0.1.1");

void mewwoof_hello_print() {
    printf("Hello, mewwoof!\n");
}

void mewwoof_hello_print_new() {
    printf("Hello, mewwoof new!\n");
}

libmewwoof_hello.ver

MWF_HE_0.1.0 {
    global:
        mewwoof_hello_print;
};
MWF_HE_0.1.1 {
    global:
        mewwoof_hello_print;
    local:
        mewwoof_hello_print_new;
};

再次编译库文件,查看符号列表:

$ objdump -T ./libmewwoof_hello.so

./libmewwoof_hello.so:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.2.5) puts
0000000000000000  w   D  *UND*  0000000000000000  Base        __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize
0000000000000000 g    DO *ABS*  0000000000000000  MWF_HE_0.1.1 MWF_HE_0.1.1
000000000000111f g    DF .text  0000000000000016 (MWF_HE_0.1.1) mewwoof_hello_print
0000000000001109 g    DF .text  0000000000000016 (MWF_HE_0.1.0) mewwoof_hello_print
0000000000000000 g    DO *ABS*  0000000000000000  MWF_HE_0.1.0 MWF_HE_0.1.0

编译主程序,这次主程序使用新编译的库文件(不是之前重命名的那个):

gcc main.c -L. -lmewwoof_hello -o mewwoof-hello

编译失败!原因是要在主程序代码中指定使用的版本:

编辑main.c

#include "libmewwoof_hello.h"

asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");

int main() {
    mewwoof_hello_print();
    return 0;
}

再次编译,可以看到,保持使用了0.1.0的符号:

0000000000000000      DF *UND*  0000000000000000 (MWF_HE_0.1.0) mewwoof_hello_print

运行,输出“Hello, mewwoof!”。

修改main.c这一行:

asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.1");

编译,查看版本:

0000000000000000      DF *UND*  0000000000000000 (MWF_HE_0.1.1) mewwoof_hello_print

运行:

$ LD_LIBRARY_PATH=./ ./mewwoof-hello
Hello, mewwoof new!

可以看到,使用了新版本的函数。

提供默认版本

通过上面的例子,可以看到如果每次都需要手动指定版本信息,显然太麻烦。事实上可以指定一个默认版本,使用两个“@”即可:

编辑libmewwoof_hello.c:

#include <stdio.h>
#include "libmewwoof_hello.h"


asm(".symver mewwoof_hello_print, mewwoof_hello_print@MWF_HE_0.1.0");
asm(".symver mewwoof_hello_print_new, mewwoof_hello_print@@MWF_HE_0.1.1");

void mewwoof_hello_print() {
    printf("Hello, mewwoof!\n");
}

void mewwoof_hello_print_new() {
    printf("Hello, mewwoof new!\n");
}

去掉主程序中的汇编语句,全部重新编译,可以看到使用了0.1.1版本的函数:

$ objdump -T ./mewwoof-hello

./mewwoof-hello:     文件格式 elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000      DF *UND*  0000000000000000 (GLIBC_2.34) __libc_start_main
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_deregisterTMCloneTable
0000000000000000  w   D  *UND*  0000000000000000  Base        __gmon_start__
0000000000000000      DF *UND*  0000000000000000 (MWF_HE_0.1.1) mewwoof_hello_print
0000000000000000  w   D  *UND*  0000000000000000  Base        _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000 (GLIBC_2.2.5) __cxa_finalize

运行:

$ LD_LIBRARY_PATH=. ./mewwoof-hello 
Hello, mewwoof new!

静态链接

这里顺便提一下静态链接。

既然动态链接表示系统查找相关的库,那么广义的静态链接就指的是系统无需查找相关库。这里实现“无需查找”有两种方式,一种是直接指定相关库的位置,另一种是直接将相关库的二进制代码打包到一起,形成一个大文件。

指定库位置

参照上文 指定库版本编译

全部打包

全部打包对库有要求:要求这个库是静态的,事实上也就是使用多个源文件直接编译并打包在一起的过程:

# 静态编译库
gcc -c libmewwoof_hello.c -o libmewwoof_hello.o
# 打包成静态库
ar rcs libmewwoof_hello.a ./libmewwoof_hello.o
# 静态编译主程序
gcc -c main.c -o mewwoof-hello.o
# 合并
gcc mewwoof-hello.o -L. -lmewwoof_hello -static -o mewwoof-hello-bin

如果使用本文的例子,以上命令最后一步将执行失败,因为静态链接是不支持符号版本控制的。

参考文献

[1] VERSION Command http://sourceware.org/binutils/docs/ld/VERSION.html

[2] https://gcc.gnu.org/wiki/SymbolVersioning

发表评论