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

GCC 完全指南 / 09 - 链接器详解

09 - 链接器详解

深入理解链接器的工作原理——符号解析、重定位、静态库和动态库的创建与链接。


9.1 链接器概述

链接器(Linker)是编译流程的最后一步,负责将多个目标文件和库组合成一个可执行文件或共享库。

┌──────────┐  ┌──────────┐  ┌──────────┐
│  main.o  │  │ greet.o  │  │ libm.so  │
│          │  │          │  │  (math)  │
└────┬─────┘  └────┬─────┘  └────┬─────┘
     │             │              │
     └─────────────┼──────────────┘
              ┌────▼────┐
              │  链接器  │
              │  (ld)   │
              └────┬────┘
              ┌────▼────┐
              │  hello   │  ← 可执行文件
              └─────────┘

链接器的两大核心任务

任务 说明
符号解析(Symbol Resolution) 将每个符号引用关联到唯一定义
重定位(Relocation) 合并各段,修正地址引用

9.2 符号解析

符号的三种状态

# 查看目标文件中的符号
nm main.o
# 输出符号类型:
# T: 已定义,text 段(函数)
# D: 已定义,data 段(已初始化全局变量)
# B: 已定义,bss 段(未初始化全局变量)
# U: 未定义(外部引用)
# W: 弱符号
# t: 已定义,局部(static 函数)
# d: 已定义,局部(static 变量)
// symbols.c - 演示各种符号类型
int global_var = 42;              // D: 已初始化全局变量
int uninitialized_var;            // B: 未初始化全局变量
static int static_var = 10;       // d: 静态全局变量

void external_func(void);         // U: 外部函数声明
int my_function(int x) {          // T: 函数定义
    return x + global_var;
}
static void local_func(void) {    // t: 静态函数
    static_var++;
}
gcc -c symbols.c
nm symbols.o
# 0000000000000000 D global_var
# 0000000000000000 T my_function
# 0000000000000000 t local_func
# 0000000000000004 d static_var
#                  U external_func
# 0000000000000004 C uninitialized_var

强符号与弱符号

// file1.c
int count = 10;          // 强符号

// file2.c
int count = 20;          // 强符号 → 链接错误: multiple definition

// 修复方法: 使用 weak 属性
// file2.c
__attribute__((weak)) int count = 20;  // 弱符号
// 链接时选择强符号,弱符号作为默认值

常见符号解析错误

# 未定义符号
gcc main.o -o hello
# main.o: In function `main':
# main.c:(.text+0x5): undefined reference to `greet'
# 原因: greet 函数未被链接

# 多重定义
gcc main.o greet.o extra.o -o hello
# multiple definition of `count'
# 原因: 多个 .o 中定义了相同符号

# 库链接顺序错误
gcc -lm main.o -o hello
# undefined reference to `pow'
# 正确: gcc main.o -lm -o hello

9.3 重定位

重定位过程

编译 main.c 时:
  main() 中调用 greet() → 地址未知 → 记录在重定位表中

链接时:
  1. 合并所有 .o 的 .text 段
  2. 确定 greet() 的最终地址
  3. 将 greet() 的调用地址修正为真实地址

查看重定位信息

# 查看重定位条目
readelf -r main.o
# Offset          Type              Sym.Name
# 00000000000b    R_X86_64_PLT32    greet-0x4

# 查看所有段的大小和地址
readelf -S main.o

重定位类型

类型 说明
R_X86_64_64 64 位绝对地址
R_X86_64_PC32 32 位 PC 相对地址
R_X86_64_PLT32 PLT(过程链接表)引用
R_X86_64_GOTPCREL GOT(全局偏移表)引用
R_X86_64_RELATIVE 相对重定位(PIE)

9.4 静态库

创建静态库

# 编译目标文件
gcc -c math_utils.c -o math_utils.o
gcc -c string_utils.c -o string_utils.o

# 创建静态库(使用 ar)
ar rcs libmyutils.a math_utils.o string_utils.o

# 查看静态库内容
ar t libmyutils.a
# math_utils.o
# string_utils.o

# 详细信息
ar dv libmyutils.a   # 显示详细信息

# 链接静态库
gcc main.o -L. -lmyutils -o hello

# 静态库本质就是 .o 文件的归档
# libmyutils.a = ar 打包的 math_utils.o + string_utils.o

使用 gcc-ar 替代 ar(LTO 兼容)

# 当使用 -flto 编译时,必须使用 gcc-ar
gcc -flto -c math_utils.c -o math_utils.o
gcc-ar rcs libmyutils.a math_utils.o
gcc-ranlib libmyutils.a

提取静态库中的目标文件

# 列出内容
ar t libmyutils.a

# 提取特定目标文件
ar x libmyutils.a math_utils.o

# 替换库中的目标文件
ar r libmyutils.a updated_math_utils.o

9.5 动态库(共享库)

创建动态库

# 编译为位置无关代码(PIC)
gcc -fPIC -c math_utils.c -o math_utils.o
gcc -fPIC -c string_utils.c -o string_utils.o

# 创建共享库
gcc -shared -o libmyutils.so math_utils.o string_utils.o

# 或一步完成
gcc -fPIC -shared -o libmyutils.so math_utils.c string_utils.c

# 链接动态库
gcc main.c -L. -lmyutils -o hello

# 运行时需要找到 .so 文件
# 方法 1: 设置 LD_LIBRARY_PATH
export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH
./hello

# 方法 2: 使用 RPATH(推荐)
gcc main.c -L. -lmyutils -Wl,-rpath,'$ORIGIN' -o hello

查看动态库信息

# 查看 .so 文件的依赖
ldd libmyutils.so

# 查看导出的符号
nm -D libmyutils.so

# 查看 SONAME
readelf -d libmyutils.so | grep SONAME

# 查看动态段信息
readelf -d libmyutils.so

# 查看动态库的版本信息
objdump -p libmyutils.so | grep SONAME

9.6 静态链接 vs 动态链接

维度 静态链接 动态链接
文件大小 较大(包含库代码) 较小(运行时加载)
内存使用 每个进程独立副本 共享内存中的同一份
启动速度 较快(无加载开销) 较慢(需加载 .so)
更新 需重新编译 更新 .so 文件即可
依赖 无运行时依赖 依赖 .so 文件存在
部署 单文件部署 需要配套 .so 文件
安全更新 需重新编译 可单独更新库

混合链接

# 静态链接特定库,动态链接其他库
gcc main.o -Wl,-Bstatic -lmyutils -Wl,-Bdynamic -lm -lc -o hello

# 完全静态链接
gcc -static main.o -lmyutils -lm -o hello_static

# 查看链接了哪些动态库
ldd hello

9.7 链接器脚本(Linker Script)

默认链接器脚本

# 查看默认链接器脚本
ld --verbose

# 将默认脚本保存到文件
ld --verbose > default.ld

# 使用自定义脚本
gcc -T custom.ld -o hello main.c

基本链接器脚本示例

/* custom.ld - 自定义链接器脚本 */
ENTRY(main)

SECTIONS
{
    . = 0x400000;           /* 起始地址 */

    .text : {
        *(.text)            /* 合并所有 .text 段 */
    }

    .rodata : {
        *(.rodata)          /* 只读数据 */
    }

    .data : {
        *(.data)            /* 已初始化数据 */
    }

    .bss : {
        *(.bss)             /* 未初始化数据 */
    }
}

嵌入式系统的链接器脚本

/* STM32 链接器脚本示例 */
MEMORY
{
    FLASH (rx)  : ORIGIN = 0x08000000, LENGTH = 512K
    RAM (rwx)   : ORIGIN = 0x20000000, LENGTH = 128K
}

SECTIONS
{
    .text : {
        *(.isr_vector)      /* 中断向量表 */
        *(.text*)
        *(.rodata*)
    } > FLASH

    .data : {
        *(.data*)
    } > RAM AT > FLASH      /* 加载到 FLASH,运行在 RAM */

    .bss : {
        *(.bss*)
    } > RAM
}

9.8 符号可见性控制

# 默认所有符号都导出(可被其他 .o 或 .so 引用)

# 使用 -fvisibility=hidden 默认隐藏所有符号
gcc -fvisibility=hidden -fPIC -shared -o libhello.so hello.c

# 在代码中显式导出需要的符号
// visibility.h
#ifndef VISIBILITY_H
#define VISIBILITY_H

#ifdef BUILDING_DLL
    #define EXPORT __attribute__((visibility("default")))
    #define HIDDEN __attribute__((visibility("hidden")))
#else
    #define EXPORT
    #define HIDDEN
#endif

EXPORT int public_function(int x);
HIDDEN int internal_function(int x);

#endif
# 编译时定义 BUILDING_DLL
gcc -DBUILDING_DLL -fvisibility=hidden -fPIC -shared -o libhello.so hello.c

# 查看导出的符号
nm -D libhello.so

9.9 链接器常用选项

选项 说明
-L<path> 库搜索路径
-l<name> 链接 lib.so 或 lib.a
-static 强制静态链接
-shared 创建共享库
-rpath <path> 设置运行时库搜索路径
-soname <name> 设置共享库的 SONAME
--gc-sections 删除未使用的段
--print-gc-sections 显示被删除的段
-z noexecstack 标记栈为不可执行
-z relro 启用 RELRO(只读重定位)
-z now 立即绑定(Full RELRO)
--as-needed 仅链接实际引用的库
--no-as-needed 链接所有指定的库
-Map=<file> 生成链接映射文件

使用链接映射文件调试

# 生成链接映射
gcc -Wl,-Map=hello.map -o hello main.c

# 查看映射文件
cat hello.map
# 包含: 各段的地址分配、符号表、库加载顺序

9.10 常见链接错误及解决

错误 原因 解决
undefined reference to 'func' 缺少定义或未链接库 检查链接顺序,添加 -l 选项
multiple definition of 'var' 多个 .o 定义了同名全局变量 使用 extern 声明或 static
cannot find -lxxx 库文件不存在或路径不对 检查 -L 路径和库名
relocation truncated to fit 代码段过大 使用 -mcmodel=large
symbol 'xxx' has wrong size 符号类型不匹配 检查声明和定义的一致性

要点回顾

要点 核心内容
链接器任务 符号解析 + 重定位
静态库 .a = .o 的归档,ar rcs 创建
动态库 .so-fPIC -shared 创建
链接顺序 被依赖的库放在后面
符号可见性 -fvisibility=hidden + 显式导出
安全选项 -z relro -z now -z noexecstack

注意事项

链接顺序很重要: 被依赖的库必须放在依赖它的目标文件/库之后。gcc main.o -lmylib,而非 gcc -lmylib main.o

PIC 性能开销: -fPIC 在 x86-64 上几乎无性能开销,但在某些架构(如 32-bit ARM)上可能有 2-5% 的性能影响。

RPATH 安全: 使用 $ORIGIN 而非绝对路径,以保证可移植性。避免使用 LD_LIBRARY_PATH 的 setuid 程序。

SONAME 版本控制: 生产环境的共享库应设置 SONAME,便于版本管理和升级兼容性。


扩展阅读


下一步

10 - 库的创建与使用:学习如何创建、安装和管理 C/C++ 库,使用 pkg-config 和 RPATH。