Linux:库与链接

发布于:2025-05-20 ⋅ 阅读:(15) ⋅ 点赞:(0)

库是预先编译好、可执⾏的⼆进制码,可以被操作系统加载到内存中执⾏。

库有两种:

静态库:.a(Linux)、.lib(Windows)

动态库:.so(Linux)、.dil(Windows)

静态库

1.程序在链接时把库的代码链接到可执⾏⽂件中,运⾏时将不再需要静态库
2.对于同名的动静态库,会优先使用动态库,我们也可以使⽤gcc的-static选项强制其使用静态库

下面演示将一个.o文件打包成库并在其他出调用的过程:

1.将.o文件打包为lib_mystdio.a

2.在另一个目录中将test.o与打包好的库进行链接,得到可执行程序test

动态库

程序在运⾏的时候才去链接动态库的代码,多个程序共享动态库的代码。

⼀个与动态库链接的可执⾏⽂件仅仅包含它⽤到的函数⼊⼝地址的⼀个表,⽽不是外部函数所在⽬ 标⽂件的整个机器码

 在可执⾏⽂件开始运⾏以前,外部函数的机器码由操作系统从磁盘上的该动态库中复制到内存中, 这个过程称为动态链接(dynamic linking)

动态库可以在多个程序间共享,所以动态链接使得可执⾏⽂件更⼩。操作系统采⽤虚拟内存机制允许物理内存中的⼀份动态库被所有进程共⽤,节省了内存和磁盘空间。

我们将.o文件打包为一个动态库,链接成功形成a.out,但是程序在执行时报错:

这是因为程序运行时要将动态库的代码加载到内存中,因此需要让OS知道该动态库的路径,有以下几种解决方案:

1.拷⻉ .so ⽂件到系统共享库路径下, 如 /usr/lib、/usr/local/lib、/lib64 等

2.在系统共享库路径下建⽴同名软链接

3.更改环境变量: LD_LIBRARY_PATH(动态库的搜索路径)

4.更改系统配置文件,使得动态库的搜索路径扩大为全局

实践中推荐前两种方案,方案三只是临时方案,如果需要永久修改需要更改系统配置文件

ELF文件

编译和链接的目的,是得到二进制代码文件,即目标文件,在Linux中,目标文件被称为ELF文件

ELF文件有以下三种形式:

可重定位⽂件(Relocatable File) :即 xxx.o ⽂件。包含适合于与其他⽬标⽂件链接来创建可执⾏⽂件或者共享⽬标⽂件的代码和数据。

可执⾏⽂件(Executable File) :即可执⾏程序,可直接被加载器加载执行。 

共享⽬标⽂件(Shared Object File) :即 xxx.so⽂件,可被动态的加载和链接。 

ELF文件结构

由于目标文件可能需要被链接器链接得到新的目标文件,也可能需要被加载器加载到内存,因此我们可以从这两种视角分别认识ELF文件:

ELF头(ELF header) :

描述⽂件的主要特性。其位于⽂件的开始位置,主要作用是定位⽂件的其他部分。

程序头表(Program header table) :

包含对段(segment)的描述。列举了所有有效的段(segments)和段的属性。记录了每个段的开始的位置和位移(offset)、⻓度。

节头表(Section header table) :

包含对节(sections)的描述。

节(Section ):

目标文件的代码和数据

使用readelf命令查看mystio.o:

ELF Header:

Section Headers:

program headers:

为什么mystdio.h中没有program headers:

只有在ELF文件被加载到内存时,加载器才会把ELF文件中的section合并成segment,因此对于可重定位目标文件,需要链接器做进一步处理,所以一定有Section Header Table;对于可执行文件,需要加载运行,所以一定有Program Header Table;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。

为什么Address全是0:

Address是这些段加载到内存中的地址,加载地址要在链接时填写,现在空缺,所以是全0。

查看另一个可执行文件test,就会发现Address不为0,验证了上述说法:

ELF文件从形成到加载

形成可执行文件

链接时,多个.o文件的相同属性的section会进行合并:

加载可执行文件

ELF文件在加载到内存的时,多个相同属性的section会合并,形成segment 。这么做可以减少内存碎⽚,提⾼内存使⽤效率并且方便内存管理。

静态链接

上面提到可重定位文件中的section的Address全是0,在链接后有了虚拟地址,Addresses也被填写了

这里分为两种情况:

1.section使用了当前文件的函数或数据

2.section使用了外部函数或数据

首先在内存中,绝对地址=起始地址+相对地址(偏移量)

对于情况1,当前文件中记录了每个section的offset(偏移量),只需根据当前文件的起始地址+偏移量即可找到

对于情况2,则存在问题:当前文件是如何得知该函数或数据的地址呢

这个问题是通过链接时的重定位来解决的:

重定位就是两次定位:两次定位就是确定外部函数或数据的起始地址和相对地址,其中起始地址就是外部函数或数据所在的文件的起始地址,偏移量就是该内容在其所在文件的偏移量

动态链接

动态库在程序运行时会被加载到内存的共享区,可以供所有进程使用

动态链接实际上将链接的全过程推迟到了程序运行时。运⾏⼀个程序时,操作系统会⾸先将程序的数据代码连同它⽤到的⼀系列动态库先加载到内存,其中每个动态库的加载地址都是不固定的,操作系统会根据当前地址空间的使⽤情况为它们动态分配⼀段内存。 当动态库被加载到内存以后,⼀旦它的内存地址被确定,就可以去修正动态库中的那些函数跳转地址了。

动态链接器:

C/C++程序的⼊⼝是 _start ,这是⼀个由C运⾏时库(通常是glibc)或链接器(如ld)提供的特殊函数。 在_start 函数中,会调⽤动态链接器的代码来解析和加载程序所依赖的动态库(shared libraries)。动态链接器会处理所有的符号解析和重定位,确保程序中的函数调⽤和变量访问能够正确地映射到动态库中的实际地址。

环境变量和配置⽂件:

Linux系统通过环境变量(如LD_LIBRARY_PATH)和配置⽂件(如/etc/ld.so.conf及其⼦配置⽂件)来指定动态库的搜索路径。这些路径会被动态链接器在加载动态库时搜索。

缓存⽂件:

为了提⾼动态库的加载效率,Linux系统会维护⼀个名为/etc/ld.so.cache的缓存⽂件。

该⽂件包含了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会⾸先 搜索这个缓存⽂件。

上述内容存在一个问题:内存中的代码不是只读的吗,怎么还能被修改地址?

全局偏移量表GOT(global offset table)

由于无法修改代码区的内容,因此在数据区专门设立了一个全局偏移量表GOT来存放函数的跳转地址,因此实际上动态链接的过程修改的是GOT中的内容

1.在不同进程的地址空间中,各动态库的绝对地址、相对位置都不同。反映到GOT表上,就是每个进程的每个动态库都有独⽴的GOT表,所以进程间不能共享GOT表,但是可以通过GOT表共享同一物理地址下的动态库。

2. 在调⽤函数的时候会⾸先查表,然后根据表中的地址来进⾏跳转,这些地址在动态库加载的时候会被修改为真正的地址。

3. 这种⽅式实现的动态链接就被叫做 PIC 与地址⽆关代码 。换句话说,我们的动态库不需要做任何修改,被加载到任意内存地址都能够正常运⾏,并且能够被所有进程共享,这也是为什么之前我们给编译器指定-fPIC参数的原因,PIC=表中相对地址+GOT地址。


网站公告

今日签到

点亮在社区的每一天
去签到