【知识讲解】 Linux动静态链接认识


目录

前言

[Part1. ELF 各段基础认知](#Part1. ELF 各段基础认知)

[Part2. ELF 文件整体构成](#Part2. ELF 文件整体构成)

[Part3. 目标文件 .o、符号、重定位](#Part3. 目标文件 .o、符号、重定位)

[Part4. 地址的概念:逻辑地址、偏移量、虚拟地址](#Part4. 地址的概念:逻辑地址、偏移量、虚拟地址)

[Part5. 操作系统加载可执行程序的完整流程](#Part5. 操作系统加载可执行程序的完整流程)

[Part6. 动态库和动态链接基础](#Part6. 动态库和动态链接基础)

[Part7. GOT表 延迟绑定](#Part7. GOT表 延迟绑定)

[Part8. 静态链接 vs 动态链接 再总结](#Part8. 静态链接 vs 动态链接 再总结)

[Part9. 结语](#Part9. 结语)


前言

Linux下对于动静态链接的过程的了解是非常重要的,接下来来跟随小编的视角来看看吧。


let's go!!!!!!!!


Part1. ELF 各段基础认知

我们使用 readelf -s xxxx 可以读取 ELF 文件各个段的信息。


ELF 将程序信息划分到不同的 section(段)中:
段名 作用
.text 代码段:存放编译后的机器指令(函数代码)

.data 数据段:已经初始化的全局变量,例如 int a = 10;

.bss 未初始化全局变量段,例如 int a;。操作系统在加载的时候会自动把 .bss 内存全部初始化为0,文件本身并不占用磁盘空间

.rodata 只读数据段,存放字符串常量,例如 char *str = "hello";,运行期间不可修改


一个重要特性:Section 的合并
.data 和 .bss 经常会被合并。内存是按页分配(一页通常4KB),如果每一个section都单独占一页,会造成大量内存碎片;把权限、属性相同的段合并在一起,可以充分利用内存空间,减少内存浪费。


区分概念:

• section:链接视角,目标文件、ELF文件内部的逻辑分段,给链接器使用

• segment:运行/加载视角,操作系统加载程序的时候,会把多个属性、权限一致的

section 打包合并为一个 segment,用来映射到进程虚拟内存。程序加载器只认识 segment,不关心 section。


Part2. ELF 文件整体构成

一份完整ELF文件由下面几大部分组成:

1. ELF Header(ELF头部):整个文件的总入口,记录各个部分的偏移位置、程序入口地址、文件类型(可重定位目标文件/可执行程序/动态库)。告诉操作系统这个ELF各个部分在哪里。

2. Program Header(程序头表):描述各个 segment,供操作系统加载器使用,用来告诉内核:哪一段要映射到进程虚拟地址空间、权限是什么、加载到哪里。

3. Section Header(段头表):描述各个 section,主要给链接器、objdump、readelf 这类工具使用。

4. 各个真正的 section(.text .data .rodata .bss ...)存放代码与数据。


常用调试工具:
objdump -d xxx:反汇编,查看 .text 段里面的汇编指令。


Part3. 目标文件 .o、符号、重定位

这是整个链接最核心的知识点。
当我们编译 main.c,得到可重定位目标文件 main.o。

在 main.c 里面调用一个外部函数 printf,编译器此时并不知道printf的内存地址,所以汇编里的 call 指令目标地址暂时填为 0,这个符号标记为 UND(undefined,未定义符号)。


静态链接做了两件大事

1. 符号解析:链接器遍历所有 .o 文件、静态库,为每一个 UND 未定义符号,找到它真正的定义,拿到符号对应的地址。比如去别的目标文件或者静态库里面找到 printf 的实现。

2. 地址重定位:拿到真实地址之后,回去修改 .text 段里面的 call 指令,把原来填的0,替换成函数真正的地址。

这就是为什么 .o 叫可重定位目标文件:它内部很多地址都是待定的,等待链接器后期修正。链接完成之后,地址全部填好,就生成了最终的可执行程序。


一句话总结静态链接:收集多个目标文件,合并段,解析符号,修正所有待定地址。


Part4. 地址的概念:逻辑地址、偏移量、虚拟地址

偏移量(相对地址)

在 .o 目标文件中,我们只关心偏移量:函数/变量距离本段头部的偏移。
举个例子:某个函数距离 .text 段开头偏移 0x200。

在可重定位文件阶段,我们不知道最终加载到内存的绝对位置,但是偏移量是固定不变的。


逻辑地址

早期一些系统的概念,逻辑地址 = 段基址 + 段内偏移。
在现代Linux下,逻辑地址 ≈ 虚拟地址,MMU内存管理单元负责把虚拟地址翻译成物理地址。


虚拟地址(线性地址)

Linux进程的核心:每一个进程都拥有独立完整的虚拟地址空间。
最终可执行程序里面写的地址,全部都是虚拟地址。
当程序被操作系统加载到内存之后,CPU最终访问的全部是虚拟地址,由MMU配合页表,翻译成物理地址。

重点:CPU从来不直接操作物理地址!CPU发出的地址永远是虚拟地址,交给MMU完成转换。


Part5. 操作系统加载可执行程序的完整流程

1. exec 系列系统调用启动程序,打开可执行ELF文件,通过inode找到磁盘上的文件内容

2. 操作系统读取ELF的 Program Header,拿到各个 segment 的信息:起始虚拟地址、长度、读写执行权限

3. 内核为进程创建 mm_struct、vm_area_struct 等内核结构体,用来描述进程的虚拟地址空间,建立虚拟地址到物理内存的映射(页映射,不一定立刻把全部数据拷贝进物理内存,缺页中断的时候才加载)

4. 设置程序入口地址(ELF header记录的入口,一般是 _start,不是main!)

5. CPU跳转到入口地址开始执行。

_start 是libc提供的入口函数,在main函数之前运行:初始化栈、初始化环境变量、做动态链接,最后调用我们写的 main。main返回之后,_start 还会调用exit结束进程。


Part6. 动态库和动态链接基础

静态链接会把库的代码直接拷贝进可执行文件;动态链接不会。

• .so 动态库本身也是一份独立的ELF,有自己独立的 section / segment

• 程序运行的时候,动态库不会在编译期合并进exe;运行时,操作系统的动态链接器ld-linux,把动态库加载到进程的虚拟地址空间

• 多个进程可以共享内存里面同一份动态库,节省物理内存。

但是这里有一个问题:动态库被加载到哪一块虚拟地址,编译阶段是无法预先确定的。
为了解决这个问题,动态库编译的时候必须使用 -fPIC,生成位置无关代码。


Part7. GOT表 延迟绑定

为什么需要GOT?

动态库里面调用外部函数(比如printf),编译期不知道这个函数最终的虚拟地址。


如果直接修改代码段 .text,有两个致命问题:

1. .text 代码段是只读的,运行时不能修改

2. 多进程共享同一份so,如果修改.text,所有进程都会被影响。

解决方案:把地址存放到一个可写的数据段里面,也就是GOT表。
代码段里面永远只写固定的偏移,去读取GOT表里面保存的函数真实地址。
call 偏移量 ; 跳转到PLT段


PLT + GOT 延迟绑定(Lazy Binding)

Linux默认使用延迟绑定:函数第一次被调用的时候,才去查找、填充GOT表里面的真实地址,而不是程序一启动就解析所有符号。

1. 第一次调用库函数,GOT里面暂时是占位地址,跳转到动态链接器

2. 动态链接器去符号表查找这个函数真正的虚拟地址,回填到GOT表

3. 第二次再调用这个函数,直接从GOT拿到已经填好的地址,直接调用,不再走解析流程。
好处:程序启动速度更快。很多库函数可能整个程序生命周期根本不会被调用,就不需要解析它的地址。

补充:GOT 存放在 .data 可写段,所以运行时可以修改。动态库自己内部也拥有属于自己的GOT表。


Part8. 静态链接 vs 动态链接 再总结

特性 静态链接 动态链接
链接时机 编译链接期一次性完成符号解析+重定位 符号解析推迟到程序加载运行阶段
库代码 库的机器码拷贝进可执行文件 不拷贝,运行时单独加载so
重定位 链接期一次性完成地址修正 运行期依靠GOT表完成地址回填
内存 每个进程拥有独立一份库代码 物理内存中只保存一份,多进程共享
体积 可执行文件体积更大 exe很小,依赖外部so文件


Part9. 结语

这篇文章我们深入的知道了动静态链接的相关知识,接下来,小编还会带来更多Linux相关的知识,敬请期待~
最后,祝大家可以:春风得意马蹄疾,一日看尽长安花!

最后的最后,要是觉得本文还可以的话,可以点点赞,关注小编一波,谢谢大家!~

相关推荐
Huangjin007_2 小时前
【Linux 系统篇(二十八)】文件(五):Ext 系列文件系统(下):Ext2块组结构,inode与文件寻址原理
linux·运维·服务器
javachen__2 小时前
JsonView:一套VO实现多角色字段返回
java·linux·服务器
睡一觉就好了。2 小时前
TcpSocket
linux
小陈运维2 小时前
使用kubeadm初始化IPV4/IPV6集群
linux·docker·云原生·容器·kubernetes
东风微鸣2 小时前
精简 Ubuntu Linux 固件包:每个 N100 节点省下约 450 MB
linux·ubuntu·postgresql
snhna2 小时前
Agent 上云部署实战(上):从 GitHub 到 Linux,用 Dify 跑通中间件、域名和 HTTPS
linux·中间件
jaronho3 小时前
【脚本】 【Linux】启动所有网卡
linux·运维·服务器
草莓熊Lotso3 小时前
【C++ 基于微服务的即时通讯系统】基建三件套:gflags + gtest + spdlog 源码编译安装与实战入门
linux·网络·数据库·c++·微服务·log4j
H.莓飛3 小时前
【C++】类与对象(末):static、友元与拷贝优化
linux·开发语言·c++·visual studio