二进制文件类型
概述
- Windows 有好几种不同的二进制格式
PE/COFF用于.exe/.dll/.obj/.sys- 独立的
.pdb格式 - 独立的
.lib格式
Linux几乎全部统一为ELF格式- 可执行文件、共享库、目标文件、内核模块、
core dump全都是ELF,只是ELF头里的类型字段不同
- 可执行文件、共享库、目标文件、内核模块、
Windows → Linux 的完整映射
|
1 2 3 4 5 6 7 8 9 10 11 |
Windows Linux 说明 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ .exe 可执行文件 无扩展名(如 /usr/bin/clang) ELF 格式 .dll 动态链接库 .so(Shared Object) ELF 格式 .lib 静态库 .a(Archive) ar 归档格式 .obj 目标文件 .o(Object) ELF 格式 .sys 内核驱动 .ko(Kernel Object) ELF 格式 .pdb 调试符号 (嵌入在 ELF 中的 DWARF) 不是独立文件 .lib 导入库 不需要 Linux 直接链接 .so — .so.1.2.3(带版本号的 .so) 版本化共享库 .dump core / core.12345 核心转储文件(ELF 格式) |
file 命令识别
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# file 命令是你识别二进制文件类型的第一工具 file /usr/bin/clang # /usr/bin/clang: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), # dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, # BuildID[sha1]=..., for GNU/Linux 3.2.0, stripped file /usr/lib/x86_64-linux-gnu/libc.so.6 # libc.so.6: ELF 64-bit LSB shared object, x86-64, ... file /usr/lib/x86_64-linux-gnu/libc.a # libc.a: current ar archive file hello.o # hello.o: ELF 64-bit LSB relocatable, x86-64, ... file /lib/modules/$(uname -r)/kernel/drivers/net/e1000/e1000.ko # e1000.ko: ELF 64-bit LSB relocatable, x86-64, ... file core # core: ELF 64-bit LSB core file, x86-64, ... # file 命令输出中的关键词: # "executable" → 可执行文件 # "shared object" → 共享库(.so) # "relocatable" → 目标文件(.o)或内核模块(.ko) # "core file" → core dump # "ar archive" → 静态库(.a) # "dynamically linked" → 依赖共享库 # "statically linked" → 不依赖共享库 # "stripped" → 调试信息被去掉了 # "not stripped" → 包含调试信息 # "pie executable" → 地址无关可执行文件(ASLR 支持) |
各类型详细说明
- 可执行文件
≈ .exe- 没有固定扩展名,靠文件权限中的 "
x"(执行位)标识
|
1 2 3 4 5 6 7 8 9 |
ls -l /usr/bin/clang # -rwxr-xr-x 1 root root 12345678 ... /usr/bin/clang # ^^^ 三组 rwx 中都有 x 表示可执行 # 和 Windows 的核心区别: # Windows 通过扩展名(.exe/.bat/.cmd)判断是否可执行 # Linux 通过文件权限的执行位判断 # 所以 Linux 上任何文件都能变成"可执行"文件: chmod +x script.sh # 给脚本加执行权限 |
- 共享库
≈ .dll- 命名规则:
lib<name>.so[.主版本.次版本.补丁版本]
|
1 2 3 4 |
ls -la /usr/lib/x86_64-linux-gnu/libstdc++* # libstdc++.so -> libstdc++.so.6 # 链接时用的符号链接 # libstdc++.so.6 -> libstdc++.so.6.0.32 # 运行时用的符号链接(soname) # libstdc++.so.6.0.32 # 实际文件 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 三层命名体系: # libstdc++.so "链接名"(linker name) # 编译时 g++ -lstdc++ 找的就是这个 # ≈ Windows 的 .lib(导入库) # # libstdc++.so.6 "soname"(shared object name) # 嵌入在 .so 文件内部,运行时动态链接器根据它查找 # 主版本号变化 = ABI 不兼容 # ≈ Windows DLL 名中的版本号(但 Windows 通常不带版本号) # # libstdc++.so.6.0.32 "真实名"(real name) # 实际的文件,包含完整版本号 |
|
1 2 3 4 5 6 7 8 9 10 |
# 查看 .so 的 soname readelf -d /usr/lib/x86_64-linux-gnu/libstdc++.so.6 | grep SONAME # (SONAME) Library soname: [libstdc++.so.6] # 查看程序依赖哪些 .so ldd /usr/bin/clang # linux-vdso.so.1 (0x00007ffca53f6000) # libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 # libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 # /lib64/ld-linux-x86-64.so.2 ← 这是动态链接器本身 |
- 静态库
≈ .lib静态版本- 命名规则:
lib<name>.a - 本质上就是一堆
.o文件打包在一起的ar归档
|
1 2 3 4 5 6 7 |
ar t /usr/lib/x86_64-linux-gnu/libc.a | head -10 # printf.o # fprintf.o # vprintf.o # malloc.o # ... # 你能看到里面全是 .o 文件 |
|
1 2 3 4 5 6 7 8 9 |
# 创建静态库 ar rcs libmylib.a file1.o file2.o file3.o # r = 替换(insert or replace) # c = 创建(create if not exist) # s = 创建索引(index,加速链接器查找符号) # 查看静态库的内容 ar t libmylib.a # 列出包含的 .o 文件 nm libmylib.a # 列出所有符号 |
- 目标文件
≈ .obj- 编译器的输出,链接器的输入
- 包含机器码,但地址还没有确定(需要链接器重定位)
|
1 2 3 4 |
g++ -c main.cpp -o main.o file main.o # main.o: ELF 64-bit LSB relocatable, x86-64, ... # ^^^^^^^^^^^ 注意是 "relocatable" |
- 内核模块
≈ .sysLinux内核驱动以.ko(Kernel Object)形式存在- 本质也是
ELF relocatable文件,但包含内核模块特有的section
|
1 2 3 4 5 6 7 8 |
file /lib/modules/$(uname -r)/kernel/drivers/net/e1000/e1000.ko # ELF 64-bit LSB relocatable, ... # 加载内核模块 sudo insmod my_module.ko # 加载(≈ Windows 的加载驱动) sudo rmmod my_module # 卸载 lsmod # 列出已加载的模块(≈ 设备管理器看驱动) modinfo e1000 # 查看模块信息 |
Core Dump≈ .dmp- 进程崩溃时的内存快照,也是
ELF格式
|
1 2 3 4 5 |
file core.12345 # core.12345: ELF 64-bit LSB core file, x86-64, ... # 包含:崩溃时的内存映射、寄存器状态、信号信息 # 用 GDB 分析:gdb ./program core.12345 |
ELF 文件的内部结构
概述
ELF(Executable and Linkable Format)是理解Linux二进制文件的核心
整体布局
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
┌──────────────────────────┐ │ ELF Header │ ← 文件最开头,描述文件的基本信息 │ (64 bytes) │ ≈ PE 的 IMAGE_NT_HEADERS ├──────────────────────────┤ │ Program Header Table │ ← 告诉加载器(loader)怎么把文件映射到内存 │ (Segments) │ ≈ PE 的 Optional Header 中的 DataDirectory │ │ 只有可执行文件和 .so 需要 │ │ .o 文件没有(还没链接,不需要加载) ├──────────────────────────┤ │ │ │ .text section │ ← 机器码(≈ PE 的 .text) │ │ ├──────────────────────────┤ │ .rodata section │ ← 只读数据(字符串常量等)(≈ PE 的 .rdata) ├──────────────────────────┤ │ .data section │ ← 已初始化的全局/静态变量(≈ PE 的 .data) ├──────────────────────────┤ │ .bss section │ ← 未初始化的全局/静态变量 │ │ 不占磁盘空间,加载时填零(≈ PE 的 .bss) ├──────────────────────────┤ │ .symtab section │ ← 符号表(函数名、变量名及其地址) │ │ ≈ PE 的 Export/Import Table + .pdb 中的符号 ├──────────────────────────┤ │ .strtab section │ ← 字符串表(符号名的实际字符串) ├──────────────────────────┤ │ .rel.text / .rela.text │ ← 重定位表(告诉链接器哪些地址需要修正) │ │ ≈ PE 的 .reloc section ├──────────────────────────┤ │ .debug_info │ ← DWARF 调试信息(类型、变量) │ .debug_line │ 源码行号映射 │ .debug_abbrev │ ≈ PE 的外部 .pdb 文件,但 Linux 嵌入在 ELF 内 │ .debug_str │ │ ... │ ├──────────────────────────┤ │ .plt / .got │ ← 动态链接支持(Procedure Linkage Table / │ .got.plt │ Global Offset Table) │ │ ≈ PE 的 IAT(Import Address Table) ├──────────────────────────┤ │ .dynamic │ ← 动态链接信息(依赖哪些 .so、soname 等) │ │ ≈ PE 的 Import Directory ├──────────────────────────┤ │ .dynsym / .dynstr │ ← 动态符号表(运行时需要的符号) │ │ .symtab 是完整符号表(可以 strip 掉) │ │ .dynsym 是运行时必需的(不能 strip) ├──────────────────────────┤ │ Section Header Table │ ← 描述所有 Section 的元数据 │ │ ≈ PE 的 Section Headers └──────────────────────────┘ |
Section vs Segment
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
链接视角(Linking View) 执行视角(Execution View) 用 Section Header Table 描述 用 Program Header Table 描述 链接器看的 加载器看的 ┌────────────┐ ┌────────────┐ │ .text │──┐ ┌──│ LOAD (R+X) │ 可读+可执行段 │ .rodata │──┤ 多个 Section │ │ │ 包含 .text + .rodata ├────────────┤ ├─ 合并成一个 ───►│ ├────────────┤ │ .data │──┤ Segment │ │ LOAD (R+W) │ 可读+可写段 │ .bss │──┘ └──│ │ 包含 .data + .bss ├────────────┤ ├────────────┤ │ .symtab │ 不需要加载到内存 │ DYNAMIC │ 动态链接信息 │ .strtab │ (调试/链接用) │ INTERP │ 动态链接器路径 │ .debug_* │ │ NOTE │ Build ID 等 │ .rela.* │ │ ... │ └────────────┘ └────────────┘ 关键理解: - Section 是链接器的视角(细粒度,每种数据一个 Section) - Segment 是加载器的视角(粗粒度,按内存权限合并) - .o 文件只有 Section,没有 Segment(还没链接) - 可执行文件和 .so 两者都有 - strip 可以去掉 Section Header Table,但不能去掉 Program Header Table (Section 信息是给工具用的,Segment 信息是运行必需的) |
查看 ELF 结构
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 |
# ===== readelf:ELF 文件的瑞士军刀 ===== # 查看 ELF Header readelf -h program # ELF Header: # Magic: 7f 45 4c 46 02 01 01 00 ... ← 0x7f 'E' 'L' 'F'(魔数) # Class: ELF64 ← 64位 # Data: 2's complement, little endian ← 小端 # Type: DYN (Position-Independent Executable) ← 类型 # Machine: Advanced Micro Devices X86-64 # Entry point address: 0x1060 ← 程序入口(≈ PE 的 AddressOfEntryPoint) # Start of program headers: 64 (bytes into file) # Start of section headers: 14328 (bytes into file) # Number of section headers: 31 # Number of program headers: 13 # Type 字段的含义: # EXEC 传统可执行文件(固定地址加载) # DYN 共享对象 / PIE 可执行文件(地址无关,支持 ASLR) # REL 可重定位文件(.o / .ko) # CORE core dump # 查看所有 Section Headers readelf -S program # [Nr] Name Type Address Off Size ES Flg Lk Inf Al # [ 1] .interp PROGBITS 0000000000000318 000318 00001c 00 A 0 0 1 # [ 2] .note.gnu... NOTE 0000000000000338 000338 000020 00 A 0 0 8 # ... # [14] .text PROGBITS 0000000000001060 001060 0001a5 00 AX 0 0 16 # [15] .rodata PROGBITS 0000000000002000 002000 000012 00 A 0 0 4 # [16] .data PROGBITS 0000000000004000 003000 000010 00 WA 0 0 8 # [17] .bss NOBITS 0000000000004010 003010 000008 00 WA 0 0 4 # # Flg 含义:A=Alloc(加载到内存) W=Write X=Execute # .text: AX = 加载到内存 + 可执行(代码段) # .data: WA = 可写 + 加载到内存(数据段) # .bss: WA + NOBITS = 可写但不占磁盘空间 # 查看 Program Headers(Segments) readelf -l program # Program Headers: # Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align # PHDR 0x000040 0x0000000000000040 0x0000000000000040 0x0002d8 0x0002d8 R 0x8 # INTERP 0x000318 0x0000000000000318 0x0000000000000318 0x00001c 0x00001c R 0x1 # LOAD 0x000000 0x0000000000000000 0x0000000000000000 0x000638 0x000638 R 0x1000 # LOAD 0x001000 0x0000000000001000 0x0000000000001000 0x0001d5 0x0001d5 R E 0x1000 # LOAD 0x002000 0x0000000000002000 0x0000000000002000 0x000128 0x000128 R 0x1000 # LOAD 0x002db8 0x0000000000003db8 0x0000000000003db8 0x000258 0x000260 RW 0x1000 # DYNAMIC 0x002dc8 0x0000000000003dc8 0x0000000000003dc8 0x0001f0 0x0001f0 RW 0x8 # # INTERP: 动态链接器的路径(/lib64/ld-linux-x86-64.so.2) # LOAD: 要加载到内存的段(R=读 W=写 E=执行) # DYNAMIC: 动态链接信息 # 查看符号表 readelf -s program # 或者 nm program # 符号类型: # T = .text 中的全局符号(函数) # t = .text 中的局部符号 # D = .data 中的全局符号(已初始化全局变量) # B = .bss 中的全局符号(未初始化全局变量) # U = 未定义(需要链接器或动态链接器解析) # W = 弱符号 # 查看重定位表 readelf -r hello.o # Relocation section '.rela.text': # Offset Info Type Sym. Value Sym. Name + Addend # 000000000015 000500000004 R_X86_64_PLT32 0000000000000000 printf - 4 # # 这条说明:.text 偏移 0x15 处有一个对 printf 的引用 # 类型 R_X86_64_PLT32:需要链接器通过 PLT 来解析 # 这就是你做工具链开发每天要处理的东西 # 查看动态链接信息 readelf -d program # Dynamic section: # Tag Type Name/Value # NEEDED Shared library: [libc.so.6] ← 依赖的共享库 # NEEDED Shared library: [libstdc++.so.6] # SONAME Library soname: [libmylib.so.1] ← 自己的 soname(.so 才有) # RPATH Library rpath: [/opt/lib] ← 运行时搜索路径 |
可执行文件的具体区别
代码
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# 从同一份源码出发,看不同类型的产物 cat > lib.cpp << 'EOF' #include <cstdio> int global_var = 42; static int static_var = 100; void hello() { printf("Hello from lib! global=%d static=%d\n", global_var, static_var); } EOF cat > main.cpp << 'EOF' extern void hello(); int main() { hello(); return 0; } EOF |
.o 目标文件
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
g++ -c -g lib.cpp -o lib.o g++ -c -g main.cpp -o main.o readelf -h lib.o | grep Type # Type: REL (Relocatable file) ← "可重定位" # .o 文件的特点: # - 有 Section Header Table,没有 Program Header Table # - 代码中的地址是"占位符"(0 或相对偏移),等链接器填入真实地址 # - 符号可能是 undefined(引用了其他 .o 里的函数/变量) # - 有重定位表,告诉链接器哪些地方需要修正地址 nm lib.o # 0000000000000000 D global_var ← D:已定义,在 .data 中 # 0000000000000004 d static_var ← d:已定义,局部(static),在 .data 中 # 0000000000000000 T hello ← T:已定义,在 .text 中 # U printf ← U:未定义,需要链接时解析 # U _GLOBAL_OFFSET_TABLE_ nm main.o # 0000000000000000 T main ← main 已定义 # U hello ← hello 未定义,需要链接 readelf -r main.o # .rela.text: # Offset Info Type Sym. Name + Addend # 0000000e 000400000004 R_X86_64_PLT32 hello - 4 # 这说明 main.o 的 .text 偏移 0x0e 处需要链接器填入 hello 的地址 |
.a 静态库
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
# 把 .o 打包成静态库 ar rcs libmylib.a lib.o file libmylib.a # libmylib.a: current ar archive # .a 本质上就是 .o 的打包,不是 ELF 格式 # 它是 ar(archiver)格式——一个简单的文件容器 # 查看里面有哪些 .o ar t libmylib.a # lib.o # 从 .a 中提取 .o ar x libmylib.a # 提取所有 .o 到当前目录 # 静态链接 g++ main.o -L. -lmylib -o program_static # -L. 在当前目录搜索库 # -lmylib 链接 libmylib.a(自动加 lib 前缀和 .a/.so 后缀) # 静态链接的结果:lib.o 的代码被完整复制到可执行文件中 nm program_static | grep hello # 0000000000401156 T hello ← hello 有了确定的地址 # 检查是否还依赖共享库 ldd program_static # 即使用了静态库,默认还是会动态链接 libc # 要完全静态链接: g++ -static main.o -L. -lmylib -o program_fully_static ldd program_fully_static # not a dynamic executable ← 完全独立,不依赖任何 .so # 但文件会大很多(libc 也被复制进来了) |
.so 共享库
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
# 编译共享库 g++ -shared -fPIC -g lib.cpp -o libmylib.so # -shared:生成共享库而不是可执行文件 # -fPIC:生成位置无关代码(Position Independent Code) # 代码中不使用绝对地址,而是通过 GOT/PLT 间接访问 # 这样 .so 可以加载到任意地址 # ≈ Windows DLL 默认就是可重定位的 file libmylib.so # libmylib.so: ELF 64-bit LSB shared object, x86-64, ... readelf -h libmylib.so | grep Type # Type: DYN (Shared object file) # .so 和 .o 的关键区别: # 1. .so 有 Program Header Table(可以被加载到内存) # 2. .so 的代码是位置无关的(通过 GOT/PLT 访问外部符号) # 3. .so 有 .dynamic section(动态链接信息) # 4. .so 有 .dynsym(动态符号表,运行时需要) readelf -d libmylib.so | head # Dynamic section: # NEEDED Shared library: [libc.so.6] ← 这个 .so 依赖 libc # 动态链接 g++ main.o -L. -lmylib -o program_dynamic # 运行前需要告诉动态链接器在哪找 .so export LD_LIBRARY_PATH=.:$LD_LIBRARY_PATH ./program_dynamic # 或者 LD_LIBRARY_PATH=. ./program_dynamic ldd program_dynamic # libmylib.so => ./libmylib.so (0x00007f...) # libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 # /lib64/ld-linux-x86-64.so.2 |
可执行文件
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# 可执行文件 = 链接完成的最终产物 readelf -h program_dynamic | grep Type # Type: DYN (Position-Independent Executable) # 现代 Linux 上的可执行文件通常也是 DYN 类型(PIE) # PIE = Position Independent Executable,支持 ASLR # 和 .so 的区别: # - 可执行文件有 entry point(程序入口) # - 可执行文件会被内核直接启动 # - .so 只能被动态链接器加载 # 在 ELF 层面,PIE 可执行文件和 .so 几乎没有结构差异 # 区别主要在于内核和动态链接器如何对待它们 |
加载过程详解
静态链接的可执行文件加载
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
用户执行 ./program │ ▼ 内核(execve 系统调用) │ ├─ 1. 读取 ELF Header,验证魔数 \x7fELF │ ├─ 2. 读取 Program Header Table │ ├─ 3. 遍历所有 PT_LOAD 段: │ 将文件中的内容 mmap 到进程地址空间 │ ┌──────────────────┐ │ │ LOAD R E │ → .text + .rodata 映射为可读+可执行 │ │ LOAD R W │ → .data 映射为可读+可写 │ │ │ .bss 分配零页(不从文件读取) │ └──────────────────┘ │ ├─ 4. 设置栈(argv, envp, auxv 压入栈顶) │ ├─ 5. 设置 RIP = Entry Point(ELF Header 中指定的地址) │ └─ 6. 切换到用户态,开始执行 │ ▼ _start(C 运行时入口,不是 main) │ ├─ 调用 __libc_start_main ├─ 初始化 C/C++ 运行时(全局构造函数等) ├─ 调用 main(argc, argv, envp) ├─ main 返回后调用 exit() └─ 调用全局析构函数,然后 _exit() 退出 |
动态链接的可执行文件加载
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
用户执行 ./program │ ▼ 内核(execve 系统调用) │ ├─ 1-3. 和静态链接一样,映射 PT_LOAD 段 │ ├─ 4. 发现 PT_INTERP 段,读取动态链接器路径 │ 通常是 /lib64/ld-linux-x86-64.so.2 │ ≈ Windows 的 ntdll.dll 中的加载器 │ ├─ 5. 把动态链接器也 mmap 到进程地址空间 │ └─ 6. 设置 RIP = 动态链接器的入口(不是程序的 entry point) │ ▼ 动态链接器(ld-linux-x86-64.so.2)接管控制 │ ├─ 7. 读取程序的 .dynamic section │ 获取依赖的共享库列表(NEEDED 条目) │ ├─ 8. 递归加载所有依赖的 .so │ 搜索顺序: │ a) DT_RPATH(ELF 中硬编码的路径,已不推荐) │ b) LD_LIBRARY_PATH 环境变量 │ c) DT_RUNPATH(ELF 中硬编码的路径,推荐) │ d) /etc/ld.so.cache(ldconfig 生成的缓存) │ e) /lib 和 /usr/lib(默认路径) │ ≈ Windows DLL 搜索顺序(程序目录 → PATH → System32) │ ├─ 9. 对每个 .so,映射其 PT_LOAD 段到内存 │ ├─ 10. 符号解析(Symbol Resolution) │ 遍历所有 .so 的 .dynsym,为每个未定义符号找到定义 │ ├─ 11. 重定位(Relocation) │ 填充 GOT 表项 │ ┌──────────────────────────────────┐ │ │ 立即绑定(LD_BIND_NOW=1): │ │ │ 启动时解析所有 GOT 表项 │ │ │ 启动慢,但运行时无额外开销 │ │ │ │ │ │ 延迟绑定(默认): │ │ │ GOT 表项初始指向 PLT 的桩代码 │ │ │ 第一次调用时才解析真实地址 │ │ │ 启动快,但首次调用有额外开销 │ │ │ ≈ Windows 的延迟加载 DLL │ │ └──────────────────────────────────┘ │ ├─ 12. 执行 .init / .init_array 中的初始化函数 │ (各 .so 的全局构造函数、__attribute__((constructor))) │ └─ 13. 跳转到程序的 entry point(_start) │ ▼ 程序开始正常执行 |
GOT/PLT 机制详解
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
# 当程序调用 printf 时(动态链接): # 源码:printf("hello"); # 编译后:call printf@PLT ← 不是直接调用 printf,而是通过 PLT 跳板 # ===== 第一次调用 printf(延迟绑定)===== call printf@PLT │ ▼ PLT[printf] 条目: jmp *GOT[printf] ← GOT 中此时存的不是 printf 的真实地址 │ 而是下一条指令的地址(push + jmp) ▼ push printf_的_重定位索引 ← 告诉动态链接器要解析哪个符号 jmp PLT[0] ← 跳到 PLT 的第 0 项(公共入口) │ ▼ PLT[0]: push GOT[1] ← link_map 结构体(标识是哪个 .so) jmp *GOT[2] ← 动态链接器的 _dl_runtime_resolve 函数 │ ▼ _dl_runtime_resolve: 根据重定位索引和 link_map,在 libc.so 中找到 printf 的真实地址 把真实地址写入 GOT[printf] ← 更新 GOT 表项 跳转到 printf 执行 ← 这次调用正常完成 # ===== 第二次及之后调用 printf ===== call printf@PLT │ ▼ PLT[printf] 条目: jmp *GOT[printf] ← GOT 中现在存的是 printf 的真实地址 │ ▼ 直接到达 printf ← 不再需要动态链接器参与 开销只有一次间接跳转 # 对比 Windows: # Windows 用 IAT(Import Address Table)实现类似功能 # 但 Windows 默认在加载时就解析所有导入(不是延迟绑定) # Windows 的延迟加载需要用 /DELAYLOAD 链接器选项显式指定 |
|
1 2 3 4 5 6 7 8 9 10 11 12 |
# 实际观察 GOT/PLT objdump -d program_dynamic | grep -A 3 "printf@plt" # 0000000000001030 <printf@plt>: # 1030: ff 25 e2 2f 00 00 jmp *0x2fe2(%rip) # 4018 <printf@GLIBC_2.2.5> # 1036: 68 00 00 00 00 push $0x0 # 103b: e9 e0 ff ff ff jmp 1020 <_init+0x20> # 查看 GOT 表 readelf -r program_dynamic | grep printf # 000000004018 R_X86_64_JUMP_SLOT printf@GLIBC_2.2.5 # GOT 地址 0x4018,类型 JUMP_SLOT(延迟绑定) |
其他
二进制之外的重要文件格式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
文件 作用 Windows 对应 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ /etc/ld.so.conf 动态库搜索路径配置 PATH / SxS /etc/ld.so.cache 动态库路径缓存(ldconfig 生成) 无直接对应 .interp 动态链接器路径 ntdll.dll ld script (.lds) 链接脚本(控制内存布局) .def + 链接器选项 .map 链接器生成的符号地址映射表 .map Makefile 构建规则 .vcxproj / .sln CMakeLists.txt 跨平台构建描述 CMakeLists.txt .pc (pkg-config) 库的编译/链接参数描述 .props / NuGet compile_commands.json 编译数据库(给 IDE/clangd 用) .vcxproj 隐含 .S / .s 汇编源文件 .asm .ld / .lds 链接器脚本 .def(部分功能) |
链接器脚本
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
# ===== 链接器脚本(Linker Script)===== # 控制链接器如何组织输出文件的内存布局 # 嵌入式和内核开发中经常用,工具链开发者需要理解 # 查看默认链接器脚本 ld --verbose | head -50 # 一个简化的链接器脚本示例: ENTRY(_start) /* 程序入口 */ SECTIONS { . = 0x400000; /* 起始地址 */ .text : { *(.text) /* 所有 .o 的 .text section 合并到这里 */ } .rodata : { *(.rodata) } . = ALIGN(0x1000); /* 页对齐 */ .data : { *(.data) } .bss : { *(.bss) } } |
pkg-config(库的元信息)
|
1 2 3 4 5 6 7 8 |
# ===== pkg-config(库的元信息)===== # 告诉编译器/链接器某个库的头文件路径和链接参数 # ≈ Windows 的 NuGet 包或 .props 文件 pkg-config --cflags libpng # 输出:-I/usr/include/libpng16 pkg-config --libs libpng # 输出:-lpng16 -lz # 在编译命令中使用: g++ main.cpp $(pkg-config --cflags --libs libpng) -o program |
compile_commands.json
|
1 2 3 4 5 6 |
# ===== compile_commands.json ===== # 编译数据库,记录每个文件的编译命令 # clangd(你的 IDE 智能补全引擎)需要它来理解项目结构 # CMake 可以自动生成: cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .. # 生成 build/compile_commands.json |
进程的内存映射
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
# ===== /proc/<pid>/maps(进程的内存映射)===== # 这不是文件格式,但对理解加载过程非常重要 cat /proc/$(pidof program)/maps # 地址范围 权限 偏移 设备 inode 路径 # 555555554000-555555555000 r--p 00000000 08:01 12345 /home/user/program # 555555555000-555555556000 r-xp 00001000 08:01 12345 /home/user/program # 555555556000-555555557000 r--p 00002000 08:01 12345 /home/user/program # 555555557000-555555558000 r--p 00002000 08:01 12345 /home/user/program # 555555558000-555555559000 rw-p 00003000 08:01 12345 /home/user/program # 7ffff7d00000-7ffff7d28000 r--p 00000000 08:01 23456 /lib/.../libc.so.6 # 7ffff7d28000-7ffff7ebd000 r-xp 00028000 08:01 23456 /lib/.../libc.so.6 # ... # 7ffff7fc3000-7ffff7fc5000 r--p 00000000 08:01 34567 /lib64/ld-linux-x86-64.so.2 # 7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0 [stack] # # 你能清楚地看到: # - 程序自身被映射到 0x555555554000 附近 # - libc.so.6 被映射到 0x7ffff7d00000 附近 # - 动态链接器被映射到 0x7ffff7fc3000 # - 栈在地址空间的最高处 # 每个 LOAD 段按权限分别映射(r-- / r-x / rw-) |
问题
默认静态链接还是动态链接
g++ main.o -L. -lmylib -o program时,链接器会在-L.指定的目录下同时搜索libmylib.so和libmylib.a- 关键在于两个文件是否同时存在:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
# 情况 1:目录下只有 libmylib.a ls ./ # libmylib.a main.o g++ main.o -L. -lmylib -o program # 链接器找到 libmylib.a → 静态链接 mylib # 但注意:libc、libstdc++ 等系统库仍然是动态链接的 ldd program # libstdc++.so.6 => /usr/lib/... ← 系统库还是动态的 # libc.so.6 => /lib/... ← 系统库还是动态的 # 没有 libmylib.so ← mylib 的代码已经被复制进 program 了 # 情况 2:目录下只有 libmylib.so ls ./ # libmylib.so main.o g++ main.o -L. -lmylib -o program # 链接器找到 libmylib.so → 动态链接 mylib ldd program # libmylib.so => ./libmylib.so ← mylib 是动态链接的 # libstdc++.so.6 => /usr/lib/... # libc.so.6 => /lib/... # 情况 3:两个都有(这才是让人困惑的情况) ls ./ # libmylib.a libmylib.so main.o g++ main.o -L. -lmylib -o program # 默认优先选择 .so(动态链接) # 这是 Linux 链接器的默认行为 |
- 真正控制静态/动态链接的方式
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
# ===== 方式 1:-static(你说的,全局静态链接)===== g++ -static main.o -L. -lmylib -o program_fully_static # 所有库都静态链接,包括 libc、libstdc++ # 生成完全独立的可执行文件 ldd program_fully_static # "not a dynamic executable" ← 不依赖任何 .so # 优点:拷贝到任何 Linux 机器都能跑,不需要安装任何依赖 # 缺点:文件很大(libc 就有几 MB),安全补丁不能通过更新 .so 生效 # ===== 方式 2:-Bstatic / -Bdynamic(精确控制每个库)===== # 这是实际开发中最常用的方式——只把特定的库静态链接 g++ main.o -L. -Wl,-Bstatic -lmylib -Wl,-Bdynamic -lstdc++ -lc -o program # ^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^ # mylib 静态链接 其余库恢复动态链接 # -Wl, 前缀表示把后面的参数传给链接器(ld/lld) # -Bstatic:从这里开始,后面的 -l 只找 .a # -Bdynamic:从这里开始,后面的 -l 恢复默认(优先 .so) # 验证: ldd program # libstdc++.so.6 => /usr/lib/... ← 动态 # libc.so.6 => /lib/... ← 动态 # 没有 libmylib.so ← mylib 被静态链接进去了 # ===== 方式 3:直接指定 .a 文件路径(最简单粗暴)===== g++ main.o ./libmylib.a -o program # 不用 -l,直接把 .a 文件路径写在命令里 # 链接器就没有"选 .so 还是 .a"的问题了 # 简单明确,适合小项目 # ===== 方式 4:-l:filename(指定精确文件名)===== g++ main.o -L. -l:libmylib.a -o program # -l:libmylib.a 表示精确查找 libmylib.a 这个文件名 # 不会自动加 lib 前缀和 .so/.a 后缀 # 也不存在 .so 和 .a 的优先级问题 |
- 为什么默认优先动态链接
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Linux 链接器默认 .so 优先于 .a,原因是: 1. 节省磁盘和内存 100 个程序都用 libc,动态链接只需要磁盘上一份 libc.so 静态链接每个程序里都有一份 libc 的副本 2. 安全更新方便 libc 发现漏洞,更新 libc.so 一个文件,所有程序立刻受益 静态链接的话需要重新编译所有程序 3. 内存共享 多个进程加载同一个 .so,内核只在物理内存中保留一份代码段 通过页表映射到各进程的虚拟地址空间(Copy-on-Write) Windows 的情况类似: 默认也是动态链接(.dll),MSVC 的 /MD 用动态运行时、/MT 用静态运行时 |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 包管理器:各平台安装卸载相关记述09/17
- ♥ Linux调试工具记录08/13
- ♥ Linux 进程间的通信方式和原理03/30
- ♥ Linux 高性能服务器编程:IP协议09/04
- ♥ Linux 线程的同步与互斥03/31
- ♥ Linux 高性能服务器编程:高性能服务器架构一12/05