Linux静态库和动态库详解

目录

前言:

一.库的认识

二.静态库

1.本质

2.生成静态库

3.链接使用:

三.动态库

1.本质:

2.生成动态库

3.使用动态库

四.ELF文件

1.定义:

2.讲解意义:

3.ELF格式:

4.ELF两大行为------链接和加载

5.理解动静态库的链接和加载


前言:

先回顾一下编译过程:

复制代码
.c/.cpp 源文件
   ↓【预处理】
.i/.ii 预处理文件
   ↓【编译】
.s    汇编代码
   ↓【汇编】
.o    目标文件(object)
   ↓【链接】
可执行文件 / 静态库 / 动态库

我们看到编译的过程了解到,在目标文件形成可执行文件的时候,往往还要链接动静态库。那么库到底是什么?为什么会有动静之分呢?库又是怎么形成的?接下来一一解答。

一.库的认识

**1.定义:**库就是别人提前写好、打包好的一堆可复用代码,你不用从零造轮子,可以直接拿来调用。

2.分类(按代码形态)

  1. 源码库 直接给 .h/.cpp、.py 等源代码,编译时和你的程序一起编译。

  2. 二进制库(编译好的) 已经提前编译成机器码,不用重新编译源码:

  • 静态库 (后缀:Windows .lib / Linux .a)
  • 动态库 / 共享库 (后缀:Windows .dll / Linux .so / macOS .dylib)

三种库的形式都有不同的使用场景,我们平常的编译通常是动态库链接,只有找不到动态库的时候,才会链接静态库,我们也可以自己设置静态库链接。

二.静态库

1.本质

静态库 = 使用 ar 工具把一堆 .o目标文件打包压缩 得到的归档包。 它不是可执行文件,也不是独立程序,仅仅是目标文件集合。

2.生成静态库

ar工具

模板:

ar 参数 库名.a 目标文件1.o 目标文件2.o ...

  • r replace:插入 / 替换归档内的 .o 文件;库不存在时自动新增文件
  • c create:创建归档文件,不会打印 "新建库" 的提示信息

3.链接使用:

// 场景1:头文件和库文件安装到系统路径下

gcc main.c -lmyc

// 场景2:头文件和库文件和我们自己的源文件在同一个路径下

gcc main.c -L. -lmyc

// 场景3:头文件和库文件有自己的独立路径

gcc main.c -I头文件路径 -L库文件路径 -lmyc

• -L: 指定库路径

• -I: 指定头文件搜索路径

• -l: 指定库名------链接 libxxx.a / libxxx.so,只写中间名字,省略 lib 和后缀

三.动态库

1.本质:

  • 不会在链接时把代码拷贝进可执行文件;
  • 程序运行时由系统动态链接器加载到内存;
  • 多个程序可共用同一份库文件,节省磁盘与内存。

2.生成动态库

  • -fPIC:生成位置无关代码,动态库强制要求,代码可加载到内存任意地址
  • -shared:告诉 gcc 生成共享库(.so),而非可执行程序
  • 库名:libxxx.so

3.使用动态库

命令:与静态库一样

有错误?

原因:

编译时 -L. 只作用于链接阶段 ,告诉编译器去哪找库; 程序运行阶段 由系统动态链接器 ld-linux.so 查找 .so,它不会自动识别当前目录,只会去系统默认库路径(/lib、/usr/lib)搜索。 虽然目录里存在 libmyc.so,但运行时系统检索不到。

解决方案:

• 拷贝 .so 文件到系统共享库路径下, 一般指 /usr/lib、/usr/local/lib、/lib64 或者开 篇指明的库路径等

• 向系统共享库路径下建立同名软连接

• 更改环境变量: LD_LIBRARY_PATH

• ldconfig方案:配置/ etc/ld.so.conf.d/ ,ldconfig更新

四.ELF文件

1.定义:

ELF(Executable and Linkable Format),Linux 下目标文件、可执行程序、动态库 、静态库内部单元统一使用的格式。

2.讲解意义:

我们通过了解ELF,可以知道可执行文件是如何进入内存的。

3.ELF格式:

四大部分:

  • ELF 头(ELF header) 位于文件开头,记录文件基础属性,用来找到程序头表、节头表位置。

  • 程序头表(Program header table)Segment 供内核加载使用,描述各个内存段在文件中的偏移、大小、权限,指导 mmap 映射到进程虚拟内存。

  • 节头表(Section header table)Section 供链接器 ld使用,记录所有节的信息,用于链接、符号解析、重定位。

  • 节(Section) ELF 内部数据载体,代码、常量、符号表、GOT、重定位信息等分别存放在不同节中。

各种节的介绍:

复制代码
代码与数据类 
1. .text存放程序编译后的机器指令(函数代码),只读。主程序、库的业务代码都在这里。
2. .rodata只读常量数据:字符串常量、const常量;不可修改。 
3. .data已初始化全局变量、静态局部变量,可读可写。 
4. .bss未初始化全局 / 静态变量。文件内不占用空间,加载时在内存开辟零初始化区域。  
动态链接核心 
5. .got(全局偏移表)Global Offset Table。存放外部全局变量运行时虚拟地址。 
6. .got.pltGOT 的子集,配合 PLT,专门存放外部函数地址,用于延迟绑定。 
7. .plt(过程链接表)Procedure Linkage Table一小段跳板汇编代码。第一次调用外部函数,经由 PLT 进入动态链接器解析符号。 
8. .dynamic动态链接核心配置块。记录依赖哪些.so、重定位表位置、符号表位置。ld.so 启动首先读取此节。 9. .rela.dyn普通重定位项,用于 .got、数据重定位;程序启动阶段处理。 
10. .rela.pltPLT 对应的重定位项,对应 .got.plt,默认延迟绑定,第一次调用函数时解析。  
符号与字符串 
11. .dynsym动态符号表。ld.so 运行时用来查找外部函数 / 变量(只保留动态链接需要的符号)。 
12. .dynstr动态符号字符串表,保存 .dynsym 用到的符号名字符串(printf、malloc 等)。 
13. .symtab完整符号表,包含所有符号;运行时 ld.so 默认不用,调试、链接阶段使用;发布程序常 strip 删掉。 
14. .strtab配合 .symtab 的字符串表。  
其他辅助节 
15. .rel / .rela重定位节(.o 目标文件中大量存在);链接器依据它修正地址、构建 GOT。  rela = 带加数项,x86_64 使用 .rela。  
16. .init_array / .fini_array共享库 / 程序构造函数、析构函数数组;加载完毕自动执行。 
17. .comment编译器版本、编译参数等注释信息。 
18. .shstrtab节名字符串表,保存各个 section 名称(.text、.got这类字符串)。

观察工具(主要的):

1. readelf

复制代码
# 查看ELF头部
readelf -h ./a.out

# 查看节头表(所有Section:.text .got .got.plt .rela.plt)
readelf -S ./a.out

# 查看程序头表(Segment,内核加载视图)
readelf -l ./a.out

# 查看动态段 .dynamic(ld.so依赖、重定位表地址)
readelf -d ./a.out

# 查看重定位表(重点!.rela.dyn .rela.plt,看见GOT偏移)
readelf -r ./a.out

# 查看动态符号表 .dynsym
readelf --dyn-syms ./a.out

2.objdump(反汇编,看汇编代码、PLT 跳板、GOT 调用指令)

复制代码
# 完整反汇编,观察 PLT 代码、@GOTPCREL 指令
objdump -d ./a.out

# 只看plt段
objdump -d -j .plt ./a.out

# 查看GOT表内存初始值(文件内的值)
objdump -s -j .got.plt ./a.out

3.ldd(查看程序依赖哪些动态库)

4.ELF两大行为------链接和加载

1.链接(合并文件生成大ELF,将ELF写进磁盘,固定格式,不会变)

输入:.o目标文件、动态库信息

视图:链接视图,操作 Section(节)

  1. 合并各个目标文件相同名称的节(.text、.data等)

  2. 符号解析:区分本地符号、外部未定义符号

  3. 构建 .got / .got.plt,为外部符号分配 GOT 条目,确定条目相对 GOT 表头的偏移,写入 ELF

  4. 执行链接期重定位:修正机器指令;确定各个地址

  5. 输出最终 ELF(可执行文件 /.so)------>磁盘

关键点:链接只约定「GOT 格子位置」,GOT 内不存放外部函数真实地址;此时不需要运行、不需要打开动态库。(后面详细说明)

2.加载(ELF进内存,内核 + ld.so 配合完成)

触发:execve()

运行程序视图:执行视图,操作 Segment(段)

前期准备:

・一个 ELF 会有多种不同的 Section,在加载到内存的时候,也会进行 Section 合并,形成 segment

・合并原则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等。

意义:

减少虚拟地址碎片、节约页表资源

权限分组天然合理

减少 PT_LOAD 段数量,减少 mmap 系统调用

实现前提:

各种格式地址是在磁盘就确定了,那么怎么"合并"的呢?

答:磁盘对齐粒度 与 内存页权限粒度不一致,催生了 Section / Segment 两套视图。

加载流程:

  1. 内核读取 ELF 程序头表(Program Header),按照 PT_LOAD 段,mmap 把主程序映射到进程虚拟地址空间。

  2. 判断是动态链接程序,加载 ld-linux,把控制权交给动态链接器 ld.so,进行动态链接。

**总结:**就是把ELF数据按ELF原地址填入内存(原地址就是虚拟地址)

一个程序被加载到内存之前有没有地址?

答案是有的------逻辑地址:链接时规划的理论虚拟地址;

ELF文件------>磁盘------>内存;地址变化过程:逻辑地址------>虚拟地址------>物理地址

链接阶段,生成 ELF 文件的时候确定,固化保存在磁盘 ELF 中。只需一个起始虚拟地址,其它全部为偏移量,就可以确定各个部分的地址了,加载进内存时内核可以在此基础上加载虚拟地址即可。

所以我们得到一个重要信息:ELF在磁盘中就已经确定所有地址了,编译器也有编码的功能。

5.理解动静态库的链接和加载

1.静态库的链接和加载

静态库链接就是ELF文件合并链接的过程:

首先根据链接器知道库文件的inode确定库的位置,根据位置找到库文件

主要作用就是执行链接期重定位:确定各个地址,前面没有确定的外部函数或者外部变量的地址都会确定。

最后面就会形成一个大文件ELF

加载就是大文件进入内存

2.动态库的链接和加载

动态库特性:

不存在于执行文件中;运行时加载;加载于共享内存中;多个进程共享。

动态库加载到内存之前也有统一的编码,方便内核直接加载。

根据动态库的特性,那么主程序文件在加载进磁盘时,内部使用到的动态库的函数和变量是怎么确定地址的?毕竟前面说了ELF在磁盘中已经编码了。动态链接器又是怎么工作的?

动态链接器:

◦ 动态链接器(如 ld-linux.so)负责在程序运行时加载动态库。

◦ 当程序启动时,动态链接器会解析程序中的动态库依赖,并加载这些库到内存中。

环境变量和配置文件:

◦ Linux 系统通过环境变量(如 LD_LIBRARY_PATH)和配置文件(如 /etc/ld.so.conf 及其子配置文件)来指定动态库的搜索路径。

◦ 这些路径会被动态链接器在加载动态库时搜索。

缓存文件:

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

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

那么之前内部使用到的动态库的函数和变量是怎么确定地址的?

难道按主程序ELF来定外部函数地址,然后使动态库的地址按照位置来修改地址吗?

这是不可能的,因为代码共享区是不可改的,改了会出大问题。

那么该怎么实现二者地址映射呢?

解决方案:GOT表

动态链接采用的做法是在 .data (可执行程序或者库自己)中专门预留一片区域用来存放函数 的跳转地址,它也被叫做全局偏移表GOT,表中每一项都是本运行模块要引用的一个全局变量或函数 的地址。

• 因为.data区域是可读写的,所以可以支持动态进行修改。

工作:

1.链接器扫描所有目标文件的重定位条目,识别所有外部未定义符号(如`printf`)。

  1. 为每一个唯一外部符号,在**当前模块(主程序/so)私有`.got/.got.plt`分配一个条目(格子)。

  2. 计算:条目相对于GOT段头部的**相对偏移**,永久固化进ELF。

4.重定向:根据条目相对于GOT段头部的**相对偏移**来确定外部未定义符号的地址(不是真实的地址)

5.运行时,写入真实地址:`libc基址 + printf在libc内部偏移`,形成假地址映射真地址

这样就缓解逻辑冲突了。

动态链接和加载的总过程:

一、开发阶段:链接过程

输入:.o目标文件、依赖动态库信息

  1. 链接器扫描所有目标文件的重定位条目,识别所有**外部未定义符号(如`printf`)。

  2. 为每一个唯一外部符号,在当前模块(主程序/so)私有`.got/.got.plt`分配一个条目(格子)。

计算:条目相对于GOT段头部的**相对偏移**,永久固化进ELF。

> 重点:此时不需要打开libc、不需要知道函数真实地址,只是预先分配格子编号。

  1. 处理代码重定位:

填充指令内xxx@GOTPCREL(%rip)`的PC相对偏移,让代码运行时能够找到这个GOT格子。

  1. 在ELF中写入 .rela.dyn / .rela.plt重定位记录。

重定位项保存:GOT条目相对GOT基址的偏移 + 对应的符号名称。

  1. 输出ELF可执行文件:

✅ GOT段布局、所有符号对应的格子偏移全部固定

❌ GOT条目内没有存放任何动态库函数地址

二、运行阶段:启动执行 ./a.out

步骤1:内核 execve

  1. 内核读取主程序ELF,mmap映射主程序各段到进程虚拟空间。

  2. 识别是动态链接程序,加载 `ld-linux-x86-64.so.2`(动态链接器),转交控制权。

步骤2:ld.so 初始化,加载依赖共享库

  1. ld.so读取主程序dynamic段,获取需要加载的动态库(libc.so.6等)。

  2. 依次搜寻库路径 → `open()`(内核通过路径查询dentry/inode定位磁盘库文件)

3.将动态库映射到进程虚拟地址空间,得到该库运行时加载基址。

注意:到此为止,GOT偏移依旧沿用链接阶段写死的值,没有任何修改。

步骤3:符号解析 + GOT填充(两种模式)

默认开启【延迟绑定 Lazy Binding】

  1. 程序启动时不填充.got.plt。

  2. 第一次调用外部函数`printf`:

  • 进入PLT桩代码,跳转到`_dl_runtime_resolve`

  • 动态链接器根据重定位信息,在libc符号表查找`printf`

  • 计算真实地址:`libc基址 + printf在libc内部偏移`

  • 使用链接期固定的GOT偏移,定位主程序GOT对应的格子

  • 将printf真实地址写入GOT条目

  1. 后续再次调用printf:直接读取GOT内保存的函数地址,跳过解析流程。

延迟绑定 Lazy Binding:调用时按照需要填入真实地址,没有一次性全部填入,可以节省资源消耗

核心:

链接阶段:决定【哪个符号占用GOT中第几个格子(相对偏移固定写入ELF)】

运行阶段:只负责【往预先定好的格子里填入符号真实地址】

相关推荐
且从容.16 分钟前
ubuntu安装flink
linux·ubuntu·flink
爱和冰阔落2 小时前
【Linux网络】TCP 服务器如何并发处理多个客户端?从 fork、僵尸进程到双重 fork
linux·运维·服务器·网络
程序员沉梦听雨2 小时前
WSL2的安装与使用
linux·wsl
微小的鱼2 小时前
Linux的渲染与显示系统链路
android·java·linux
lisanmengmeng2 小时前
alertmanger 标签 rules 打上分组标签
linux·运维·服务器
Android系统攻城狮3 小时前
Linux FFmpeg深度解析之av_write_frame调用流程与实战(三十三)
linux·运维·服务器·ffmpeg·音视频进阶·ffmpeg音视频
Linux-lucky15 小时前
45-Linux学习之旅之防火墙与WEB加固
linux·前端·网络·学习·ubuntu
禁默15 小时前
启元 Q1 二次开发实践:Ubuntu 部署与仿真验证,从 PrimeConsole 到 ROS 2 Humble + RViz
linux·运维·ubuntu
skywalk816316 小时前
把 去也网站 从 Ubuntu 搬到 FreeBSD:一次 Service Jail + nginx 的部署实录
linux·运维·nginx·ubuntu·freebsd
爱莉希雅&&&18 小时前
GitLab 完整学习笔记
linux·运维·服务器