Linux库制作与原理(1):静态库、动态库和ELF文件

什么是库?

库(Library) 是一批提前编译完成、可被其他程序重复调用的通用二进制代码集合,它对外暴露固定的调用接口,使用者只需要知道「怎么调用」,不需要关心「内部如何实现」

在 Linux 系统中:

库本质是遵循 ELF 格式的二进制文件 ,内部封装了函数实现、全局变量、符号信息、重定位信息等。它不具备独立运行能力,必须被其他可执行程序「调用、链接」后,才能发挥作用

库的完整组成:头文件 + 库文件

第一部分:头文件(.h / .hpp

作用:给「编译器」看 ------ 做接口声明

头文件中只存放:函数声明(函数名、参数列表、返回值);宏定义;结构体、枚举、类型别名;外部全局变量声明

第二部分:二进制库文件(.a / .so

作用:给「链接器 / 动态链接器」看 ------ 做函数实现

库文件是真正存放函数二进制实现的载体

生命周期

  • 静态库(.a):编译链接阶段被整合进可执行文件,程序运行时不再依赖原库文件;
  • 动态库(.so):编译阶段被记录依赖,运行阶段必须加载,程序运行全程依赖该库。

库的分类

静态库(Static Library)

  • 文件后缀:.a(Archive,归档文件)
  • 链接时机:编译链接阶段
  • 核心逻辑:链接器会把库中被调用的代码,完整拷贝到最终可执行文件中。
  • 特点:编译完成后,可执行文件和原静态库彻底解绑。

动态库(Dynamic Library / 共享库 Shared Library)

  • 文件后缀:.so(Shared Object,共享对象)
  • 链接时机:编译阶段仅记录依赖,运行阶段完成链接
  • 核心逻辑:编译时不拷贝库代码,只在可执行文件中记录「我依赖哪个库、哪个函数」;程序运行时,由系统动态链接器加载库到内存。
  • 特点:多个程序可以共享同一份库内存镜像,节省资源。

Windows 对应类比:静态库 .lib,动态库 .dll

Linux 库的文件格式与强制命名规范

统一命名格式

所有 Linux 库文件都遵循:

bash 复制代码
lib + 库名 + 后缀

示例:

  • C 标准静态库:libc.a
  • C 标准动态库:libc.so

链接时的简写规则(-l 参数)

使用 gcc 链接库时,参数 -l 后面只写库名,省略 lib 和后缀

示例:

库文件 libmylib.a / libmylib.so

链接命令写:

bash 复制代码
gcc main.c -o main -lmylib

库路径参数 -L

当库不在系统默认目录时,需要用 -L 指定库所在目录

bash 复制代码
gcc main.c -o main -L/xxx/yyy -lmylib

-L路径:告诉编译器「去这个目录下找库文件」

补充:-L 只在编译链接阶段生效,运行阶段动态库不会读取该路径(这也是后续「动态库找不到」问题的根源)

头文件搜索路径指定选项 -I

作用:告诉 gcc 编译器:除了默认路径外,去我指定的目录里查找头文件(.h

bash 复制代码
gcc [源文件] -o [输出文件] -I [头文件所在目录] ...

规则:

  • -I 和 路径之间可以加空格,也可以不加
  • 路径支持 相对路径绝对路径
  • 可以写多个 -I,指定多个头文件目录

区分:

参数 作用 针对文件 场景
-I 指定头文件搜索路径 .h 解决头文件找不到
-L 指定静态库搜索路径 .a 解决库文件找不到
-l 指定链接的库名 libxxx.a 省略lib.a,直接写xxx

静态库

静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中,程序运行的时候将不再需要静态库

静态库制作核心流程(底层逻辑)

  1. 把源码 .c 编译成 目标文件 .o
  2. ar 归档工具,把 一个 / 多个 .o 文件打包 → 生成静态库 .a

静态库生成

多文件源码制作静态库:

1. 准备多个库源码文件

补充mystdio.hmystdio.c前文Linux基础I/O(2)模拟glibc库 的文件

mystring.h:

c 复制代码
#pragma once     
    
int my_strlen(const char *s);

mystring.c:

c 复制代码
#include "mystring.h"    
    
int my_strlen(const char *s)    
{    
    const char *start = s;    
    while(*s)    
    {    
        s++;    
    }    
    
    return s - start;    
}

2. 批量编译 .c.o

3. 批量打包 .o → 静态库

补充:ar 指令

用途:打包 .o 目标文件 → 生成 .a 静态库

基础语法:ar [选项参数] 生成的静态库名 目标文件1.o 目标文件2.o ...

核心参数

参数 英文全称 核心作用
r replace 向库中插入 / 替换目标文件(不存在则添加,存在则覆盖)
c create 自动创建新的库文件(库不存在时不报错)
s index 生成符号索引表

静态库使用

1. 编写测试主程序

如:main.c(调用静态库的函数)

2. 编译链接静态库

命令格式:

bash 复制代码
gcc 主程序.c -o 可执行文件 -I头文件路径 -L库文件路径 -l库名

3. 运行程序

如:./main

示例:

高阶版:Makefile 制作静态库

Makefile文件:

bash 复制代码
libmyc.a:mystdio.o mystring.o    
    ar -rc $@ $^    
mystdio.o:mystdio.c    
    gcc -c $<    
mystring.o:mystring.c    
    gcc -c $<    
    
.PHONY:output    
output:    
    mkdir -p lib/include    
    mkdir -p lib/mylib    
    cp -f *.h lib/include    
    cp -f *.a lib/mylib                                                                             
    tar czf lib.tgz lib     
    
.PHONY:clean    
clean:    
    rm -rf *.o libmyc.a lib lib.tgz

使用:

动态库

动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码

动态库专属核心参数

和静态库制作完全不同,动态库必须用两个 gcc 专属参数

参数 作用 必要性
-fPIC 生成位置无关代码(让库能加载到任意内存地址,实现多进程共享) 强制必须
-shared 告诉 gcc 生成动态共享库 强制必须

动态库 vs 静态库

维度 静态库(.a) 动态库(.so)
制作工具 gcc -c + ar rc gcc -c -fPIC + gcc -shared
链接时机 编译时拷贝代码 编译时仅记录依赖
运行依赖 不依赖原库 必须依赖原 .so 文件
可执行文件体积
内存占用 高(多进程多副本) 低(多进程共享)

动态库生成

沿用静态库的代码,源码 / 头文件完全不用改 ,仅修改编译命令

  • 编译生成 位置无关目标文件(.o)
    必须加 -fPIC
bash 复制代码
gcc -fPIC -c *.c
  • 链接生成 动态库(.so)
    必须加 -shared,生成共享库:
bash 复制代码
gcc -shared -o libmyc.so *.o

Makefile 自动化制作动态库

Makefile:

bash 复制代码
libmyc.so:mystdio.o mystring.o    
    gcc -shared -o $@ $^    
mystdio.o:mystdio.c    
    gcc -fPIC -c $<    
mystring.o:mystring.c    
    gcc -fPIC -c $<    
    
.PHONY:output    
output:    
    mkdir -p lib/include    
    mkdir -p lib/mylib    
    cp -f *.h lib/include    
    cp -f *.so lib/mylib    
    tar czf lib.tgz lib     
    
.PHONY:clean    
clean:    
    rm -rf *.o libmyc.so lib lib.tgz

动态库使用

1. 编写测试主程序(和静态库完全一样)

2. 编译链接动态库(命令和静态库完全相同)

3. 运行程序(必现错误 )

示例:

为什么动态库运行会找不到,静态库永远不会有这个问题?

  • 动态库的核心机制:编译时不拷贝代码,只记录「我需要这个库」
  • 编译路径 -L 不生效 :因为它只告诉gcc 库在哪,没告诉运行时系统。运行时,系统根本不知道你当前目录有库,只会去系统默认路径搜索
  • 静态库不报错:因为编译链接时,直接把库代码复制到可执行文件里,运行时不需要原始库文件

库运行搜索路径

问题:动态库运行找不到库

Linux动态库默认搜索顺序:

  1. 编译时指定的 RPATH 路径
  2. 环境变量 LD_LIBRARY_PATH 指定路径
  3. 系统默认库配置路径 /etc/ld.so.conf
  4. 默认系统路径:/lib/usr/lib(64 位系统还有/lib64/usr/lib64

解决方案:

  • 方法一:.so 动态库拷贝到系统默认共享库路径 ,如:/usr/local/lib/usr/lib64/lib64

示例:

  • 方法二:向系统共享库路径建立同名软链接

示例:

  • 方法三:更改临时环境变量 LD_LIBRARY_PATH

示例:

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

示例:

  • 结论1 :同一个目录下,同时存在同名的静态库和动态库时,gcc/g++ 链接器会默认优先选择 动态库(.so)进行链接

如何强制 gcc 链接 静态库?

方法:-static 参数

让所有库都强制静态链接:

bash 复制代码
gcc -o code usercode.c -I lib/include/ -L lib/mylib/ -lmyc -static
  • 结论2:在Linux系统下,默认情况安装的大部分库,默认都优先安装的是动态库

使用外部库

我们现在没接触过太多的库,唯一接触过的就是C、C++标准库,这里我们可以推荐一个好玩的图形库:ncurses

安装 ncurses:

bash 复制代码
# CentOS
sudo yum install -y ncurses-devel
# Ubuntu
sudo apt install -y libncurses-dev

示例:Hello ncurses + 颜色版(AI生成的测试代码)

c 复制代码
#include <ncurses.h>
#include <signal.h>

// 信号处理函数:确保程序退出时恢复终端
void cleanup(int sig) {
    endwin();
    exit(0);
}

int main() {
    signal(SIGINT, cleanup);  // 捕获 Ctrl+C 信号
    
    // 1. 初始化
    initscr();
    cbreak();
    noecho();
    keypad(stdscr, TRUE);
    curs_set(0);  // 隐藏光标(0=隐藏,1=正常,2=高亮)
    
    // 2. 颜色设置(需先检查终端是否支持颜色)
    if (has_colors()) {
        start_color();
        init_pair(1, COLOR_RED, COLOR_YELLOW);  // 颜色对1:红文字+黄背景
        init_pair(2, COLOR_GREEN, COLOR_BLACK); // 颜色对2:绿文字+黑背景
    }
    
    // 3. 绘制内容
    clear();
    box(stdscr, 0, 0);  // 用默认字符绘制边框
    
    // 移动到屏幕中心并输出彩色文字
    move(LINES/2, (COLS-16)/2);  // LINES/COLS 是终端尺寸常量
    attron(COLOR_PAIR(1));       // 启用颜色对1
    addstr("Hello ncurses!");
    attroff(COLOR_PAIR(1));      // 禁用颜色对
    
    // 底部提示信息
    move(LINES-2, 2);
    attron(COLOR_PAIR(2));
    addstr("Press any key to exit...");
    attroff(COLOR_PAIR(2));
    
    refresh();  // 刷新显示
    getch();    // 等待用户输入
    
    // 4. 清理
    endwin();
    return 0;
}

编译与运行

bash 复制代码
gcc test.c -lncurses
./a.out

目标文件

通过 gcc -c 编译生成的 .o 文件即为目标文件 ,是源码编译后的中间二进制文件,未完成链接、地址重定位,无法直接运行。

所有.o目标文件、.a静态库、.so动态库、可执行文件,本质都是ELF格式文件

如何生成目标文件?

使用 gcc -c 命令:只编译,不链接

示例:

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

// code.c
#include<stdio.h>
void run() {
	printf("running...\n");
}

// 编译两个源⽂件
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o

为什么需要目标文件?

1. 模块化编译

如果项目有 100 个 .c 文件:

  • 不用每次全部重新编译
  • 只修改 1 个文件 → 只重新生成 1 个 .o
  • 链接器直接合并所有 .o → 极大节省编译时间

2. 制作动静态库

  • 静态库(.a)= 多个目标文件的打包归档
  • 动态库(.so)= 位置无关的目标文件 + 共享链接
  1. 链接生成可执行文件
    多个目标文件 + 系统库 → 链接成最终程序

查看目标文件

示例:

ELF文件

ELF = Executable and Linkable Format

可执行与可链接格式

它是 Linux 系统的标准二进制文件格式,统一管理系统中所有需要编译、链接、加载、运行的二进制文件

ELF 文件的 4 种标准类型

类型 英文全称 对应文件 作用
1. 可重定位文件 Relocatable File .o 目标文件 存放编译后的机器码,供链接器使用
2. 可执行文件 Executable File 最终程序 main 链接完成,可直接被系统加载运行
3. 共享目标文件 Shared Object File .so 动态库 位置无关代码,运行时动态加载
4. 核心转储文件 Core Dump File core 程序崩溃时生成,用于调试

ELF 文件的核心结构

1. ELF Header(ELF 文件头)

定位

位于 ELF 文件最开头 ,是文件的第一个字节,所有 ELF 必须拥有

核心作用

告诉系统:这个文件是什么、能干嘛、其他结构在哪里

关键存储信息

  1. 魔数(Magic)0x7F 45 4C 46 → 标记这是 ELF 文件
  2. 位数:32 位 / 64 位
  3. 文件类型
  • REL:可重定位文件(.o
  • EXEC:可执行文件
  • DYN:动态库(.so
  1. 机器架构:x86_64 / ARM
  2. 程序入口地址(可执行文件才有)
  3. 程序头表 / 节头表的位置和大小

实操查看

bash 复制代码
readelf -h main.o   # 查看目标文件头
readelf -h main     # 查看可执行文件头

2. Program Header Table(程序头表)

定位

在 ELF 头之后,只有可执行文件 / 动态库(.so)拥有

核心作用

给操作系统内核看 → 描述「如何把文件加载到内存运行」

核心存储信息

Segment(程序段) 分组,描述:

  • 段的权限(只读 / 可写 / 可执行)
  • 段在文件中的位置
  • 段要加载到内存的哪个地址
  • 段的大小

实操查看

bash 复制代码
readelf -l main     # 查看可执行文件的程序头表
readelf -l libmylib.so  # 查看动态库的程序头表

3. Sections(节区)

定位

ELF 文件的实际数据存储区,所有代码、数据、符号都存在这里

核心作用

存放二进制真实内容,是链接器操作的最小单位

6 个核心节

节名 类型 存储内容 作用
.text 代码节 函数机器指令(如 addprintf 调用) 程序执行的代码
.data 数据节 已初始化的全局 / 静态变量 存放有初始值的数据
.bss 零数据节 未初始化的全局 / 静态变量 不占磁盘空间,仅占位
.symtab 符号表 函数名、变量名、外部符号 链接的核心!解析函数用
.rel.text 重定位节 未确定的地址标记 告诉链接器需要修改地址
.dynsym 动态符号节 动态库的符号表 动态链接专用

适用文件

所有 ELF 都有

实操查看

bash 复制代码
readelf -S main.o

4. Section Header Table(节头表)

定位

在 ELF 文件末尾,所有 ELF 必须拥有

核心作用

给链接器(ld)看 → 节区的「目录索引」

它记录了:

  • 每个节叫什么名字
  • 每个节在文件的哪个位置
  • 每个节多大
  • 每个节是什么类型
    链接器通过它快速找到 .text/.symtab 等节,完成链接工作

实操查看

bash 复制代码
readelf -S main

ELF从形成到加载轮廓

ELF形成可执行

  • step-1:将多份 C/C++ 源代码,翻译成为目标 .o 文件 + 动静态库(ELF)
  • step-2:将多份 .o 文件section进行合并

注意:实际合并是在链接时进行的,但是并不是这么简单的合并,也会涉及对库合并,此处不做过多追究

ELF可执行文件加载

  • 一个ELF会有多种不同的Section,在加载到内存的时候,也会进行Section合并,形成segment
  • 合并规则:相同属性,比如:可读,可写,可执行,需要加载时申请空间等
  • 这样,即便是不同的Section,在加载到内存中,可能会以segment的形式,加载到一起
  • 很显然,这个合并工作也已经在形成 ELF 的时候,合并方式已经确定了,具体合并原则被记录在了 ELF程序头表(Program header table)

为什么要将Section 合并成 Segment?

  1. Section合并的主要原因是为了减少页面碎片,提高内存使用效率。
  2. 内存权限管理
    Linux 内存有严格权限:
  • 代码:必须 只读 + 可执行(防止被篡改,安全)
  • 数据:必须 可读 + 可写(允许修改变量)
  • 其他 Section(符号表、节头表):不需要加载到内存
    链接器把权限相同的 Section 合并成一个 Segment,统一设置权限

核心定论

  • Section(节) :是链接器 用的细粒度拆分(代码、数据、符号分开存)
  • Segment(段) :是操作系统 用的粗粒度内存块
  • 多个 Section(节) → 合并 → 一个 Segment(段)

实操验证

  • 查看所有 Section
bash 复制代码
[user1@VM-0-17-centos lesson22]$ readelf -S main.exe
There are 30 section headers, starting at offset 0x1960:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       000000000000001c  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002b8  000002b8
       0000000000000060  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           0000000000400318  00000318
       000000000000003d  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400356  00000356
       0000000000000008  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400360  00000360
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             0000000000400380  00000380
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             0000000000400398  00000398
       0000000000000048  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         00000000004003e0  000003e0
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400400  00000400
       0000000000000040  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         0000000000400440  00000440
       0000000000000192  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004005d4  000005d4
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004005e0  000005e0
       0000000000000028  0000000000000000   A       0     0     8
...
  • 查看所有 Segment

链接视图和执行视图

  • 链接视图 = 节 + 节头表 → 给链接器拼文件用 ,.o 专属
    ◦ 文件结构的粒度更细,将文件按功能模块的差异进行划分,静态链接分析的时候一般关注的是链接视图,能够理解 ELF 文件中包含的各个部分的信息
  • 执行视图 = 段 + 程序头表 → 给操作系统跑程序用 ,可执行 / 动态库专属
    ◦ 告诉操作系统,如何加载可执行文件,完成进程内存的初始化。⼀个可执行程序的格式中,一定有 program header table
  • 说白了就是:一个在链接时作用,一个在运行加载时作用
维度 链接视图 (Linking View) 执行视图 (Execution View)
使用者 链接器 (ld) 操作系统、动态链接器
核心单位 Section(节) Segment(程序段)
核心表格 节头表 (Section Header) 程序头表 (Program Header)
核心作用 编译、链接、制作库 加载程序、运行代码
地址类型 相对地址(未确定) 绝对地址(已确定)
必备结构 节头表 + 节区 程序头表 + 程序段
相关推荐
文青小兵1 小时前
Linux云计算——docker部分技术、命令 (一)
linux·docker·云计算
流星白龙1 小时前
【MySQL高阶】5.MySQL服务器简介
服务器·mysql·adb
文青小兵1 小时前
Linux云计算——docker 监控(五)
linux·docker·云计算·grafana·prometheus
byte轻骑兵1 小时前
【AVRCP】规范精讲[21]: 从轮询到主动推送,AVRCP通知事件全解析
服务器·网络·人机交互·avrcp·音频控制
团象科技1 小时前
跨境服务与产品多地域迭代场景下 生成式AI安全部署的实操路径观察
服务器·人工智能
STDD1 小时前
ATLAS MMO 专用服务器搭建教程:海盗生存 MMO 服务器开服指南
运维·服务器·php
ThinkPet2 小时前
记事-vue3项目部署Jenkins实现CICD流程
运维·nginx·jenkins·jenkinsfile·cicd流水线
j_xxx404_2 小时前
Linux 线程同步硬核解析:从条件变量、阻塞队列到信号量环形队列
linux·运维·服务器·c++·人工智能·ai·中间件
minji...2 小时前
Linux高级IO(五)epoll 的两种工作模式(LT/ET),多路转接之epoll版本的TCP服务器,对比 select/poll/epoll
linux·运维·服务器·epoll·epoll的工作模式·selectpollepoll·水平触发边缘触发