程序编译的四个核心阶段
程序从源代码(.c/.cpp)到可执行文件,需经过预处理、编译、汇编、链接四个步骤,每个步骤生成不同中间文件,最终输出可执行程序。
预处理
-
作用:展开预处理指令(
#include
/#define
/#if
等)、删除注释、处理条件编译,不做语法检查。 -
输入文件:源代码文件(.c/.cpp)
-
输出文件:预处理文件(.i)
-
核心命令:
gcc 源文件 -o 输出文件 -E
-
代码示例:
# 对hello.c进行预处理,生成hello.i(展开#include <stdio.h>等) # -E:仅执行预处理,停止后续编译步骤 gcc hello.c -o hello.i -E
编译
-
作用:对预处理后的.i 文件做词法分析、语法分析、语义分析,将 C 代码翻译成对应硬件平台的汇编语言。
-
输入文件:预处理文件(.i)
-
输出文件:汇编语言文件(.s)
-
核心命令:
gcc 预处理文件 -o 输出文件 -S
-
代码示例:
# 对hello.i编译,生成hello.s(汇编代码) # -S:仅执行编译,生成汇编文件后停止 gcc hello.i -o hello.s -S
汇编
-
作用:将汇编语言文件(.s)翻译成处理器可识别的二进制机器码,生成可重定位目标文件。
-
输入文件:汇编文件(.s)
-
输出文件:可重定位目标文件(.o,ELF 格式)
-
核心命令:
gcc 汇编文件 -o 输出文件 -c
-
代码示例:
# 对hello.s汇编,生成hello.o(二进制机器码,不可直接运行) # -c:仅执行汇编,生成.o文件后停止,不进行链接 gcc hello.s -o hello.o -c
链接
-
作用:将多个.o 文件、依赖的库文件(静态库 / 动态库)合并,分配统一的内存地址(重定位),生成可执行文件。
-
输入文件:多个.o 文件 + 库文件(.a/.so)
-
输出文件:可执行文件(无后缀,如 hello)
-
核心命令:
gcc 目标文件 -o 可执行文件 -L库路径 -l库名
-
代码示例:
# 将hello.o与C标准库(libc.a/libc.so)链接,生成可执行文件hello # -lc:链接C标准库(libc.a/libc.so),-l后接库名(去掉lib前缀和.a/.so后缀) # 若依赖自定义库,需加-L指定库路径,如-L./lib(表示从当前lib目录找库) gcc hello.o -o hello -lc
ELF 文件格式(Executable and Linkable Format)
ELF 是 Linux/Unix 下统一的二进制文件格式,涵盖可重定位文件(.o)、可执行文件、动态库(.so) 三种类型,核心是通过 “段(Section)” 组织数据和代码。
ELF 文件的核心结构
ELF 文件由以下关键部分组成,链接时会将多个文件的相同段合并(如所有.o 的.text 段合并为可执行文件的.text 段):
段名 | 作用 | 权限 |
---|---|---|
.text |
存放二进制机器码(函数指令) | 只读 + 可执行 |
.data |
存放已初始化的全局变量 / 静态变量 | 可读 + 可写 |
.bss |
存放未初始化的全局变量 / 静态变量 | 可读 + 可写 |
.rodata |
存放常量(如字符串常量"hello" ) |
只读 |
.symtab |
符号表(记录函数名、变量名与地址映射) | 只读 |
重定位
- 概念:.o 文件中函数 / 变量的地址是 “相对地址”(如相对于当前.o 的偏移),链接时需给这些符号分配统一的绝对地址,确保程序运行时能正确找到函数 / 变量,这个过程就是重定位。
- 举例:a.o 中的
printf
调用地址是偏移量,链接时会替换为 C 标准库中printf
的实际绝对地址。
ELF 相关工具命令
readelf
:查看 ELF 文件详情
# 查看ELF文件头部信息(类型、机器架构、入口地址等)
# -h:显示ELF文件头部(Header)
readelf -h a.out# 查看ELF文件的所有段信息(段名、偏移、大小、权限等)
# -S:显示段表(Section Table)
readelf -S a.out# 查看ELF文件的符号表(函数名、变量名、地址、类型等)
# -s:显示符号表(Symbol Table),可用于排查“undefined reference”错误
readelf -s a.out
ldd
:查看可执行文件 / 动态库的动态依赖(list dynamic dependencies)
# 查看a.out运行时依赖的所有动态库(路径、版本等)
# 若运行时提示“找不到xxx.so”,可先用ldd确认依赖是否缺失
ldd a.out
静态库
静态库是.o
文件的归档集合(后缀.a
),编译链接时会将程序依赖的库代码完整复制到可执行文件中,生成的可执行文件不依赖外部库。
静态库的命名规范
- 格式:
lib库名.a
(必须以lib
开头,.a
结尾) - 示例:
libmath.a
(库名是math
,链接时用lmath
)
静态库的制作步骤
步骤 1:生成.o 文件(原材料)
# 将a.c编译为a.o,b.c编译为b.o(-c仅汇编,不链接)
# a.c/b.c:包含可复用的函数(如加法、减法函数)
gcc a.c -o a.o -c
gcc b.c -o b.o -c
步骤 2:用ar
(Archive Tool)命令归档为静态库
# 将a.o、b.o归档为libcalc.a静态库
# ar:归档工具,c(create)=创建库(不存在则新建)
#r(replace/insert)=插入/替换.o(重名则覆盖),s=生成索引(加速链接)
ar crs libcalc.a a.o b.o
静态库的常用操作(ar
命令)
# 查看静态库中包含的.o文件列表(t=table,以列表形式展示)
ar t libcalc.a # 输出:a.o b.o# 从静态库中删除指定.o文件(d=delete)
ar d libcalc.a b.o # 删除libcalc.a中的b.o
ar t libcalc.a # 输出:a.o# 向静态库中添加/替换.o文件(r=replace,不存在则添加,存在则替换)
ar r libcalc.a b.o # 重新添加b.o到libcalc.a
ar t libcalc.a # 输出:a.o b.o# 从静态库中提取指定.o文件(x=extract,不指定则提取所有)
ar x libcalc.a a.o # 提取a.o到当前目录
ar x libcalc.a # 提取所有.o到当前目录
静态库的使用
示例:主程序main.c
调用静态库libcalc.a
的函数
步骤 1:编写静态库的源文件与头文件
制作一个 “计算库”libcalc.a
,包含add
(加法)和sub
(减法)两个函数:
- 库的头文件
calc.h
(声明函数接口)
/*** 计算库头文件:声明对外提供的函数接口* @note 所有使用该库的程序,只需包含此头文件即可调用函数*/
#ifndef __CALC_H__
#define __CALC_H__
// 加法函数:返回a + b的结果
int add(int a, int b);// 减法函数:返回a - b的结果
int sub(int a, int b);#endif // __CALC_H__
- 库的源文件
add.c
(实现加法)
// 实现add函数,需包含头文件(确保声明与实现一致)
#include "calc.h"
int add(int a, int b) {return a + b;
}
- 库的源文件
sub.c
(实现减法)
#include "calc.h"
int sub(int a, int b) {return a - b;
}
步骤 2:制作静态库 libcalc.a
# 生成.o文件(原材料):将add.c、sub.c编译为可重定位目标文件
# -c:仅汇编,不链接;-Wall:显示警告,确保代码规范
gcc -c add.c -o add.o -Wall
gcc -c sub.c -o sub.o -Wall# 用ar命令归档为静态库:将add.o、sub.o打包为libcalc.a
# c:若libcalc.a不存在则新建;r:插入/替换.o文件;s:生成索引(加速链接)
ar crs libcalc.a add.o sub.o# 查看库中包含的.o文件(验证制作成功)
ar t libcalc.a # 输出:add.o sub.o
步骤 3:主程序调用静态库
编写主程序main.c
,通过包含calc.h
头文件调用库中的函数:
/*** 主程序:调用静态库libcalc.a的add和sub函数* @note 只需包含库的头文件calc.h,无需关心函数实现*/
#include <stdio.h>
#include "calc.h" // 包含库的头文件,获取函数声明
int main() {int a = 10, b = 5;// 调用静态库中的add函数int sum_res = add(a, b);printf("%d + %d = %d\n", a, b, sum_res); // 输出:10 + 5 = 15// 调用静态库中的sub函数int sub_res = sub(a, b);printf("%d - %d = %d\n", a, b, sub_res); // 输出:10 - 5 = 5return 0;
}
步骤 4:编译主程序并链接静态库
根据静态库的存放路径不同,编译命令分为 “当前目录”“自定义目录”“系统目录” 三种场景:
- 场景 1:静态库在当前目录(与 main.c 同路径)
# 编译main.c,链接当前目录的libcalc.a
# -L./:指定静态库路径为当前目录(./可省略,默认优先找当前目录)
# -lcalc:链接libcalc.a(-l后接“库名”,即去掉lib前缀和.a后缀)
# -o main:指定输出的可执行文件名为main
gcc main.c -o main -L./ -lcalc -Wall# 运行程序(无需依赖libcalc.a,可单独拷贝main执行)
./main
场景 2:静态库在自定义目录(如./lib)
实际开发中,常将库文件放在单独的lib
目录,需用-L
指定路径:
# 创建lib目录,将libcalc.a移动到lib目录
mkdir lib
mv libcalc.a ./lib/# 编译时用-L./lib指定库路径
gcc main.c -o main -L./lib -lcalc -Wall# 运行程序(main已包含库代码,无需带lib目录)
./main
场景 3:静态库在系统目录(如 /usr/lib)
若静态库安装到系统默认库路径(如/usr/lib
),编译时无需指定-L
(系统会自动搜索):
bash
# 将libcalc.a复制到系统目录(需sudo权限)
sudo cp libcalc.a /usr/lib/# 编译时直接链接,无需-L
gcc main.c -o main -lcalc -Wall# 运行程序
./main
静态库的更新与维护
当静态库的功能需要修改时,需重新制作库并重新链接主程序(因为静态库代码已嵌入可执行文件,旧程序无法自动更新):
示例:更新add
函数功能(支持浮点数加法)
-
修改库源文件与头文件:
// 更新calc.h:将add函数改为支持float float add(float a, float b);// 更新add.c:实现float版本的add #include "calc.h" float add(float a, float b) {return a + b; }
-
重新制作静态库:
# 重新生成add.o gcc -c add.c -o add.o -Wall# 替换库中的旧add.o(或重新制作库) ar r libcalc.a add.o # r=替换旧的add.o# 验证更新:查看add函数的类型 nm libcalc.a | grep add # 输出:0000000000000000 T add(类型已变为float)
-
重新编译主程序:
# 主程序需同步修改调用方式(传入float参数) gcc main.c -o main -L./lib -lcalc -Wall# 运行更新后的程序 ./main
静态库使用的注意事项
-
头文件与库版本匹配:确保主程序包含的头文件(如
calc.h
)与静态库的版本一致,避免 “声明与实现不匹配”(如头文件是int add
,库中是float add
)。 -
库的命名规范:严格遵循
libxxx.a
格式,否则lxxx
无法识别(如calc.a
需改为libcalc.a
,链接时用lcalc
)。 -
静态库重名符号冲突:
- 原因:库中多个.o 有同名函数 / 变量。
- 解决:调整.o 添加到静态库的顺序(排在前面的优先),或重命名符号。
-
链接顺序:若主程序依赖多个静态库且存在依赖关系(如
libb.a
依赖liba.a
),被依赖的库必须放在链接命令的后面:# 正确:libb依赖liba,-la放后面 gcc main.c -o main -L./lib -lb -la# 错误:-la放前面,会导致libb找不到liba的符号 gcc main.c -o main -L./lib -la -lb # 报错:undefined reference to xxx(liba中的符号)
-
可执行文件体积:静态库会导致可执行文件体积增大(代码复制),若对体积敏感,可考虑使用动态库。
动态库
动态库是.o
文件的共享集合(后缀.so
),编译链接时仅记录 “依赖关系”,不复制代码;程序运行时才动态加载库文件,多个程序可共享同一个动态库,节省内存。
动态库的核心概念
动态库与静态库的本质差异
特性 | 静态库(.a) | 动态库(.so) |
---|---|---|
链接时机 | 编译时(代码复制到可执行文件) | 运行时(仅记录依赖,不复制代码) |
可执行文件体积 | 大(包含库代码) | 小(仅包含依赖信息) |
内存占用 | 大(多程序运行时多份拷贝) | 小(多程序共享一份库内存) |
升级兼容性 | 需重新编译程序 | 主版本不变时,无需重新编译 |
运行依赖 | 无(可执行文件独立) | 有(必须找到动态库才能运行) |
动态库的命名规范(含版本管理)
动态库的命名需包含 “版本信息” 和 “SONAME(共享对象名称)”,确保升级兼容,格式如下:
名称类型 | 格式示例 | 作用 |
---|---|---|
实际库文件(真实文件) | libcalc.so.1.2.3 |
包含完整版本(主。次. 修订),是实际的库代码文件 |
SONAME(软链接) | libcalc.so.1 |
稳定的 “链接名”,仅含主版本号,供程序运行时查找 |
编译链接名(软链接) | libcalc.so |
无版本号,供编译时链接(指向 SONAME) |
- 版本号含义:
- 主版本号(1):重大变更(如删除接口、修改参数),不兼容旧版本,需重新编译程序;
- 次版本号(2):兼容升级(如新增接口、修复 bug),不破坏旧功能;
- 修订版本号(3):小修复(如性能优化、小 bug 修复),无功能变更。
- SONAME 的核心作用:程序编译时仅记录 “依赖 SONAME(如
libcalc.so.1
)”,而非具体版本(如libcalc.so.1.2.3
)。后续升级动态库时,只要 SONAME 不变(主版本不变),程序无需重新编译,直接加载新版本库。
动态库的制作步骤
以 “计算库libcalc.so
” 为例,包含add
(加法)、sub
(减法)两个函数,完整步骤如下:
步骤 1:准备库的源文件与头文件
动态库必须配合头文件(.h) 提供接口声明,否则主程序无法调用库函数(这是之前遗漏的关键细节)。
- 头文件
calc.h
(声明接口)
/*** 计算库头文件:声明对外提供的函数接口* @note 所有使用该库的程序,必须包含此头文件*/
#ifndef __CALC_H__
#define __CALC_H__
// 加法:返回a + b
int add(int a, int b);// 减法:返回a - b
int sub(int a, int b);#endif // __CALC_H__
- 源文件
add.c
(实现加法)
#include "calc.h" // 包含头文件,确保接口声明与实现一致
int add(int a, int b) {return a + b;
}
- 源文件
sub.c
(实现减法)
#include "calc.h"
int sub(int a, int b) {return a - b;
}
步骤 2:生成位置无关代码(.o 文件)
动态库加载时会被随机分配到内存的任意位置,必须用 -fPIC
生成 “位置无关代码”(代码不依赖固定内存地址):
# 编译add.c为a.o,-fPIC是动态库的强制选项
gcc -c add.c -o add.o -fPIC -Wall #fPIC 是 force Position-Independent Code
# 编译sub.c为b.o
gcc -c sub.c -o sub.o -fPIC -Wall
步骤 3:链接生成动态库(指定 SONAME)
用 -shared
标记生成动态库,并用 -Wl,-soname
指定 SONAME(传递给链接器):
# 生成实际库文件libcalc.so.1.2.3,指定SONAME为libcalc.so.1
# -shared:生成动态库(而非可执行文件)
# -Wl,-soname,libcalc.so.1:Wl=Wrap Linker(传递参数给链接器),逗号后无空格
gcc -shared -fPIC add.o sub.o -o libcalc.so.1.2.3 -Wl,-soname,libcalc.so.1 -Wall# 验证SONAME是否生效(输出"SONAME: libcalc.so.1"表示正确)
readelf -d libcalc.so.1.2.3 | grep "SONAME"
步骤 4:创建软链接(SONAME 与编译链接名)
动态库需要两个软链接:
- SONAME 软链接:供程序运行时查找(指向实际库文件);
- 编译链接名软链接:供编译时链接(指向 SONAME)。
# 方法1:用ldconfig自动创建SONAME软链接(推荐,避免手动写错)
# -n:仅在当前目录处理,不更新系统全局缓存
ldconfig -n .# 方法2:手动创建SONAME软链接(等效于ldconfig -n .)
# ln -s libcalc.so.1.2.3 libcalc.so.1# 创建编译链接名软链接(供gcc编译时用-lcalc查找)
ln -s libcalc.so.1 libcalc.so# 最终文件结构(用ls -l查看,软链接用->表示)
# libcalc.so -> libcalc.so.1 (编译链接名)
# libcalc.so.1 -> libcalc.so.1.2.3 (SONAME)
# libcalc.so.1.2.3 (实际库文件)
ls -l libcalc*
动态库的使用(编译与运行)
动态库的使用分 “编译时链接” 和 “运行时加载” 两步,核心是解决 “编译时找到库” 和 “运行时找到库” 两个问题。
编译时链接动态库
主程序 main.c
调用动态库函数,需指定头文件路径(-I)、库路径(-L)、库名(-l):
- 主程序
main.c
#include <stdio.h>
#include "calc.h" // 包含库的头文件(需确保编译器能找到)
int main() {int a = 20, b = 10;printf("%d + %d = %d\n", a, b, add(a, b)); // 调用addprintf("%d - %d = %d\n", a, b, sub(a, b)); // 调用subreturn 0;
}
- 编译命令(分场景)
根据动态库和头文件的存放位置,分 “当前目录”“自定义目录” 两种场景:
场景 1:库和头文件在当前目录
# 编译main.c,链接动态库libcalc.so
# -I./:指定头文件路径(当前目录,若头文件在当前目录可省略)
# -L./:指定库路径(当前目录)
# -lcalc:链接libcalc.so(-l后接库名,去掉lib前缀和.so后缀)
# -Wall:显示警告,确保代码规范
gcc main.c -o main -I./ -L./ -lcalc -Wall# 验证编译结果(查看可执行文件依赖的动态库)
ldd main | grep "libcalc" # 输出:libcalc.so.1 => ./libcalc.so.1 (0x...)
场景 2:库在自定义目录(如./lib)、头文件在./include
实际开发中,常将库放在 lib
目录、头文件放在 include
目录,需调整 -I
和 -L
:
# 整理目录结构
mkdir lib include
mv libcalc.so* lib/ # 库文件移到lib
mv calc.h include/ # 头文件移到include# 编译时指定头文件和库路径
gcc main.c -o main -I./include -L./lib -lcalc -Wall# 验证依赖(可执行文件依赖的是libcalc.so.1,路径正确)
ldd main | grep "libcalc" # 输出:libcalc.so.1 => not found(暂时找不到,正常)
运行时加载动态库(解决 “找不到.so” 问题)
编译通过后,运行程序若提示 “error while loading shared libraries: libcalc.so.1: cannot open shared object file
”,表示系统找不到动态库。需通过以下 3 种方法指定运行时路径(按推荐度排序):
方法 1:编译时固化路径(-Wl,-rpath,永久生效)
用 -Wl,-rpath
将运行时路径 “写入可执行文件”,程序运行时会优先从该路径找库,无需额外配置:
# 编译时添加-rpath,指定动态库的运行时路径(如./lib)
gcc main.c -o main -I./include -L./lib -lcalc -Wl,-rpath=./lib -Wall #rpath:Runtime PATH# 直接运行(无需其他配置,路径已固化)
./main # 输出:20 + 10 = 30 / 20 - 10 = 10
- 优势:永久生效,拷贝可执行文件和库到其他目录,只要相对路径不变,仍可运行;
- 场景:项目内固定路径的动态库。
方法 2:设置环境变量(LD_LIBRARY_PATH,临时生效)
LD_LIBRARY_PATH
是动态链接器(ld-linux.so
)的优先查找路径,适合临时测试:
# 临时添加库路径到环境变量(当前终端有效,关闭终端失效)
# $LD_LIBRARY_PATH:保留原有路径,避免覆盖
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./lib# 运行程序(动态链接器从./lib找到libcalc.so.1)
./main# 取消环境变量(可选)
unset LD_LIBRARY_PATH
- 优势:无需重新编译,灵活;
- 缺点:临时生效,多终端需重复设置;不建议用于生产环境(易导致路径冲突)。
方法 3:修改系统默认库路径(不推荐,污染系统)
将库路径添加到系统配置,供所有程序共享,但可能污染系统库目录,仅用于 “系统级共享库”:
# 在系统库配置目录添加路径文件(需sudo权限)
sudo vi /etc/ld.so.conf.d/calc.conf
# 在文件中写入动态库路径(如/home/gec/project/lib)
/home/gec/project/lib# 更新系统动态库缓存(让系统识别新路径)
sudo ldconfig# 运行程序(系统从配置路径找到库)
./main
- 风险:普通用户误操作可能导致系统无法启动(如写错路径);
- 场景:仅用于需全局共享的库(如系统工具依赖的库)。
动态库的动态加载(运行时按需加载)
动态加载是指程序运行时手动加载指定动态库(而非编译时链接),适用于 “插件化开发”(如可扩展的检测系统、插件框架),核心依赖 dlfcn.h
头文件的 4 个函数。
核心函数解析
#include <dlfcn.h>
/*** 打开动态库,获取操作句柄* @param filename 动态库路径(如"./libcolor.so",相对/绝对路径均可)* @param flag 加载模式:* - RTLD_LAZY:延迟解析(调用函数时才解析地址,效率高,适合无循环依赖的库)* - RTLD_NOW:立即解析(打开库时就解析所有符号,避免运行时错误,适合有循环依赖的库)* @return 成功:动态库句柄(void*,后续操作依赖此句柄);失败:NULL(用dlerror()查错误)* @note 编译时必须加-ldl(链接dl库,动态加载的依赖库)*/
void *dlopen(const char *filename, int flag);/*** 从动态库中查找符号(函数/变量的地址)* @param handle 由dlopen()返回的动态库句柄(不可为NULL)* @param symbol 要查找的符号名(函数名/变量名,如"detection")* @return 成功:符号地址(需强制转换为对应类型);失败:NULL(用dlerror()查错误)* @note 符号名必须与库中完全一致(区分大小写)*/
void *dlsym(void *handle, const char *symbol);/*** 获取动态加载的错误信息* @return 成功:错误信息字符串(const char*);无错误:NULL* @note 每次调用后错误信息会被清空,需及时保存(如用strdup()复制)*/
const char *dlerror(void);/*** 关闭动态库,释放资源* @param handle 由dlopen()返回的动态库句柄(不可为NULL)* @return 成功:0;失败:非0(如句柄已关闭)* @note 若多个dlopen()打开同一库,需对应次数的dlclose()才会真正释放内存*/
int dlclose(void *handle);
实战:可扩展检测系统(插件化开发)
需求:程序运行时读取配置文件,加载指定检测模块(动态库),无需重新编译即可新增检测功能。
步骤 1:编写检测模块(动态库插件)
- 插件 1:颜色检测(libcolor.so)
// color.c:颜色检测功能(接口名detection需与其他插件一致)
#include <stdio.h>
void detection() {printf("正在检测产品涂层颜色是否均匀...\n");
}
# 编译为动态库(动态加载无需指定SONAME)
gcc -shared -fPIC color.c -o libcolor.so -Wall
- 插件 2:外观检测(libshape.so)
// shape.c:外观检测功能(接口名与color.c一致,确保主程序可统一调用)
#include <stdio.h>
void detection() {printf("正在检测产品外观是否有破损...\n");
}
# 编译为动态库
gcc -shared -fPIC shape.c -o libshape.so -Wall
步骤 2:编写配置文件(指定加载的插件)
创建 config
文件,记录要加载的动态库名(主程序通过读取此文件决定加载哪个插件):
# config文件:仅一行,指定要加载的动态库(相对路径)
libcolor.so
步骤 3:主程序(动态加载插件)
主程序通过 dlopen
/dlsym
加载配置文件指定的库,统一调用 detection
接口:
#include <stdio.h>
#include <dlfcn.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main() {// 读取配置文件(获取要加载的动态库名)FILE *fp = fopen("config", "r");if (fp == NULL) {fprintf(stderr, "打开配置文件失败:%s\n", strerror(errno));return 1;}char lib_path[30] = {0};// fgets会读取换行符"\n",需去掉fgets(lib_path, sizeof(lib_path), fp);fclose(fp);lib_path[strcspn(lib_path, "\n")] = '\0'; // 移除换行符// 打开动态库(RTLD_NOW:立即解析,避免运行时符号缺失)void *handle = dlopen(lib_path, RTLD_NOW);if (handle == NULL) {fprintf(stderr, "加载动态库[%s]失败:%s\n", lib_path, dlerror());return 1;}// 查找库中的detection函数(关键:强制转换为正确的函数指针类型)// 定义函数指针类型:void (*函数名)(void)(与detection接口匹配)typedef void (*DetectFunc)(void);DetectFunc detect = (DetectFunc)dlsym(handle, "detection");// 检查dlsym是否成功(必须用dlerror(),因为dlsym返回NULL可能是符号值为NULL)const char *err = dlerror();if (err != NULL) {fprintf(stderr, "查找函数[detection]失败:%s\n", err);dlclose(handle); // 失败时关闭库,避免资源泄漏return 1;}// 调用检测函数(统一接口,无需关心具体插件实现)detect();// 关闭动态库,释放资源dlclose(handle);return 0;
}
步骤 4:编译与运行(必须加 - ldl)
# 编译主程序,-ldl是动态加载的强制选项(链接dl库)
gcc main.c -o detector -ldl -Wall# 运行程序(加载config指定的libcolor.so)
./detector # 输出:正在检测产品涂层颜色是否均匀...# 修改config为libshape.so,再次运行(无需重新编译)
echo "libshape.so" > config # > 是 “覆盖重定向”,会先清空目标文件中所有原有内容,然后将 echo 的输出写入文件(如果文件不存在,会创建新文件)
./detector # 输出:正在检测产品外观是否有破损...
动态库的升级与兼容性
动态库的核心优势是 “无缝升级”,关键在于保持 SONAME 不变(主版本不变),确保旧程序无需重新编译即可使用新版本库。
兼容升级(主版本不变)
示例:将 add
函数从 “a+b” 升级为 “a+b+1000”,主版本仍为 1(SONAME 不变)。
步骤 1:修改库代码
// add.c:升级add函数功能
#include "calc.h"
int add(int a, int b) {return a + b + 1000; // 原功能:return a + b;
}
步骤 2:重新制作动态库(保持 SONAME 不变)
# 重新生成.o文件
gcc -c add.c -o add.o -fPIC -Wall# 生成新版本库文件libcalc.so.1.2.4,SONAME仍为libcalc.so.1
gcc -shared -fPIC add.o sub.o -o libcalc.so.1.2.4 -Wl,-soname,libcalc.so.1 -Wall# 用ldconfig更新SONAME软链接(指向新版本)
ldconfig -n .# 查看软链接(已更新为指向1.2.4)
ls -l libcalc.so.1 # 输出:libcalc.so.1 -> libcalc.so.1.2.4
步骤 3:运行旧程序(无需重新编译)
# 运行之前编译的main程序(原依赖libcalc.so.1.2.3)
./main # 输入20和10,输出:20 + 10 = 1030 / 20 - 10 = 10(自动使用新版本功能)
不兼容升级(主版本变更)
若库接口发生不兼容变更(如删除 sub
函数、修改 add
参数为 float
),需升级主版本号(如 SONAME 改为libcalc.so.2
),此时旧程序必须重新编译才能使用新库。
步骤 1:修改库并指定新 SONAME
// add.c:修改接口为float类型(不兼容旧版本)
#include "calc.h"
float add(float a, float b) {return a + b;
}
# 生成新版本库文件libcalc.so.2.0.0,SONAME改为libcalc.so.2
gcc -shared -fPIC add.o -o libcalc.so.2.0.0 -Wl,-soname,libcalc.so.2 -Wall# 创建新SONAME软链接
ldconfig -n .
ln -s libcalc.so.2 libcalc.so # 新的编译链接名
步骤 2:重新编译程序(必须)
# 主程序需同步修改调用方式(传入float参数),并重新编译
gcc main.c -o main_v2 -I./include -L./lib -lcalc -Wl,-rpath=./lib -Wall# 运行新程序(依赖libcalc.so.2)
./main_v2 # 正常运行
步骤 3:旧程序无法运行(不兼容)
# 旧程序main仍依赖libcalc.so.1,无法加载新库
./main # 报错:error while loading shared libraries: libcalc.so.1: cannot open...
兼容性原则
- 向前兼容:新版本库支持旧程序的所有接口(新增接口、不删除 / 修改旧接口),主版本不变;
- 向后兼容:旧版本库支持新版本程序的接口(一般不要求,除非特殊场景);
- 不兼容场景:删除旧接口、修改接口参数 / 返回值、改变全局变量类型,必须升级主版本号。
动态库常见问题与解决
编译时 “找不到头文件”
# 错误:fatal error: calc.h: No such file or directory
gcc main.c -o main -L./lib -lcalc -Wall
- 原因:编译器找不到
calc.h
,未指定头文件路径; - 解决:用
I
指定头文件路径,如I./include
。
编译时 “undefined reference to xxx”
# 错误:undefined reference to `add'
gcc main.c -o main -I./include -lcalc -Wall
- 原因:未指定库路径(
L
),编译器找不到libcalc.so
; - 解决:添加
L
指定库路径,如L./lib
。
运行时 “找不到 libxxx.so.x”
# 错误:error while loading shared libraries: libcalc.so.1: cannot open...
./main
- 原因:动态链接器找不到库,未配置运行时路径;
- 解决:用
Wl,-rpath
固化路径,或设置LD_LIBRARY_PATH
,或修改系统配置。
动态加载时 “dlsym 返回 NULL”
# 错误:查找函数[detection]失败:./libcolor.so: undefined symbol: detection
./detector
-
原因:库中无该符号,或符号名拼写错误(区分大小写);
-
解决:用
nm
查看库中的符号,确认是否存在:nm -D libcolor.so | grep "detection" # 无输出表示符号不存在
软链接失效(ln: failed to create symbolic link)
# 错误:ln: failed to create symbolic link 'libcalc.so': File exists
ln -s libcalc.so.1 libcalc.so
-
原因:已有同名软链接 / 文件;
-
解决:删除旧链接后重新创建,或用
f
强制覆盖:ln -sf libcalc.so.1 libcalc.so # -f:强制覆盖