(Turn) Static analysis and dynamic library

DLL and static library is a very common thing in programming, but such common things I get to know the real intentions carded again after it was discovered that there are so many doorways. This article describes the next wave of Linux platform, especially the GCC compiler generated DLL dependent on the static library association, and even expand the wave of popular compiler tool usage. While there is some content to see when that knows, but I promise, there will be so few do not know, so-called hard-core.

The purpose of the library

From the surface in terms of a rough point of view, the usefulness of the library is to facilitate code reuse and release all know ha, but I think that we should say this in depth stuff.

Library static library and dynamic library points (and you know), in which the dynamic library is loaded and used to perform the mapping, if there are a large number of executable program code that uses a lot of common parts, then you can put part of the code as a dynamic library to independence, although this adds some speed, but a great help to reduce the memory footprint. And libraries can solve the problem of version upgrade, we had a good agreement API interface, and then call these interfaces in their own program inside, wait until you need to upgrade features or just need to de-bug time dynamic libraries update what you can, all without moving to business logic of the code, Glibc is.

To conclude a dynamic library on two major purposes:

  1. Volume savings, increased rate of code reuse.
  2. Easy to maintain, upgrade, version management.

Dynamic Library features:

  1. When loading operation, providing high multiplexing operation.
  2. Do not take up too much volume executable file itself, and after the upgrade executable file without recompiling links, namely students ready to use.

Static load the library would not be running, and what repeatedly multiplexing function like this does not exist, it is a collection of high code package with reusable, but it only provides code to write, compile time reusability without providing program is loaded, run time of reusability. Features static library is a direct copy of the symbol table when compiling to an executable file inside, it also means that the size will be much larger executable files and run-time reusability is broken. But when it has the advantage that without loading additional libraries at runtime executable file, we transfer the executable file on different physical machines only need to copy a few files.

The purpose of the static library:

  1. Providing code, compile time code reusability.
  2. Reduce dependence executable file, the file needs a few copies when copying.

Static library features:

  1. The resulting executable file size is large, because it contains the actual snippet, undermining the reusability runtime, but without relying on additional libraries.
  2. Collection target file itself does not provide additional information other than the destination file.
  3. When you upgrade executable files need to be recompiled, the upgrade process compared to the dynamic library dependencies easier.

Decomposition library

Static library is made entirely .o object files one by one composition, does not contain any dynamic link library inside the symbol table information, etc., and dynamic library stuff inside is clearly more complex than a static library, dynamic library loader is needed because the size, location information of the symbol table, the symbol dependency and so on, I am here to choose a self-built dynamic and static libraries, for example.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib1.so
 
Dynamic section at offset 0xf00 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
0x0000000c (INIT) 0x3c4
0x0000000d (FINISH) 0x5b0
0x00000019 (INIT_ARRAY) 0x1ef4
0x0000001b (INIT_ARRAYSZ) 4 (bytes)
0x0000001a (FINI_ARRAY) 0x1ef8
0x0000001c (FINI_ARRAYSZ) 4 (bytes)
0x6ffffef5 (GNU_HASH) 0x138
0x00000005 (STRTAB) 0x258
0x00000006 (SYMTAB) 0x178
0x0000000a (STRSZ) 198 (bytes)
0x0000000b (SYMENT) 16 (bytes)
0x00000003 (PLTGOT) 0x1ff4
0x00000002 (PLTRELSZ) 24 (bytes)
0x00000014 (PLTREL) REL
0x00000017 (JMPREL) 0x3ac
0x00000011 (REL) 0x36c
0x00000012 (RELSZ) 64 (bytes)
0x00000013 (RELENT) 8 (bytes)
0x6ffffffe (VERNEED) 0x33c
0x6fffffff (VERNEEDNUM) 1
0x6ffffff0 (VERSYM) 0x31e
0x6ffffffa (RELCOUNT) 3
0x00000000 (NULL) 0x0
X@ubuntu:~/workstation/apps/compiles/Libs$ nm --size-sort -r libdylib1.so
00000029 T dy1_print
00000029 T dy1_clean
00000001 b completed.6874

My dynamic library which only two functions: dy1_printdy1_clean. I used to compile link options are: -fPIC --shared. Nm command can be seen that there are two global code segments, respectively  dy1_printdy1_cleanusing readelf can see that the dynamic libraries that depend  libc.so.6 this dynamic library.

Here is a static library of information:

1
2
3
4
5
6
7
8
9
10
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libstlib1.a
 
File: libstlib1.a(stlib1.o)
X@ubuntu:~/workstation/apps/compiles/Libs$ nm --size-sort -r libstlib1.a
 
stlib1.o:
00000029 T st1_print
00000029 T st1_clean
X@ubuntu:~/workstation/apps/compiles/Libs$ ar -t libstlib1.a
stlib1.o

 

可以看到使用 readelf 完全看不出来静态库的依赖什么的,只有一个 File,它代表该静态库是由 stlib1.o 这个目标文件组成。nm 可以看到符号表有 st1_printst1_clean。从这里也可以看出,静态库单纯就是把目标文件打包在一块,避免每次链接时候需要写一大堆文件文件的尴尬,除此之外,它与单纯的多个 .o 目标文件组合链接生成可执行文件没有任何区别。

库与库之间的依赖

库与库之间的依赖可是非常大的一个知识点儿,与其说是知识点,不如说是坑点,不知道其它公司项目的库与库之间的依赖关系是如何,就我自己接触到的稍微大点的项目,库与库之间的依赖关系简直是一团乱麻,稍有不慎编译的时候就万劫不复。

动态库依赖动态库

我在我的本地环境构造了几个动态库,其中动态库1(libdylib1.so)里面包含 dy1_printdy1_clean 两个符号,动态库2(libdylib2.so)里面包含 dy2_printdy2_clean 两个符号,其中 dy2_clean 调用到了动态库1里面的 dy1_clean。下面我按照这样的方法生成动态库与可执行文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
gcc -o libdylib1.so -fPIC --shared dylib1.c
gcc -o libdylib2.so -fPIC --shared dylib2.c
X@ubuntu:~/workstation/apps/compiles/Libs$ cat main.c
#include <stdio.h>
int main(int argc, char *argv[])
{
dy2_clean();
return 0;
}
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -fPIC -L. -ldylib2
./libdylib2.so: undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -fPIC -L. -ldylib1 -ldylib2
./libdylib2.so: undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -L. -ldylib2 -ldylib1
X@ubuntu:~/workstation/apps/compiles/Libs$ export LD_LIBRARY_PATH=~/workstation/apps/compiles/Libs
X@ubuntu:~/workstation/apps/compiles/Libs$ ./main
This is dylib2's clean funciton.
This is dylib1's clean funciton.

可以看到只有最后一种编译链接方式可以成功编译、生成、运行程序,可以看到如果动态库之间有相互依赖的话在最终链接可执行文件的时候需要把被依赖项放到后面,否则就会提示找不到某某符号,这里也可以看出其依赖解析关系是从前往后的,前面的动态库会往后面找依赖项。

采用这种方式编译出来的库与可执行文件使用 readelf 查看得到依赖关系如下(隐藏部分不关注信息):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib1.so
 
Dynamic section at offset 0xf00 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib2.so
 
Dynamic section at offset 0xf00 contains 24 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d main
 
Dynamic section at offset 0xef8 contains 26 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib2.so]
0x00000001 (NEEDED) Shared library: [libdylib1.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -s libdylib2.so
 
Symbol table '.dynsym' contains 15 entries:
7: 00000000 0 NOTYPE GLOBAL DEFAULT UND dy1_clean

 

可以看到在库层级使用这种方式是看不出来真实的依赖关系的,也就是 libdylib2.so->libdylib1.so 这一层依赖,从这里也可以猜到在动态库生成的时候是没有链接这一步骤的,否则肯定会出现错误提示,并且使用 readelf -s 可以看到动态库2里面的 dy1_clean 属于未定义符号,总结如下:

  1. 动态库生成的时候没有链接动作,并且默认允许未定义符号的存在。
  2. 动态库的依赖关系是从前往后解析的,被依赖者需要放在使用者的前面。
  3. 可执行文件需要解析最终的真实依赖关系,因此必须把所有的动态库全部链接进来。

上面的方式有某些弊端,那就是如果我是直接拿到 [libdylib2.so],[libdylib1.so] 两个成品动态库的话,不加以分析我是不知道它们两个之间的依赖关系的,这种情况下如果我没有更加详细的文档的话我是不知道如何去链接这些动态库的,这个时候可以采用下面的方式进行动态库的生成以及可执行文件的链接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o libdylib2.so -fPIC --shared -Wl,--no-undefined dylib2.c
/tmp/ccrLpPaL.o: In function `dy2_clean':
dylib2.c:(.text+0x4e): undefined reference to `dy1_clean'
collect2: error: ld returned 1 exit status
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o libdylib2.so -fPIC --shared -Wl,--no-undefined dylib2.c -L. -ldylib1
X@ubuntu:~/workstation/apps/compiles/Libs$ gcc -o main main.c -L. -ldylib2
yellow@ubuntu:~/workstation/apps/compiles/Libs$ ./main
This is dylib2's clean funciton.
This is dylib1's clean funciton.
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d libdylib2.so
 
Dynamic section at offset 0xef8 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib1.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]
 
X@ubuntu:~/workstation/apps/compiles/Libs$ readelf -d main
 
Dynamic section at offset 0xf00 contains 25 entries:
Tag Type Name/Value
0x00000001 (NEEDED) Shared library: [libdylib2.so]
0x00000001 (NEEDED) Shared library: [libc.so.6]

 

秘诀在于 -Wl,--no-undefined 这个链接选项,它要求在生成动态库的时候不能有未定义的符号,这个选项可以帮助我们检查动态库之间的依赖关系,最终使用 -l 链接生成动态库之后可以看到器依赖项里面包含了 libdylib1.so,这个时候在生成可执行文件的时候就无需再指定链接多个动态库了,只需要指定链接一个 libdylib2.so 即可,剩下的链接工作编译器自己会去做的。

为什么编译的时候不默认使用 -Wl,--no-undefined 选项呢,这个在后面的库于库之间相互依赖这节会说明。

动态库依赖静态库

动态库依赖静态库是十分不推荐的,因为这违背了动态库的初心,这里就不再去重复做试验了,直接说结果,如果动态库里面有调用到某个静态库里面的函数,并且在生成动态库的时候没有去做 -Wl,--no-undefined 的限定动作,那么在生成可执行文件的时候无论如何都是无法成功生成的。

况且,思考一下,这种依赖关系本身就是畸形的,动态库依赖动态库还是可以理解的,它们都是动态库,种类相同,个性相似,功效雷同。但是动态库一旦依赖了静态库,这就变态了,因为它们的目的不同,动态库本质上是为了运行时动态加载,而静态库则是完全拷贝代码段,这两者的属性是相互排斥的,一旦出现这种依赖关系,动态库还怎么动态加载!!!如果允许这种行为的话,动态库就失去了动态库的意义,从设计哲学上来讲就是不应该允许的。

如果无法避免出现这种情况,那就加上 -Wl,--no-undefined 这个链接选项,这样静态库里面的被依赖代码段就会被拷贝到动态库里面成为动态库的一部分,虽然体积会变大,但是完全不影响它的功能与初心。

静态库依赖静态库

关注静态库的时候,就要把它一眼看透,不要关注它的表面,而要关注它的内在,外在衣物不重要,重要的是里面的东西,那才是本质。那么静态库的本质就是一个个的 .o 目标文件的集合,既然是一个集合,就完全可以按照我们常规理解的 .o 文件之间的相互依赖来进行解析。

静态库依赖静态库就需要按照库的解析顺序,上文说过,需要把被依赖者放在使用者的后面,仅此而已。静态库的生成使用类似 ar -cr libstlibname.a x.o xx.o xxx.o 的命令来生成,它是完全没有链接的过程的,也就是不会有上面的 -Wl,--no-undefined 链接选项可用,在最终生成可执行文件的时候必须得人为解析、指定依赖关系,并且遵循一定的依赖先后顺序。

静态库依赖动态库

静态库依赖动态库与普通目标文件的依赖没有二致,也是需要在链接的时候把相关的动态库放在这个静态库的后面,这样就能完成正确的链接过程。

那么为什么静态库就可以依赖动态库而又不至于变态呢,因为静态库的本质是目标文件的合集,你可以完全把它当做是 main.c 文件的一部分,把左右的目标文件看作一个整体,这样就可以想象得到为什么静态库可以依赖动态库了,本质上它与 main.c 文件依赖动态库是一样的性质。

循环依赖

循环依赖就是库 A 依赖库 B,同时库 B 又依赖库 A,完全就是鸡生蛋、蛋生鸡,虽然在实际的开发过程当中不推荐这种依赖关系,但是有时候又会不可避免的出现这种依赖关系,本质上也很难说到底是不是设计缺陷,但是事实是这种情况是会发生的。

有循环依赖的时候上面有几个点就失效了:

  1. 不能使用 -Wl,--no-undefined 来生成动态库,因为相互依赖问题无法在生成动态库的时候解决。
  2. 生成可执行文件的时候依赖顺序规则失效,不管两个库谁在先谁在后都无法完成链接过程。

我构造两个动态库,libdylib1.so 里面调用 libdylib2.so 里面的函数,libdylib2.so 调用 libdylib1.so 里面的函数,形成相濡以沫的关系,接下来我会在 main.c 里面调用两者里面的一个函数。这样就形成了相互依赖的关系。

相互依赖的关系可以在链接的时候写两遍库来解决,比如:

1
gcc -o main main.c -L. -ldylib1 -ldylib2 -ldylib1 -ldylib2

 

也可以使用:

1
gcc -o main main.c -L. -Wl,--start-group -ldylib1 -ldylib2 -Wl,--end-group

 

不过我发现在一些高版本的编译器中,比如我使用的 gcc-4.9 里面就不用加这些额外的选项,貌似它会自行去解决这些循环依赖关系的。

库的加载

静态库全部都是运行前加载的,在链接时候就全部导入到可执行文件里面的,这个就不说了。动态库有两种加载方式,一个是运行前加载,一个是运行时加载。

  1. 运行前加载
    运行前加载意思就是在程序被执行的时候,在 main 函数之前程序会先去加载它链接时候指定的动态库,等准备好之后才会跑到 main 函数处执行。
  2. 运行时加载
    运行时加载就需要依靠 dlopen、dlsym、dlclose 这些动态库辅助加载函数来完成。这类动态库无需在函数 main 函数之前完成加载,而是在程序里面随用随加载。

使用 strace 跟踪一个可执行文件的运行前加载动态库状况如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
X@ubuntu:~/workstation/apps/compiles/Libs$ strace ./main
access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such file or directory)
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/i686/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/tls/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/i686/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/sse2/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/sse2/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/cmov/libdylib1.so", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/home/yellow/workstation/apps/compiles/Libs/libdylib1.so", O_RDONLY|O_CLOEXEC) = 3
open("/home/yellow/workstation/apps/compiles/Libs/libdylib2.so", O_RDONLY|O_CLOEXEC) = 3
open("/home/yellow/workstation/apps/compiles/Libs/libc.so.6", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3

 

可以看到它找动态库的路径是极为冗长的,这是跟 ld.so 程序的一个特性有关的,具体的特性是一个动态库版本查找规则,这里就不深入去说了,这个冗长的步骤是无法去除的,也就是无法一步到位,因为这是个特性哈,知道有这个过程就行,并且在 ld.so.cache 文件里面可以自定义程序的库路径查找逻辑。

拓展

  1. 动态库的裁剪
    可以通过 readelf -d 来分析动态库的依赖关系,可执行文件的依赖关系来确认是否有用不到的动态库,如果用不到,删除之,当然要注意添加白名单,主要是照顾那些运行时使用 dl 库函数加载的动态库。
  2. 静态库打包到动态库里面
    有时候我们可能需要把一个静态库成品转化为一个动态库,就可以使用:
    1
    gcc -shared -o libdylib1.so -L. -Wl,--whole-archive libstlib1.a -Wl,--no-whole-archive

这个会把静态库全部打包进动态库,使用 readelf -s 可以看到动态库里面多了一些符号。

    1. 动态库的预加载
      写一个空的 main 函数,只包含一个 hello world,但是链接的时候添加想要预加载的动态库,就可以使用该袖珍版程序提前把动态库加载到内存里面,在真正的可执行程序运行的时候这个动态库的家在过程就快很多了。通常这个特性会用在嵌入式设备的快速启动优化当中,不细讲了,仅抛砖引玉。
    2. 符号表大小分析
      使用 nm 命令可以分析 elf 文件里面的符号表,特别关注其大小信息就可以使用 nm --size-sort -r elf,这个可以把符号表由大到小排列,用于裁剪程序的体积。
    3. 不加载未使用的函数
      在静态库链接的时候可能会有很多未使用到的函数、变量等等,可以使用 -fdata-sections-ffunciton-sections 等选项来去掉那些用不到的函数,这里去掉只是在最终的可执行文件里面看不到而已,并不是从静态库里面删掉。

Guess you like

Origin www.cnblogs.com/Spider-spiders/p/11699266.html