0

0

深入了解linux系统—— 库的链接和加载

雪夜

雪夜

发布时间:2025-08-15 14:06:21

|

1015人浏览过

|

来源于php中文网

原创

一、目标文件

我们知道源文件经过编译链接形成可执行程序,在

Windows
下这两个步骤被
IDEA
封装的很完美,我们使用起来也非常方便;

Linux
中,我们可以通过
gcc
编译器来完成编译链接这一系列操作。

而源文件经过编译形成

.o
文件,而库文件是由
.o
文件形成的;那
.o
文件是什么呢?

.o
文件被称为目标文件,也被称为可重定位目标文件;

目标文件是一个二进制文件,其格式是

ELF

二、ELF文件

而我们还知道,我们源文件经过编译形成

.o
文件,然后再经过链接(将所有的
.o
文件合并,链接库文件)才能形成可执行文件。

那在链接的过程中做了什么呢?我们的

.o
文件为什么能够和库文件进行链接呢?

目标文件的文件格式是

ELF
、库文件的文件格式也是
ELF
、可执行文件的文件格式也是
ELF

深入了解linux系统—— 库的链接和加载

可以看到

.o
目标文件、共享目标文件、可执行文件的文件格式都是
ELF

ELF文件组成

.o
、库文件
.so
(静态库
.a
.o
的归档文件)以及可执行文件都是
ELF
格式文件,那
ELF
文件都包含什么呢?

一般

ELF
文件都包含以下几部分:

ELF
头(
ELF Header
)程序头表(
program Header Table
)节(
Section
)节头表(
Section Header Table
深入了解linux系统—— 库的链接和加载

那这些部分都包含哪些内容呢?

Linux
系统中,我们可以通过指令
readelf
来查看
ELF
格式文件的这几个部分的内容。

节(

Section

节是

ELF
文件中的基本组成单位,包含了特定类型的数据;
ELF
文件的各种信息和数据都存储在不同的节中。

例如

.text
.data
.bss
等。

节头表(

Section Header Table

ELF
文件中存在非常多的节,那如何区分这些节呢?

在节头表

Section Header Table
中,就记录了每一个节的描述信息。(比如
Name
Type
Address

我们可以使用

readelf -S
来查看一个ELF文件的节头表:

深入了解linux系统—— 库的链接和加载

这里内容比较多,只截取了一部分;

在这里面,我们可以看到存在

.text
代码、
.data
数据、
rodata
只读数据、
.bss
等。

程序头表

program Header Table

在程序头表中,记录了所有有效段和它们的属性。

在表中记录着每一段的开始位置和位移

offset
、长度;以及这些段如何去合并

我们可以使用

readelf -l
查看ELF文件的程序头表

深入了解linux系统—— 库的链接和加载

ELF Header

ELF Header
中,记录了文件的主要特性,程序的入口地址、节的个数,大小等等。

readelf -h
可以查看一个ELF文件的
ELF Header

深入了解linux系统—— 库的链接和加载
可执行程序的形成

了解了ELF文件的组成,感觉还是云里雾里的;(这里了解ELF文件中有哪几个部分组成,每一个部分大概内容即可)

.o
文件,库文件
.so
以及可执行文件都是ELF文件,那我们的可执行程序(文件)如何形成的呢?

这个就比较简单了,因为我们的

.o
文件的格式都是ELF,所以我们所有的
.o
文件形成可执行时,只需要将所有相同的数据节进行合并形成一个大的数据节;也就是形成一个大的ELF格式的文件。

深入了解linux系统—— 库的链接和加载

这里简单了解一下,在后续链接和加载内容中详细说明。

ELF可执行文件的加载

我们知道一个ELF文件中存在非常多的

Section
,在加载到内存时,也会进行
Section
的合并,形成
segment

合并规则:相同属性,可读/可写/可执行等等。

这样不同的

Section
在加载到内存时,就会合并成
segment

而合并方式在形成ELF时就已经确定了,在ELF的程序头表

Paogram Header Table
中我们可以查看。

深入了解linux系统—— 库的链接和加载

可以看到我们的

.text
代码段和
.rodata
只读数据段是被合并到一个
segment
的;

.got
.data
.bss
段这些可读可写的数据是合并到一个
segment
的。

而我们ELF文件中存在那么多节,如果我们不进行合并就发发现在内存中有非常多的块
4
KB空间中都存在浪费的空间,而合并节形成
segment
就是为了减小页面碎片,提供内存使用率。还用一点就是:将相同权限的节进行合并,这样具有相同属性的节就会形成一个大的
segment
;这样就可以更好的进行内存管理和权限访问控制。三、链接和加载静态链接

首先我们要知道,静态链接本质上就是将我们所有的

.o
文件和静态库文件进行合并,形成可执行;(静态库就是
.o
文件的归档,所以:静态链接本质上就和将所有的
.o
链接起来)

.o
文件是如何链接的呢?

现在有存在

code.c
fun.c
两份源文件,简单代码:

代码语言:javascript代码运行次数:0运行复制
//code.c#include void fun();int main(){    fun();printf("code: \n");return 0;}//fun.c#include void fun(){printf("fun: \n");}

我们知道编译形成的

.o
以及最终形成的可执行程序都是二进制文件,我们可以使用
objdump
对这些二进制文件进行反汇编。

objdump -d
对代码部分进行反汇编。

深入了解linux系统—— 库的链接和加载

我们可以发现,在

code.c
fun.c
各自经过编译后形成的
.o
文件,经过反汇编,我们发现,在
code.s
中调用
fun
函数时,
cal
调用的函数地址是
0
,调用
printf
函数的地址也是
0
;在
fun.s
call
调用
printf
的地址也是
0

我们再来对可执行程序进行返汇编查看一下:

深入了解linux系统—— 库的链接和加载

可以看到,在可执行程序反汇编形成的文件中,

callq
调用函数时 就有了函数的地址。

readelf -S
可以查看ELF文件的符号表

MTTSHOP包包免费商城系统
MTTSHOP包包免费商城系统

一款非常包包、衣服、鞋子类网站,页面干净清洁、一目了然,mttshop打造精致、简单、易用、免费的商城。 系统要求:IIS5.1以后,必须安装.net 3.5 安装步骤: 1、下载完成后,直接解压文件mttshop.rar 2、附加数据库:解压后的可以找一个叫db的文件夹,解压后直接附加就可以,支持SQL 2000、2005、2008 3、配置web.config文件,找到key=&qu

下载
深入了解linux系统—— 库的链接和加载

在链接时,对照符号表,根据表里记录的地址来修正函数的地址。

深入了解linux系统—— 库的链接和加载

所以,链接的本质上就是编译之后的所有目标文件连同用到的一些静态库运行时库组合,形成一个独立的可执行文件。

当所有模块组合在一起之后,链接器就会根据我们的

.o
文件或者静态库中的重定位表找到那些被重定位的函数,从而修改它们的地址。

我们链接的过程中就会涉及到对目标文件

.o
的地址修正(地址重定位);所以
.o
目标文件也被称为可重定位目标文件。

加载ELF和进程地址空间

这里有一个问题:进程地址空间

mm_struct
vm_area_struct
在进程刚创建时,初始化数据从哪里来?

我们对

.o
文件、可执行文件进行反汇编可以发现,一个ELF文件,在还没有被加载到内存时,在其内部就存在地址;

深入了解linux系统—— 库的链接和加载

上图中最左侧就是ELF文件中的地址;严格意义上将,这种地址应该叫做逻辑地址:起始位置+偏移量。

而当我们把起始位置当做

0
,此时就成了虚拟地址;也就是说,在我们的程序还没加载到内存时,就已经把可执行程序进行统一编址了。

所以,我们进程在创建时,虚拟地址空间

mm_struct
vm_arae_sruct
的初始化数据从哪里来?就显而易见了;

从ELF中的

segment
中来,而每一个
segment
都有自己的起始位置和长度,就用来初始化内核数据结构
vm_area_struct
[start , end]
等数据,以及初始化页表。

所以虚拟地址,不仅操作系统要支持,而编译器也要支持。(因为程序在还没有加载到内存时,就已经进行统一编址了)

进程地址空间

所以,在程序运行时,该可执行程序要加载到内存;而进程的进程地址空间

mm_struct
中的虚拟地址从可执行程序中来,而可执行程序的代码和数据要加载到内存;操作系统就要为这些代码和数据开辟空间,然后填充页表。

这样进程才能被

CPU
调度,那
CPU
是如何知道进程的起始地址呢?

还记得在ELF格式的

ELF Header
中,存在一个
Entry point address
入口地址,所以在
CPU
调度进程时,CPU中的
EIP
寄存器,就会记录下一条要运行指令的地址;而在
CPU
在还存在
CR3
寄存器,它指向当前进程的页表。

所以在进程被调度时,就会把

Entry point address
入口地址拷贝到
CPU
中的
EIP
寄存器中,然后再修改CPU中其他信息(如
CR3
等);这样
CPU
就知道进程的起始地址;而且还知道当前进程的页表,根据进程地址空间中的虚拟地址,查找页表就可以找到当前进程代码和数据的物理地址。

深入了解linux系统—— 库的链接和加载
动态库

动态链接,简单来说就是将可执行程序和库产生关联,然后在程序运行时再加载动态库;

这也是我们在动态链接我们自己的库,生成可执行,在运行时还需要让系统找到我们的库的原因。

程序在运行时,才会加载动态库,那进程如何看到我们的动态库呢?

了解了一个进程如何找到我们的动态库;我们还知道动态库也被称为共享目标文件,也就是说我们的库可以被多个进程共享的。

所以在我们进程去找自己依赖的库时,如果当前库已经被加载到内存了,当前进程就可以根据库文件的

struct file
找到库文件的
struct path
,再根据
path
找到
struct dentry
然后就可以找到库文件的
inode
这样就可以找到库文件。

这样讲库映射到进程的地址空间中,这样多个进程就共享同一个库了;而在内存中库文件就只存在一份

深入了解linux系统—— 库的链接和加载
动态链接

我们知道静态链接就是将静态库合并到我们的可执行程序中,这样静态链接形成的可执行不依赖库,就可以执行;按理来说应该比的链接更加方便。

但是,当我们静态库文件特别大,我们如果使用静态链接,这样形成的可执行都包含一份静态库代码;而当程序运行起来时,在内存中就势必会存在多份源文件代码。

再看动态链接,只是将可执行文件和动态库文件产生关联,在程序运行时才进行链接,可执行文件中不存在库代码;而且在内存中,多个进程可以共享一个动态库,在内存中也不会出现多份库代码。

那动态链接如何实现的呢?

可执行程序被编译器处理过

C/C++
程序开始执行时,它并不是直接执行
main
函数;

实际是程序的入口不是

main
,而是
_start

深入了解linux系统—— 库的链接和加载

可以看到可执行程序的入口地址并不是

main
、而是
_start
Linux
下);
_start
函数是C运行时库(
glibc
)或者链接器(
id
)提供的特殊函数。

_start
函数它做了什么呢?

设置堆栈:为程序创建堆栈环境。初始化数据段:将程序的数据段(全局变量/静态变量)从初始化数据段拷贝到相应的内存位置,清零未初始化的数据段。动态链接:(关键)
_start
函数会调用动态链接器的代码来解析和加载程序所以来的动态库;动态链接器会处理所有符号解析和重定位,确保程序中的函数调用和变量访问能正确映射到动态库中的实际位置。调用
_libc_start_main
:链接完成之后,
_start
函数就会调用
libc_start_main
函数(
glibc
提供的);
lib_start_main
函数负责执行额外的初始化工作(例如设置信号处理函数,初始化线程库等);调用
main
函数:
lib_start_main
调用程序的
main
函数,此时程序的执行才到了用户编写的代码;处理
main
返回值:
main
返回时,
lib_start_main
就会处理这个返回值,然后调用
_exit
终止程序。

动态链接器:负责在程序运行时加载动态库

深入了解linux系统—— 库的链接和加载

可以看到这里程序都依赖

ld-linux-x86-64.so.2
这一个库。也就是动态链接器库

在程序启动时,动态链接器就会解析程序中的动态库依赖,并将这些库加载到内存中;
Linux
系统通过环境变量(
LD_LIBRARY_PATH
)/配置文件(
etc/ld.so.conf
)来指定动态库的搜索路径;这些路径会被动态链接器在加载动态库时进行搜索。

缓存文件:

为了提高动态库的加载效率,
Linux
系统会维护一个
/etc/ld.so.cache
的缓冲文件;该文件保存了系统中所有已知动态库的路径和相关信息,动态链接器在加载动态库时会优先搜索这个缓冲文件。库函数调用

说了这么多,那动态库该如何链接和加载的呢?我们的程序又是如何调用库函数呢?

那我们就清楚了,库映射到进程地址空间后,我们只需要知道库的起始虚拟地址和要调用方法的偏移量就可以找到库中的方法。

那也就是说,我们在动态链接的过程中,我们只需要记录下来调用库函数依赖的库名称,和该库函数中动态库中的偏移量地址即可。

深入了解linux系统—— 库的链接和加载
全局偏移量表
GOT
在动态链接的过程中,在程序中就下来了库函数所依赖的库名称,以及该函数在动态库中的偏移量地址。这样在程序运行时,要进行动态库的加载(如果库在内存中已经存在,就直接映射),这样在程序的地址空间中就存在了动态库的映射;那也就知道了库的起始虚拟地址;这样,我们再对加载到内存中的程序的库函数调用处,修改动态库的地址;在内存中完成二次地址重定位。

但是,我们知道代码区是只读的;那如何修改呢?代码区是不能修改的。

深入了解linux系统—— 库的链接和加载

所以,在动态链接的时候,在可执行程序中就存在了全局偏移量表

GOT
;在动态链接时在表中就存放了调用库函数的库名称和函数的 偏移量地址。

这样执行加载时,库加载映射到进程的进程地址空间中,然后修改

GOT
表中的库的虚拟地址即可。

深入了解linux系统—— 库的链接和加载
在一个
.so
动态库中,
GOT
表和
.text
的相对位置都是固定的,就可以使用
CPU
的相对寻址来查找
GOT
表;在调用库函数时,就会先查
GOT
表,根据表中的地址进行跳转,跳转到要调用函数的位置。(这里表中的地址在库加载就会被修改成真实的地址)。

这种方式实现动态链接被称为 地址无关码; 简单来说就是:我们动态库不需要做任何修改,被加载到任意内存地址处都能正常运行,且能够被所有进程共享。 这也就是在制作动态库,生成

.o
文件要带
-fPIC
选项的原因。

什么是
plc
深入了解linux系统—— 库的链接和加载

我们通过查看汇编代码可以发现,在进行库函数调用时,存在一个

plc
)。

这里

plc
指的是什么呢?

plc
简单来说就是延迟绑定

我们知道动态链接在程序加载时需要进程大量函数地址的重定位(修改大量的函数地址),显然是非常耗费时间的;并且有很多函数我们并没有调用。

为了进一步降低消耗,操作系统就会做优化:延迟绑定也就是

plc

最后在这里补充一点:库函数也会调用其他库函数,也就是库之间也存在依赖。

到这里本篇文件内容就大致结束了

简单总结:

ELF文件:
ELF Header
program Header Table
Section Header Table
Section
静态链接:将所有目标文件和静态库文件进行合并,进程地址重定位。进程地址空间:在ELF格式文件中存在逻辑地址(起始位置+偏移量),起始位置为
0
,偏移量地址也就是虚拟地址;在进程创建时进程地址空间
mm_struct
vm_area_struct
中初始化数据就来源于可执行文件中的地址。动态链接:链接时在可执行文件中记录库函数所依赖的库和偏移量地址(GOT表),在加载时根据动态库在进程地址空间的映射位置进行地址重定位;这样无论库加载到内存的什么地方,都要映射到进程地址空间中,这样在执行函数时通过查找GOT表的方式进行调用。
plc
延迟绑定:在第一次进行调用函数时,
GOT
表中指向辅助代码
/stup
,去查找函数真正的跳转地址,并更新
GOT
表;再次调用函数时,就直接跳转到函数的真正地址。

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

556

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

732

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

414

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

991

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

658

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

552

2023.09.20

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

68

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
【web前端】Node.js快速入门
【web前端】Node.js快速入门

共16课时 | 2万人学习

Webpack4.x---十天技能课堂
Webpack4.x---十天技能课堂

共20课时 | 1.4万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号