linux下的动态链接小结

想了解linux下的动态链接,包括如何产生so文件,如何link等等,在网上搜索了一把,资料都比较散,也有几个问题没想明白,上论坛发了个帖子,终于弄清楚了大部分,想着把这几天的成果组织一把,弄一篇文字出来将来看看,也给需要的网友一个参考。

环境:centOS 7
代码放在: /home/dodomouse/workspace/Test

首先需要了解gcc/g++和ld之间的关系,gcc/g++是GNU的编译器,ld是GNU的链接器,link主要是ld的活。但是gcc/g++不是单纯的编译器,可以通过调用ld完成链接的功能,比如编译的时候,通过-l和-L的参数,可以分别指定so文件名和搜索该文件的path。但需要注意的是gcc并不是简单的调用ld,而是通过 collect2这个程序去调的,collect2在调用ld之前还会干一些其他事情。可以通过gcc -v选项查看gcc调用的过程并产生结果,gcc -###则只是查看调用过程不产生结果。
实例说话:以下文中的小程序为例,通过ld去link.o文件的时候,会产生奇怪的错误:
ld apple.o test.o -o test -lc
ld: warning: cannot find entry symbol _start; defaulting to 00000000004002a0

通过gcc -v产生可执行文件并查看过程可发现gcc做了很多事情:
gcc -v apple.o test.o -o test
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-bootstrap --enable-shared --enable-threads=posix --enable-checking=release --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,obj-c++,java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --with-isl=/builddir/build/BUILD/gcc-4.8.2-20140120/obj-x86_64-redhat-linux/isl-install --with-cloog=/builddir/build/BUILD/gcc-4.8.2-20140120/obj-x86_64-redhat-linux/cloog-install --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.2 20140120 (Red Hat 4.8.2-16) (GCC) 
COMPILER_PATH=/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/:/usr/libexec/gcc/x86_64-redhat-linux/4.8.2/:/usr/libexec/gcc/x86_64-redhat-linux/:/usr/lib/gcc/x86_64-redhat-linux/4.8.2/:/usr/lib/gcc/x86_64-redhat-linux/
LIBRARY_PATH=/usr/lib/gcc/x86_64-redhat-linux/4.8.2/:/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/:/lib/../lib64/:/usr/lib/../lib64/:/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../:/lib/:/usr/lib/
COLLECT_GCC_OPTIONS='-v' '-o' 'test' '-mtune=generic' '-march=x86-64'
 /usr/libexec/gcc/x86_64-redhat-linux/4.8.2/collect2 --build-id --no-add-needed --eh-frame-hdr --hash-style=gnu -m elf_x86_64 -dynamic-linker /lib64/ld-linux-x86-64.so.2 -o test /usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/crt1.o /usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/crti.o /usr/lib/gcc/x86_64-redhat-linux/4.8.2/crtbegin.o -L/usr/lib/gcc/x86_64-redhat-linux/4.8.2 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64 -L/lib/../lib64 -L/usr/lib/../lib64 -L/usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../.. apple.o test.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-redhat-linux/4.8.2/crtend.o /usr/lib/gcc/x86_64-redhat-linux/4.8.2/../../../../lib64/crtn.o
可以看出的是,gcc/g++不光是编译器,也起到协调的作用,可以集中调用链接器,完成程序运行环境的初始化工作。
本文使用gcc调用链接器以避免繁琐的运行环境初始化问题。

其次需要区分使用环境:so文件在编译链接可执行文件的时候需要用到,在运行可执行文件的时候也需要用到,但是这两个使用是不一样的。编译链接的时候,主要是给程序员使用,可以比较灵活的控制。运行的可执行文件时候,则是给最终客户使用了,需要加入版本号并方便升级,避免陷入windows的"dll hell"。
先讲运行可执行文件的情形~~~运行以下命令去完成编译、链接的过程

gcc -shared -fPIC -c apple.cpp -o apple.o

gcc -shared apple.o -o libapple.so -Wl,-soname=libapple.so

gcc -c test.cpp -o test.o

gcc test.o -o test -lapple -L/home/dodomouse/workspace/Test 

尝试运行test:
./test
./test: error while loading shared libraries: libapple.so: cannot open shared object file: No such file or directory
可以看出虽然编译链接都通过并生成test可执行文件,但运行时却提示找不到libapple.so这个文件。这是因为运行时搜索动态库文件的方式与编译链接时无关。 linux提供了ldconfig命令去配置运行时动态库的绑定,以搜索所需的动态库文件,命令的用法可以查看man手册。
因此,当运行完以下命令时,便可以运行test了:
ldconfig /home/dodomouse/workspace/Test
./test 
In printApple()

那怎么进行版本管理呢?这就跟开发时的设定有关系了。在日常版本管理中,so一般都带有版本号,例如libapple.so.1.2 ----1是主版本号,2是此版本号,当内容的更新不涉及函数接口时,往往只改变次版本号,如果是一些大改动或者要修改函数接口,则需要升级主版本号,可以看出,主版本号相对于次版本号相对稳定一些,因此,在实际使用的时候,往往会有一个只带主版本号的so链接文件,以方便使用。
现在实际上有三个so文件(或者so文件名字)了,一个是真正的so文件,带有主版本号和次版本号,一个是只带主版本号的so文件链接,还有一个是用于编译链接用的so文件。分析以下命令:
gcc -shared -fPIC -c apple.cpp -o apple.o
gcc -shared apple.o -o libapple.so.1.1 -Wl,-soname=libapple.so.1
该命令编译apple.cpp,产生apple.o,然后把apple.o打包为libapple.so.1.1,这个便是真正的so文件,带有主版本号和次版本号。至于-soname=libapple.so.1起什么作用呢?我们可以通过readelf去分析libapple.so.1.1了解。
readelf -a libapple.so.1.1 | grep apple
 0x000000000000000e (SONAME)             Library soname: [libapple.so.1]
    34: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS apple.cpp
可以看出,在libapple.so.1.1中,已经包含了这个只带主版本号的名字,它将libapple.so.1.1与libapple.so.1关联起来。现在的一个问题是libapple.so.1还不存在,我们可以通过手动的方式创建它"ln -s libapple.so.1.1 libapple.so.1"或者是直接运行ldconfig,让这个系统程序帮我们创建libapple.so.1出来。
尝试运行" ldconfig /home/dodomouse/workspace/Test",可以发现已经创建出libapple.so.1这个软链接了,这也是在readelf libapple.so.1.1中为什么有libapple.so.1信息的原因,即是为ldconfig自动创建准备的。
光准备了libapple.so.1和libapple.so.1.1还不够,我们还需要编译出可执行文件才可以。
运行以下命令,提示找不到libapple.so(-l选项中是apple,即除掉lib和.so的中间这一段名字),可以看出,链接时需要精确匹配so文件名的(这个可能有争议,我没法不创建这个libapple.so):
gcc test.o -o test -lapple -L/home/dodomouse/workspace/Test
/usr/bin/ld: cannot find -lapple
collect2: error: ld returned 1 exit status
我们为libapple.so.1创建名为libapple.so的软链接: ln -s libapple.so.1 libapple.so
之后,编译成功。通过 readelf -a test | grep apple 可以看到在test中,实际链接到libapple.so.1
 0x0000000000000001 (NEEDED)             Shared library: [libapple.so.1]

如果我们对printApple()有小改动,例如把函数改成以下内容:

void printApple()
{
printf("In printApple() and new version \n");
}

编译出libapple.so.1.2之后,运行ldconfigldconfig会自动将libapple.so.1链接到最新次版本号即libapple.so.1.2去。又因为test程序中链接的so名字是libapple.so.1,所以之后就不必做其他事情,就可以直接运行test以获取新特性:

./test
In printApple() and new version 

但是如果把函数接口改了,那就不一样了,需要升级主版本号,创建libapple.so.2.1和libapple.so.2并重新编译test,这样,系统中会同时存在libapple.so.1和libapple.so.2,不会影响各自调用程序;如果不修改主版本号而只修改次版本号,那运行时就会报错。例如把apple.cpp改成以下内容:
void printApple1()
{
printf("In printApple1() and new version \n");
}
void printApple2()
{
printf("In printApple2() and new version \n");
}
如果直接编译成libapple.so.1.3然后运行ldconfig的话,就会报错了:
./test
./test: symbol lookup error: ./test: undefined symbol: _Z10printApplev

需要注意的是,如果编译的时候不指定版本号或者不指定so文件链接的名字,例如
gcc -shared apple.o -o libapple.so
或者
gcc -shared apple.o -o libapple.so.1.1 -Wl,-soname=libapple.so
这样子升级的时候比较方便,不用创建不带版本号的编译链接用so文件,对于第一种,整个系统中只有一个名字,可执行文件test中链接的也是这个名字,因此不管是主版本更新还是次版本更新,都是直接替换so文件以实现新功能;对于第二种,可执行文件中链接的是libapple.so,由于没有主版本号,当有新主版本更新时,会把所有link到该so的可执行文件都更新;这两种方式都是很危险的,都会导致系统可执行文件的不可运行或出错,就是dll hell的状态了。因此,要避免出现这种情况。


本文所用到的小程序:

apple.cpp

test.cpp

#include<stdio.h>

void printApple()

{

printf("In printApple()\n");

}

#include<stdio.h>

void printApple();

int main(int argc, char*argv[])

{

printApple();

}


如果有错误之处,请不吝指出,十分感谢!!!





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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值