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

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_6464 位绝对地址
R_X86_64_PC3232 位 PC 相对地址
R_X86_64_PLT32PLT(过程链接表)引用
R_X86_64_GOTPCRELGOT(全局偏移表)引用
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。