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

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

参考: 《深入理解计算机系统》 以下简称(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 -E、gcc -S、gcc -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_main → main)

进程虚拟地址空间布局:

复制代码
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,后者再调用 main。main 返回后,__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.out 内 x/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 编译器选项大全

相关推荐
fpcc3 小时前
c++编程实践—堆和栈越界调试
c++
yi0113 小时前
LeetCode 219:存在重复元素 II——哈希表记录“最近一次出现的位置”
数据结构·人工智能·笔记·python·算法·leetcode·哈希表
志尊宝4 小时前
Vue3 零基础每日笔记(073):keep-alive 页面缓存实战——列表页返回不丢状态
vue.js·笔记·缓存·#vue #前端 #前端开发·#javascript·#vue.js
weiabc4 小时前
MSYS2 UCRT64 + g++ C++ 中文输出乱码 完整全过程
c++
Escalating_xu5 小时前
【C 语言】数据在内存中的存储:补码、大小端、整型陷阱与 IEEE 754 全解析
java·c语言·网络
灯澜忆梦5 小时前
【面向对象编程C++】| 基础语法
java·c++·算法
weixin_448119945 小时前
Datawhale Easy Data × AI:笔记1 已重做,看新的笔记
笔记
要开心吖ZSH6 小时前
MySQL 慢 SQL 排查操作手册-个人笔记版
java·笔记·sql·mysql·慢查询
aqiu1111117 小时前
【C++算法打怪专栏】LeetCode 165. 比较版本号(双指针与字符串流法)
c++·算法·leetcode
咖啡忍者7 小时前
【SAP】程制造生产计划PP-PI(Production Planning for Process Industries)
笔记