
编译和链接这两个步骤,在Windows下被IDE封装的很完美,我们一般是使用一键编译并运行,但是当链接出错的话我们就束手无措了。在Linux下有gcc/g++编译器,可以直接展示出编译链接的过程。

在软件开发中,编译是将程序的源代码(通常是人类可读的高级语言,如 C/C++)翻译成 CPU 能够直接执行的机器代码(二进制代码)。通过这一步骤,源文件被转换为目标文件,为后续的链接奠定基础。
以一个简单的例子为例:假设我们有一个源文件 hello.c,其内容如下:
// hello.c
#include <stdio.h>
int main() {
printf("hello world!\n");
return 0;
}使用 gcc 编译器,我们可以通过以下命令编译该源文件:
$ gcc -c hello.c编译完成后,生成一个扩展名为 .o 的文件(例如 hello.o),被称为目标文件(Object File)。我们可以通过以下命令查看生成的文件:
$ ls
hello.c hello.ofile 命令可以检查目标文件的类型,例如:$ file hello.o
hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped通过编译过程,我们可以生成目标文件,并了解 ELF 格式作为二进制文件封装的重要作用。
如果我们修改了一个源文件,那么我们只需要单独编译这一个文啊进,而不是重新编译整个工会测过,将目标文件编译后重新链接即可。
为了全面理解编译和链接的细节,我们需要深入了解 ELF(Executable and Linkable Format)文件格式。ELF 是一种通用的二进制文件格式,在 Linux 系统中广泛用于目标文件、可执行文件、共享库以及内核转储等。以下是 ELF 文件的四种主要类型及其特点:
.o 文件(目标文件)。gcc -c 命令生成,尚未进行最终的地址解析和链接。a.out 或其他二进制可执行文件)。.so 文件(动态库)。一个 ELF 文件由以下四个主要部分组成:
.bss(未初始化的全局变量和静态变量)、.rodata(只读数据,如字符串字面量)等,具体取决于文件类型和编译选项。
ELF(Executable and Linkable Format)文件是 Linux 系统中编译和链接的核心格式。为了生成可执行文件,涉及以下两个主要步骤,并补充相关的知识点:
.o 文件gcc 或 g++)将 C/C++ 源代码(.c 或 .cpp 文件)翻译成目标文件(.o 文件)。编译器会执行以下几个阶段: #include、宏定义和条件编译指令,生成预处理文件(.i 文件)。.s 文件),生成特定架构的机器指令。.o 文件),格式为 ELF 可重定位文件。gcc -c 编译源文件,例如:$ gcc -c source1.c -o source1.o
$ gcc -c source2.c -o source2.o.text Section)、数据(.data 和 .bss Section)、符号表(.symtab)和重定位信息(.rela),但地址尚未最终确定(符号引用未解析)。gcc -Wall 可启用警告选项,gcc -g 可生成调试信息(.debug Section),便于调试。.o 文件的 Section 进行合并ld)将多个目标文件(.o 文件)的各个 Section 合并,并可能与库文件(如静态库 .a 或动态库 .so)结合,生成最终的可执行文件(.out 或指定名称)。.text(代码)、.data(初始化数据)、.bss(未初始化数据)、.rodata(只读数据)等 Section。.symtab)和重定位表(.rela),解决未定义符号(如函数或变量的引用),确保所有地址引用正确。.dynsym)和全局偏移表/过程链接表(.got.plt),为运行时加载动态库做准备。$ gcc source1.o source2.o -o program或直接从源文件生成:
$ gcc source1.c source2.c -o program.a)内容直接嵌入可执行文件;动态链接则引用动态库(.so),仅记录加载信息,运行时由动态链接器(如 /lib64/ld-linux-x86-64.so.2)加载。.debug Section,便于使用 gdb 调试。ld 的 linker script),可自定义内存布局和 Section 合并规则。
当生成的 ELF 可执行文件加载到内存中时,操作系统会根据其结构完成对ELF中不同的Section的合并,形成segment。
.text、.data、.rodata)是链接视图的逻辑单元,描述文件内容的组织方式。.text 和 .rodata)、可读写段(如包含 .data 和 .bss)或可执行段。.text 部分为 4097 字节,.init 部分为 512 字节,未合并时需 3 个页面(4096 × 3 = 12288 字节);合并后可能仅需 2 个页面(4096 × 2 = 8192 字节)。**程序头表(Program header table)**确定,程序头表记录了每个 Segment 的起始地址、长度、权限和文件偏移等信息。.dynamic 和 .got.plt Section,用于运行时解析共享库符号。readelf -S 命令。例如:$ readelf -S a.out输出显示可执行文件中的各个 Section,如 .text(代码)、.data(初始化数据)、.rodata(只读数据)、.bss(未初始化数据)等,及其属性(地址、偏移、大小、权限等)。
readelf -l 命令。例如:$ readelf -l a.out输出显示程序头表中的 Segment 信息,包括类型(如 LOAD、DYNAMIC)、虚拟地址、文件偏移、文件大小、内存大小、权限(R/W/E)和对齐方式。
- `LOAD` 段:需要加载到内存的代码和数据段,可能包含 `.text`、`.data` 等 Section。
- `DYNAMIC` 段:用于动态链接,包含动态库加载信息。
- `GNU_STACK` 段:指定栈的权限(通常可读写)。$ readelf -S a.outThere are 30 section headers, starting at offset 0x1a50:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000 # 空 Section,用于占位
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000400238 00000238 # 程序解释器路径(动态链接器)
000000000000001c 0000000000000000 A 0 0 1
[ 2] .note.ABI-tag NOTE 0000000000400254 00000254 # ABI 版本信息
0000000000000020 0000000000000000 A 0 0 4
[ 3] .note.gnu.build-i NOTE 0000000000400274 00000274 # 构建 ID,唯一标识可执行文件
0000000000000024 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000400298 00000298 # GNU 哈希表,加速符号查找
0000000000000024 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000004002c0 000002c0 # 动态符号表
00000000000000f0 0000000000000018 A 6 1 8
[ 6] .dynstr STRTAB 00000000004003b0 000003b0 # 动态字符串表(符号名称)
000000000000008b 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 000000000040043c 0000043c # 符号版本信息
0000000000000014 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000400450 00000450 # 符号版本需求信息
0000000000000020 0000000000000000 A 6 1 8
[ 9] .rela.dyn RELA 0000000000400470 00000470 # 动态重定位表
0000000000000030 0000000000000018 A 5 0 8
[10] .rela.plt RELA 00000000004004a0 000004a0 # PLT(过程链接表)重定位表
00000000000000c0 0000000000000018 AI 5 23 8
[11] .init PROGBITS 0000000000400560 00000560 # 程序初始化代码
000000000000001a 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 0000000000400580 00000580 # 过程链接表(PLT)
0000000000000090 0000000000000010 AX 0 0 16
[13] .text PROGBITS 0000000000400610 00000610 # 程序代码段
00000000000001e2 0000000000000000 AX 0 0 16
[14] .fini PROGBITS 00000000004007f4 000007f4 # 程序终止代码
0000000000000009 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000400800 00000800 # 只读数据段
0000000000000024 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000400824 00000824 # 异常处理框架头
0000000000000034 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000400858 00000858 # 异常处理框架数据
00000000000000f4 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000600de0 00000de0 # 初始化函数指针数组
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000600de8 00000de8 # 终止函数指针数组
0000000000000008 0000000000000008 WA 0 0 8
[20] .jcr PROGBITS 0000000000600df0 00000df0 # Java 类注册信息
0000000000000008 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000600df8 00000df8 # 动态链接信息
0000000000000200 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000600ff8 00000ff8 # 全局偏移表(GOT)
0000000000000008 0000000000000008 WA 0 0 8
[23] .got.plt PROGBITS 0000000000601000 00001000 # PLT 相关的 GOT
0000000000000058 0000000000000008 WA 0 0 8
[24] .data PROGBITS 0000000000601058 00001058 # 数据段
0000000000000004 0000000000000000 WA 0 0 1
[25] .bss NOBITS 0000000000601060 0000105c # 未初始化数据段
0000000000000010 0000000000000000 WA 0 0 16
[26] .comment PROGBITS 0000000000000000 0000105c # 编译器注释信息
000000000000002d 0000000000000001 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 00001090 # 符号表
0000000000000678 0000000000000018 28 46 8
[28] .strtab STRTAB 0000000000000000 00001708 # 字符串表(符号名称)
000000000000023f 0000000000000000 0 0 1
[29] .shstrtab STRTAB 0000000000000000 00001947 # Section 名称字符串表
0000000000000108 0000000000000000 0 0 1
.text:
.data:
.bss(better save space):
data,而是在bss中记录有多少个变量,因为所有的全局变量都是未知的,没有初始化,过于臃肿。bss读取,初始化为0。这就是为什么为初始化的变量会自动初始化为0的原因。$ readelf -l a.outElf 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 # 注释信息(ABI、构建 ID)
0x0000000000000044 0x0000000000000044 R 4
GNU_EH_FRAME 0x00000000000005a0 0x00000000004005a0 0x00000000004005a0 # 异常处理框架信息
0x000000000000004c 0x000000000000004c R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 # 栈权限(RW,不可执行)
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 .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .plt.got .text .fini .rodata .eh_frame_hdr .eh_frame # 代码段
03 .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss # 数据段
04 .dynamic # 动态链接信息
05 .note.ABI-tag .note.gnu.build-id # 注释信息
06 .eh_frame_hdr # 异常处理框架信息
07
08 .init_array .fini_array .jcr .dynamic .got # 初始化相关段.text 为可执行只读,.data 为可读写),由操作系统通过内存保护机制(如 MMU)实现,提高程序安全性。ld-linux.so)会解析 .dynamic 和 .got.plt Section,加载共享库并绑定符号。.text:保存机器指令,是程序执行的核心,权限通常为可执行只读。.data:保存已初始化的全局变量和局部静态变量,权限为可读写。.rodata:保存只读数据(如字符串字面量),只能存在于只读段(通常与 .text 合并)。.bss:为未初始化的全局变量和局部静态变量预留空间,实际数据在运行时初始化,权限为可读写。.symtab:符号表,记录函数名、变量名与代码或数据的对应关系,用于链接阶段解析符号引用。.rela:重定位表,记录需要调整地址的符号引用位置,链接器根据此表修正地址。.debug:调试信息,包含源代码行号、变量名和类型等,供调试工具(如 gdb)使用。.got.plt:全局偏移表和过程链接表,用于动态链接,保存共享库函数的间接引用地址,运行时由动态链接器修改。readelf -S 命令查看目标文件(如 hello.o)或可执行文件(如 a.out)的节头表。LOAD、DYNAMIC、GNU_STACK),描述每个 Segment 的虚拟地址、文件偏移、大小、权限和对齐方式。**Program Header Table**LOAD:需要加载到内存的代码和数据段,包含 .text、.data、.rodata 等 Section。DYNAMIC:动态链接信息,包含共享库依赖和符号解析数据。GNU_STACK:栈段的权限设置(通常可读写)。GNU_RELRO:只读重定位段,保护动态链接后的数据免受修改。readelf -l 命令查看可执行文件的程序头表。 /lib64/ld-linux-x86-64.so.2)解析 .dynamic 和 .got.plt,加载共享库并绑定符号,确保程序运行时能访问外部函数。总结:
二者说白了就是,一个在链接时用,一个在运行时用。 执行命令查看的内容在
3.2.2中已展示。

.symtab 符号表的基本概念.symtab 是 ELF 文件中的一个重要 Section(节),称为符号表(Symbol Table)。它是存储程序中符号(函数名、变量名等)及其相关信息的表格,用于描述源码中的标识符(如函数、变量)与目标文件或可执行文件中代码和数据的对应关系。.symtab 通常位于目标文件(.o)或可执行文件(.out)中,属于链接视图(Linking View)的部分,存储在节头表(Section Header Table)中描述的 Section 中。main、变量 label),确保程序的正确连接和地址分配。gdb)提供符号信息,映射源码中的标识符到内存地址,便于定位和分析。gcc -s)去除 .symtab,减小文件大小,但会失去调试能力。类似于数组,将每个符号分隔,独立存储。
.symtab 与源码的对应关系.symtab 是源码中函数名、变量名和代码对应关系的“桥梁”,具体来说:
int main(void))和变量(如 char label[] = "helloworld";)。这些名称是人类可读的标识符。.symtab 中,并关联到目标文件中对应的代码(.text Section)或数据(.data、.bss 或 .rodata Section)。.symtab 记录每个符号的名称、类型、地址(或偏移量)、大小和所属 Section。例如: main 可能记录在 .text Section,符号表条目显示其类型为 FUNC(函数),地址为某个虚拟地址。label 可能记录在 .data 或 .rodata Section,符号表条目显示其类型为 OBJECT(对象/变量),地址为数据段的偏移量。printf),但未在当前文件定义,.symtab 会标记这些符号为 UND(未定义),等待链接器从其他目标文件或库(如 libc)中解析和绑定。.symtab 的结构与内容符号表(.symtab)由多个符号表条目(Symbol Table Entries)组成,每个条目包含以下字段(可以通过 nm 或 readelf -s 查看):
main、label、printf)。NOTYPE:未指定类型(通常为未定义符号)。OBJECT:变量或数据对象(如 label)。FUNC:函数(如 main)。SECTION:Section 本身。FILE:源文件名称。LOCAL:本地符号,仅在当前文件可见。GLOBAL:全局符号,可被其他文件引用。WEAK:弱符号,如果未定义则可被忽略。.text、.data、.bss 或 UND 表示未定义)。示例:符号表条目
假设有一个简单的 C 源码:
#include <stdio.h>
char label[] = "helloworld";
int main(void) {
printf("Hello, world!\n");
return 0;
}编译生成目标文件 example.o:
$ gcc -c example.c -o example.o
$ nm example.o输出可能如下(简化):
0000000000000000 T main # 地址 0x0,类型 FUNC,绑定 GLOBAL,位于 .text
0000000000000000 D label # 地址 0x0,类型 OBJECT,绑定 GLOBAL,位于 .data
U printf # 未定义,类型 NOTYPE,绑定 GLOBAL,位于 UND(需链接 libc)main:函数,存储在 .text Section,地址为 0(可重定位文件中的相对地址,链接后确定)。label:变量,存储在 .data Section,地址为 0(链接后确定)。printf:未定义符号,标记为 U,需从标准库 libc 中解析。链接生成可执行文件 example:
$ gcc example.o -o example
$ nm example输出中 main 和 label 的地址变为具体值(如 0x401000),printf 的地址也从 libc 绑定。
.symtab 的生成与使用gcc)在编译源代码时,解析源码中的函数和变量,生成目标文件(.o)。.symtab Section,记录符号的名称、类型和临时地址(相对于 Section 的偏移)。UND,等待链接器处理。ld)读取 .symtab,解析未定义符号(如 printf),从库文件(如 libc.a 或 libc.so)或其他目标文件中查找定义,分配最终地址。gdb)使用 .symtab 将源码中的函数名和变量名映射到内存地址,方便设置断点、查看变量值。nm 和 readelf -s 可查看符号表,分析程序结构和依赖。.symtab您可以使用以下命令查看符号表:
nm 命令:$ nm hello.o # 查看目标文件的符号表
$ nm a.out # 查看可执行文件的符号表- 输出显示符号名称、地址、类型和 Section(如 `T` 为 `.text`,`D` 为 `.data`,`U` 为未定义)。readelf -s 命令:$ readelf -s hello.o # 详细查看目标文件的符号表- 输出包括符号的名称、值、大小、类型、绑定和 Section 索引,提供更详细的信息。示例输出(如 readelf -s hello.o):
Symbol table '.symtab' contains 13 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
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 17 OBJECT GLOBAL DEFAULT 3 label
5: 0000000000000000 0 FUNC GLOBAL DEFAULT 1 main
6: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printflabel:OBJECT 类型,GLOBAL 绑定,位于 Section 3(可能为 .data 或 .rodata),大小为 17 字节(字符串 "helloworld\0" 的长度加终止符)。main:FUNC 类型,GLOBAL 绑定,位于 Section 1(.text),大小为 0(实际大小由链接后确定)。printf:NOTYPE 类型,GLOBAL 绑定,位于 UND(未定义),需链接器从 libc 解析。strip 命令去除 .symtab 和调试信息:$ strip hello.o # 去除符号表和调试信息但这会使调试变得困难。
.dynsym(动态符号表),用于动态链接,记录与共享库相关的符号(如 libc 中的函数)。.symtab 和 .dynsym 的区别在于:.symtab 包含所有符号(包括本地和全局),而 .dynsym 只包含与动态链接相关的全局符号。.symtab 可能占较大空间,尤其在包含大量函数和变量的程序中。通过优化代码或使用 gcc -fvisibility=hidden 减少导出符号,可以减小符号表大小。.symtab.symtab 是源码中函数名、变量名和代码对应关系的“映射表”,记录程序的符号及其在目标文件或可执行文件中的位置和属性。int main(void) 对应 .symtab 中的 main 条目,指向 .text Section 的代码。char label[] = "helloworld"; 对应 .symtab 中的 label 条目,指向 .data 或 .rodata Section 的数据。printf)标记为未定义(UND),链接时从标准库(如 libc)解析。nm、readelf -s 查看符号表,结合源码和目标文件理解符号的定义和引用。
ELF 头(ELF Header)位于文件开头,描述文件的基本信息,并定位程序头表和节头表。
查看目标文件(**.o**** 文件)**
$ readelf -h hello.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64 # 64 位 ELF 文件
Data: 2's complement, little endian # 小端字节序
Version: 1 (current) # ELF 格式版本
OS/ABI: UNIX - System V # 目标操作系统
ABI Version: 0 # ABI 版本
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: 728 (bytes into file) # 节头表偏移
Flags: 0x0 # 特定标志(无特殊标志)
Size of this header: 64 (bytes) # ELF 头大小
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 # 节名字符串表的索引查看可执行文件
$ gcc *.o -o a.out
$ readelf -h a.out
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 # ELF文件的标识符
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file) # 文件类型为共享对象(可执行文件)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1060 # 程序入口地址
Start of program headers: 64 (bytes into file) # 程序头表偏移
Start of section headers: 14768 (bytes into file) # 节头表偏移
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 13
Size of section headers: 64 (bytes)
Number of section headers: 31
Section header string table index: 30ELF 头的作用:
Magic(魔数 7f 45 4c 46),每个二进制文件都有,随机,系统可以通过magic标识文件为 ELF 格式,防止误解析。 exe会直接运行。Type 字段区分文件类型:REL(可重定位)、EXEC(可执行)、DYN(共享对象)。Start of program headers 和 Start of section headers)用于定位文件的其他部分,确保解析器正确读取数据。Entry point address)指定程序启动时的起始指令地址(通常指向用于存储代码的 .text Section 的起始位置)。ELF 文件的整体结构:像一本书
想象 ELF 文件是一本书,每个部分都有自己的“页码”(偏移量)和作用:
e_phoff 和 e_shoff 指定)。e_phoff 字段指定。.text)、数据段(.data)等。e_phoff 决定。例如,如果 e_phoff = 64,意味着程序头表从文件第 64 字节开始。e_phnum 指定。.text:存储程序的机器代码。.data:存储初始化过的全局变量。.bss:存储未初始化的全局变量(不占文件空间,只记录大小)。.symtab:存储符号表(函数名、变量名等)。.text 节可能从偏移量 1024 开始,.data 节从 2048 开始。e_shoff 字段指定。.text 节从偏移量 1024 开始,大小 512 字节,可执行。e_shoff 决定。例如,e_shoff = 5000 意味着节头表从文件第 5000 字节开始。e_shnum 指定。.text)从 10 页开始,讲故事;第 2 章(.data)从 20 页开始,放插图。” 它的“页码”(偏移量)由 ELF 头指明。ELF 文件中的每个区域通过偏移量紧密关联,以下是它们的位置和依赖关系:
e_phoff)和节头表(e_shoff)的偏移量。e_phoff 指定,描述段的偏移量和内存映射。e_shoff 指定,通常在末尾,记录所有节的偏移量和属性。文件偏移量 (字节):
0 64 128 1024 2048 5000
|----------|-----------|-----------|----------|----------|----------|
ELF 头 程序头表 (其他内容) .text 节 .data 节 节头表.text 和 .rodata 合成一个只读段)。段的偏移量记录在程序头表中。readelf 或 objdump)定位 ELF 文件的每一部分。