深入理解库、静态库、动态库与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  # 创建动态库
相关推荐
Run_Teenage5 小时前
Linux:深刻理解缓冲区
linux
水力魔方5 小时前
SWMM深度二次开发专题7:网络分析-获取网络
网络·经验分享·swmm
youxiao_905 小时前
kubernetes 概念与安装(一)
linux·运维·服务器
凡梦千华5 小时前
logrotate日志切割
linux·运维·服务器
wdfk_prog5 小时前
[Linux]学习笔记系列 -- [fs][proc]
linux·笔记·学习
ELI_He9996 小时前
Airflow docker 部署
运维·docker·容器
拜托啦!狮子6 小时前
安装和使用Homer(linux)
linux·运维·服务器
木鱼布6 小时前
聊聊防火墙技术
网络·网络协议·tcp/ip
liulilittle6 小时前
XDP VNP虚拟以太网关(章节:一)
linux·服务器·开发语言·网络·c++·通信·xdp
Sapphire~6 小时前
Linux-13 火狐浏览器书签丢失解决
linux