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

GCC 完全指南 / 10 - 库的创建与使用

10 - 库的创建与使用

学习如何创建、安装、版本管理 C/C++ 库,掌握 pkg-config 和 RPATH 机制。


10.1 静态库的完整创建流程

项目结构

mylib/
├── include/
│   └── mylib.h
├── src/
│   ├── math_utils.c
│   └── string_utils.c
├── Makefile
└── example/
    └── main.c

头文件

// include/mylib.h
#ifndef MYLIB_H
#define MYLIB_H

#ifdef __cplusplus
extern "C" {
#endif

// 版本信息
#define MYLIB_VERSION_MAJOR 1
#define MYLIB_VERSION_MINOR 2
#define MYLIB_VERSION_PATCH 0

// 数学工具
int mylib_add(int a, int b);
int mylib_multiply(int a, int b);
double mylib_sum_array(const double *arr, int n);

// 字符串工具
char *mylib_str_reverse(const char *str);
int mylib_str_count_char(const char *str, char c);

#ifdef __cplusplus
}
#endif

#endif /* MYLIB_H */

源文件

// src/math_utils.c
#include "mylib.h"

int mylib_add(int a, int b) {
    return a + b;
}

int mylib_multiply(int a, int b) {
    return a * b;
}

double mylib_sum_array(const double *arr, int n) {
    double sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += arr[i];
    }
    return sum;
}
// src/string_utils.c
#include "mylib.h"
#include <stdlib.h>
#include <string.h>

char *mylib_str_reverse(const char *str) {
    int len = strlen(str);
    char *result = malloc(len + 1);
    if (!result) return NULL;
    for (int i = 0; i < len; i++) {
        result[i] = str[len - 1 - i];
    }
    result[len] = '\0';
    return result;
}

int mylib_str_count_char(const char *str, char c) {
    int count = 0;
    while (*str) {
        if (*str == c) count++;
        str++;
    }
    return count;
}

Makefile

CC = gcc
CFLAGS = -Wall -Wextra -std=c17 -Iinclude
AR = ar
ARFLAGS = rcs

STATIC_LIB = libmylib.a
SHARED_LIB = libmylib.so

SRCDIR = src
OBJDIR = obj
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(OBJDIR)/%.o,$(SRCS))

# 静态库
static: $(STATIC_LIB)

$(STATIC_LIB): $(OBJS)
	$(AR) $(ARFLAGS) $@ $^

$(OBJDIR)/%.o: $(SRCDIR)/%.c | $(OBJDIR)
	$(CC) $(CFLAGS) -c -o $@ $<

$(OBJDIR):
	mkdir -p $(OBJDIR)

# 动态库
shared: CFLAGS += -fPIC
shared: $(SHARED_LIB)

$(SHARED_LIB): $(OBJS)
	$(CC) -shared -o $@ $^

# 示例程序
example: $(STATIC_LIB)
	$(CC) $(CFLAGS) -o example/main example/main.c -L. -lmylib

clean:
	rm -rf $(OBJDIR) $(STATIC_LIB) $(SHARED_LIB) example/main

.PHONY: static shared example clean

使用静态库

// example/main.c
#include <stdio.h>
#include <stdlib.h>
#include "mylib.h"

int main(void) {
    printf("mylib version: %d.%d.%d\n",
           MYLIB_VERSION_MAJOR, MYLIB_VERSION_MINOR, MYLIB_VERSION_PATCH);

    printf("2 + 3 = %d\n", mylib_add(2, 3));
    printf("4 * 5 = %d\n", mylib_multiply(4, 5));

    char *rev = mylib_str_reverse("hello");
    printf("reverse: %s\n", rev);
    free(rev);

    return 0;
}
# 构建静态库
make static
# 生成 libmylib.a

# 构建示例
make example
# 生成 example/main

10.2 动态库的完整创建流程

版本号管理

动态库通常使用三段式版本号:lib<name>.so.MAJOR.MINOR.PATCH

SONAME:       libmylib.so.1      ← 主版本号,ABI 不兼容时递增
实际文件名:   libmylib.so.1.2.0  ← 完整版本号
开发链接名:   libmylib.so        ← 符号链接,供编译时使用

兼容性规则:
  - MAJOR 递增 → ABI 不兼容,需要重新编译依赖程序
  - MINOR 递增 → 新增接口,旧程序无需重新编译
  - PATCH 递增 → 修复错误,完全兼容

创建带版本号的动态库

# 编译为 PIC
gcc -fPIC -Wall -Wextra -std=c17 -Iinclude -c src/math_utils.c -o obj/math_utils.o
gcc -fPIC -Wall -Wextra -std=c17 -Iinclude -c src/string_utils.c -o obj/string_utils.o

# 创建带 SONAME 的共享库
gcc -shared -Wl,-soname,libmylib.so.1 \
    -o libmylib.so.1.2.0 \
    obj/math_utils.o obj/string_utils.o

# 创建符号链接
ln -sf libmylib.so.1.2.0 libmylib.so.1
ln -sf libmylib.so.1 libmylib.so

# 验证
readelf -d libmylib.so.1.2.0 | grep SONAME
#  0x000000000000000e (SONAME)  Library soname: [libmylib.so.1]

安装动态库

# 标准安装路径
INSTALL_LIB=/usr/local/lib
INSTALL_INCLUDE=/usr/local/include

# 复制库文件
sudo cp libmylib.so.1.2.0 $INSTALL_LIB/
sudo ln -sf libmylib.so.1.2.0 $INSTALL_LIB/libmylib.so.1
sudo ln -sf libmylib.so.1 $INSTALL_LIB/libmylib.so

# 复制头文件
sudo cp include/mylib.h $INSTALL_INCLUDE/

# 更新动态链接器缓存
sudo ldconfig

# 验证
ldconfig -p | grep mylib

完整的安装 Makefile

PREFIX = /usr/local
LIBDIR = $(PREFIX)/lib
INCLUDEDIR = $(PREFIX)/include

SONAME = libmylib.so.1
REALNAME = libmylib.so.1.2.0
LINKNAME = libmylib.so

install-shared: $(REALNAME)
	install -d $(DESTDIR)$(LIBDIR)
	install -d $(DESTDIR)$(INCLUDEDIR)
	install -m 755 $(REALNAME) $(DESTDIR)$(LIBDIR)/
	ln -sf $(REALNAME) $(DESTDIR)$(LIBDIR)/$(SONAME)
	ln -sf $(SONAME) $(DESTDIR)$(LIBDIR)/$(LINKNAME)
	install -m 644 include/mylib.h $(DESTDIR)$(INCLUDEDIR)/
	ldconfig

uninstall:
	rm -f $(DESTDIR)$(LIBDIR)/$(REALNAME)
	rm -f $(DESTDIR)$(LIBDIR)/$(SONAME)
	rm -f $(DESTDIR)$(LIBDIR)/$(LINKNAME)
	rm -f $(DESTDIR)$(INCLUDEDIR)/mylib.h
	ldconfig

10.3 pkg-config

pkg-config 是一个帮助编译和链接库的工具,自动提供正确的编译和链接选项。

创建 .pc 文件

# mylib.pc
prefix=/usr/local
exec_prefix=${prefix}
libdir=${exec_prefix}/lib
includedir=${prefix}/include

Name: mylib
Description: My utility library
Version: 1.2.0
Cflags: -I${includedir}
Libs: -L${libdir} -lmylib
Libs.private: -lm

安装 .pc 文件

# 标准 pkg-config 路径
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig

# 安装 .pc 文件
sudo mkdir -p $PKG_CONFIG_PATH
sudo cp mylib.pc $PKG_CONFIG_PATH/

# 验证
PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --modversion mylib
# 1.2.0

PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --cflags mylib
# -I/usr/local/include

PKG_CONFIG_PATH=/usr/local/lib/pkgconfig pkg-config --libs mylib
# -L/usr/local/lib -lmylib

使用 pkg-config

# 在 Makefile 中使用
CFLAGS = $(shell pkg-config --cflags mylib)
LDFLAGS = $(shell pkg-config --libs mylib)

# 或在命令行中使用
gcc $(pkg-config --cflags --libs mylib) -o hello main.c

# 检查库是否可用
pkg-config --exists mylib && echo "mylib found"

# 在 CMake 中使用
# find_package(PkgConfig REQUIRED)
# pkg_check_modules(MYLIB REQUIRED mylib)
# target_include_directories(app ${MYLIB_INCLUDE_DIRS})
# target_link_libraries(app ${MYLIB_LIBRARIES})

.pc 文件中的变量

变量说明
Name库名称
Description库描述
Version版本号
Cflags编译选项(头文件路径等)
Libs链接选项(库路径和名称)
Libs.private静态链接时需要的额外库
Requires依赖的其他包
Requires.private私有依赖
Conflicts冲突的包
# 示例:依赖另一个包的 .pc 文件
Name: mylib-ext
Description: My extended library
Version: 2.0.0
Requires: mylib >= 1.2.0
Cflags: -I${includedir}
Libs: -L${libdir} -lmylib-ext

10.4 RPATH

RPATH(Runtime Path)嵌入在可执行文件或共享库中,告诉动态链接器在运行时去哪里查找依赖的 .so 文件。

RPATH 选项

# 使用绝对路径
gcc main.c -L. -lmylib -Wl,-rpath,/usr/local/lib -o hello

# 使用 $ORIGIN(相对于可执行文件的位置)
gcc main.c -L. -lmylib -Wl,-rpath,'$ORIGIN/lib' -o hello

# 多个 RPATH
gcc main.c -L. -lmylib \
    -Wl,-rpath,'$ORIGIN/lib:/opt/mylib/lib' -o hello

# 查看 RPATH
readelf -d hello | grep -i 'rpath\|runpath'
objdump -x hello | grep -i 'rpath\|runpath'

RPATH vs RUNPATH

特性RPATHRUNPATH
选项-rpath-rpath + --enable-new-dtags
优先级高于 LD_LIBRARY_PATH低于 LD_LIBRARY_PATH
推荐特定场景通常推荐
# 使用 RUNPATH(推荐,GCC 默认行为)
gcc main.c -Wl,-rpath,'$ORIGIN/lib' -Wl,--enable-new-dtags -o hello

# 使用 RPATH(覆盖 LD_LIBRARY_PATH)
gcc main.c -Wl,-rpath,'$ORIGIN/lib' -Wl,--disable-new-dtags -o hello

$ORIGIN 的作用

$ORIGIN 替换为可执行文件或库所在的目录:

  /opt/myapp/
  ├── bin/
  │   └── hello          ← RPATH: $ORIGIN/../lib
  └── lib/
      └── libmylib.so    ← hello 运行时自动找到此文件

$ORIGIN 使得应用可以安装在任意位置而不需要绝对路径

10.5 符号版本控制

版本脚本

# mylib.version
MYLIB_1.0 {
    global:
        mylib_add;
        mylib_multiply;
        mylib_str_reverse;
    local:
        *;       # 其他符号不导出
};

MYLIB_1.1 {
    global:
        mylib_sum_array;   # 新增接口
} MYLIB_1.0;               # 继承 1.0 的所有导出
# 使用版本脚本创建库
gcc -fPIC -shared -Wl,--version-script=mylib.version \
    -Wl,-soname,libmylib.so.1 \
    -o libmylib.so.1.1.0 obj/*.o

# 查看版本节点
nm -D libmylib.so.1.1.0 | head
# mylib_add@@MYLIB_1.0
# mylib_multiply@@MYLIB_1.0
# mylib_sum_array@@MYLIB_1.1

10.6 库的 ABI 兼容性

ABI 兼容性检查工具

# 安装 abi-compliance-checker
sudo apt install abi-compliance-checker

# 使用
abi-compliance-checker -lib mylib -old old.xml -new new.xml

# 或使用 abi-dumper
abi-dumper libmylib.so.1.0.0 -o old.dump
abi-dumper libmylib.so.1.1.0 -o new.dump
abi-compliance-checker -l mylib -old old.dump -new new.dump

维护 ABI 兼容性的规则

操作兼容性影响
新增函数兼容(MINOR 递增)
修改函数参数不兼容(MAJOR 递增)
删除函数不兼容(MAJOR 递增)
修改结构体大小不兼容(MAJOR 递增)
修改枚举值可能不兼容
添加结构体成员(尾部)兼容(如果是堆分配)
修改全局变量类型不兼容

10.7 C++ 库注意事项

// include/mylib.hpp
#ifndef MYLIB_HPP
#define MYLIB_HPP

#include <string>
#include <vector>

namespace mylib {

class Calculator {
public:
    Calculator();
    ~Calculator();

    void add_value(double value);
    double get_sum() const;
    const std::vector<double> &get_values() const;

private:
    struct Impl;             // Pimpl 模式隐藏实现
    std::unique_ptr<Impl> pimpl;
};

// 导出 C 接口(避免 C++ ABI 问题)
extern "C" {
    Calculator *calculator_new();
    void calculator_delete(Calc *c);
    void calculator_add_value(Calc *c, double value);
    double calculator_get_sum(const Calc *c);
}

} // namespace mylib

#endif

C++ 库的 ABI 问题

# GCC 5.1 引入了新的 libstdc++ ABI
# 旧 ABI: std::string 使用 COW(Copy-on-Write)
# 新 ABI: std::string 使用 SSO(Small String Optimization)

# 编译时选择 ABI
g++ -D_GLIBCXX_USE_CXX11_ABI=0 -std=c++17 -o hello main.cpp  # 旧 ABI
g++ -D_GLIBCXX_USE_CXX11_ABI=1 -std=c++17 -o hello main.cpp  # 新 ABI(默认)

# 检查 ABI 版本
strings libmylib.so | grep GLIBCXX | tail -1

要点回顾

要点核心内容
静态库ar rcs 创建,.o 的归档,编译时嵌入
动态库-fPIC -shared 创建,运行时加载,版本管理重要
SONAMElibname.so.MAJOR,ABI 兼容性标记
pkg-config.pc 文件提供编译和链接选项
RPATH$ORIGIN 实现可移植的运行时库搜索
版本脚本控制符号导出,实现多版本兼容

注意事项

始终使用 -fPIC 创建动态库: 在某些架构上不使用 PIC 编译的代码无法创建共享库。

SONAME 很重要: 没有 SONAME 的共享库在库更新时会导致程序加载错误版本。

RPATH 中 $ORIGIN 必须用单引号: shell 会展开 $ORIGIN,必须用单引号 ' 保护。

C++ 库的 ABI 稳定性: 建议用 C 接口暴露 C++ 库的公共接口,避免 ABI 兼容性问题。


扩展阅读


下一步

11 - 交叉编译:深入学习交叉编译的原理、工具链配置和 sysroot 管理。