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)】

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

相关推荐
撩得Android一次心动1 小时前
Linux编程笔记4【个人用】
linux·笔记·学习
七牛云行业应用3 小时前
Ollama 本地部署 DeepSeek 完全指南:macOS / Windows / Linux 三端安装 + GPU 配置 + API 调用
linux·windows·macos
顧棟3 小时前
Zookeeper热缩容解析
linux·服务器·zookeeper
三言老师3 小时前
CentOS7.9:Redis服务器部署结构化实战教程
linux·运维·服务器·数据库
味悲4 小时前
Linux提权
linux·服务器·安全
无足鸟ICT4 小时前
【RHCA+】替换变量
linux
尘似鹤4 小时前
rk3506的uboot源码分析(三)
linux·uboot
2023自学中5 小时前
imx6ull 开发板 贪吃蛇, C++11 SDL2 无硬件GPU优化版
linux·c++