强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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-gnuDebian/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 */

⚠️ 注意点

  1. 绝对不要在动态库头文件中暴露 STL 容器(C++ 项目)——不同编译器/版本的 ABI 不兼容
  2. 始终使用 -fPIC 编译动态库的目标文件
  3. 避免在库的头文件中使用 using namespace
  4. 注意符号冲突——多个库定义相同符号会导致未定义行为
  5. 静态库不等于线程安全——线程安全取决于代码实现,而非库类型

💡 提示

  1. 调试库问题LD_DEBUG=libs 是最强大的调试工具
  2. 检查 ABI 变化:使用 abi-dumper + abi-compliance-checker
  3. 减少符号表大小:使用 -fvisibility=hidden + 显式导出
  4. 确定性构建ar 使用 D 标志(确定性模式)确保可重复构建
  5. 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)

扩展阅读