深入理解库、静态库、动态库与ELF文件格式,CPU执行流程(1)

🎬 胖咕噜的稞达鸭个人主页
🔥 个人专栏 : 《数据结构《C++初阶高阶》
《Linux系统学习》
《算法日记》

⛺️技术的杠杆,撬动整个世界!



什么是库?

库是写好的可以复用的代码,依赖库可以实现某些代码。

静态库:.a[Linux] .lib[windows]

动态库:.so[Linux] .dll[windows]

有时候电脑的杀毒软件会杀掉动态库。

静态库的制作

静态库静态库在程序编译链接时把库的代码链接到可执行文件中,程序运行时不再需要静态库。

  1. 动静态库中,要不要包含main函数呢?NO!不要
  2. 头文件包含了方法的声明,.c文件包含了方法的实现,原来所有的库(无论是动态还是静态,都是源文件对应的.o文件)!!
  3. 静态库本质上就是很多个.o文件打包形成的。
    特点:
  • 生成的可执行文件体积较大

  • 运行时不需要外部依赖

  • 库更新需要重新编译链接整个程序
    生成静态库:
    makefile

    libmystdio.a: my_stdio.o my_string.o
    ar -rc @ ^

使用静态库:

cpp 复制代码
# 不同场景下的使用方式
gcc main.c -lmystdio                    # 系统路径下
gcc main.c -L. -lmymath                 # 当前目录下
gcc main.c -I头文件路径 -L库文件路径 -lmymath  # 自定义路径
cpp 复制代码
[keda@VM-0-4-centos my_stdio]$ ar -rc libmyc.a *.o
[keda@VM-0-4-centos my_stdio]$ ll
total 44
-rwxrwxr-x 1 keda keda 13224 Jan  1 20:21 code
-rw-rw-r-- 1 keda keda    74 Jan  1 14:55 Makefile
-rw-rw-r-- 1 keda keda  3170 Jan  4 15:50 mylib.a
-rw-rw-r-- 1 keda keda  1593 Jan  4 15:49 mystdio.c
-rw-rw-r-- 1 keda keda   505 Jan  3 17:20 mystdio.h
-rw-rw-r-- 1 keda keda     0 Jan  4 15:48 mystring.c
-rw-rw-r-- 1 keda keda     0 Jan  4 15:48 mystring.h
-rw-rw-r-- 1 keda keda   944 Jan  4 15:49 mystring.o
-rw-rw-r-- 1 keda keda   412 Jan  4 15:46 usercode.c
-rw-rw-r-- 1 keda keda  2024 Jan  4 15:49 usercode.o

.a静态库,本质是一种归档文件,不需要使用者解包,而是用gcc/g++直接进行连接即可。

复制代码
ar -rc libmyc.a *.o
|   replace and create
archive 

静态库命名规则:lib开头,.a结尾

如果我们要连接这个静态库:gcc -o usercode usercode.o -llibmyc.a

cpp 复制代码
[keda@VM-0-4-centos my_stdio]$ gcc -o usercode usercode.o -llibmyc.a
/usr/bin/ld: cannot find -llibmyc.a
collect2: error: ld returned 1 exit status

[keda@VM-0-4-centos my_stdio]$ gcc -o usercode usercode.o -L. -lmyc

-L:去哪里找;-lmyc:找什么库
gcc -o usercode usercode.c -I ./lib/include/ -L ./lib/mylib/ -l myc
gcc -I可以指定头文件。
gcc -L指定库目录.

动态库

动态库在程序运行时才链接库的代码,多个程序可以共享使用库的代码。

特点:

  • 生成的可执行文件体积较小
  • 运行时需要库文件存在
  • 库更新无需重新编译程序
  • 内存中只需一份副本,可被多个进程共享
cpp 复制代码
[keda@VM-0-4-centos my_stdio]$ gcc -shared -o libmyc.so *.o
[keda@VM-0-4-centos my_stdio]$ ll
total 56
-rwxrwxr-x 1 keda keda 13224 Jan  5 14:21 code
drwxrwxr-x 4 keda keda  4096 Jan  5 14:28 lib
-rwxrwxr-x 1 keda keda  8304 Jan  5 14:39 libmyc.so
-rw-rw-r-- 1 keda keda   302 Jan  5 14:27 makefile
-rw-rw-r-- 1 keda keda  1593 Jan  4 15:49 mystdio.c
-rw-rw-r-- 1 keda keda   505 Jan  3 17:20 mystdio.h
-rw-rw-r-- 1 keda keda     0 Jan  4 15:48 mystring.c
-rw-rw-r-- 1 keda keda     0 Jan  4 15:48 mystring.h
-rw-rw-r-- 1 keda keda   944 Jan  5 14:37 mystring.o
-rw-rw-r-- 1 keda keda   412 Jan  4 15:46 usercode.c
-rw-rw-r-- 1 keda keda  2080 Jan  5 14:37 usercode.o
[keda@VM-0-4-centos my_stdio]$ file libmyc.so
libmyc.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=94299a5a479b0a7084fad7cba886dba742c0367e, not stripped
复制代码
libmyc.a:mystdio.o mystring.o                                                                                                                                                
  2     ar -rc $@ $^
  3 mystdio.o:mystdio.c
  4     gcc -c $<
  5 mystring.o:mystring.c
  6     gcc -c $<
  7 
  8 .PHONY:output
  9 output:
 10     mkdir -p lib/include
 11     mkdir -p lib/mylib
 12     cp -f *.h lib/include
 13     cp -f *.a lib/mylib
 14     tar czf lib.tgz lib
 15   
 16 .PHONY:clean
 17 clean:
 18     rm -rf *.o libmyc.a lib lib.tgz

操作系统在运行的时候也会寻找动态库:
[keda@VM-0-4-centos my_stdio]$ echo $LD_LIBRARY_PATH
:/home/keda/.VimForCpp/vim/bundle/YCM.so/el7.x86_64

动态库的路径搜索问题:

当程序依赖动态库时,需要确保系统能找到这些库文件。常见解决方案:

  1. 拷贝到系统路径/usr/lib/usr/local/lib/lib64
  2. 建立软连接
  3. 设置环境变量export LD_LIBRARY_PATH=/path/to/libs
  4. ldconfig配置 :在/etc/ld.so.conf.d/中添加路径,执行ldconfig

结论:

  1. g++/gcc默认使用的是动态库
  2. 如果一定要使用静态链接,要使用-static,一旦-static。就必须存在对应的静态库
  3. 如果只存在静态库,可以执行程序,对于该库,就只能静态链接了。
    结论2:
    在Linux系统下,默认情况安装的大部分库默认都优先安装动态库。
    结论3:库:应用程序 = 1 : n
    结论4:vs不仅形成可执行程序,也能形成动静态库。

ELF文件格式:理解编译链接的关键

什么是ELF文件?

ELF(Executable and Linkable Format)是Linux下的可执行文件、目标文件、共享库的标准格式。

ELF文件的四种类型:

  1. 可重定位文件.o文件):包含代码和数据,适合链接
  2. 可执行文件:可直接执行的程序
  3. 共享目标文件.so文件):动态链接库
  4. 内核转储文件:进程执行上下文

ELF文件结构

一个ELF文件由四部分组成:

复制代码
┌─────────────────┐
│   ELF Header    │  ← 描述文件特性,定位其他部分
├─────────────────┤
│ Program Headers │  ← 列举所有有效的段及其属性
├─────────────────┤
│    Sections     │  ← 基本组成单位,存储特定类型数据
├─────────────────┤
│ Section Headers │  ← 包含对节的描述
└─────────────────┘

常见的重要节(Sections)

  • .text:代码节,存储可执行指令
  • .data:数据节,存储已初始化的全局变量和静态变量
  • .rodata:只读数据节
  • .bss:未初始化的全局变量和静态变量预留位置
  • .symtab:符号表,记录函数名、变量名和代码的对应关系
  • .got:全局偏移表,支持动态链接的关键数据结构

两个重要视图

  1. 链接视图(Linking View)- 对应节头表

    • 将文件按功能模块划分
    • 静态链接时关注此视图
  2. 执行视图(Execution View)- 对应程序头表

    • 告诉操作系统如何加载可执行文件
    • 运行加载时使用此视图

从源码到可执行程序的过程

编译阶段

复制代码
// hello.c
#include<stdio.h>
void run();
int main() {
    printf("hello world!\n");
    run();
    return 0;
}

编译生成目标文件:

复制代码
gcc -c hello.c    # 生成 hello.o
gcc -c code.c     # 生成 code.o

目标文件的特点

目标文件(.o文件)是不完整的:

  • 函数调用地址暂时设为0
  • 包含重定位表,记录需要修正的地址
  • 通过符号表记录未定义的符号

链接过程

链接器将多个目标文件合并:

  1. 合并相同类型的节(如将所有.text节合并)
  2. 修正地址,填充函数调用的真实地址
  3. 生成最终的可执行文件

静态链接 :将所有用到的库代码合并到可执行文件中
动态链接:在运行时加载和链接库代码

动态链接的工作原理

GOT和PLT机制

由于代码段(.text)是只读的,不能直接修改函数调用地址。动态链接采用以下机制:

  1. GOT(全局偏移表) :位于.data段(可读写),存储函数和变量的实际地址
  2. PLT(过程链接表):实现延迟绑定,在函数第一次被调用时解析地址

动态链接过程

  1. 程序启动时,动态链接器(如ld-linux.so)加载所需库
  2. 为每个库确定加载地址
  3. 填充GOT表,记录库中函数的实际地址
  4. 程序调用函数时,通过PLT→GOT→实际函数的路径执行

这是在描述:多个目标文件(.o文件)在链接时合并成最终可执行程序(main)的过程

多个独立的目标文件

图中显示了四个目标文件,每个都有相同的ELF结构:
ELF Header:每个文件都有自己的头部信息
Program Header Table:程序头表(可选,目标文件可能没有)
Sections(节):包括.text(代码节)、.data(数据节)等
Section Header Table:节头表,描述各个节的信息

合并后的可执行文件
main的ELF格式:最终生成的可执行程序

各个目标文件的同名节被合并:

所有.text节合并成新的.text节

所有.data节合并成新的.data节

其他节也类似合并

一个ELF的构成

ELF⽂件由以下四部分组成:

• ELF头(ELF header) :描述文件的主要特性。其位于⽂件的开始位置,它的主要⽬的是定位文件的其他部分。

• 程序头表(Program header table) :列举了所有有效的段(segments)和他们的属性。表里记着每个段的开始的位置和位移(offset)、长度,毕竟这些段,都是紧密的放在⼆进制文中,需要段表的描述信息,才能把他们每个段分割开。

• 节头表(Section header table) :包含对节(sections)的描述。

• 节(Section ):ELF文件中的基本组成单位,包含了特定类型的数据。ELF文件的各种信息和数据都存储在不同的节中,如代码节存储了可执行代码,数据节存储了全局变量和静态数据等。

这个过程的关键点

  1. 静态链接的本质
    链接器将多个目标文件的相同类型的节合并
    例如:code1.o的.text + code2.o的.text + ... = main的.text
  2. 为什么需要合并?
    减少页面碎片,提高内存的使用效率,占用的空间少一点;
    此外操作系统在加载程序的时候,会将具有相同属性的section合并成一个大的segment,这样就可以实现不同的访问权限,从而优化内存管理和权限访问控制。
    举例子:
    权限管理问题
    • .text(可执行)、.rodata(只读)→ 合并成只读可执行段
    • .data(可读写)、.bss(可读写)→ 合并成可读写段
    • 系统只需设置2种权限,而不是为每个section单独设置

问题:
问题一:静态库是如何形成可执行程序的?
ELF文件为两个不同的使用者提供了两套说明书:一套给链接器(怎么做链接),一套给操作系统(怎么做加载)。

链接视图(节头表)

给谁用 :链接器(如 ld
看什么readelf -S hello.o
内容

  • .text(代码节)
  • .data(数据节)
  • .rodata(只读数据节)
  • .symtab(符号表)
  • ...等几十个节
    为什么需要这个视图?
    链接器需要知道:"这个.o文件有哪些代码(.text)、哪些数据(.data)、哪些符号(.symtab)",才能正确地把多个.o文件拼起来。
执行视图(程序头表)

给谁用 :操作系统加载器
看什么readelf -l a.out
内容

  • LOAD Segment 1:可执行(.text + .init + ...)
  • LOAD Segment 2:可读写(.data + .bss + .got + ...)

为什么需要这个视图?

操作系统需要知道:"这个程序应该加载到内存哪里?哪些可以执行?哪些可以读写?"才能安全高效地加载程序。

复制代码
[keda@VM-0-4-centos lesson20]$ readelf -h /usr/bin/ls
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x404324
  Start of program headers:          64 (bytes into file)
  Start of section headers:          115688 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29

静态链接就是把库中的.o文件都进行合并,并进行统一的编址,链接的时候会修改.o中没有确定的函数地址,在合并完成之后,进行相关call地址,完成代码的调用。

详细解释:文件在编译的时候,查看它的反汇编会发现其地址为全0,链接的时候将相同属性的数据节进行合并(也就是将这个程序需要的所有文件链接在一起),并将地址修改了。

问题二:ELF程序是如何 加载到内存的,(找到它,路径+文件名),ELF程序是如何转化为进程的(逻辑地址,物理地址,虚拟地址),虚拟地址空间

一个可执行程序,如果没有加载到内存中,该可执行程序,有没有地址?

YOU!有的!

对可执行程序,完成在磁盘上的编址,所有的可执行程序,就是一个seg,所有的seg所有函数,变量编址起始偏移量都从0开始。

虚拟地址空间:不仅仅是进程看代内存的方式,磁盘上的可执行程序,代码和数据编址其实就是虚拟地址的统一编址。操作系统支持,编译器也要支持。
cpu怎么知道,你的可执行程序的起始地址是什么?也就是说CPU怎么知道从哪里 开始执行的呢

进入到CPU中的地址全部都是虚拟地址!

CPU执行流程

步骤1:操作系统告诉CPU

复制代码
// 进程创建时,操作系统设置:
PCB.entry_point = 0x1060;  // 程序入口地址
CPU.PC = 0x1060;          // 设置程序计数器

步骤2:CPU获取指令

复制代码
CPU: "我要执行0x1060处的指令"
MMU: "0x1060是虚拟地址,我来查页表..."
MMU: "对应物理地址是0x12345678"
CPU: "好,我从0x12345678取指令执行"

实用命令总结

查看文件信息

复制代码
file a.out                # 查看文件类型
readelf -h a.out          # 查看ELF头
readelf -l a.out          # 查看程序头表
readelf -S a.out          # 查看节头表
objdump -d a.out          # 反汇编代码段

查看依赖关系

复制代码
ldd a.out                 # 查看动态库依赖
nm a.out                  # 查看符号表

库操作

复制代码
ar -t libxxx.a           # 列出静态库内容
ar -rc libxxx.a *.o      # 创建静态库
gcc -shared -fPIC -o libxxx.so *.o  # 创建动态库
相关推荐
大树888 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠8 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质8 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush48 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行5208 小时前
Linux 11 动态监控指令top
linux
Inhand陈工9 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
网络研究院10 小时前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智10 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest10 小时前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
不会C语言的男孩10 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言