题外话:在编译程序时包含调试信息

使用gcc/cc编译时指定-g选项可以使得程序中包含调试信息,所带来的影响是可执行文件的体积增大。
如果使用strip(1)命令: strip - Discard symbols from object files,可以从可执行文件和库文件中删除调试信息。

#include <stdio.h>

int sum(int a,int b)
{
    return a + b;
}

int g_count = 0;

int main()
{
    printf("Hello world.\n");
    return 0;
}
[root@centos-7 workspace]# cat strip.c
#include <stdio.h>

int sum(int a,int b)
{
    return a + b;
}

int g_count = 0;

int main()
{
    printf("Hello world.\n");
}
[root@centos-7 workspace]# gcc -g -o execute  strip.c 
[root@centos-7 workspace]# ll execute 
-rwxr-xr-x. 1 root root 9696 4月  27 23:00 execute
[root@centos-7 workspace]# nm execute 
000000000060102c B __bss_start
000000000060102c b completed.6355
0000000000601028 D __data_start
0000000000601028 W data_start
0000000000400460 t deregister_tm_clones
00000000004004d0 t __do_global_dtors_aux
0000000000600e18 t __do_global_dtors_aux_fini_array_entry
00000000004005d8 R __dso_handle
0000000000600e28 d _DYNAMIC
000000000060102c D _edata
0000000000601038 B _end
00000000004005c4 T _fini
00000000004004f0 t frame_dummy
0000000000600e10 t __frame_dummy_init_array_entry
0000000000400740 r __FRAME_END__
0000000000601030 B g_count
0000000000601000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004005f0 r __GNU_EH_FRAME_HDR
00000000004003c8 T _init
0000000000600e18 t __init_array_end
0000000000600e10 t __init_array_start
00000000004005d0 R _IO_stdin_used
0000000000600e20 d __JCR_END__
0000000000600e20 d __JCR_LIST__
00000000004005c0 T __libc_csu_fini
0000000000400550 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
0000000000400531 T main
                 U puts@@GLIBC_2.2.5
0000000000400490 t register_tm_clones
0000000000400430 T _start
000000000040051d T sum
0000000000601030 D __TMC_END__
[root@centos-7 workspace]# 
[root@centos-7 workspace]# 
[root@centos-7 workspace]# strip ./execute 
[root@centos-7 workspace]# 
[root@centos-7 workspace]# nm execute 
nm: execute:无符号

去除可执行文件中的符号表后将导致无法使用gdb进行调试。

静态库

源代码

int max(int a,int b)
{
    return (a>b)?a:b;
}
int min(int a,int b)
{
    return (a>b)?b:a;
}
[root@centos-7 soft]# gcc -c max.c
[root@centos-7 soft]# gcc -c min.c
[root@centos-7 soft]# 
将max.o min.o打包成libmath.a
[root@centos-7 soft]# ar cr libmath.a max.o min.o
[root@centos-7 soft]# 
显示静态库中的目录表。
[root@centos-7 soft]# ar tv libmath.a
rw-r--r-- 0/0   1240 Apr 27 23:32 2019 max.o
rw-r--r-- 0/0   1240 Apr 27 23:32 2019 min.o
[root@centos-7 soft]# 
删除静态库中的目标文件
[root@centos-7 soft]# ar d libmath.a min.o
[root@centos-7 soft]# 
[root@centos-7 soft]# 
[root@centos-7 soft]# ar tv libmath.a 
rw-r--r-- 0/0   1240 Apr 27 23:32 2019 max.o
[root@centos-7 soft]# 
使用静态库有如下两种方式
[root@centos-7 soft]# gcc -g -o main main.c -L. -lmath
[root@centos-7 soft]# gcc -g -o main main.c libmath.a

动态库

生成动态库方法:

[root@centos-7 soft]# gcc -g -c -fPIC -Wall max.c min.c
[root@centos-7 soft]# gcc -g -shared -o libmath.so max.o min.o
[root@centos-7 soft]# ls libmath.so 
libmath.so

为了确定一个既有目标文件在编译时是否使用了-fPIC选项,可以使用下面的两个命令中的一个来检查目标文件符号表中是否存在名称_GLOBAL_OFFSET_TABLE_

使用如下两个命令,如果产生了任何输出,那么指定的共享库中至少存在一个目标模块在编译时没有指定-fPIC选项。

[root@centos-7 soft]# objdump --all-headers libmath.so | grep TEXTREL
[root@centos-7 soft]# readelf -d libmath.so | grep TEXTREL

使用任意以下两个命令,如果在符号表中找不到_GLOBAL_OFFSET_TABLE_,则可以确定一个既有目标文件在编译时没有指定-fPIC选项。

[root@centos-7 soft]# nm libmath.so | grep _GLOBAL_OFFSET_TABLE_
0000000000201000 d _GLOBAL_OFFSET_TABLE_
[root@centos-7 soft]# readelf -s libmath.so | grep _GLOBAL_OFFSET_TABLE_
    48: 0000000000201000     0 OBJECT  LOCAL  DEFAULT   21 _GLOBAL_OFFSET_TABLE_
[root@centos-7 soft]# 

使用动态库

main.c

#include <stdio.h>
int main()
{
    int a = 10;
    int b = 20;
    printf("%d\n",max(a,b));
    printf("%d\n",min(a,b));
    return 0;
}
[root@centos-7 soft]# ls
libmath.so  main.c  max.c  max.o  min.c  min.o
[root@centos-7 soft]# gcc -g -Wall -o main main.c libmath.so 
[root@centos-7 soft]# ./main
./main: error while loading shared libraries: libmath.so: cannot open shared object file: No such file or directory
[root@centos-7 soft]# 
[root@centos-7 soft]# readlink /lib/ld-linux.so.2 
ld-2.17.so
[root@centos-7 soft]# 

运行程序报错,解决这个问题需要了解:动态链接。
动态链接即在运行时解析内嵌的库名。这个任务是由动态连接器来完成的。动态链接器本身也是一个共享库,其名称为/lib/ld-linux.so.2,所有使用共享库的ELF可执行文件都会使用这个共享库。
通过readlink可以知道ld-linux.so.2指向ld-version.so,version表示安装在系统上的glibc的版本-例如上面我们看到的ld-2.17.so.
动态链接器会检查程序所需的共享库清单并使用一组预先定义好的规则来在文件系统上找出相关的库文件。其中一些规则指定了一组存放共享库的标准目录,如很多共享库位于/lib和/usr/lib中。之所以出现上面的错误消息是因为程序所需的库位于当前工作目录中,而不位于动态链接器搜索的标准目录清单中。

共享库soname

$ gcc -g -c -fPIC -Wall min.c max.c
$ gcc -g -shared -Wl,-soname,libmathsimple.so -o libmath.so min.o max.o

-Wl,-soname,libmathsimple.so是传递给连接器的指令以将共享库libmath.so的soname设置为libmathsimple.so。
如果要确定一个既有共享库的soname,可以使用如下命令:

readelf -d libmathsimple.so | grep SONAME

然后使用如下命令创建可执行程序:

[root@centos-7 soft]# gcc -g -o main main.c libmath.so 
[root@centos-7 soft]# ./main
./main: error while loading shared libraries: libmathsimple.so: cannot open shared object file: No such file or directory
[root@centos-7 soft]# ln -s libmath.so libmathsimple.so
[root@centos-7 soft]# ./main
./main: error while loading shared libraries: libmathsimple.so: cannot open shared object file: No such file or directory
[root@centos-7 soft]# LD_LIBRARY_PATH=. ./main

我们第一次运行./main的时候提示是找不到库文件libmathsimple.so,原因是因为连接器检查到库libmath.so包含了soname libmathsimple.so,于是将这个so嵌入到了可执行文件中。

使用共享库的有用工具

ldd命令

ldd命令显示一个可执行程序运行所需的共享库:

[root@centos-7 soft]# ldd main
    linux-vdso.so.1 =>  (0x00007ffc17f2a000)
    libmathsimple.so => ./libmathsimple.so (0x00007fa715295000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fa714ec8000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fa715497000)

对于大多数ELF可执行文件来讲,ldd至少会列出与ld-linux.x86064.so.2、libc.so.6相关的条目。

nm命令

nm命令会列出目标库或可执行程序中定义的一组符号,这个命令的一种用途是找出哪些库定义了一组符号。例如找出哪个库定义了crypt()函数,可以使用如下命令:

nm -A /usr/lib/lib*.so 2 > /dev/null | grep ' crypt$'

nm的-A选项指定了在显示符号的每一行的开头处应该列出库的名称。$符号表示:匹配输入字符串的结束位置。
执行结果:

[root@centos-7 soft]# nm  -A /usr/lib/lib*.so 2>/dev/null | grep ' crypt$'
/usr/lib/libcrypt-2.17.so:00000ef0 T crypt
[root@centos-7 soft]# 
[root@centos-7 soft]# nm /usr/lib/lib*.so 2>/dev/null | grep ' crypt$'
00000ef0 T crypt
[root@centos-7 soft]# nm  -A /usr/lib/lib*.so 2>/dev/null | grep ' crypt'
/usr/lib/libcrypt-2.17.so:00000ef0 T crypt
/usr/lib/libcrypt-2.17.so:00000b40 W crypt_r

共享库的版本和命名

[root@centos-7 soft]# gcc -g -c -fPIC -Wall min.c max.c
[root@centos-7 soft]# gcc -g -shared -Wl,-soname,libdemo.so.1 -o libdemo.so.1.0.1 min.o max.o
[root@centos-7 soft]# ls
libdemo.so.1.0.1  main  main.c  max.c  max.o  min.c  min.o
[root@centos-7 soft]# ln -s libdemo.so.1.0.1 libdemo.so.1
[root@centos-7 soft]# ln -s libdemo.so.1 libdemo.so
[root@centos-7 soft]# 
[root@centos-7 soft]# ls -l libdemo*
lrwxrwxrwx. 1 root root   12 5月   1 11:28 libdemo.so -> libdemo.so.1
lrwxrwxrwx. 1 root root   16 5月   1 11:28 libdemo.so.1 -> libdemo.so.1.0.1
-rwxr-xr-x. 1 root root 9192 5月   1 11:28 libdemo.so.1.0.1
[root@centos-7 soft]# ls -l libdemo* | awk '{print $1,$9,$10,$11}'
lrwxrwxrwx. libdemo.so -> libdemo.so.1
lrwxrwxrwx. libdemo.so.1 -> libdemo.so.1.0.1
-rwxr-xr-x. libdemo.so.1.0.1  
[root@centos-7 soft]# 
[root@centos-7 soft]# gcc -g -Wall -o main main.c -L. -ldemo
[root@centos-7 soft]# LD_LIBRARY_PATH=. ./main
[root@centos-7 soft]# 
ldconfig

ldconfig example
ldconfig命令的用途主要是在默认搜寻目录/lib和/usr/lib以及动态库配置文件/etc/ld.so.conf内所列的目录下,搜索出可共享的动态链接库(格式如lib*.so*),进而创建出动态装入程序(ld.so)所需的连接和缓存文件。缓存文件默认为/etc/ld.so.cache,此文件保存已排好序的动态链接库名字列表,为了让动态链接库为系统所共享,需运行动态链接库的管理命令ldconfig,此执行程序存放在/sbin目录下。

ldconfig通常在系统启动时运行,而当用户安装了一个新的动态链接库时,就需要手工运行这个命令。
ldconfig -p命令会显示/etc/ld.so.cache文件内容。
/etc/ld.so.conf文件内容如下:

include ld.so.conf.d/*.conf

意思是ld.so.conf包含ld.so.conf.d目录下所有的conf文件中所包含的路径信息。

ldconfig解决了共享库的两个潜在问题:
共享库可以位于各种目录中,如果动态链接器需要通过搜索所有目录来找出一个库并加载这个库,那么效率非常低。

当安装了新版本的库或者删除了旧版本的库,soname符号链接不是最新的。

在目标文件中指定库搜索目录

到目前为止我们已经知道了两种通知动态链接器共享库的位置的方式:使用LD_LIBRARY_PATH环境变量和将共享库安装到其中一个标准库目录中(/lib、/usr/lib或在/etc/ld.so.conf中列出的其中一个目录)。
第三种方式:在静态编译阶段可以在可执行文件中插入一个在运行时搜索共享库的目录列表。
例如:
gcc -g -Wall -Wl,-rpath,/home/mtk/pdir -o prog prog.c libdemo.so

运行时符号解析


假设主程序和共享库,他们两个都定义了一个全局函数xyz(),并且共享库中的另一个函数调用了xyz(),如上图所示。

$ gcc -g -c -fPIC -Wall -c foo.c
$ gcc -g -shared -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$LD_LIBRARY_PATH=. ./prog
main-xyz

从上面的输出可以看出:主程序中的xyz()定义覆盖了共享库中的定义。
如果想要确保在共享库中对xyz()的调用确实调用库中定义的相应函数,那么再构建共享库的时候就需要使用-Bsymbolic链接器选项:

$ gcc -g -c -fPIC -Wall -c foo.c
$ gcc -g -shared -Wl,-Bsymbolic -o libfoo.so foo.o
$ gcc -g -o prog prog.c libfoo.so
$LD_LIBRARY_PATH=. ./prog
foo-xyz

-Bsymbolic链接器选项指定了共享库中对全局符号的引用应该优先被绑定到库中的相应定义上(如果存在的话).不管是否使用了这个选项,在主程序中调用xyz()总是会调用主程序中定义的xyz().
总结:
目标库是一组编译过的目标模块的聚合,它可以用来与程序进行链接,Linux提供了两种目标库:静态库与动态库。
由于和静态库相比,动态库存在很多优势,因此在当代UNIX系统上动态库用的最多。动态库的优势主要源自这样一个事实,即当一个程序与库进行链接时,程序所需的目标模块的副本不会被包含进结果可执行文件中。相反静态链接器会在可执行文件中添加与程序在运行时所需的共享库相关的信息。当文件被执行时,动态链接器会使用这些信息来加载所需的动态库。在运行时所有使用同一动态库的程序共享该库在内存中的单个副本。由于动态库不会被复制到可执行文件中,并且在运行时所有程序都是用动态库在内存中的单个副本,因此动态库能够降低系统所需的磁盘空间和内存。

如果一个动态库拥有一个soname,那么在由静态链接器产生的可执行文件中将会记录这个soname,而不是库的真实名称,根据动态库命名规范,其真实名称的形式为libname.so.major-id.minor-id,其soname的形式为libname.so.major-id.这种规范使得程序能够自动使用动态库的最新次要版本,同时也允许创建库的新的不兼容的主要版本。

为了在运行时能够找到动态库,动态链接器遵循了一组标准的搜索规则,其中包括搜索一组大多数动态库的安装的目录(如/lib和/usr/lib)