Skip to content

LV015-链接顺序

如果一个程序依赖于多个库,如果库是相互独立的,则顺序不重要,怎么写都行,那要是所依赖的库又依赖于其他库,或者两个库相互依赖,这个时候怎么办?

Tips:动态库也存在顺序问题,下面以警惕阿酷为例,动态库类似。

一、链接器如何处理符号

链接器在处理目标文件和库时,遵循以下规则:

  • 从左到右扫描:链接器按照命令行中指定的顺序,从左到右依次处理每个目标文件和库。
  • 符号解析:链接器维护一个 "未解析符号表",记录当前已遇到但尚未找到定义的符号。
  • 库的处理:当处理到一个库时,链接器 只提取那些能解决当前未解析符号的目标模块,库中其他未使用的目标模块会被忽略。
  • 一次性原则:链接器 只会扫描每个库一次,不会回头重新扫描。

基于上述工作原理,我们可以得出以下核心规则:

text
┌─────────────────────────────────────────────────────────────────────────┐
│  链接顺序法则:被依赖者必须出现在依赖者之后                                     │
│                                                                         │
│  gcc -o app main.o -lA -lB  # 如果 A 依赖 B,则 B 必须在 A 之后             │
│         │      │    │                                                   │
│         │      └────┴──────→ 从左到右扫描                                 │
│         │                                                               │
│         └──→ 依赖者在前,被依赖者在后                                        │
└─────────────────────────────────────────────────────────────────────────┘

二、单向依赖

1. 场景说明

1.1 依赖关系

单向依赖是最常见的情况,例如:

  • 主程序 main.o 依赖库 libA.a
  • libA.a 又依赖库 libB.a
text
main.o → libA.a → libB.a
  (使用 A 的符号)   (A 使用 B 的符号)

1.2 示例代码

1.2.1 源文件
c
/* libB.c*/
#include <stdio.h>
void func_B(void) {
    printf("func_B called\n");
}
c
/* libA.c - 依赖 libB */
#include <stdio.h>
extern void func_B(void);
void func_A(void) {
    printf("func_A called\n");
    func_B();  // A 依赖 B
}
c
/* main.c - 主程序 */
extern void func_A(void);
int main() {
    func_A();
    return 0;
}
1.2.2 Makefile

下面的makefile仅为生成静态库和动态库来测试。但是后面基本都是直接手敲在,这里可以作为参考。

makefile
CC = gcc
AR = ar

# 默认目标: 构建所有库
all: libs

libs: libA.a libB.a libA.so libB.so

# 静态库
libA.a: libA.o
	$(AR) rcs $@ $<

libB.a: libB.o
	$(AR) rcs $@ $<

# 动态库
libA.so: libA.o
	$(CC) -shared -fPIC -o $@ $<

libB.so: libB.o
	$(CC) -shared -fPIC -o $@ $<

# 清理
clean:
	rm -f *.o *.a *.so app_*

.PHONY: all clean libs

2. 编译测试

shell
# 1. 创建库文件
gcc -c libB.c -o libB.o
ar rcs libB.a libB.o

gcc -c libA.c -o libA.o
ar rcs libA.a libA.o

gcc -c main.c -o main.o

# 2. 正确链接:依赖链从左到右,被依赖者始终在右侧
gcc -o app_correct main.o -lA -lB -L.
./app_correct
# 输出:
# func_A called
# func_B called

# 3. 错误链接:被依赖者 B 出现在依赖者 A 之前
gcc -o app_wrong main.o -lB -lA -L.
# 报错:undefined reference to `func_B'

3. 失败原因分析

text
┌─────────────────────────────────────────────────────────────────────────┐
│  错误顺序的分析:gcc -o app main.o -lB -lA                                 │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  步骤 1: 处理 main.o                                                     │
│    - 发现未解析符号:func_A (来自 libA)                                    │
│    - 未解析符号表:{func_A}                                               │
│                                                                        │
│  步骤 2: 处理 libB.a                                                     │
│    - 扫描 libB,查找能解决 {func_A} 的模块                                  │
│    - 结果:libB 中没有 func_A,跳过所有模块                                  │
│    - 未解析符号表:{func_A} (不变)                                         │
│                                                                         │
│  步骤 3: 处理 libA.a                                                      │
│    - 扫描 libA,发现可以提供 func_A                                         │
│    - 提取包含 func_A 的模块                                                │
│    - 但是 func_A 又依赖 func_B (来自 libB)                                │
│    - 尝试在剩余位置查找 func_B... 没有了!libB 已经扫描过了                    │
│    - 未解析符号表:{func_B} (func_A 已解决,但新增 func_B 未解决)             │
│                                                                         │
│  结果:链接失败 - undefined reference to `func_B`                          │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

4. 更多的库?

要是有更长的依赖链呢?那按顺序写喽:

shell
# 按照依赖关系的反方向排列
gcc -o app main.o -lA -lB -lC -lD

#依赖箭头指向右方
# main → A → B → C → D

那如果更多了怎么办?

4.1 --start-group--end-group

我们看一下 Using LD, the GNU linker,是这样介绍这个参数的:

text
The archives should be a list of archive files. They may be either explicit file names, or `-l' options. The specified archives are searched repeatedly until no new undefined references are created. Normally, an archive is searched only once in the order that it is specified on the command line. If a symbol in that archive is needed to resolve an undefined symbol referred to by an object in an archive that appears later on the command line, the linker would not be able to resolve that reference. By grouping the archives, they all be searched repeatedly until all possible references are resolved. Using this option has a significant performance cost. It is best to use it only when there are unavoidable circular references between two or more archives.

大概意思就是正常情况,链接的时候库文件只会按它们出现在命令行的顺序搜索一遍,如果包里有未定义的引用标号,而且该包还被放在命令行的后面, 这样链接器就无法解决该标号的引用问题。--start-group--end-group 的作用是将指定范围内的库文件视为一个整体,并在其中反复扫描,直到所有符号都被正确解析。使用该选项将降低性能。只有在无法避免多个包之间互相引用的情况下才使用。

使用格式如下:

shell
gcc -o [可执行文件名] [源文件] -L[库路径] -Wl,--start-group -lA -lB -l... -Wl,--end-group

4.2 使用示例

还是上面的示例,我们编译的时候这样来写:

shell
# 3. 之前的错误写法:被依赖者 B 出现在依赖者 A 之前
gcc -o app_wrong main.o -lB -lA -L.
# 报错:undefined reference to `func_B'

# 现在的正确写法
gcc -o app main.o -Wl,--start-group -lB -lA -Wl,--end-group -L.

实际效果如下:

image-20260324190214510

三、循环依赖

Tips:此处仅做了解,理论上错误的写法都没有报错,大概率应该是现代的链接器自动处理掉了这种循环依赖,日常开发过程中还没有遇到过有这种所谓的循环依赖的情况,后续踩坑了再详细研究。

1. 场景说明

1.1 依赖关系

循环依赖是指两个或多个库相互依赖的情况,例如:

  • libA.a 中的某些符号依赖库 libB.a
  • 同时,库 libB.a 中的某些符号也依赖库 libA.a
text
       ┌──────────────┐
       │              ▼
    libA.a ←────── libB.a

但是这个测试过程中发现,这个循环依赖的条件并不是那么容易满足,满足循环依赖的时候,就是一个死循环的程序了,这里就简单了解下算了。

1.2 示例代码

1.2.1 示例一

下面这种情况并不是真正的循环依赖:

c
/* libA.c*/
#include <stdio.h>
extern void func_B(void);
void func_A(void) {
    printf("func_A called\n");
    func_B();  // A 调用 B
}
void helper_A(void) {
    printf("helper_A\n");
}
c
/* libB.c*/
#include <stdio.h>
extern void helper_A(void);
void func_B(void) {
    printf("func_B called\n");
    helper_A();  // B 调用 A
}
c
/* main.c */
extern void func_A(void);
int main() {
    func_A();
    return 0;
}

虽然 libA.c 调用 func_B,libB.c 调用 helper_A,但从符号解析角度:

  • main.o 需要 func_A(在 libA.a 中)
  • func_A 需要 func_B(在 libB.a 中)
  • func_B 需要 helper_A(也在 libA.a 中)
  • 由于 helper_A 和 func_A 都在同一个库 libA.a 中,当链接器扫描 -lA 时,会提取所有需要的符号(包括 func_A 和 helper_A)。
1.2.2 示例二

下面的程序倒是可以称得上是循环依赖,但是这其实是一个死循环的程序,不过这里只是为了看现象,能演示效果即可。

c
// libA.c
#include <stdio.h>

// 声明 funcB 在 libB 中定义
extern void funcB(void);

void funcA(void) {
    printf("funcA called\n");
    funcB();  // 调用 libB 的函数
}
c
// libB.c
#include <stdio.h>

// 声明 funcA 在 libA 中定义
extern void funcA(void);

void funcB(void) {
    printf("funcB called\n");
    funcA();  // 调用 libA 的函数
}
c
// main.c
#include <stdio.h>

// 声明 funcA 在 libA 中定义
extern void funcA(void);

int main(void) {
    printf("main: calling funcA\n");
    funcA();
    return 0;
}

2. 解决方式

2.1 方法一:重复指定库

最简单有效的方法是 多次指定相互依赖的库

shell
# 方法 1:重复整个依赖组
gcc -o app main.o -lA -lB -lA

# 方法 2:重复整个依赖组(对称形式)
gcc -o app main.o -lA -lB -lB -lA

工作原理

text
┌─────────────────────────────────────────────────────────────────────────┐
│  循环依赖的处理:gcc -o app main.o -lA -lB -lA                             │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  第一次扫描 libA:                                                         │
│    - 提取能解决 main.o 未解析符号的模块                                      │
│    - 但可能留下一些依赖 libB 的符号未解决                                     │
│                                                                         │
│  扫描 libB:                                                              │
│    - 提取能解决当前未解析符号的模块                                           │
│    - 包括 libA 中某些符号所依赖的 libB 中的符号                               │
│    - 但 libB 中可能还有依赖 libA 的符号未解决                                 │
│                                                                         │
│  第二次扫描 libA:                                                         │
│    - 再次检查,提取剩余需要的模块                                            │
│    - 解决 libB 中符号所依赖的 libA 中的符号                                  │
│                                                                         │
│  结果:所有符号都被正确解析                                                  │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 方法二:使用 --start-group--end-group

GNU 链接器提供了一组选项来处理循环依赖:

shell
gcc -o app main.o -Wl,--start-group -lA -lB -Wl,--end-group

--start-group--end-group 之间的库会被 反复扫描,直到所有符号都被解析或没有新的符号可以被解析。优点就是链接器自动处理循环依赖,无需手动指定多次,而且代码更清晰,意图更明确。

3. 编译测试

这里写了一个 makefile, 使用的是 示例二

makefile
CC = gcc
AR = ar rcs

.PHONY: all clean test_fail test_success test_verbose

all: libA.a libB.a app_demo

# 编译静态库
libA.o:
	$(CC) -c libA.c -o libA.o

libB.o:
	$(CC) -c libB.c -o libB.o

libA.a: libA.o
	$(AR) $@ $<

libB.a: libB.o
	$(AR) $@ $<

# 测试1:正常链接(观察链接器行为)
test_normal: libA.a libB.a
	@echo "=== 正常链接顺序 -lA -lB ==="
	@$(CC) -o app_demo_normal main.c -L. -lA -lB 2>&1 && echo "链接成功" || echo "链接失败"

# 测试2:反向链接顺序
test_reverse: libA.a libB.a
	@echo "=== 反向链接顺序 -lB -lA ==="
	@$(CC) -o app_demo_reverse main.c -L. -lB -lA 2>&1 && echo "链接成功" || echo "链接失败"

# 测试3:使用 --start-group
test_group: libA.a libB.a
	@echo "=== 使用 group ==="
	@$(CC) -o app_demo_group main.c -Wl,--start-group -L. -lA -lB -Wl,--end-group 2>&1 && echo "链接成功" || echo "链接失败"

# 测试4:使用 -Wl,--as-needed(严格模式,禁止循环依赖自动处理)
test_strict: libA.a libB.a
	@echo "=== 严格模式 --as-needed(禁用自动循环处理)==="
	@$(CC) -o app_demo_strict main.c -Wl,--as-needed -L. -lA -lB 2>&1 && echo "链接成功" || echo "链接失败"

# 测试5:使用旧版链接器行为 -Wl,-no-undefined
test_undefined: libA.a libB.a
	@echo "=== 严格模式 -Wl,-no-undefined ==="
	@$(CC) -o app_demo_undefined main.c -Wl,-no-undefined -L. -lA -lB 2>&1 && echo "链接成功" || echo "链接失败"

clean:
	rm -f *.o *.a app_demo*

这里面只有 make test_reverse 会报错找不到 funcB,其他的都能正常编过,所以其实这里的示例并不明显,仅做简单了解。

四、常见问题与调试

1. 常见错误信息

text
undefined reference to `xxx'

可能原因:

  • 库的顺序错误
  • 缺少必要的库
  • 库路径不正确(-L 选项)
  • 库名称不正确(-l 选项)

2. 调试方法

2.1 使用 nm 检查库中的符号

shell
# 查看库中定义的符号
nm libxxx.a | grep " T "

# 查看库中未定义的符号(依赖其他库的符号)
nm libxxx.a | grep " U "

2.2 使用 ldd 检查动态库依赖

shell
ldd ./app

2.3 使用 objdump 分析依赖

shell
# 查看可执行文件依赖的库
objdump -p ./app | grep NEEDED

3. 依赖分析工具

shell
# 使用 ldd 查看动态依赖
ldd executable

# 使用 readelf 查看 ELF 文件信息
readelf -d executable

# 使用 objdump 查看符号表
objdump -T executable

参考资料:

动态库依赖动态库,静态库依赖静态库,顺序 - bw_0927 - 博客园

(7 封私信) 静态库依赖顺序 - 知乎

linux 程序链接时库依赖顺序_在动态链接过程中, 即使动态库(如 libb.so)已经链接了依赖库 a(如 liba.so), 二进制-CSDN 博客

g++链接时,--start-group 和--end-group 的作用是什么?如何正确使用它们解决依赖问题?_编程语言-CSDN 问答