~Linux C_20_静态库与动态库

Linux系统中“动态库”和“静态库”那点事儿 - 阅读笔记


 

ELF

在Linux操作系统中,普遍使用ELF格式作为可执行程序或者程序生成过程中的中间格式。

ELF(Executable and Linking Format,可执行连接格式)是UNIX系统实验室(USL)作为应用程序二进制接口(Application BinaryInterface,ABI)而开发和发布的。

工具接口标准委员会(TIS)选择了正在发展中的ELF标准作为工作在32位Intel体系上不同操作系统之间可移植的二进制文件格式

 

gcc编译过程三步骤

源代码到可执行程序的转换时需要经历如下图所示的过程:c语言 --> 汇编 --> 二进制文件

l 编译是指把用高级语言编写的程序转换成相应处理器的汇编语言程序的过程。从本质上讲,编译是一个文本转换的过程。对嵌入式系统而言,一般要把用C语言编写的程序转换成处理器的汇编代码。编译过程包含了C语言的语法解析和汇编码的生成两个步骤。编译一般是逐个文件进行的,对于每一个C语言编写的文件,可能还需要进行预处理。

l 汇编是从汇编语言程序生成目标系统的二进制代码(机器代码)的过程。机器代码的生成和处理器有密切的联系。相对于编译过程的语法解析,汇编的过程相对简单。这是因为对于一款特定的处理器,其汇编语言和二进制的机器代码是一一对应的。汇编过程的输入是汇编代码,这个汇编代码可能来源于编译过程的输出,也可以是直接用汇编语言书写的程序。

l 连接是指将汇编生成的多段机器代码组合成一个可执行程序。一般来说,通过编译和汇编过程,每一个源文件将生成一个目标文件。连接器的作用就是将这些目标文件组合起来,组合的过程包括了代码段、数据段等部分的合并,以及添加相应的文件头。

 

运行 “可执行程序”

作为UNIX操作系统的一种,Linux的操作系统提供了一系列的接口,这些接口被称为系统调用(System Call)。在UNIX的理念中,系统调用"提供的是机制,而不是策略"。C语言的库函数通过调用系统调用来实现,库函数对上层提供了C语言库文件的接口。在应用程序层,通过调用C语言库函数和系统调用来实现功能。一般来说,应用程序大多使用C语言库函数实现其功能,较少使用系统调用。

 

 

问题:ELF的结构细节

 

 

ELF 三大种类

那么最后的可执行文件到底是什么样子呢?前面已经说过,这里我们不深入分析ELF文件的格式,只是给出它的一个结构图和一些简单的说明,以方便大家理解。

ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。

1.可执行文件(应用程序)

可执行文件包含了代码和数据,是可以直接运行的程序。

2.可重定向文件(*.o)

可重定向文件又称为目标文件,它包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)。

*.o文件参与程序的连接(创建一个程序)和程序的执行(运行一个程序),它提供了一个方便有效的方法来用并行的视角看待文件的内容,这些*.o文件的活动可以反映出不同的需要。

Linux下,我们可以用gcc -c编译源文件时可将其编译成*.o格式。

3.共享文件(*.so)

也称为动态库文件,它包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为ld.so.1,libc.so.1或者 ld-linux.so.1。我的CentOS6.0系统中该文件为:/lib/ld-2.12.so

 

连接器和加载器 (单独讲)

俩角度看ELF文件:

  • 从连接器(Linker)的角度看,是一些节的集合;
  • 从程序加载器(Loader)的角度看,它是一些段(Segments)的集合。

ELF格式的程序和共享库具有相同的结构,只是段的集合和节的集合上有些不同。

 

 

问题:何为"段“,啥为"节"

 

 

静态库 和 动态库

那么到底什么是库呢?

库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行。

库分 静态库 和 动态库 两种。

  • 静态库:这类库的名字一般是libxxx.a,xxx为库的名字。利用静态函数库编译成的文件比较大,因为整个函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。
  • 动态库:这类库的名字一般是libxxx.M.N.so,同样的xxx为库的名字,M是库的主版本号,N是库的副版本号。当然也可以不要版本号,但名字必须有。相对于静态函数库,动态函数库在编译的时候并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。linux系统有几个重要的目录存放相应的函数库,如/lib /usr/lib。

当要使用静态的程序库时,连接器会找出程序所需的函数,然后将它们拷贝到执行文件,由于这种拷贝是完整的,所以一旦连接成功,静态程序库也就不再需要了。

动态库会在执行程序内留下一个标记指明当程序执行时,首先必须载入这个库。由于动态库节省空间,linux下进行连接的缺省操作是首先连接动态库,也就是说,如果同时存在静态和动态库,不特别指定的话,将与动态库相连接。

  

制作 “静态链接库”

静态库*.a文件的存在主要是为了支持较老的a.out格式的可执行文件而存在的。[历史使命]

ar -rsv libmytest.a st1.o st2.o

ar -t libmytest.a
  st1.o
  st2.o

gcc -o test main.c -L./ -lmytest

 

搜索 "动态库"

有时候当我们的应用程序无法运行时,它会提示我们说它找不到什么样的库。

那么应用程序它是怎么知道需要哪些库的呢?命令ldd,用就是用来查看一个文件到底依赖了那些so库文件。

  1. Linux系统中动态链接库的配置文件一般在/etc/ld.so.conf文件内,它里面存放的内容是可以被Linux共享的动态联库所在的目录的名字。
  2. 然后/etc/ld.so.conf.d/目录下存放了很多*.conf文件。
  3. 其中每个conf文件代表了一种应用的库配置内容。

 

高缓动态库

在/etc目录下还存在一个名叫ld.so.cache的文件。

对,您说的一点没错。为了使得动态链接库可以被系统使用,当我们修改了/etc/ld.so.conf或/etc/ld.so.conf.d/目录下的任何文件,或者往那些目录下拷贝了新的动态链接库文件时,都需要运行一个很重要的命令:ldconfig,该命令位于/sbin目录下,主要的用途就是负责搜索/lib和/usr/lib,

 

更新动态链接库文件时,都需要运行一个很重要的命令:ldconfig,负责:

  1. 搜索/lib和/usr/lib。
  2. 配置文件/etc/ld.so.conf里所列的目录下搜索可用的动态链接库文件,
  3. 然后创建处动态加载程序 /lib/ld-linux.so.2 所需要的连接 和 (默认) 缓存文件 /etc/ld.so.cache (此文件里保存着已经排好序的动态链接库名字列表)。

 

制作 “动态库”

我们有一个头文件 my_so_test.h 和 三个源文件test_a.c、test_b.c和test_c.c,将他们制作成一个名为libtest.so的动态链接库文件:

gcc -c test.c test_a.c test_b.c test_c.c
gcc -shared -fPCI -o libtest.so test_a.o test_b.o test_c.o 

-shared 该选项指定生成动态连接库(让连接器生成 T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件。

-fPIC:表示编译为位置独立的代码;不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。

 

 

问题:导出符号表?

 

 

使用 “动态库”

动态链接库的使用有两种方法:

  • 既可以 - 在运行时对其进行动态链接,
  • 又可以 - 动态加载在程序中使用它们。

 

用法一:动态链接

“-ltest”标记 来告诉GCC驱动程序在连接阶段引用共享函数库libtest.so。

“-L.”标记 告诉GCC函数库可能位于当前目录。否则GNU连接器会查找标准系统函数目录。

  • 问:这里我们注意,ldd的输出它说我们的libtest.so它没找到。
  • 答:因为我们的libtest.so既不在/etc/ld.so.cache里,又不在/lib、/usr/lib或/etc/ld.so.conf所指定的任何一个目录中。怎么办?

 

给大家演示动态库的用法,完了之后我就把libtest.so给删了,然后再重构ld.so.cache,对我的系统不会任何影响。

倘若是开发一款软件,或者给自己的系统DIY一个非常有用的功能模块,那么

将libtest.so拷贝到/lib、/usr/lib目录下,或者

  1. 还有可能在/usr/local/lib/目录下新建一文件夹xxx,将so库拷贝到那儿去,【放进去】
  2. 并在/etc/ld.so.conf.d/目录下新建一文件mytest.conf,内容只有一行“/usr/local/lib/xxx/libtest.so”,【指定位置】
  3. 再执行ldconfig。【生效】

 

方法二:动态加载

动态加载是非常灵活的,它依赖于一套Linux提供的标准API来完成。在源程序里,你可以很自如的运用API来加载、使用、释放so库资源。以下函数在代码中使用需要包含头文件:dlfcn.h

const char *dlerror(void)

当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。

void *dlopen(const char *filename, int flag)

用于打开指定名字(filename)的动态链接库,并返回操作句柄。调用失败时,将返回NULL值,否则返回的是操作句柄。

void *dlsym(void *handle, char *symbol)

根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。

int dlclose (void *handle)

用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。2.2在程序中使用动态链接库函数。

 

 

Goto: 静态库动态库的区别

静态库可以理解为:.o文件的集合。

 

 

重定位&编译选项:-fPIC

fPIC的目的是什么?共享对象可能会被不同的进程加载到不同的位置上,如果共享对象中的指令使用了绝对地址、外部模块地址,那么在共享对象被加载时就必须根据相关模块的加载位置对这个地址做调整,也就是修改这些地址,让它在对应进程中能正确访问,而被修改到的段就不能实现多进程共享一份物理内存,它们在每个进程中都必须有一份物理内存的拷贝。
-fPIC指令就是为了让使用到同一个共享对象的多个进程能尽可能多的共享物理内存,它背后把那些涉及到绝对地址、外部模块地址访问的地方都抽离出来,保证代码段的内容可以多进程相同,实现共享。
 
抽离出这部分特殊的指令、地址之后,放到了一个叫做GOT(Global Offset Table)的地方,它放在数据段中,每一个进程都有独立的一份,里面的内容可能是变量的地址、函数的地址,不同进程它的内容很可能是不同的,这部分就是被隔离开的“地址相关”内容。
模块被加载的时候,会把GOT表的内容填充好(在没有延迟绑定的情况下)。代码段要访问到GOT时,通过类似于window的call/pop/sub指令得到GOT对应项的地址。
 
对于模块中全局变量的访问,为了解决可执行文件跟模块可能拥有同一个全局变量的问题(此时,模块内的全局变量会被覆盖为可执行文件中的全局变量),对模块中的全局变量访问也通过GOT间接访问。
这样子,每一次访问全局变量、外部函数都需要去计算在GOT中的位置,然后再通过对应项的值访问变量、调用函数。从运行性能上来说,比装载时重定位要差点。装载时重定位就是不使用fPIC参数,代码段需要一个重定位表,在装载时修正所有特殊地址,以后运行时不需要再有GOT位置计算和间接访问。(但是,我在自己机子上测试,编译链接共享库时,没法不使用fPIC参数,可能多数系统都要求必须有fPIC)
 
如果在装载时就去计算GOT的内容,那么会影响加载速度,于是就有了延迟绑定(Lazy Binding),直到用时才去填充GOT。它使用到了PLT(Procedure Linkage Table):每一项都是一小段代码,对应于本运行模块要引用的函数。函数调用时,先到这里,然后再到GOT。在函数第一次被调用时,进入PLT跳到加载器,加载器计算函数的真正地址,然后将地址写入GOT对应项,以后的调用就直接从PLT跳到GOT记录的函数位置。这样也减少了运行时多次调用多次计算GOT位置。
PIC的共享对象也会有重定位表,数据段中的GOT、数据的绝对地址引用,这些都是需要重定位的。

 

End.

posted @ 2019-06-27 18:11  郝壹贰叁  阅读(174)  评论(0)    收藏  举报