【Linux】库制作与原理(二):ELF格式与静态链接原理

文章目录

    • 库制作与原理(二):ELF格式与静态链接原理
    • 一、编译链接回顾
      • [1.1 从源码到可执行程序](#1.1 从源码到可执行程序)
      • [1.2 目标文件是什么](#1.2 目标文件是什么)
    • 二、ELF文件格式
      • [2.1 什么是ELF](#2.1 什么是ELF)
      • [2.2 ELF文件的四种类型](#2.2 ELF文件的四种类型)
      • [2.3 ELF文件的整体结构](#2.3 ELF文件的整体结构)
      • [2.4 查看ELF文件头](#2.4 查看ELF文件头)
    • 三、ELF的Section详解
      • [3.1 查看所有Section](#3.1 查看所有Section)
      • [3.2 常见Section说明](#3.2 常见Section说明)
        • [3.2.1 .text - 代码段](#3.2.1 .text - 代码段)
        • [3.2.2 .data - 已初始化数据段](#3.2.2 .data - 已初始化数据段)
        • [3.2.3 .bss - 未初始化数据段](#3.2.3 .bss - 未初始化数据段)
        • [3.2.4 .rodata - 只读数据段](#3.2.4 .rodata - 只读数据段)
        • [3.2.5 .symtab - 符号表](#3.2.5 .symtab - 符号表)
        • [3.2.6 .strtab - 字符串表](#3.2.6 .strtab - 字符串表)
        • [3.2.7 .rel.text / .rela.text - 重定位表](#3.2.7 .rel.text / .rela.text - 重定位表)
    • 四、静态链接原理
      • [4.1 链接的核心任务](#4.1 链接的核心任务)
      • [4.2 符号表详解](#4.2 符号表详解)
        • [4.2.1 符号的类型](#4.2.1 符号的类型)
        • [4.2.2 符号解析过程](#4.2.2 符号解析过程)
      • [4.3 地址重定位](#4.3 地址重定位)
        • [4.3.1 为什么需要重定位](#4.3.1 为什么需要重定位)
        • [4.3.2 重定位表的作用](#4.3.2 重定位表的作用)
        • [4.3.3 实战:观察重定位过程](#4.3.3 实战:观察重定位过程)
      • [4.4 Section的合并](#4.4 Section的合并)
    • 五、ELF加载与执行视图
      • [5.1 链接视图vs执行视图](#5.1 链接视图vs执行视图)
      • [5.2 查看Program Header](#5.2 查看Program Header)
      • [5.3 为什么要合并Section为Segment](#5.3 为什么要合并Section为Segment)
      • [5.4 程序入口点](#5.4 程序入口点)
    • 六、静态库的链接
      • [6.1 静态库的本质](#6.1 静态库的本质)
      • [6.2 链接静态库的过程](#6.2 链接静态库的过程)
      • [6.3 验证静态链接](#6.3 验证静态链接)
    • 七、链接时的符号解析规则
      • [7.1 强符号与弱符号](#7.1 强符号与弱符号)
      • [7.2 符号解析规则](#7.2 符号解析规则)
      • [7.3 避免符号冲突](#7.3 避免符号冲突)
    • 八、实战:分析完整的链接过程
      • [8.1 准备测试代码](#8.1 准备测试代码)
      • [8.2 编译但不链接](#8.2 编译但不链接)
      • [8.3 查看符号表](#8.3 查看符号表)
      • [8.4 查看重定位表](#8.4 查看重定位表)
      • [8.5 链接](#8.5 链接)
      • [8.6 查看链接后的符号](#8.6 查看链接后的符号)
      • [8.7 反汇编对比](#8.7 反汇编对比)
    • 九、工具大全
      • [9.1 readelf - 查看ELF信息](#9.1 readelf - 查看ELF信息)
      • [9.2 objdump - 反汇编工具](#9.2 objdump - 反汇编工具)
      • [9.3 nm - 查看符号](#9.3 nm - 查看符号)
      • [9.4 ar - 静态库工具](#9.4 ar - 静态库工具)
      • [9.5 ldd - 查看动态库依赖](#9.5 ldd - 查看动态库依赖)
      • [9.6 file - 识别文件类型](#9.6 file - 识别文件类型)
    • 十、总结

库制作与原理(二):ELF格式与静态链接原理

💬 欢迎讨论:在上一篇中,我们学习了如何制作和使用静态库与动态库。但你是否好奇:编译器是如何将多个.o文件链接成可执行文件的?静态库中的函数是如何被找到并调用的?本篇将深入ELF文件格式,揭示静态链接的底层原理,带你理解从目标文件到可执行程序的完整过程。

👍 点赞、收藏与分享:这篇文章包含了ELF文件结构、符号表、重定位表的完整解析,配合大量实战命令,内容硬核,如果对你有帮助,请点赞、收藏并分享!

🚀 循序渐进:建议先学习第一篇的库制作基础,这样理解本篇的链接原理会更轻松。


一、编译链接回顾

1.1 从源码到可执行程序

在深入链接原理之前,我们先回顾一下完整的编译过程:

bash 复制代码
源文件(.c/.cpp)
    ↓ 预处理(gcc -E)
预处理文件(.i)
    ↓ 编译(gcc -S)
汇编文件(.s)
    ↓ 汇编(gcc -c)
目标文件(.o)
    ↓ 链接(ld)
可执行文件(a.out)

详细步骤:

bash 复制代码
# 1. 预处理:展开宏、处理#include
gcc -E main.c -o main.i

# 2. 编译:生成汇编代码
gcc -S main.i -o main.s

# 3. 汇编:生成机器码
gcc -c main.s -o main.o

# 4. 链接:生成可执行文件
gcc main.o -o main

一步到位:

bash 复制代码
gcc main.c -o main
# 等价于上面4步

1.2 目标文件是什么

目标文件(.o):

  • 包含机器码的二进制文件
  • 但还不能直接运行
  • 需要经过链接才能成为可执行文件

为什么需要链接?

c 复制代码
// main.c
#include <stdio.h>
extern int add(int a, int b);  // 声明,但未定义

int main() {
    int result = add(3, 5);
    printf("Result: %d\n", result);
    return 0;
}
c 复制代码
// add.c
int add(int a, int b) {
    return a + b;
}

编译:

bash 复制代码
gcc -c main.c -o main.o
gcc -c add.c -o add.o

问题:

  • main.o中调用了add函数,但add的代码在add.o
  • main.o中还调用了printf,代码在C库中
  • 链接器的任务:将这些目标文件组合在一起!

二、ELF文件格式

2.1 什么是ELF

ELF (Executable and Linkable Format):可执行与可链接格式

Linux下的所有二进制文件都采用ELF格式:

  • 目标文件(.o)
  • 可执行文件(a.out)
  • 动态库(.so)
  • 静态库(.a) - 从上篇文章我们就知道这实际上是.o文件的归档

查看文件类型:

bash 复制代码
file main.o
main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, not stripped

file libmystdio.so
libmystdio.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), not stripped

2.2 ELF文件的四种类型

类型 说明 文件扩展名
Relocatable File (可重定位文件) 目标文件 .o
Executable File (可执行文件) 可执行程序 通常无扩展名
Shared Object (共享目标文件) 动态库 .so
Core Dump File (核心转储文件) 程序崩溃时的内存快照 core / core.*

2.3 ELF文件的整体结构

ELF文件由四个主要部分组成:

bash 复制代码
┌─────────────────────────────────┐
│  ELF Header (文件头)              │ ← 描述整个文件的基本信息
├─────────────────────────────────┤
│  Program Header Table (程序头表)  │ ← 描述如何加载到内存(可执行文件)
├─────────────────────────────────┤
│                                 │
│  Sections (节区)                 │ ← 实际的代码和数据
│  ├─ .text (代码段)               │
│  ├─ .data (已初始化数据)          │
│  ├─ .bss (未初始化数据)           │
│  ├─ .rodata (只读数据)           │
│  ├─ .symtab (符号表)             │
│  ├─ .strtab (字符串表)           │
│  ├─ .rel.text (重定位表)         │
│  └─ ...                         │
│                                 │
├─────────────────────────────────┤
│  Section Header Table (节头表)   │ ← 描述各个Section
└─────────────────────────────────┘

两个重要概念:

  • Section(节):编译和链接时使用
  • Segment(段):程序加载时使用

2.4 查看ELF文件头

使用readelf命令:

bash 复制代码
readelf -h main.o

ELF Header:
  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF64
  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
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          1088 (bytes into file)
  Flags:                             0x0
  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:         14
  Section header string table index: 13

关键字段:

  • Type: REL - 可重定位文件(目标文件)
  • Entry point address: 0x0 - 目标文件没有入口点
  • Start of section headers - Section Header Table的位置
  • Number of section headers: 14 - 有14个Section

查看可执行文件:

bash 复制代码
readelf -h a.out

ELF Header:
  Type:                              EXEC (Executable file)
  Entry point address:               0x400430
  Start of program headers:          64 (bytes into file)
  Number of program headers:         9

对比:

  • 可执行文件类型是EXEC
  • 有入口点地址0x400430
  • 有Program Header Table

三、ELF的Section详解

3.1 查看所有Section

bash 复制代码
readelf -S main.o

There are 14 section headers, starting at offset 0x440:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .text             PROGBITS         0000000000000000  00000040
       000000000000002a  0000000000000000  AX       0     0     1
  [ 2] .rela.text        RELA             0000000000000000  00000318
       0000000000000048  0000000000000018   I      11     1     8
  [ 3] .data             PROGBITS         0000000000000000  0000006a
       0000000000000000  0000000000000000  WA       0     0     1
  [ 4] .bss              NOBITS           0000000000000000  0000006a
       0000000000000000  0000000000000000  WA       0     0     1
  [ 5] .rodata           PROGBITS         0000000000000000  0000006a
       000000000000000b  0000000000000000   A       0     0     1
  [ 6] .comment          PROGBITS         0000000000000000  00000075
       000000000000002e  0000000000000001  MS       0     0     1
  [ 7] .note.GNU-stack   PROGBITS         0000000000000000  000000a3
       0000000000000000  0000000000000000           0     0     1
  [ 8] .eh_frame         PROGBITS         0000000000000000  000000a8
       0000000000000038  0000000000000000   A       0     0     8
  [ 9] .rela.eh_frame    RELA             0000000000000000  00000360
       0000000000000018  0000000000000018   I      11     8     8
  [10] .symtab           SYMTAB           0000000000000000  00000228
       00000000000000d8  0000000000000018          12     8     8
  [11] .strtab           STRTAB           0000000000000000  00000300
       0000000000000015  0000000000000000           0     0     1
  [12] .shstrtab         STRTAB           0000000000000000  00000378
       00000000000000c1  0000000000000000           0     0     1

3.2 常见Section说明

3.2.1 .text - 代码段

存储内容:编译后的机器指令

bash 复制代码
readelf -x .text main.o

Hex dump of section '.text':
  0x00000000 554889e5 4883ec10 c745fc03 000000c7 UH..H....E......
  0x00000010 45f80500 00008b55 fc8b45f8 89d7e800 E......U..E.....
  0x00000020 000000c9 c3                          .....

反汇编查看:

bash 复制代码
objdump -d main.o

main.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	48 83 ec 10          	sub    $0x10,%rsp
   8:	c7 45 fc 03 00 00 00 	movl   $0x3,-0x4(%rbp)
   f:	c7 45 f8 05 00 00 00 	movl   $0x5,-0x8(%rbp)
  16:	8b 55 fc             	mov    -0x4(%rbp),%edx
  19:	8b 45 f8             	mov    -0x8(%rbp),%eax
  1c:	89 d7                	mov    %edx,%edi
  1e:	e8 00 00 00 00       	callq  23 <main+0x23>
  23:	c9                   	leaveq 
  24:	c3                   	retq

注意 :地址0x1e处调用函数的地址是00 00 00 00,这是因为链接器还没有填入真实地址!

3.2.2 .data - 已初始化数据段

存储内容:已初始化的全局变量和静态变量

c 复制代码
// test.c
int global_init = 10;      // 存在.data
static int static_init = 20;  // 存在.data

int main() {
    return 0;
}
bash 复制代码
gcc -c test.c
readelf -x .data test.o

Hex dump of section '.data':
  0x00000000 0a000000 14000000                   ........
           ↑        ↑
          10       20 (小端序)
3.2.3 .bss - 未初始化数据段

存储内容:未初始化的全局变量和静态变量

c 复制代码
// test.c
int global_uninit;           // 存在.bss
static int static_uninit;    // 存在.bss

int main() {
    return 0;
}
bash 复制代码
readelf -S test.o | grep bss
  [ 4] .bss              NOBITS           0000000000000000  0000006a
       0000000000000008  0000000000000000  WA       0     0     4
                                                   ↑
                                                8字节(两个int)

注意: .bss段不占用文件空间(NOBITS),只记录大小,加载时由系统分配并清零!

3.2.4 .rodata - 只读数据段

存储内容:字符串常量、const变量

c 复制代码
// test.c
#include <stdio.h>

int main() {
    const char *str = "Hello World";  // 字符串存在.rodata
    printf("%s\n", str);
    return 0;
}
bash 复制代码
readelf -x .rodata test.o

Hex dump of section '.rodata':
  0x00000000 48656c6c 6f20576f 726c6400 257325    Hello World.%s%
3.2.5 .symtab - 符号表

存储内容:所有符号的信息(函数名、变量名等)

bash 复制代码
readelf -s main.o

Symbol table '.symtab' contains 10 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS main.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1 
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3 
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4 
     5: 0000000000000000     0 SECTION LOCAL  DEFAULT    5 
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    7 
     7: 0000000000000000     0 SECTION LOCAL  DEFAULT    8 
     8: 0000000000000000    37 FUNC    GLOBAL DEFAULT    1 main
     9: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND add

关键信息:

  • main:函数,全局符号,定义在Section 1(.text),大小37字节
  • add:未定义符号(UND),需要链接时解析

注意:

.symtab:链接/调试更完整的符号表(常见于 .o 和未 strip 的文件)

.dynsym:运行时动态链接器需要的子集符号表(常见于可执行文件/so)

3.2.6 .strtab - 字符串表

存储内容:符号表中用到的字符串

bash 复制代码
readelf -x .strtab main.o

Hex dump of section '.strtab':
  0x00000000 006d6169 6e2e6300 6d61696e 00616464 .main.c.main.add
  0x00000010 00                                  .

所有字符串以\0分隔,符号表通过偏移量引用字符串。

3.2.7 .rel.text / .rela.text - 重定位表

存储内容:需要重定位的位置信息

  • REL:重定位项里没有 Addend,Addend 存在于被重定位的位置
  • RELA:重定位项里带 Addend 字段(x86-64 常用 RELA)
bash 复制代码
readelf -r main.o

Relocation section '.rela.text' at offset 0x318 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000001f  000900000004 R_X86_64_PLT32    0000000000000000 add - 4
000000000028  000500000002 R_X86_64_PC32     0000000000000000 .rodata + 0
00000000002d  000a00000004 R_X86_64_PLT32    0000000000000000 printf - 4

解释:

  • 偏移0x1f处需要填入add函数的地址
  • 偏移0x28处需要填入.rodata的地址
  • 偏移0x2d处需要填入printf函数的地址

四、静态链接原理

4.1 链接的核心任务

链接器要完成两个主要任务:

  1. 符号解析(Symbol Resolution):找到所有未定义符号的定义
  2. 重定位(Relocation):修正代码和数据中的地址引用

4.2 符号表详解

4.2.1 符号的类型

全局符号(Global Symbols):

  • 可以被其他目标文件引用
  • 函数定义、全局变量定义

局部符号(Local Symbols):

  • 只在本文件内部可见
  • static函数、static变量

外部符号(External Symbols):

  • 在本文件中使用,但在其他文件中定义
  • extern声明的符号

示例:

c 复制代码
// main.c
int global_var = 10;          // 全局符号(定义)
static int static_var = 20;   // 局部符号
extern int add(int, int);     // 外部符号(未定义)

int main() {
    return add(global_var, static_var);
}
c 复制代码
// add.c
int add(int a, int b) {       // 全局符号(定义)
    return a + b;
}

查看符号:

bash 复制代码
nm main.o
0000000000000000 D global_var
0000000000000000 T main
0000000000000004 d static_var
                 U add

nm add.o
0000000000000000 T add

符号类型标识:

  • T - 定义在.text段的全局符号
  • D - 定义在.data段的全局符号
  • d - 定义在.data段的局部符号
  • U - 未定义符号
4.2.2 符号解析过程

链接器的工作:

bash 复制代码
1. 扫描所有输入的.o文件,收集所有符号
   main.o: 定义了 main, global_var
           引用了 add
   
   add.o:  定义了 add

2. 建立符号表
   符号名    定义位置      类型
   main     main.o       FUNC
   global_var main.o     OBJECT
   add      add.o        FUNC

3. 解析符号引用
   main.o中引用的add → 在add.o中找到定义 ✓

4. 如果有未定义符号 → 链接失败

链接失败示例:

c 复制代码
// main.c
extern int foo();

int main() {
    return foo();  // foo未定义
}
bash 复制代码
gcc main.c -o main
/usr/bin/ld: /tmp/ccXXXXXX.o: undefined reference to `foo'
collect2: error: ld returned 1 exit status

4.3 地址重定位

4.3.1 为什么需要重定位

问题本质

在编译阶段,编译器并不知道函数或变量在最终可执行文件中的真实地址。

因此,目标文件(.o)中的地址都是"临时的" ,这些地址在链接完成前是无法确定的

示意说明:

bash 复制代码
main.o 中的代码(逻辑视角):
地址0x00: push %rbp
地址0x01: mov %rsp,%rbp
...
地址0x1e: call 0x00000000   ← 这里要调用 add,但 add 的地址未知
bash 复制代码
add.o 中的代码:
地址0x00: push %rbp
地址0x01: mov %rsp,%rbp
...

此时存在两个关键事实:

  • main.oadd.o分别编译
  • main.o 并不知道 add 最终会被放到哪里

链接完成后:

bash 复制代码
最终可执行文件中:
main 从地址 0x400430 开始
add  从地址 0x400460 开始

地址 0x40044e: call 0x400460   ← 链接器填入 add 的真实地址

📌 结论:

编译阶段无法确定地址

👉 链接阶段必须"回过头来"修改指令中的地址

👉 这个过程就叫 地址重定位(Relocation)


4.3.2 重定位表的作用

重定位表(Relocation Table) 的作用是:

明确告诉链接器:

  • 哪些指令位置需要修改
  • 使用哪个符号的地址
  • 采用什么重定位方式

查看目标文件的重定位表:

bash 复制代码
readelf -r main.o
bash 复制代码
Relocation section '.rela.text' at offset 0x318 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000001f  000900000004 R_X86_64_PLT32    0000000000000000 add - 4

字段解释:

  • Offset: 0x1f

    → 需要修改 .text 段中 偏移 0x1f 处 的指令操作数

  • Type: R_X86_64_PLT32

    → 表示 x86-64 平台的 32 位 PC 相对重定位(常用于函数调用)

  • Sym. Name: add

    → 要引用的目标符号

  • Addend: -4

    → 用于修正相对地址计算(与指令长度有关)

x86-64 下的重定位计算公式:

text 复制代码
重定位值 = 符号地址 + Addend - 重定位位置

📌 对 call 指令来说,

最终编码的是 "目标地址 - 下一条指令地址"


4.3.3 实战:观察重定位过程

下面通过一个最小示例,完整观察重定位是如何发生的

准备代码:

c 复制代码
// main.c
extern int add(int a, int b);

int main() {
    return add(3, 5);
}
c 复制代码
// add.c
int add(int a, int b) {
    return a + b;
}

只编译,不链接:

bash 复制代码
gcc -c main.c -o main.o
gcc -c add.c  -o add.o

反汇编 main.o:

bash 复制代码
objdump -d main.o
bash 复制代码
0000000000000000 <main>:
   0:  55                    push   %rbp
   1:  48 89 e5              mov    %rsp,%rbp
   4:  bf 03 00 00 00        mov    $0x3,%edi
   9:  be 05 00 00 00        mov    $0x5,%esi
   e:  e8 00 00 00 00        callq  13 <main+0x13>
                     ↑
          call 的目标地址是 0(占位符)
   13: 5d                    pop    %rbp
   14: c3                    retq

👉 此时 call 指令中的偏移量还没有被填充


查看 main.o 的重定位表:

bash 复制代码
readelf -r main.o
bash 复制代码
Relocation section '.rela.text' at offset 0x200 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
00000000000f  000500000004 R_X86_64_PLT32    0000000000000000 add - 4
              ↑
        需要修正 call 指令的操作数字段

执行链接:

bash 复制代码
gcc main.o add.o -o main

查看链接后的反汇编:

bash 复制代码
objdump -d main
bash 复制代码
0000000000401106 <main>:
  401106: 55                    push   %rbp
  401107: 48 89 e5              mov    %rsp,%rbp
  40110a: bf 03 00 00 00        mov    $0x3,%edi
  40110f: be 05 00 00 00        mov    $0x5,%esi
  401114: e8 07 00 00 00        callq  401120 <add>
                            ↑
                    链接器已完成重定位
  401119: 5d                    pop    %rbp
  40111a: c3                    retq
bash 复制代码
0000000000401120 <add>:
  401120: 55                    push   %rbp
  401121: 48 89 e5              mov    %rsp,%rbp
  ...

验证重定位计算是否正确:

text 复制代码
call 指令地址:      0x401114
下一条指令地址:    0x401114 + 5 = 0x401119
call 操作数:        0x7
目标地址:          0x401119 + 0x7 = 0x401120

成功跳转到 add 函数

📌 本节小结:

  • .o 文件中的地址都是未定的
  • 重定位表精确描述"哪里需要改、怎么改"
  • 链接器根据重定位表填充真实地址
  • 这是静态链接和动态链接的共同基础

4.4 Section的合并

链接时的Section合并:


五、ELF加载与执行视图

5.1 链接视图vs执行视图

ELF文件有两种视图:

链接视图(Section):

  • 编译和链接时使用
  • 以Section为单位
  • 通过Section Header Table描述

执行视图(Segment):

  • 程序加载时使用
  • 以Segment为单位
  • 通过Program Header Table描述
bash 复制代码
链接时:关注Section的内容和符号
┌───────────────────┐
│ Section Header    │
│ .text .data .bss  │ ← 链接器看这个
└───────────────────┘

运行时:关注内存如何布局
┌───────────────────┐
│ Program Header    │
│ LOAD LOAD DYNAMIC │ ← 加载器看这个
└───────────────────┘

5.2 查看Program Header

bash 复制代码
readelf -l a.out

Elf file type is EXEC (Executable file)
Entry point 0x401040
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      0x8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      0x1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000448 0x0000000000000448  R E    0x200000
  LOAD           0x0000000000000e10 0x0000000000600e10 0x0000000000600e10
                 0x0000000000000228 0x0000000000000230  RW     0x200000
  DYNAMIC        0x0000000000000e28 0x0000000000600e28 0x0000000000600e28
                 0x00000000000001d0 0x00000000000001d0  RW     0x8
  ...

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .dynamic .got .got.plt .data .bss 
   04     .dynamic 

关键信息:

  • LOAD类型:表示需要加载到内存
  • VirtAddr:虚拟地址
  • FileSiz:文件中的大小
  • MemSiz:内存中的大小(.bss会使MemSiz > FileSiz)
  • Flags:R(可读) W(可写) E(可执行)

5.3 为什么要合并Section为Segment

原因:减少内存碎片,提高效率!

bash 复制代码
如果每个Section单独映射:
.text   → 4KB页
.rodata → 4KB页  
.data   → 4KB页
.bss    → 4KB页
共需要4个页表项

合并为Segment:
Segment 1 (.text + .rodata)  → 需要的页数
Segment 2 (.data + .bss)     → 需要的页数
减少页表项数量

合并原则:相同属性的Section合并

bash 复制代码
可读可执行的Section → 合并为一个Segment
  .text
  .init
  .fini
  .rodata  (有些系统会独立出来)

可读可写的Section → 合并为另一个Segment
  .data
  .bss

5.4 程序入口点

bash 复制代码
readelf -h a.out | grep Entry
  Entry point address:               0x401040

这个地址是什么?

bash 复制代码
objdump -d a.out | grep -A 5 "401040"

0000000000401040 <_start>:
  401040:	31 ed                	xor    %ebp,%ebp
  401042:	49 89 d1             	mov    %rdx,%r9
  401045:	5e                   	pop    %rsi
  401046:	48 89 e2             	mov    %rsp,%rdx
  401049:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp

程序的真正入口是_start,不是main

_start函数会:

  1. 初始化运行环境
  2. 调用动态链接器(如果有动态库)
  3. 调用main函数
  4. 调用exit退出

六、静态库的链接

6.1 静态库的本质

静态库(.a)是.o文件的归档包!

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

创建静态库:

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

静态库 .a 本质是 ar archive,里面按成员形式存放多个 .o。

它和 tar类似都是"打包归档",但格式不同,工具链也不同(ar/ranlib)

6.2 链接静态库的过程

bash 复制代码
gcc main.o -L. -lmystdio -o main

链接器的工作:

bash 复制代码
1. 解析命令行
   输入:main.o
   库:libmystdio.a

2. 从main.o中收集未定义符号
   main.o: 需要 my_strlen, mfopen, mfwrite, mfclose

3. 在libmystdio.a中查找定义
   解包libmystdio.a:
     my_stdio.o: 定义了 mfopen, mfwrite, mfclose
     my_string.o: 定义了 my_strlen

4. 提取需要的.o文件
   只提取 my_stdio.o 和 my_string.o

5. 链接所有.o文件
   main.o + my_stdio.o + my_string.o → main

注意:只提取需要的.o文件!

bash 复制代码
# 如果静态库中有10个.o文件
# 但你只用了其中2个.o中的函数
# 链接器只会提取这2个.o文件

6.3 验证静态链接

编译链接:

bash 复制代码
gcc main.c -L. -lmystdio -o main

查看符号:

bash 复制代码
nm main | grep my_
0000000000401156 T mfclose
0000000000401136 T mfflush
00000000004010c6 T mfopen
0000000000401166 T mfwrite
0000000000401202 T my_strlen

所有符号都在可执行文件中!

删除静态库:

bash 复制代码
rm libmystdio.a
./main  # 仍然可以运行!

证明代码已经被链接进可执行文件!


七、链接时的符号解析规则

7.1 强符号与弱符号

强符号(Strong Symbol):

  • 函数定义
  • 已初始化的全局变量

弱符号(Weak Symbol):

  • 未初始化的全局变量
c 复制代码
// strong.c
int strong_var = 10;    // 强符号
int weak_var;           // 弱符号

void func() {           // 强符号
    // ...
}

7.2 符号解析规则

规则1:不允许多个强符号

c 复制代码
// file1.c
int x = 1;

// file2.c
int x = 2;  // 错误:重复定义

gcc file1.c file2.c
/usr/bin/ld: multiple definition of `x'

规则2:如果有一个强符号和多个弱符号,选择强符号

c 复制代码
// file1.c
int x = 1;   // 强符号

// file2.c
int x;       // 弱符号

gcc file1.c file2.c  # 成功,使用x=1

规则3:如果有多个弱符号,任选一个

c 复制代码
// file1.c
int x;       // 弱符号

// file2.c
int x;       // 弱符号

gcc file1.c file2.c  # 成功,但行为未定义

警告:这会导致难以发现的bug!

c 复制代码
// file1.c
int x;       // 被分配为4字节

// file2.c
double x;    // 需要8字节,但只分配了4字节!

// 访问x可能导致内存溢出

7.3 避免符号冲突

方法1:使用static限制作用域

c 复制代码
// file1.c
static int x = 1;  // 仅在file1.c可见

// file2.c
static int x = 2;  // 仅在file2.c可见,不冲突

方法2:使用命名空间(C++)

cpp 复制代码
// file1.cpp
namespace lib1 {
    int x = 1;
}

// file2.cpp
namespace lib2 {
    int x = 2;
}

方法3:添加前缀

c 复制代码
// file1.c
int lib1_x = 1;

// file2.c
int lib2_x = 2;

八、实战:分析完整的链接过程

8.1 准备测试代码

c 复制代码
// main.c
#include <stdio.h>

extern int add(int a, int b);
extern int sub(int a, int b);

int global = 100;

int main() {
    int result1 = add(global, 50);
    int result2 = sub(global, 30);
    printf("add: %d, sub: %d\n", result1, result2);
    return 0;
}
c 复制代码
// math.c
int add(int a, int b) {
    return a + b;
}

int sub(int a, int b) {
    return a - b;
}

8.2 编译但不链接

bash 复制代码
gcc -c main.c -o main.o
gcc -c math.c -o math.o

8.3 查看符号表

bash 复制代码
# main.o的符号
nm main.o
                 U add
0000000000000000 D global
0000000000000000 T main
                 U printf
                 U sub

# math.o的符号
nm math.o
0000000000000000 T add
0000000000000015 T sub

8.4 查看重定位表

bash 复制代码
readelf -r main.o

Relocation section '.rela.text' at offset 0x1f8 contains 5 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000010  000500000002 R_X86_64_PC32     0000000000000000 global - 4
00000000001a  000800000004 R_X86_64_PLT32    0000000000000000 add - 4
000000000024  000500000002 R_X86_64_PC32     0000000000000000 global - 4
00000000002e  000900000004 R_X86_64_PLT32    0000000000000000 sub - 4
000000000038  000a00000004 R_X86_64_PLT32    0000000000000000 printf - 4

5个位置需要重定位!

8.5 链接

bash 复制代码
gcc main.o math.o -o main

8.6 查看链接后的符号

bash 复制代码
nm main
0000000000401136 T add
0000000000404028 D global
0000000000401106 T main
                 U printf@@GLIBC_2.2.5
000000000040114b T sub

所有符号都有了地址!

8.7 反汇编对比

链接前:

bash 复制代码
objdump -d main.o | grep -A 10 "<main>"

0000000000000000 <main>:
   0:	push   %rbp
   1:	mov    %rsp,%rbp
   ...
  10:	mov    0x0(%rip),%eax  # 引用global,地址为0
  1a:	callq  1f <main+0x1f>  # 调用add,地址未定

链接后:

bash 复制代码
objdump -d main | grep -A 10 "<main>"

0000000000401106 <main>:
  401106:	push   %rbp
  401107:	mov    %rsp,%rbp
  ...
  401116:	mov    0x2f0c(%rip),%eax  # 404028 <global>
  401120:	callq  401136 <add>

地址都被正确填入了!


九、工具大全

9.1 readelf - 查看ELF信息

bash 复制代码
# 查看ELF头
readelf -h file.o

# 查看Section Header
readelf -S file.o

# 查看Program Header
readelf -l a.out

# 查看符号表
readelf -s file.o

# 查看重定位表
readelf -r file.o

# 查看动态段
readelf -d libxxx.so

9.2 objdump - 反汇编工具

bash 复制代码
# 反汇编代码段
objdump -d file.o

# 反汇编所有段
objdump -D file.o

# 显示Section信息
objdump -h file.o

# 显示符号表
objdump -t file.o

# 显示动态符号表
objdump -T libxxx.so

# 查看Section的十六进制内容
objdump -s -j .rodata file.o

9.3 nm - 查看符号

bash 复制代码
# 查看符号表
nm file.o

# 只显示未定义符号
nm -u file.o

# 显示符号大小
nm -S file.o

# 动态符号(用于动态库)
nm -D libxxx.so

# C++符号解码
nm -C file.o

9.4 ar - 静态库工具

bash 复制代码
# 创建静态库
ar -rc libxxx.a file1.o file2.o

# 查看静态库内容
ar -t libxxx.a

# 详细查看
ar -tv libxxx.a

# 提取.o文件
ar -x libxxx.a

# 删除.o文件
ar -d libxxx.a file.o

9.5 ldd - 查看动态库依赖

bash 复制代码
# 查看程序依赖的动态库
ldd a.out

# 查看动态库的依赖
ldd libxxx.so

9.6 file - 识别文件类型

bash 复制代码
file main.o
main.o: ELF 64-bit LSB relocatable, x86-64

file a.out
a.out: ELF 64-bit LSB executable, x86-64

file libxxx.so
libxxx.so: ELF 64-bit LSB shared object, x86-64

十、总结

本文深入讲解了ELF格式和静态链接原理:

核心知识点:

  1. ELF文件结构

    • 四部分:ELF Header、Program Header Table、Sections、Section Header Table
    • 常见Section:.text、.data、.bss、.rodata、.symtab、.strtab
    • 链接视图(Section)vs执行视图(Segment)
  2. 符号表

    • 全局符号、局部符号、外部符号
    • 强符号与弱符号
    • 符号解析规则
  3. 静态链接过程

    • 符号解析:找到所有未定义符号的定义
    • 重定位:修正代码和数据中的地址引用
    • Section合并:相同属性的Section合并为Segment
  4. 重定位机制

    • 重定位表记录需要修改的位置
    • 链接器根据重定位表填入正确地址
    • 不同的重定位类型(R_X86_64_PC32、R_X86_64_PLT32等)
  5. 静态库链接

    • 静态库是.o文件的归档
    • 只提取需要的.o文件
    • 代码被完全链接进可执行文件

完整的静态链接流程图:

bash 复制代码
源文件(.c)
    ↓ gcc -c
目标文件(.o)
    ↓
    ├→ 符号表(定义了哪些符号,引用了哪些符号)
    └→ 重定位表(哪些位置需要修改)
    
链接器工作:
1. 收集所有符号 → 建立全局符号表
2. 解析符号引用 → 找到每个未定义符号的定义
3. 合并Section → .text合并、.data合并...
4. 重定位 → 根据重定位表修正地址
5. 生成可执行文件 → 包含Program Header

💡 思考题

  1. 为什么.bss段不占用文件空间?
  2. 如果两个.o文件定义了同名的全局变量,链接会发生什么?
  3. 静态库中的所有.o文件都会被链接进可执行文件吗?
  4. 重定位表中的Addend字段有什么作用?

下一篇我们将探索动态链接的奥秘,揭示GOT/PLT机制!

相关推荐
落贯一2 小时前
C Programming Language | Manipulating arrays in functions
c语言
KingRumn2 小时前
Linux信号之信号安全
linux·算法
Trouvaille ~2 小时前
【Linux】库制作与原理(三):动态链接与加载机制
linux·c语言·汇编·got·动静态库·动态链接·plt
一个不知名程序员www2 小时前
算法学习入门---C/C++输入输出
c语言·c++
写代码的橘子n2 小时前
IPV6复习(基础入手版)
运维·服务器·网络
APIshop2 小时前
高性能采集方案:淘宝商品 API 的并发调用与数据实时处理
linux·网络·算法
ICT技术最前线2 小时前
H3C双WAN口策略路由配置技术教程
运维·网络·h3c·策略路由
一分半心动2 小时前
windows docker desktop 安装VibeVoice
运维·docker·容器
松涛和鸣3 小时前
DAY38 TCP Network Programming
linux·网络·数据库·网络协议·tcp/ip·算法