库制作与原理 · 链接知识笔记

库制作与原理 · 链接知识笔记

参考: 《深入理解计算机系统》 以下简称(CS:APP)第7章:链接


📖 目录


一、链接的基本概念

什么是链接

链接(Linking) 是将各种代码和数据片段收集并组合成一个单一文件的过程。这个文件可以加载到内存中执行

链接在编译的最后一步 执行,由 链接器(Linker, ld) 完成。

为什么需要链接

  • 模块化:程序拆分成多个源文件,各自独立编译、维护
  • 效率:修改一个文件只需重新编译该文件,再链接即可(增量构建)
  • 复用:通过库(Library)复用标准函数(printf、malloc 等)

编译器驱动程序的工作流程

gcc main.c code.c -o prog 为例,完整流程如下:

复制代码
main.c ──→ [预处理器 cpp] ──→ main.i ──→ [编译器 ccl] ──→ main.s ──→ [汇编器 as] ──→ main.o
code.c ──→ [预处理器 cpp] ──→ code.i ──→ [编译器 ccl] ──→ code.s ──→ [汇编器 as] ──→ code.o
                                                                                          │
                                                                                          ▼
                                                                         [链接器 ld] ──→ prog(可执行文件)

各阶段说明:

阶段 工具 输入 → 输出 工作内容
预处理 cpp .c.i 头文件展开、宏替换、条件编译
编译 ccl .i.s 语法分析、生成汇编代码
汇编 as .s.o 汇编代码转机器指令(生成可重定位目标文件)
链接 ld .o → 可执行文件 符号解析、重定位

"语言→编译→汇编→链接(Windows 的 IDE 通常集成这些步骤,Linux 下 gcc 默认一站式完成,也可分步执行 gcc -Egcc -Sgcc -c)"


二、库的本质与分类

库的本质

库本质上就是一组预编译好的目标文件(.o)的集合,被封装成一个文件,方便链接器使用。

两种库的对比

特性 静态库(Static Library) 动态库 / 共享库(Shared Library)
Linux 后缀 .a(archive) .so(shared object)
Windows 后缀 .lib .dll
链接时机 编译时(链接阶段) 加载时或运行时
代码归属 复制到可执行文件中 在内存中共享一份
可执行文件大小
更新库 需重新链接 替换 .so 即可(需兼容 ABI)
内存占用(多进程) 每个进程各有一份 物理内存仅一份,共享使用
运行依赖 无(独立运行) 需要系统中有对应 .so

系统库的常见位置

bash 复制代码
# Ubuntu / Debian
$ ls -l /lib/x86_64-linux-gnu/libc-2.31.so     # 动态库(C)
$ ls -l /lib/x86_64-linux-gnu/libc.a           # 静态库(C)
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.so  # C++ 动态库
$ ls /usr/lib/gcc/x86_64-linux-gnu/9/libstdc++.a   # C++ 静态库

# CentOS / RHEL
$ ls /lib64/libc-2.17.so -l
$ ls /lib64/libc.a -l

三、静态库的制作与使用

3.1 制作静态库

利用 ar(archiver)命令将多个 .o 文件打包成 .a 文件:

bash 复制代码
$ ar -rc libmystdio.a my_stdio.o my_string.o

ar 参数说明

  • r:replace,将目标文件插入库中(替换同名文件)
  • c:create,创建库
  • t:列出库中包含的目标文件
  • v:verbose,显示详细信息

查看静态库内容:

bash 复制代码
$ ar -tv libmystdio.a
rw-rw-r-- 1000/1000 2848 Oct 29 14:35 2024 my_stdio.o
rw-rw-r-- 1000/1000 1272 Oct 29 14:35 2024 my_string.o

3.2 静态库的 Makefile 示例

makefile 复制代码
# 完整的生产级 Makefile
libmystdio.a:my_stdio.o my_string.o
	@ar -rc $@ $^
	@echo "build $^ to $@ ... done"

%.o:%.c
	@gcc -c $<
	@echo "compling $< to $@ ... done"

.PHONY:clean
clean:
	@rm -rf *.a *.o stdc*
	@echo "clean ... done"

.PHONY:output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.a stdc/lib
	@tar -czf stdc.tgz stdc
	@echo "output stdc ... done"

3.3 使用静态库

bash 复制代码
# 方式1:直接指定路径
$ gcc main.c libmystdio.a

# 方式2:通过 -L 指定库路径,-l 指定库名(去掉 lib 前缀和 .a 后缀)
$ gcc main.c -L. -lmystdio

# 方式3:完整指定头文件和库路径
$ gcc main.c -I ./include -L ./lib -lmystdio

参数说明

  • -I:指定头文件搜索路径(找 .h
  • -L:指定库文件搜索路径(找 .a / .so
  • -l:指定库名
  • -static:强制使用静态链接(即使有 .so 也优先用 .a)

CS:APP 补充 :链接器在符号解析时,会从左到右扫描命令行中的输入文件。对于每个未解析的引用 ,去后面的库中查找定义。因此 -l 库应放在调用它的目标文件之后


四、动态库(共享库)的制作与使用

4.1 制作动态库

bash 复制代码
# 编译时需加 -fPIC(位置无关代码),链接时加 -shared
$ gcc -fPIC -c my_stdio.c
$ gcc -fPIC -c my_string.c
$ gcc -shared -o libmystdio.so my_stdio.o my_string.o

Makefile 示例

makefile 复制代码
libmystdio.so:my_stdio.o my_string.o
	gcc -o $@ $^ -shared

%.o:%.c
	gcc -fPIC -c $<

.PHONY:clean
clean:
	@rm -rf *.so *.o stdc*

.PHONY:output
output:
	@mkdir -p stdc/include
	@mkdir -p stdc/lib
	@cp -f *.h stdc/include
	@cp -f *.so stdc/lib
	@tar -czf stdc.tgz stdc

4.2 关键概念:-fPIC

PIC(Position Independent Code,位置无关代码) 是动态库的核心技术。

为什么需要 PIC?

  • 动态库在加载时被映射到进程的虚拟地址空间中
  • 如果多个进程共用同一个 .so,它们在各自地址空间中的加载地址可能不同
  • PIC 代码不依赖绝对地址,通过 GOT(Global Offset Table) 和 PC-relative 寻址,无论加载到哪个地址都能正确执行

CS:APP 补充 :PIC 代码的编译和链接过程与非 PIC 代码相同,只是在编译时需要加 -fPIC 标志。链接时,链接器会自动将 PIC 代码合并到最终的可执行文件中。

4.3 使用动态库

bash 复制代码
# 编译链接(与静态库命令相同,但优先链接 .so)
$ gcc main.c -L. -lmystdio

# 查看生成的 a.out 依赖哪些动态库
$ ldd a.out
	linux-vdso.so.1 =>  (0x00007fff...)
	libmystdio.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007f...)
	/lib64/ld-linux-x86-64.so.2 (0x00007f...)

五、动态库的加载问题

5.1 "not found" 错误的根源

编译链接时,链接器只在 .o 文件中记录 .so 的名字 (通过 .dynamic 段),不嵌入路径 。运行时,动态链接器 /lib64/ld-linux-x86-64.so.2 需要找到该 .so 文件。

5.2 解决方案(三种方法)

方法 操作 适用范围 持久性
环境变量 export LD_LIBRARY_PATH=. 当前 shell 会话 临时
配置缓存 编辑 /etc/ld.so.conf.d/*.conf 后运行 ldconfig 全系统 永久
系统目录 将 .so 复制到 /usr/lib/lib64 全系统 永久
bash 复制代码
# 方法1:LD_LIBRARY_PATH(临时)
$ export LD_LIBRARY_PATH=.
$ ./a.out

# 方法2:ldconfig(永久)
$ cat /etc/ld.so.conf.d/bit.conf
/root/tools/linux
$ ldconfig   # 刷新缓存 /etc/ld.so.cache

# 方法3:拷贝到系统目录
$ sudo cp libmystdio.so /usr/lib/

CS:APP 补充 :动态链接器(ld-linux.so)本身也是一个共享库,它通过读取可执行文件中的 .interp 段知道自己的路径。它的工作流程是:加载器加载程序 → 读取 .interp → 启动动态链接器 → 加载所有依赖的 .so → 执行重定位 → 将控制权交给程序

5.3 ldd 命令详解

ldd 用于查看一个可执行文件或共享库依赖哪些动态库:

bash 复制代码
$ ldd /usr/bin/ls
	linux-vdso.so.1      (0x00007fff...)
	libselinux.so.1      => /lib/x86_64-linux-gnu/libselinux.so.1
	libc.so.6            => /lib/x86_64-linux-gnu/libc.so.6
	libpcre2-8.so.0      => /lib/x86_64-linux-gnu/libpcre2-8.so.0
	libdl.so.2           => /lib/x86_64-linux-gnu/libdl.so.2
	/lib64/ld-linux-x86-64.so.2

六、ELF 文件格式详解

6.1 ELF 三种类型

ELF(Executable and Linkable Format) 是 Linux 下目标文件的标准格式。CS:APP 将目标文件分为三类:

类型 后缀 说明 readelf -h 中的 Type
可重定位目标文件 (Relocatable) .o 编译器和汇编器产出,尚未链接 REL (Relocatable file)
可执行目标文件 (Executable) (无/.out 可直接加载运行 EXEC (Executable file)(现代 gcc 显示为 DYN
共享目标文件 (Shared Object) .so 动态库,可加载时链接或运行时动态加载 DYN (Shared object file)
bash 复制代码
# 查看 .o 文件的 ELF 类型
$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

6.2 ELF Header

ELF 文件最开头是 ELF 头,包含文件的基本元信息。通过 readelf -h 查看:

bash 复制代码
$ readelf -h hello.o
ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00   # ELF魔数
  Class:                             ELF64                     # 64位
  Data:                              2's complement, little endian  # 小端
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)    # 可重定位文件
  Machine:                           Advanced Micro Devices X86-64
  Entry point address:               0x0                       # .o没有入口点
  Start of program headers:          0 (bytes into file)       # .o没有程序头
  Start of section headers:          728 (bytes into file)
  Size of this header:               64 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           64 (bytes)
  Number of section headers:         13
  Section header string table index: 12

可执行文件 vs 可重定位文件的 ELF 头对比

字段 hello.o(可重定位) a.out(可执行)
Type REL DYN(Shared object file)
Entry point 0x0 0x1060(有入口地址)
Program headers 0 个 13 个(LOAD、DYNAMIC 等)
Section headers 13 个 31 个

📄 PDF 第7-1节显示了 ELF Header 的完整结构示意图。

6.3 Section Header Table(节头表)

readelf -S 查看可执行文件的所有节(Section)。

bash 复制代码
$ readelf -S a.out
There are 31 section headers, starting at offset 0x19d8:

Section Headers:
[Nr] Name              Type            Address          Offset
     Size              EntSize         Flags  Link  Info  Align
[ 0]                   NULL            0000000000000000 00000000
     0000000000000000  0000000000000000           0     0     0
[ 1] .interp           PROGBITS        0000000000400238 00000238
     000000000000001c  0000000000000000   A       0     0     1
[ 2] .note.ABI-tag     NOTE            0000000000400254 00000254
     ...
[16] .text             PROGBITS        0000000000400640 00000640
     00000000000004b2  0000000000000000  AX       0     0    16
[25] .data             PROGBITS        0000000000601060 00001060
     0000000000000004  0000000000000000  WA       0     0     1
[26] .bss              NOBITS          0000000000601064 00001064
     0000000000000004  0000000000000000  WA       0     0     1
...

关键 Section 一览

Section 全称 说明 权限
.text 代码段 已编译的机器指令 AX(可分配、可执行)
.data 数据段 已初始化的全局/静态变量 WA(可写、可分配)
.bss Block Started by Symbol 未初始化的全局/静态变量(不占文件空间) WA
.rodata 只读数据段 字符串常量、const 变量等 A(只读)
.symtab Symbol Table 符号表(函数名、全局变量名) ---
.debug 调试信息 -g 编译生成的调试符号 ---
.line 行号表 源代码行号与机器指令的映射 ---
.strtab String Table 符号表中符号名的字符串数据 ---
.init / .fini 初始化/终止代码 构造/析构函数代码 AX
.got Global Offset Table 全局偏移表(动态链接用) WA
.got.plt GOT for PLT PLT 使用的 GOT WA
.plt Procedure Linkage Table 过程链接表 AX
.dynamic 动态链接信息 动态链接器需要的元数据 WA
.interp Interpreter 动态链接器路径(如 /lib64/ld-linux-x86-64.so.2 A

🔑 CS:APP 关键点.bss 段在目标文件中不占用磁盘空间 ,它是一个占位符。在加载时,加载器为其分配内存并初始化为零。这就是为什么声明 int arr[10000]; 不会让 .o 文件变大的原因。

6.4 Program Header Table(段头表)

readelf -l 查看 Program Header(也称 Segment Header):

bash 复制代码
$ readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x4003e0
There are 9 program headers, starting at offset 64

Program Headers:
  Type          Offset             VirtAddr           PhysAddr
                FileSiz            MemSiz              Flags  Align
  PHDR          0x0000000000000040 0x0000000000400040 0x0000000000400040
                0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP        0x0000000000000238 0x0000000000400238 0x0000000000400238
                0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD          0x0000000000000000 0x0000000000400000 0x0000000000400000
                0x0000000000000744 0x0000000000000744  R E    200000
  LOAD          0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                0x0000000000000218 0x0000000000000220  RW     200000
  DYNAMIC       0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                0x00000000000001d0 0x00000000000001d0  RW     8
  NOTE          0x0000000000000254 0x0000000000400254 0x0000000000400254
                0x0000000000000044 0x0000000000000044  R      4
  GNU_STACK     0x0000000000000000 0x0000000000000000 0x0000000000000000
                0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO     0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                0x00000000000001f0 0x00000000000001f0  R      1

 Section to Segment mapping:
  Segment Sections...
   00
   01     .interp
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym ...
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss
   04     .dynamic
   ...

核心概念:Section vs Segment

Section(节) Segment(段)
视角 链接视图(Linking View) 执行视图(Execution View)
用途 链接器处理(合并符号、重定位) 加载器加载到内存(mmap)
头表 Section Header Table Program Header Table
存在文件 .o 和可执行文件都有 仅可执行文件和 .so
粒度 细(.text、.data、.bss...) 粗(多个 section 合并为 1 个 segment)

为什么 Section 要合并成 Segment?

  • 每个 Segment 的加载单元是 页(4KB) ,如果 .text 占 4097 字节,.init 占 512 字节,分开映射就需要3个页(浪费 12KB - 多出来的页实际用不上)
  • 合并后,内容相近的 Section 在同一个 Segment 里,共享权限(如 .init + .text + .rodata 合并为 R E 段,.data + .bss + .got 合并为 RW 段)

6.5 ELF 相关 C 结构体定义

来自 Linux 内核 <elf.h>

c 复制代码
// ELF Header (64位)
typedef struct elf64_hdr {
    unsigned char e_ident[16];   /* ELF魔数,0x7f 'E' 'L' 'F' */
    Elf64_Half  e_type;          /* 类型: REL/EXEC/DYN */
    Elf64_Half  e_machine;       /* 架构: x86-64 */
    Elf64_Word  e_version;
    Elf64_Addr  e_entry;         /* 入口点虚拟地址 */
    Elf64_Off   e_phoff;         /* Program header table 文件偏移 */
    Elf64_Off   e_shoff;         /* Section header table 文件偏移 */
    Elf64_Word  e_flags;
    Elf64_Half  e_ehsize;        /* ELF头大小 */
    Elf64_Half  e_phentsize;     /* 每个 Program header 的大小 */
    Elf64_Half  e_phnum;         /* Program header 的数量 */
    Elf64_Half  e_shentsize;     /* 每个 Section header 的大小 */
    Elf64_Half  e_shnum;         /* Section header 的数量 */
    Elf64_Half  e_shstrndx;      /* Section name string table 的索引 */
} Elf64_Ehdr;

// Program Header (64位)
typedef struct elf64_phdr {
    Elf64_Word  p_type;          /* 类型: LOAD/DYNAMIC/INTERP... */
    Elf64_Word  p_flags;         /* 权限: R/RW/RX */
    Elf64_Off   p_offset;        /* 在文件中的偏移 */
    Elf64_Addr  p_vaddr;         /* 虚拟地址 */
    Elf64_Addr  p_paddr;         /* 物理地址(大多数平台忽略) */
    Elf64_Xword p_filesz;        /* 文件中的大小 */
    Elf64_Xword p_memsz;         /* 内存中的大小(>= filesz) */
    Elf64_Xword p_align;         /* 对齐要求 */
} Elf64_Phdr;

// Section Header (64位)
typedef struct elf64_shdr {
    Elf64_Word  sh_name;         /* Section名(在 .shstrtab 中的索引) */
    Elf64_Word  sh_type;         /* 类型 */
    Elf64_Xword sh_flags;        /* 属性 */
    Elf64_Addr  sh_addr;         /* 虚拟地址 */
    Elf64_Off   sh_offset;       /* 文件偏移 */
    Elf64_Xword sh_size;         /* 大小 */
    Elf64_Word  sh_link;         /* 链接到其他 section 的索引 */
    Elf64_Word  sh_info;         /* 附加信息 */
    Elf64_Xword sh_addralign;    /* 对齐 */
    Elf64_Xword sh_entsize;      /* 如果包含表,每个条目的大小 */
} Elf64_Shdr;

七、链接的完整过程

链接分为两步:符号解析(Symbol Resolution)重定位(Relocation)

7.1 符号解析(Symbol Resolution)

符号表

readelf -s 查看目标文件的符号表:

bash 复制代码
# hello.o 的符号表
$ readelf -s hello.o
Symbol table '.symtab' contains 14 entries:
   Num: Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hello.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     ...
    10: 0000000000000000    37 FUNC    GLOBAL DEFAULT    1 main      # 已定义
    11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts      # 未定义
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND run       # 未定义

# code.o 的符号表
$ readelf -s code.o
Symbol table '.symtab' contains 13 entries:
   Num: Value          Size Type    Bind   Vis      Ndx Name
    10: 0000000000000000    23 FUNC    GLOBAL DEFAULT    1 run       # 已定义
    12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts     # 未定义

符号类型

符号类型 说明
GLOBAL 定义 本模块定义的全局符号(函数、全局变量),可被其他模块引用
GLOBAL 引用 (UND) 本模块引用、但在其他模块定义的符号(外部符号)
LOCAL 本模块私有的符号(static 函数/变量),不可被其他模块引用
链接器如何解析符号
复制代码
hello.o 定义了: main
hello.o 引用(UND): puts, run

code.o 定义了: run
code.o 引用(UND): puts

链接器扫描所有 .o 文件:

  1. 维护一个未解析符号表
  2. 扫描到 hello.o → 记录 main 已定义,记录 puts, run 未解析
  3. 扫描到 code.o → 记录 run 已定义,run 解析成功
  4. 最后 puts 仍未解析 → 去 libc 中查找 → 在 libc.so 中找到定义 → 解析成功

如果最终仍有未解析符号 → 链接错误(undefined reference)

链接前后反汇编对比

链接前(hello.o) :所有外部调用的目标地址都是 0x00000000(占位)

asm 复制代码
# hello.o 反汇编
0000000000000000 <main>:
   0:  55                   push   %rbp
   ...
   f:  e8 00 00 00 00       callq  14 <main+0x14>    # 调 printf(地址待定)
  19:  e8 00 00 00 00       callq  1e <main+0x1e>    # 调 run(地址待定)

链接后(a.out):外部调用的地址被填为实际地址

asm 复制代码
# a.out 反汇编
0000000000001149 <run>:
    1149:  55               push   %rbp
    1151:  48 8d 3d ac 0e 00 00   lea    0xeac(%rip),%rdi   # puts的参数
    1158:  e8 f3 fe ff ff         callq  1050 <puts@plt>      # 通过 PLT 调 puts

0000000000001160 <main>:
    1160:  55               push   %rbp
    1168:  48 8d 3d a0 0e 00 00   lea    0xea0(%rip),%rdi
    116f:  e8 dc fe ff ff         callq  1050 <puts@plt>      # 调 puts(通过 PLT)
    1179:  e8 cb ff ff ff         callq  1149 <run>            # 调 run(已解析)

7.2 重定位(Relocation)

链接器完成符号解析后,执行重定位:

  1. 合并相同的 Section :将所有 .o.text 合并为一个 .text.data 合并为一个 .data

  2. 为每个 Section 分配运行时地址:确定最终虚拟地址

  3. 修改符号引用:根据重定位条目,将占位地址修改为实际地址

    链接过程可视化:

    hello.o ──.text──┐ ┌── .text (0x400640)
    ├── 合并 ──────→├── .rodata (0x400b00)
    code.o ──.text──┘ ├── .data (0x601060)
    └── ...其他 section


八、程序加载与动态链接

8.1 加载器如何工作

当用户在 shell 中输入命令执行程序时:

复制代码
shell → fork() → execve() → 加载器(loader) → 加载 ELF 到内存 → 启动

加载过程详述

  1. Shell 调用 execve:传入可执行文件名、参数列表、环境变量
  2. 加载器读取 ELF Header:解析 Entry point、Program Header Table
  3. 根据 Program Header 映射 Segment 到内存
    • 每个 LOAD 类型的 Segment 调用 mmap() 映射到进程地址空间
    • .text Segment 映射到代码区(R E)
    • .data + .bss Segment 映射到数据区(RW)
  4. 读取 .interp :找到动态链接器路径(/lib64/ld-linux-x86-64.so.2
  5. 启动动态链接器 :内核把控制权交给 ld-linux.so
  6. 动态链接器执行
    • 加载所有依赖的共享库(递归的)
    • 执行重定位(重定位 GOT 中的条目)
    • 执行 .init 段中的初始化代码
  7. 跳转到 Entry point_start__libc_start_mainmain

进程虚拟地址空间布局

复制代码
0x00000000 ┌─────────────────────┐
           │  内核空间           │ (用户态不可见)
0x40000000 ├─────────────────────┤
           │  代码段 (.text)     │ R E(只读可执行)
           │  只读数据 (.rodata) │ R(只读)
           │  数据段 (.data)     │ RW(可读写)
           │  BSS 段 (.bss)      │ RW(初始为0)
           │  堆 (heap)          │
           │      ↓              │
           │  ... 空闲 ...       │
           │      ↑              │
           │  共享库映射          │ (libc.so 等)
           │      ↑              │
           │  栈 (stack)         │ RW(向下增长)
0x7fffffff └─────────────────────┘

CS:APP 补充 :C/C++ 程序的 main 函数并非真正"第一个"执行的函数。ELF 的 Entry point 是 _start,由汇编编写(位于 crt0.o / crt1.o 中),它调用 __libc_start_main,后者再调用 mainmain 返回后,__libc_start_main 会调用 exit() 执行清理。

详细启动链

复制代码
_start (Entry point)
  └─→ __libc_start_main(main, argc, argv, __libc_csu_init, __libc_csu_fini, ...)
        ├─→ __libc_csu_init()  →  执行全局构造函数 (.init_array)
        └─→ main(argc, argv)
              └─→ return
        └─→ _exit()  →  系统调用退出

8.2 GOT 与 PLT 机制

这是动态链接最核心的机制。为什么需要 GOT 和 PLT?

  • 动态库(.so)的代码在编译时不知道会被加载到哪个地址
  • 为了实现位置无关(PIC),对全局变量 的访问通过 GOT(Global Offset Table)
  • 函数调用 的访问通过 PLT(Procedure Linkage Table) + GOT
GOT(Global Offset Table)

GOT 是一个位于 .got / .got.plt 段中的数据表,每个全局符号(函数/变量)在 GOT 中有一个条目,存储该符号的实际地址。

复制代码
.got.plt 段布局:

GOT[0] = .dynamic 段地址
GOT[1] = link_map(动态链接器内部数据)
GOT[2] = dl_resolve(动态链接器的符号解析入口)
GOT[3] = &puts          ← 初始指向 PLT 中的 push/resolve 指令
GOT[4] = &printf        ← 初始指向 PLT 中的 push/resolve 指令
...
PLT(Procedure Linkage Table)

PLT 是代码段(.plt 段)中的一组存根(stub)代码,每个外部函数有一个条目。

第一次调用时(延迟绑定 / Lazy Binding)

复制代码
main 调用 puts:
  └─→ call <puts@plt>        ← 跳转到 PLT 条目
        └─→ jmp *GOT[puts]   ← 此时 GOT[puts] 指向 PLT 中的下一行
              └─→ push 索引号  ← 将 puts 在符号表中的索引压栈
                    └─→ jmp 公共PLT条目
                          └─→ 调用动态链接器 _dl_runtime_resolve
                                └─→ 找到 puts 在 libc.so 中的地址
                                └─→ 写入 GOT[puts]
                                └─→ 跳转到 puts 函数执行

第二次及以后调用

复制代码
main 调用 puts:
  └─→ call <puts@plt>        ← 同前
        └─→ jmp *GOT[puts]   ← GOT[puts] 已指向 puts 的实际地址!
              └─→ 直接跳转到 puts 函数执行 ← **不再经过解析过程**

这就是延迟绑定的威力:只有被调用的函数才被解析,不被调用的函数根本不解析,节省了启动时间。

查看 GOT 和 PLT
bash 复制代码
# 查看 .got.plt 段
$ readelf -S a.out
  [24] .got.plt   PROGBITS   0000000000601000  00001000
                  0000000000000060  0000000000000008  WA   0     0    8

# 反汇编查看 PLT
$ objdump -d a.out
...
0000000000400590 <.plt>:
  400590: ff 35 72 0a 20 00    pushq  0x200a72(%rip)   # GOT[1]
  400596: ff 25 74 0a 20 00    jmpq   *0x200a74(%rip)   # GOT[2]
  40059c: 0f 1f 40 00          nopl   0x0(%rax)

00000000004005a0 <puts@plt>:
  4005a0: ff 25 72 0a 20 00    jmpq   *0x200a72(%rip)   # GOT[3]
  4005a6: 68 00 00 00 00       pushq  $0x0              # 索引0
  4005ab: e9 e0 ff ff ff       jmpq   400590 <.plt>     # 回到公共条目

九、第三方库使用示例(ncurses)

PDF 第4节以 ncurses 为例展示了安装和使用第三方库的过程。

安装

bash 复制代码
# CentOS
$ sudo yum install -y ncurses-devel

# Ubuntu
$ sudo apt install -y libncurses-dev

编译链接

bash 复制代码
$ gcc progress.c -lncurses

进度条示例程序

c 复制代码
#include <stdio.h>
#include <string.h>
#include <ncurses.h>
#include <unistd.h>

#define PROGRESS_BAR_WIDTH 30
#define BORDER_PADDING 2
#define WINDOW_WIDTH (PROGRESS_BAR_WIDTH + 2 * BORDER_PADDING + 2)
#define WINDOW_HEIGHT 5
#define PROGRESS_INCREMENT 3
#define DELAY 300000   // 300ms

int main() {
    initscr();
    start_color();
    init_pair(1, COLOR_GREEN, COLOR_BLACK);
    init_pair(2, COLOR_RED, COLOR_BLACK);
    cbreak();
    noecho();
    curs_set(FALSE);

    int max_y, max_x;
    getmaxyx(stdscr, max_y, max_x);
    int start_y = (max_y - WINDOW_HEIGHT) / 2;
    int start_x = (max_x - WINDOW_WIDTH) / 2;

    WINDOW *win = newwin(WINDOW_HEIGHT, WINDOW_WIDTH, start_y, start_x);
    box(win, 0, 0);
    wrefresh(win);

    int progress = 0;
    int max_progress = PROGRESS_BAR_WIDTH;

    while (progress <= max_progress) {
        werase(win);
        int completed = progress;
        int remaining = max_progress - progress;
        int bar_x = BORDER_PADDING + 1;
        int bar_y = 1;

        // 已完成部分(绿色)
        attron(COLOR_PAIR(1));
        for (int i = 0; i < completed; i++)
            mvwprintw(win, bar_y, bar_x + i, "#");
        attroff(COLOR_PAIR(1));

        // 未完成部分(红色)
        attron(A_BOLD | COLOR_PAIR(2));
        for (int i = completed; i < max_progress; i++)
            mvwprintw(win, bar_y, bar_x + i, " ");
        attroff(A_BOLD | COLOR_PAIR(2));

        // 百分比
        char percent_str[10];
        snprintf(percent_str, sizeof(percent_str), "%d%%",
                 (progress * 100) / max_progress);
        int percent_x = (WINDOW_WIDTH - strlen(percent_str)) / 2;
        mvwprintw(win, WINDOW_HEIGHT - 1, percent_x, percent_str);

        wrefresh(win);
        progress += PROGRESS_INCREMENT;
        usleep(DELAY);
    }

    delwin(win);
    endwin();
    return 0;
}

该程序原理解析:ncurses 库提供了终端图形界面的封装,通过 initscr() 初始化终端窗口,newwin() 创建子窗口,box() 绘制边框,mvwprintw() 在指定位置输出文本,可实现各种终端 UI 效果。编译时通过 -lncurses 链接该库。


十、常用工具命令速查表

文件查看

命令 用途 示例
file 查看文件类型(是否为 ELF) file a.out
size 查看各段大小 size a.out
strings 提取文件中的可打印字符串 `strings a.out
du -sh 查看文件磁盘占用 du -sh *.o

ELF 分析

命令 用途 示例
readelf -h 查看 ELF Header readelf -h a.out
readelf -S 查看 Section Header Table readelf -S a.out
readelf -l 查看 Program Header Table readelf -l a.out
readelf -s 查看符号表 readelf -s a.out
readelf -r 查看重定位条目 readelf -r hello.o
readelf -d 查看 .dynamic 段 readelf -d a.out
objdump -d 反汇编代码段 objdump -d a.out
objdump -S 反汇编并混编源码 objdump -S a.out
objdump -h 查看 Section 摘要 objdump -h a.out
objdump -t 查看符号表 objdump -t a.out

库管理

命令 用途 示例
ar -rc 创建静态库 ar -rc libxxx.a a.o b.o
ar -tv 查看静态库内容 ar -tv libxxx.a
ar -x 解压静态库 ar -x libxxx.a
ldd 查看动态库依赖 ldd a.out
ldconfig 刷新动态库缓存 sudo ldconfig
nm 查看目标文件符号 nm a.out

调试与运行时

命令 用途 示例
strace 追踪系统调用(看 mmap 加载段) `strace ./a.out 2>&1
gdb 调试器,可检查 GOT 表 gdb a.outx/8gx &_GLOBAL_OFFSET_TABLE_
gcc -v 查看编译链接完整过程 gcc -v main.c
gcc -Wl,--verbose 查看链接器详细输出 gcc -Wl,--verbose main.c

📚 参考资源

  1. 《深入理解计算机系统》(CS:APP)第3版,第7章:链接
  2. Linux 内核源码 <include/linux/elf.h> --- ELF 结构体定义
  3. GCC 官方文档 :关于 -fPIC-shared-static 等选项的详细说明
  4. man ld / man ld.so --- 链接器和动态链接器的行为说明
  5. man gcc --- GCC 编译器选项大全

相关推荐
qz5zwangzihan119 小时前
题解:Atcoder Beginner Contest abc467 A~D
c++·题解·atcoder·abc467
caimouse19 小时前
mm学习笔记_04:VAD树算法与地址空间分配
笔记·学习·算法·reactos
智者知已应修善业20 小时前
【使用D触发器74HC175实现0001 0011 0111 1111 1110 1100 1000的循环彩灯电路。】2025-4-28
驱动开发·经验分享·笔记·硬件架构·硬件工程
龙仔7251 天前
人大金仓KingbaseES V8 手动单库备份&恢复操作笔记
数据库·笔记·oracle·人大金仓
jinyishu_1 天前
模拟实现 C++ 栈和队列——从适配器模式看懂 STL 容器之美
java·c++·适配器模式
hehelm1 天前
AI大模型接入SDK—通用模块设计
linux·开发语言·c++
zhangrelay1 天前
消费电子原装锂电池服役8–20年且健康度>40%公开实测案例完整调研分析报告
笔记·学习
疯狂打码的少年1 天前
【软件工程】软件测试:黑盒测试(等价类/边界值/因果图)
笔记·软件工程
圣光SG1 天前
Java Web入门基础知识笔记
java·前端·笔记