头像


🎬 个人主页艾莉丝努力练剑

专栏传送门:《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平

🎬 艾莉丝的简介:

在这里插入图片描述



在这里插入图片描述


1 ~> 库的分类与基本原理

1.1 静态库与动态库的定义

静态库(.a文件)是指程序在编译链接时将库的代码完整复制到可执行文件中,程序运行时不再需要外部库文件。静态库在链接阶段被直接嵌入到最终的可执行文件中,形成独立的二进制程序。

动态库(.so文件)则采用不同的机制:程序运行时才去链接动态库代码。与动态库链接的可执行文件仅包含所用函数的入口地址表,而非整个目标文件的机器码。在程序执行前,操作系统将动态库从磁盘加载到内存,这个过程称为动态链接。

1.2 系统中的库示例

在典型的 Linux 系统中,可以观察到标准库的实现:

# Ubuntu系统标准库示例
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so
-rwxr-xr-x 1 root root 2029592 May  1 02:20 /lib/x86_64-linux-gnu/libc-2.31.so

$ ls -l /lib/x86_64-linux-gnu/libc.a
-rw-r--r-- 1 root root 5747594 May  1 02:20 /lib/x86_64-linux-gnu/libc.a

从文件大小可以看出,静态库通常比动态库大,因为静态库包含了所有函数的完整实现代码。

2 ~> 静态库的制作与使用

2.1 静态库制作流程

静态库的制作基于目标文件(.o文件)的归档过程。以下是完整的制作示例:

源代码文件结构:

// my_stdio.h - 头文件定义
#pragma once
#define SIZE 1024

struct IO_FILE {
    int flag;
    int fileno;
    char outbuffer[SIZE];
    int cap;
    int size;
};

typedef struct IO_FILE mFILE;

mFILE *mfopen(const char *filename, const char *mode);
int mfwrite(const void *ptr, int num, mFILE *stream);
void mfflush(mFILE *stream);
void mfclose(mFILE *stream);
// my_stdio.c - 实现文件
#include "my_stdio.h"
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

mFILE *mfopen(const char *filename, const char *mode) {
    int fd = -1;
    if(strcmp(mode, "r") == 0) {
        fd = open(filename, O_RDONLY);
    } else if(strcmp(mode, "w") == 0) {
        fd = open(filename, O_CREAT|O_WRONLY|O_TRUNC, 0666);
    }
    // 其余实现代码...
    return mf;
}

Makefile 配置:

libmystdio.a: my_stdio.o my_string.o
	@ar -rc $@ $^
	@echo "build $^ to $@ ... done"

%.o: %.c
	@gcc -c $<
	@echo "compling $< to $@ ... done"

.PHONY: clean
clean:
	@rm -rf *.a *.o stdc*

使用 ar 工具创建静态库:

$ make libmystdio.a
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o

2.2 静态库的使用方式

编译选项详解:

# 场景1:库文件在系统路径
$ gcc main.c -lmystdio

# 场景2:库文件在当前目录
$ gcc main.c -L. -lmystdio

# 场景3:自定义头文件和库路径
$ gcc main.c -I./include -L./lib -lmystdio

关键编译选项说明:

  • -L:指定库文件搜索路径
  • -I:指定头文件搜索路径
  • -l:指定要链接的库名(去掉 lib 前缀和.a 后缀)

3 ~> 动态库的制作与使用

3.1 动态库制作要点

动态库制作需要位置无关代码(PIC)支持:

libmystdio.so: my_stdio.o my_string.o
	gcc -o $@ $^ -shared

%.o: %.c
	gcc -fPIC -c $<

关键编译选项:

  • -fPIC:生成位置无关代码,这是动态库的必要条件
  • -shared:指示生成共享库(动态库)

3.2 动态库运行时搜索路径

动态库在运行时需要被系统找到,否则会出现加载错误:

# 查看程序依赖的动态库
$ ldd myprogram
	linux-vdso.so.1 (0x00007ffeeff3fe000)
	libmystdio.so => not found
	libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f8a1b200000)

解决方案:

  1. 将库文件复制到系统库目录(如/usr/lib
  2. 设置LD_LIBRARY_PATH环境变量
  3. 修改/etc/ld.so.conf配置文件
  4. 使用rpath编译选项

4 ~> ELF 文件格式深度解析

4.1 ELF 文件类型与结构

ELF(Executable and Linkable Format)是 Linux 下的标准二进制文件格式,主要分为四种类型:

ELF 类型 描述 典型文件
REL 可重定位文件 .o 目标文件
EXEC 可执行文件 编译后的程序
DYN 共享目标文件 .so 动态库
CORE 核心转储文件 core dump 文件

4.2 ELF 文件的四部分组成

ELF 文件由四个核心部分组成:

  1. ELF Header:描述文件的基本属性和组织结构
  2. Program Header Table:执行视图,用于程序加载
  3. Section Header Table:链接视图,用于编译链接
  4. Sections:实际的代码、数据等内容

4.3 最常见的节区(Sections)

# 查看可执行文件的节区
$ readelf -S hello
Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [13] .text             PROGBITS         00000000004004c0  000004c0
       000000000000016d  0000000000000000  AX       0     0     16
  [24] .data             PROGBITS         0000000000601000  00002000
       0000000000000010  0000000000000000  WA       0     0      8
  [25] .bss              NOBITS           0000000000601010  00002010
       0000000000000024  0000000000000000  WA       0     0      1
  [12] .dynsym           DYNSYM           0000000000400390  00000390
       00000000000000a0  0000000000000018   A       5     1      8

关键节区说明:

  • .text:存放程序代码
  • .data:已初始化的全局变量
  • .bss:未初始化的全局变量
  • .plt:过程链接表,用于动态链接
  • .got:全局偏移表,存储外部符号地址

5 ~> 静态链接机制深度剖析

5.1 静态链接的本质

静态链接是将多个目标文件(.o)合并成一个可执行文件的过程。链接器主要完成以下工作:

  1. 符号解析:将符号引用与符号定义关联
  2. 地址重定位:为符号分配最终的内存地址
  3. 节区合并:将相同类型的节区合并

5.2 查看目标文件符号表

# 查看目标文件的符号表
$ nm my_stdio.o
0000000000000000 T mfclose
0000000000000146 T mfflush
0000000000000000 T mfopen
00000000000001a8 T mfwrite
                 U write
# 查看重定位信息
$ readelf -r my_stdio.o
Relocation section '.rela.text' at offset 0x648 contains 6 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000002a  001500000002 R_X86_64_PC32     0000000000000000 write - 4

6 ~> 动态链接与程序加载过程

6.1 程序启动流程

动态链接程序的启动涉及复杂的协作过程:

  1. 内核加载:操作系统将可执行文件映射到进程地址空间
  2. 解释器介入:通过PT_INTERP段指定的动态链接器(如/lib64/ld-linux-x86-64.so.2)开始工作
  3. 符号解析:动态链接器解析所有未定义的符号
  4. 重定位完成:更新 GOT/PLT 表中的地址

6.2 动态库的共享机制

动态库的核心优势在于进程间共享:

// 进程A和进程B共享同一个libc.so的物理内存页面
进程A虚拟地址空间 → [libc代码] ← 进程B虚拟地址空间
0x7fxxxxxx (libc)      [内存页]      0x7fxxxxxx (libc)

这种共享机制通过虚拟内存和写时复制(Copy-on-Write)技术实现,显著减少了内存占用。

6.3 GOT/PLT 延迟绑定机制

动态链接采用延迟绑定(Lazy Binding)优化性能:

  • GOT(Global Offset Table):存储外部函数和变量的实际地址
  • PLT(Procedure Linkage Table):包含跳转到 GOT 的桩代码
# PLT条目示例
push %GOT[1]       # 第一次跳转到动态链接器
jmp *%GOT[2]       # 调用动态链接器

第一次调用函数时,流程为:PLT → 动态链接器(解析真实地址)→ 更新 GOT → 直接跳转到目标函数。后续调用直接通过 GOT 跳转,避免重复解析。

7 ~> 动静态链接对比总结

7.1 特性对比表格

特性 静态链接 动态链接
文件大小 较大(包含全部库代码) 较小(仅包含引用信息)
内存占用 每个进程独立副本 多进程共享库代码
部署便利性 简单(单文件部署) 需要确保库文件存在
更新维护 需要重新编译整个程序 只需替换库文件
加载速度 较快(无运行时链接) 首次加载稍慢
兼容性 库版本固定 可能存在版本冲突

7.2 应用场景选择

选择静态链接的情况:

  • 需要单文件部署的应用程序
  • 对性能要求极高的场景
  • 嵌入式系统等资源受限或环境不可控

选择动态链接的情况:

  • 需要多个程序共享库代码
  • 频繁更新功能的场景
  • 磁盘和内存资源需要优化
  • 大型系统应用程序

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

艾莉丝努力练剑

C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主


👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。
❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。
【收藏】 把核心知识点存好,在需要时随时查、随时用。
💬 【评论】 分享你的经验或疑问,评论区一起交流避坑!

不要忘记给博主“一键四连”哦!

“今日练剑达成!”

“技术之路难免有困惑,但同行的人会让前进更有方向。”

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主“一键四连”哦!

往期回顾

【Linux:文件】库的制作与原理进阶

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡
૮₍ ˶ ˊ ᴥ ˋ˶₎ა

在这里插入图片描述

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐