C/C++ Linux 开发教程(GCC + CMake) / GCC 链接与库(静态库/动态库)
GCC 链接与库(静态库/动态库)
库是将可复用代码打包为二进制模块的方式。Linux 下有两种主要的库类型:静态库(.a)和动态库(.so)。本文将深入讲解库的创建、使用、调试和最佳实践。
静态库(Static Library)
什么是静态库
静态库在链接阶段将目标代码直接复制到最终可执行文件中。运行时不再需要原始库文件。
优点:
- 部署简单,无需分发额外的
.so文件 - 运行时不依赖库文件路径
- 不存在 ABI 兼容性问题
缺点:
- 可执行文件体积较大
- 库更新需要重新编译所有依赖程序
- 多个程序运行时会各自占用内存(无法共享)
创建静态库
// mathlib.h
#ifndef MATHLIB_H
#define MATHLIB_H
int add(int a, int b);
int multiply(int a, int b);
double average(const int *arr, int n);
#endif
// mathlib.c
#include "mathlib.h"
int add(int a, int b) {
return a + b;
}
int multiply(int a, int b) {
return a * b;
}
double average(const int *arr, int n) {
if (n <= 0) return 0.0;
long sum = 0;
for (int i = 0; i < n; i++) {
sum += arr[i];
}
return (double)sum / n;
}
# 步骤 1: 编译为目标文件(必须使用 -fPIC 以便复用)
gcc -c -fPIC -Wall -O2 mathlib.c -o mathlib.o
# 步骤 2: 使用 ar 创建静态库
ar rcs libmathlib.a mathlib.o
# 查看静态库内容
ar t libmathlib.a
# 查看静态库详细信息
ar tv libmathlib.a
ar参数说明:r= 替换/插入,c= 创建,s= 生成索引(等价于ranlib)
使用静态库
// main.c
#include <stdio.h>
#include "mathlib.h"
int main() {
printf("add(3, 4) = %d\n", add(3, 4));
printf("multiply(3, 4) = %d\n", multiply(3, 4));
int arr[] = {1, 2, 3, 4, 5};
printf("average = %.2f\n", average(arr, 5));
return 0;
}
# 方法 1: 直接链接
gcc -Wall main.c libmathlib.a -o app
# 方法 2: 使用 -L 和 -l
gcc -Wall -L. main.c -lmathlib -o app
# 方法 3: 将静态库放到标准路径
sudo cp libmathlib.a /usr/local/lib/
sudo cp mathlib.h /usr/local/include/
gcc -Wall main.c -lmathlib -o app
⚠️
-l参数的顺序很重要!-lmathlib必须放在使用它的源文件之后,因为链接器按顺序扫描符号。
链接顺序问题
# ❌ 错误:-l 在源文件之前
gcc -lmathlib main.c -o app
# 可能报错:undefined reference to `add`
# ✅ 正确:源文件在前,-l 在后
gcc main.c -lmathlib -o app
# ✅ 使用 --start-group 解决循环依赖
gcc main.o libA.a libB.a -Wl,--start-group libX.a libY.a -Wl,--end-group -o app
动态库(Shared Library)
什么是动态库
动态库在运行时才被加载到内存。多个程序可以共享同一个动态库的内存映射。
优点:
- 可执行文件体积小
- 库更新无需重新编译程序(ABI 兼容前提下)
- 多进程共享内存映射,节省物理内存
- 支持插件架构(
dlopen)
缺点:
- 部署时需要分发
.so文件 - 存在版本兼容性问题(“DLL Hell”)
- 运行时有轻微的加载开销
创建动态库
// stringlib.h
#ifndef STRINGLIB_H
#define STRINGLIB_H
#include <stddef.h>
// 使用 visibility 属性控制符号导出
#ifdef __GNUC__
#define EXPORT __attribute__((visibility("default")))
#define HIDDEN __attribute__((visibility("hidden")))
#else
#define EXPORT
#define HIDDEN
#endif
EXPORT char *string_reverse(char *str);
EXPORT size_t string_count_char(const char *str, char c);
EXPORT int string_is_palindrome(const char *str);
// 内部函数,不导出
HIDDEN void internal_helper(void);
#endif
// stringlib.c
#define _GNU_SOURCE
#include "stringlib.h"
#include <string.h>
#include <ctype.h>
EXPORT char *string_reverse(char *str) {
if (!str) return NULL;
size_t len = strlen(str);
for (size_t i = 0; i < len / 2; i++) {
char tmp = str[i];
str[i] = str[len - 1 - i];
str[len - 1 - i] = tmp;
}
return str;
}
EXPORT size_t string_count_char(const char *str, char c) {
if (!str) return 0;
size_t count = 0;
for (; *str; str++) {
if (*str == c) count++;
}
return count;
}
EXPORT int string_is_palindrome(const char *str) {
if (!str) return 0;
size_t len = strlen(str);
for (size_t i = 0; i < len / 2; i++) {
if (tolower((unsigned char)str[i]) !=
tolower((unsigned char)str[len - 1 - i])) {
return 0;
}
}
return 1;
}
HIDDEN void internal_helper(void) {
// 内部实现,不应被外部使用
}
# 步骤 1: 编译为位置无关代码(Position Independent Code)
gcc -c -fPIC -Wall -O2 stringlib.c -o stringlib.o
# 步骤 2: 创建动态库
gcc -shared -Wl,-soname,libstringlib.so.1 -o libstringlib.so.1.0.0 stringlib.o
# 步骤 3: 创建符号链接
ln -sf libstringlib.so.1.0.0 libstringlib.so.1
ln -sf libstringlib.so.1.0.0 libstringlib.so
# 一步完成
gcc -shared -fPIC -Wall -O2 \
-Wl,-soname,libstringlib.so.1 \
-o libstringlib.so.1.0.0 stringlib.c
使用动态库
// main.c
#include <stdio.h>
#include <string.h>
#include "stringlib.h"
int main() {
char str[] = "hello";
printf("Original: %s\n", str);
string_reverse(str);
printf("Reversed: %s\n", str);
printf("Count 'l' in 'hello': %zu\n",
string_count_char("hello", 'l'));
printf("Is 'racecar' palindrome? %s\n",
string_is_palindrome("racecar") ? "yes" : "no");
return 0;
}
# 编译链接
gcc -Wall -L. main.c -lstringlib -o app
# 方法 1: 使用 LD_LIBRARY_PATH 运行
LD_LIBRARY_PATH=. ./app
# 方法 2: 使用 rpath 嵌入库路径
gcc -Wall -Wl,-rpath,'$ORIGIN' -L. main.c -lstringlib -o app
./app
# 方法 3: 安装到系统路径
sudo cp libstringlib.so.1.0.0 /usr/local/lib/
sudo ldconfig
gcc -Wall main.c -lstringlib -o app
./app
库搜索路径
链接时搜索路径
# 使用 -L 指定链接器搜索路径
gcc -L/usr/local/lib -L./libs main.c -lmylib -o app
# 搜索顺序:
# 1. -L 指定的路径(按顺序)
# 2. 环境变量 LIBRARY_PATH
# 3. 默认路径: /lib, /usr/lib, /usr/local/lib
运行时搜索路径
# 方法 1: LD_LIBRARY_PATH(临时)
LD_LIBRARY_PATH=/opt/mylib/lib ./app
# 方法 2: rpath(嵌入到可执行文件)
gcc -Wl,-rpath,/opt/mylib/lib main.c -lmylib -o app
# 方法 3: RPATH 使用 $ORIGIN(相对路径)
gcc -Wl,-rpath,'$ORIGIN/../lib' main.c -lmylib -o app
# 方法 4: ldconfig(系统级配置)
echo "/opt/mylib/lib" | sudo tee /etc/ld.so.conf.d/mylib.conf
sudo ldconfig
# 查看运行时搜索路径
ldd ./app
objdump -x ./app | grep RPATH
查看和设置 RPATH
# 查看可执行文件的 RPATH
readelf -d app | grep -E "RPATH|RUNPATH"
chrpath -l app
# 使用 patchelf 修改 RPATH
patchelf --set-rpath '$ORIGIN/lib' app
# 删除 RPATH
patchelf --remove-rpath app
binutils 工具链
nm — 查看符号
# 查看库中所有符号
nm libmathlib.a
# 查看动态库导出符号
nm -D libstringlib.so
# 只看已定义的符号
nm --defined-only libmathlib.a
# 按符号类型筛选
# T = text (代码), D = data (已初始化数据), U = undefined (未定义)
nm libmathlib.a | grep " T "
# 查看 C++ 符号(demangled)
nm -C libmylib.so
ldd — 查看动态依赖
# 查看可执行文件依赖的所有动态库
ldd ./app
# 输出示例:
# linux-vdso.so.1 => (0x00007ffd...)
# libstringlib.so.1 => ./libstringlib.so.1 (0x00007f...)
# libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f...)
# /lib64/ld-linux-x86-64.so.2 (0x00007f...)
# 查找缺失的库
ldd ./app | grep "not found"
# 使用 LD_DEBUG 查看详细加载过程
LD_DEBUG=libs ./app 2>&1 | head -30
# 注意: 不要用 ldd 检查不可信的二进制文件
# 安全替代: objdump -p app | grep NEEDED
objdump — 反汇编和分析
# 查看动态库的头部信息
objdump -p libstringlib.so | head -20
# 查看需要的动态库(NEEDED)
objdump -p libstringlib.so | grep NEEDED
# 反汇编指定函数
objdump -d -S libmathlib.a | less
# 查看所有段信息
objdump -h libstringlib.so
# 查看符号表
objdump -T libstringlib.so
readelf — ELF 文件分析
# 查看 ELF 头
readelf -h app
# 查看动态段(SONAME, NEEDED, RPATH 等)
readelf -d libstringlib.so
# 查看所有段
readelf -l app
# 查看符号表
readelf -s libstringlib.so
符号可见性
为什么需要控制符号可见性
| 问题 | 说明 |
|---|---|
| 名称污染 | 导出过多内部符号,可能与用户代码冲突 |
| 性能 | 过多导出符号影响加载速度和 GOT/PLT 开销 |
| 安全 | 内部实现细节不应暴露 |
| ABI 稳定性 | 明确的导出 API 更容易维护 ABI |
版本脚本(Version Script)
# libstringlib.map — 版本脚本
cat > libstringlib.map << 'EOF'
LIBSTRINGLIB_1.0 {
global:
string_*; # 导出所有 string_ 开头的符号
local:
*; # 隐藏其他所有符号
};
EOF
# 使用版本脚本创建动态库
gcc -shared -fPIC -Wall -O2 \
-Wl,--version-script=libstringlib.map \
-Wl,-soname,libstringlib.so.1 \
-o libstringlib.so.1.0.0 stringlib.c
符号版本化(Symbol Versioning)
# 支持多个版本的同名符号
cat > libstringlib.map << 'EOF'
LIBSTRINGLIB_1.0 {
global:
string_reverse;
string_count_char;
};
LIBSTRINGLIB_1.1 {
global:
string_is_palindrome;
} LIBSTRINGLIB_1.0;
EOF
# 在代码中使用版本标签
__asm__(".symver string_reverse_v1, string_reverse@LIBSTRINGLIB_1.0");
__asm__(".symver string_reverse_v2, string_reverse@@LIBSTRINGLIB_1.1");
动态加载(dlopen/dlsym)
使用 dlopen 运行时加载动态库
// plugin.h
#ifndef PLUGIN_H
#define PLUGIN_H
typedef struct {
const char *name;
const char *version;
int (*execute)(const char *input, char *output, int output_size);
} Plugin;
#endif
// plugin_hello.c — 一个插件实现
#include "plugin.h"
#include <stdio.h>
#include <string.h>
static int hello_execute(const char *input, char *output, int output_size) {
snprintf(output, output_size, "Hello, %s!", input ? input : "World");
return 0;
}
// 导出插件结构体
Plugin plugin_info = {
.name = "hello",
.version = "1.0.0",
.execute = hello_execute,
};
// plugin_loader.c — 插件加载器
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include "plugin.h"
int main(int argc, char *argv[]) {
if (argc < 2) {
fprintf(stderr, "Usage: %s <plugin.so> [args...]\n", argv[0]);
return 1;
}
// 加载插件
void *handle = dlopen(argv[1], RTLD_LAZY);
if (!handle) {
fprintf(stderr, "dlopen error: %s\n", dlerror());
return 1;
}
// 获取插件信息
Plugin *plugin = (Plugin *)dlsym(handle, "plugin_info");
const char *error = dlerror();
if (error) {
fprintf(stderr, "dlsym error: %s\n", error);
dlclose(handle);
return 1;
}
printf("Plugin: %s v%s\n", plugin->name, plugin->version);
// 执行插件
char output[256];
const char *input = (argc > 2) ? argv[2] : NULL;
plugin->execute(input, output, sizeof(output));
printf("Result: %s\n", output);
// 关闭插件
dlclose(handle);
return 0;
}
# 编译插件
gcc -shared -fPIC -Wall -O2 plugin_hello.c -o plugin_hello.so
# 编译加载器
gcc -Wall -O2 plugin_loader.c -ldl -o plugin_loader
# 运行
./plugin_loader plugin_hello.so "Linux"
# ⚠️ 注意: 链接 dlopen 需要 -ldl
# ⚠️ RTLD_LAZY 表示延迟解析符号(推荐)
# ⚠️ RTLD_NOW 立即解析所有符号(更安全但更慢)
库安装路径
标准路径规范
| 路径 | 用途 |
|---|---|
/lib | 系统启动必需的库 |
/usr/lib | 系统软件包的库 |
/usr/local/lib | 用户手动安装的库 |
/usr/lib/x86_64-linux-gnu | Debian/Ubuntu 多架构库 |
/opt/<project>/lib | 第三方商业软件 |
安装规则
# 安装到 /usr/local/lib
sudo install -m 755 libstringlib.so.1.0.0 /usr/local/lib/
sudo ldconfig # 更新库缓存并创建符号链接
# 安装头文件
sudo install -m 644 stringlib.h /usr/local/include/
# 安装 pkg-config 文件
sudo install -m 644 stringlib.pc /usr/local/lib/pkgconfig/
pkg-config 集成
编写 .pc 文件
# stringlib.pc
cat > stringlib.pc << 'EOF'
prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include
Name: stringlib
Description: String utility library
Version: 1.0.0
Libs: -L${libdir} -lstringlib
Cflags: -I${includedir}
EOF
使用 pkg-config
# 查看库信息
pkg-config --modversion stringlib
pkg-config --libs stringlib
pkg-config --cflags stringlib
# 在编译命令中使用
gcc $(pkg-config --cflags stringlib) main.c \
$(pkg-config --libs stringlib) -o app
# 在 CMake 中使用
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(STRINGLIB REQUIRED stringlib)
# 设置 PKG_CONFIG_PATH
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --libs stringlib
库设计最佳实践
SONAME 版本策略
# 版本号格式: lib<name>.so.<major>.<minor>.<patch>
# major: ABI 不兼容变更(SONAME 跟随改变)
# minor: 新增 API(ABI 兼容)
# patch: Bug 修复
# 示例
libfoo.so.2.3.1
# SONAME = libfoo.so.2 (major 版本)
# 真实文件 = libfoo.so.2.3.1
# 开发链接 = libfoo.so
头文件设计
// mylib.h — 正确的头文件写法
#ifndef MYLIB_H
#define MYLIB_H
#ifdef __cplusplus
extern "C" {
#endif
/* 版本宏 */
#define MYLIB_VERSION_MAJOR 1
#define MYLIB_VERSION_MINOR 0
#define MYLIB_VERSION_PATCH 0
/* 导出宏 */
#ifdef MYLIB_SHARED
#ifdef MYLIB_BUILDING
#define MYLIB_API __attribute__((visibility("default")))
#else
#define MYLIB_API
#endif
#else
#define MYLIB_API
#endif
/* 公开 API */
MYLIB_API int mylib_init(void);
MYLIB_API void mylib_cleanup(void);
MYLIB_API const char *mylib_version(void);
#ifdef __cplusplus
}
#endif
#endif /* MYLIB_H */
⚠️ 注意点
- 绝对不要在动态库头文件中暴露 STL 容器(C++ 项目)——不同编译器/版本的 ABI 不兼容
- 始终使用
-fPIC编译动态库的目标文件 - 避免在库的头文件中使用
using namespace - 注意符号冲突——多个库定义相同符号会导致未定义行为
- 静态库不等于线程安全——线程安全取决于代码实现,而非库类型
💡 提示
- 调试库问题:
LD_DEBUG=libs是最强大的调试工具 - 检查 ABI 变化:使用
abi-dumper+abi-compliance-checker - 减少符号表大小:使用
-fvisibility=hidden+ 显式导出 - 确定性构建:
ar使用D标志(确定性模式)确保可重复构建 - LTO 兼容:静态库如果要支持 LTO,需使用
gcc-ar而非ar
工程场景
场景 1:同时生成静态库和动态库
# 通用 Makefile 片段
SRC = stringlib.c
OBJ = $(SRC:.c=.o)
STATIC_LIB = libstringlib.a
SHARED_LIB = libstringlib.so.1.0.0
CFLAGS = -Wall -Wextra -O2 -fPIC
all: $(STATIC_LIB) $(SHARED_LIB)
$(OBJ): $(SRC)
gcc $(CFLAGS) -c $< -o $@
$(STATIC_LIB): $(OBJ)
ar rcs $@ $^
$(SHARED_LIB): $(OBJ)
gcc -shared -Wl,-soname,libstringlib.so.1 -o $@ $^
ln -sf $(SHARED_LIB) libstringlib.so.1
ln -sf libstringlib.so.1 libstringlib.so
clean:
rm -f $(OBJ) $(STATIC_LIB) libstringlib.so*
场景 2:解决库版本冲突
# 查看加载了哪个版本
LD_DEBUG=libs ./app 2>&1 | grep "trying file"
# 强制使用指定路径的库
LD_PRELOAD=/opt/custom/lib/libfoo.so.1 ./app
# 查看符号解析
LD_DEBUG=symbols ./app 2>&1 | grep "symbol="
场景 3:CMake 项目中生成库
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyLibrary VERSION 1.0.0)
# 生成动态库
add_library(stringlib SHARED src/stringlib.c)
target_include_directories(stringlib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
set_target_properties(stringlib PROPERTIES
VERSION ${PROJECT_VERSION}
SOVERSION 1
C_VISIBILITY_PRESET hidden
VISIBILITY_INLINES_HIDDEN ON
)
# 生成静态库
add_library(stringlib_static STATIC src/stringlib.c)
target_include_directories(stringlib_static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 安装规则
install(TARGETS stringlib stringlib_static
LIBRARY DESTINATION lib
ARCHIVE DESTINATION lib
)
install(FILES include/stringlib.h DESTINATION include)