目录
1.什么是库
成熟的代码库是经过验证、可复用的现成解决方案。在实际开发中,任何程序都依赖于大量基础库的支持,开发者无需从零开始编写所有代码,这正是代码库的重要价值所在。
从根本上说,库是可直接执行的二进制代码文件,能够被操作系统加载到内存中运行。库主要分为两种类型:
• 静态库文件扩展名:
- Linux平台:.a
- Windows平台:.lib
• 动态库文件扩展名:
- Linux平台:.so
- Windows平台:.dll
2.静态库
• 静态库(.a):在编译链接阶段将库代码直接嵌入到可执行文件中,程序运行时不再依赖静态库文件。
• 一个可执行程序可能同时使用静态库和动态库。默认情况下,编译器优先链接动态库(.so),仅在找不到同名动态库时才会使用静态库。如需强制静态链接,可通过gcc的-static选项实现。
2.1静态库生成
cpp
//以下是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*
echo "clean ... done"
.PHONY:output
output: (我们一般用这些命令将头文件和生成的静态库组织起来,然后给别人使用)
mkdir -p stdc/include
mkdir -p stdc/lib
cp -f *.h stdc/include
cp -f *.a stdc/lib
tar -czf stdc.tgz stdc
echo "output stdc ... done"
AR 是 GNU 归档工具,rc 参数表示"替换并创建"(replace and create)。通常我们使用 -rc 选项来进行静态库的打包操作。常用选项:
t
:列出静态库中的文件v
:显示详细信息(verbose)
2.2静态库使用
编译选项说明
- -L:指定库文件搜索路径
- -I:指定头文件搜索路径
- -l:指定链接的库名称
静态库特性
- 程序编译链接后,即使删除静态库文件,生成的可执行文件仍能正常运行
库文件命名规则
- 引用库时需去除前缀
lib
及后缀(如.so
或.a
)
示例:libc.so
→ 链接时使用-lc
场景1:
将头文件和库文件安装到系统路径下(此时编译 main.c 只需使用 -l 参数)
cpp
gcc main.c -lmystdio
场景2:
头文件、库文件与源文件位于同一目录
cpp
gcc main.c -L. -lmystdio
场景3:
头文件和库文件拥有各自的独立存储路径。
cpp
gcc main.c -I头⽂件路径 -L库⽂件路径 -lmystdio
3.动态库
• 动态库(.so):程序运行时才链接动态库代码,多个程序可共享调用同一库代码。
• 动态链接的可执行文件仅包含所用函数的入口地址表,而非外部函数的完整机器码。
• 程序运行前,操作系统会将动态库中的外部函数机器码从磁盘加载到内存,这一过程称为动态链接(dynamic linking)。
• 动态库可被多个程序共享,因此动态链接能显著减小可执行文件体积,节省磁盘空间。操作系统通过虚拟内存机制,使物理内存中的同一动态库可被多个进程共享,从而优化内存和磁盘空间利用率。
3.1动态库生成
cpp
//以下是makefile文件:
libmystdio.so:my_stdio.o my_string.o
gcc -o $@ $^ -shared (我们一般生成动态库只需在编译器加上---shared)
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
rm -rf *.so *.o stdc*
echo "clean ... done"
.PHONY:output
output:
mkdir -p stdc/include
mkdir -p stdc/lib
cp -f *.h stdc/include
cp -f *.so stdc/lib
tar -czf stdc.tgz stdc
echo "output stdc ... done"
- shared: 生成共享库格式
- fPIC: 生成位置无关代码 (Position Independent Code) (目的是代码可以被加载器加载到内存的任意位置都可以正确的执行)
- 库名规则: 遵循 libxxx.so 命名规范
3.2动态库使用
场景1:
将头文件和库文件安装到系统路径下
cpp
gcc main.c -lmystdio
场景2:
头文件、库文件与源文件位于同一目录
cpp
gcc main.c -L. -lmystdio
场景3:
头文件和库文件拥有各自的独立存储路径。、
cpp
gcc main.c -I头⽂件路径 -L库⽂件路径 -lmystdio
3.3库运行搜索路径
ldd 可以查看可执行文件所依赖的库:
cpp
ldd 可执行文件
cpp
ldd a.out
linux-vdso.so.1 => (0x00007fff4d396000)
libmystdio.so => not found
libc.so.6 => /lib64/libc.so.6 (0x00007fa2aef30000)
/lib64/ld-linux-x86-64.so.2 (0x00007fa2af2fe000)
上图显示系统无法找到生成a.out可执行文件所需的库文件。
解决方法:
• 将.so文件复制到系统共享库目录中,常见路径包括/usr/lib、/usr/local/lib或/lib64等,也可根据项目需求存放到指定库路径
• 在系统共享库路径下创建同名符号链接
• 通过修改LD_LIBRARY_PATH环境变量来指定库文件路径
• 采用ldconfig方案:配置/etc/ld.so.conf.d/目录后执行ldconfig命令更新库缓存

4.目标文件
在Windows环境下,IDE通常将编译和链接这两个步骤封装得十分完善,开发者只需一键构建即可完成。然而,当遇到错误时,特别是链接阶段的问题,很多人往往感到束手无策。

让我们深入剖析编译和链接的全过程,以便更清晰地理解动静态库的工作原理。
简单来说,编译就是将程序源代码转换为CPU能够直接执行的机器代码的过程。
例如:假设有个源文件 hello.c,它简单地输出"helloworld!"并调用 run 函数。这个 run 函数定义在另一个源文件 code.c 中。此时我们可以使用 gcc -c 命令分别编译这两个源文件。
cpp
$ gcc -c hello.c
$ gcc -c code.c
$ ls
code.c code.o hello.c hello.o
编译完成后会生成两个扩展名为.o的目标文件。需要注意的是,如果只修改了某个源文件,只需单独重新编译该文件即可,无需重新编译整个工程。目标文件采用ELF格式,这是一种二进制封装形式。
注意:
file 命令用于识别文件类型。
cpp
file 文件名
5.ELF文件
要深入理解编译链接的细节,我们首先需要认识ELF文件。实际上,以下四种文件都属于ELF格式:
-
可重定位文件(Relocatable File):即.o文件,包含可与其他目标文件链接生成可执行文件或共享目标文件的代码和数据。
-
可执行文件(Executable File):可直接运行的程序。
-
共享目标文件(Shared Object File):即.so文件,作为动态链接库使用。
-
内核转储文件(Core Dumps):记录进程执行上下文,用于调试和故障分析。
ELF文件由四个主要部分组成:
-
ELF头(ELF Header):位于文件起始位置,描述文件的基本特征,并定位其他组成部分。
-
程序头表(Program Header Table):列出所有有效的段(segments)及其属性,包括每个段的起始位置、偏移量和长度等信息。
-
节头表(Section Header Table):描述文件中各个节(sections)的信息。
-
节(Sections):ELF文件的基本组成单元,存储特定类型的数据。例如:
- 代码节存储可执行指令
- 数据节存储全局变量和静态数据等
常见节区类型:
• 代码段(.text):存储程序的可执行指令,构成程序的核心运行逻辑。
• 数据段(.data):存放已初始化的全局变量及静态局部变量。
6.ELF从形成到加载轮廓
6.1ELF形成可执行
- 步骤1:将多份 C/C++ 源代码编译为目标文件 (.o)
- 步骤2:合并多个 .o 文件中的段

6.2ELF可执行文件加载
一个ELF文件包含多个不同的Section,在加载到内存时会将这些Section合并成Segment。
合并原则基于相同属性,例如:
- 可读性
- 可写性
- 可执行性
- 是否需要加载时申请内存空间
因此,具有相同属性的不同Section可能会被合并到同一个Segment中加载到内存。
这种合并方式在ELF文件生成时就已确定,具体的合并规则被记录在ELF的程序头表(Program Header Table)中。(本文就浅显讲解ELF,不过多讲解了)