当前位置: 首页 > news >正文

深入理解目标文件:从ELF格式到链接核心

作为程序员,我们每天都在和“编译-链接”打交道——写好的C/C++代码经过gcc编译后生成.o文件,再链接成可执行程序。但你是否好奇过,那个不起眼的.o文件(目标文件)里到底装了什么?为什么链接器能把多个.o文件“拼”成可执行程序?今天我们就以Linux下的ELF格式为核心,拆解目标文件的内部结构,揭开编译链接的底层逻辑。

一、目标文件:编译与链接的“中间桥梁”

首先要明确一个概念:目标文件是源代码编译后、未链接的中间文件,它包含了机器指令、数据,以及链接所需的符号、重定位等信息。从广义上看,目标文件与可执行文件、动态库(.so)、静态库(.a)的格式是“同源”的——它们都基于ELF(Executable Linkable Format)标准,只是用途不同。

1. 主流目标文件格式对比

不同操作系统对目标文件格式的选择不同,但本质上都源于COFF(Common Object File Format)标准:

平台目标文件格式可执行文件格式动态库格式静态库格式
WindowsPE-COFF(.obj)PE(.exe).dll.lib
Linux/macOSELF(.o)ELF(无后缀).so.a

为什么Linux选择ELF?因为它具备跨架构性(支持x86、ARM、RISC-V等)、灵活性(可按需扩展段结构)和高效性(链接和装载速度快),是目前类Unix系统的事实标准。

2. 目标文件与可执行文件的区别

很多人会混淆目标文件(.o)和可执行文件(ELF),其实核心差异在“链接状态”:

  • 目标文件(.o):未完成链接,包含未解析的符号(如引用的外部函数printf),虚拟地址(VMA)为0,需要链接器进一步处理。
  • 可执行文件(ELF):已完成链接,所有符号地址已确定,包含程序头表(Program Header Table),操作系统可直接装载到内存运行。

举个例子,用file命令查看两者的区别:

# 目标文件:ELF 64-bit LSB relocatable(可重定位文件)
file SimpleSection.o
SimpleSection.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped# 可执行文件:ELF 64-bit LSB pie executable(位置无关可执行文件)
file a.out
a.out: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, not stripped

二、ELF目标文件的核心结构:段与表

ELF文件的本质是“按功能划分的段(Section)+ 描述段的表(Table)”。可以把它想象成一个“档案夹”:每个段是一份“文件”(如代码、数据),段表是“档案目录”(记录每个文件的位置、大小、属性)。

1. 三大核心段:.text、.data、.bss

目标文件中最关键的三个段,对应程序的“代码”和“数据”,也是编译器对源代码的核心拆分:

段名存储内容读写属性是否占用文件空间典型示例
.text编译后的机器指令(函数代码)只读/可执行main()func1()的指令
.data已初始化的全局变量/静态变量可读可写int global_init_var = 84;static int static_var = 85;
.bss未初始化的全局变量/静态变量可读可写int global_uninit_var;static int static_var2;

这里有两个关键细节需要扩展:

(1).bss段为什么不占文件空间?

未初始化的变量默认值为0,ELF文件不会在磁盘上存储“一堆0”(浪费空间),而是在段表中记录.bss段的大小。当程序加载到内存时,操作系统会分配一块“零初始化”的内存页,映射到.bss段的地址,既节省磁盘空间,又保证变量初始值为0。

size命令可以验证这一点:

# 查看目标文件的段大小(单位:字节)
size SimpleSection.otext    data     bss     dec     hex filename100       8       8     116      74 SimpleSection.o

其中.bss的8字节是“需要分配的内存大小”,而非文件中实际占用的空间。

(2)局部变量为什么不在任何段中?

main()中的int a = 1;int b;这类局部变量,编译后并不会存储在.data.bss中——它们的内存是在程序运行时,通过栈(Stack) 动态分配的(栈指针%rsp偏移实现)。只有“生命周期与程序一致”的变量(全局变量、静态变量)才会存入.data.bss

2. 其他重要段:支撑链接与调试

除了三大核心段,目标文件中还有多个“辅助段”,用于链接、调试等功能:

段名作用关键用途
.rodata只读数据(字符串常量、const变量)printf("%d\n", i);中的"%d\n"
.symtab符号表(记录函数、变量的名称和地址)链接时解析符号(如printf
.strtab符号名称字符串表存储.symtab中符号的名称(如"main")
.rela.text重定位表(针对.text段的地址修正信息)修正printf等外部函数的调用地址
.shstrtab段名的字符串表存储所有段的名称(如".text"、“.data”)
.debug_info调试信息(源码行号、变量类型)gdb调试时显示源码对应关系

3. 段表(Section Header Table):ELF的“目录”

段表是ELF文件的“大脑”,它是一个结构体数组(每个结构体对应一个段),记录了每个段的位置、大小、属性、关联段等关键信息。链接器(ld)和装载器(操作系统)完全依赖段表来定位和操作各个段。

readelf -S可以查看完整的段表:

readelf -S SimpleSection.o
There are 14 section headers, starting at offset 0x410:Section Headers:[Nr] Name              Type             Address           OffsetSize              EntSize          Flags  Link  Info  Align[ 0]                   NULL             0000000000000000  000000000000000000000000  0000000000000000           0     0     0[ 1] .text             PROGBITS         0000000000000000  000000400000000000000064  0000000000000000  AX       0     0     16[ 2] .rela.text        RELA             0000000000000000  000003280000000000000078  0000000000000018   I      11     1     8...[11] .symtab           SYMTAB           0000000000000000  000001580000000000000138  0000000000000018          12     8     8[12] .strtab           STRTAB           0000000000000000  000002c00000000000000066  0000000000000000           0     0     1[13] .shstrtab         STRTAB           0000000000000000  000003d00000000000000074  0000000000000000           0     0     1

段表中每个字段的含义(以.text段为例):

  • Type(类型)PROGBITS表示“程序代码/数据段”;
  • Flags(属性)AX表示SHF_ALLOC(需要分配内存)和SHF_EXECINSTR(可执行);
  • Offset(偏移).text段在文件中的起始位置是0x40
  • Size(大小).text段大小是0x64(100字节);
  • Align(对齐):16字节对齐(x86-64下指令段默认16字节对齐,提升CPU缓存命中率)。

4. ELF文件头(ELF Header):文件的“身份证”

ELF文件的最开头是“文件头”,用Elf64_Ehdr(64位)或Elf32_Ehdr(32位)结构体表示,记录了整个文件的基本属性,让操作系统/工具能快速识别文件类型。

readelf -h查看文件头:

readelf -h SimpleSection.o
ELF Header:Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00Class:                             ELF64Data:                              2's complement, little endianVersion:                           1 (current)OS/ABI:                            UNIX - System VABI Version:                       0Type:                              REL (Relocatable file)Machine:                           Advanced Micro Devices X86-64Version:                           0x1Entry point address:               0x0Start of program headers:          0 (bytes into file)Start of section headers:          1040 (bytes into file)Flags:                             0x0Size of this header:               64 (bytes)Size of program headers:           0 (bytes)Number of program headers:         0Size of section headers:           64 (bytes)Number of section headers:         14Section header string table index: 13

其中最关键的是“Magic(魔数)”:7f 45 4c 46是ELF文件的唯一标识(任何ELF文件开头都是这4个字节),后续字节分别表示:

  • 02:64位ELF(01表示32位);
  • 01:小端序(02表示大端序);
  • 01:ELF版本(当前固定为1)。

三、链接的核心:符号与重定位

为什么多个目标文件能链接成一个可执行程序?核心是“符号解析”和“重定位”——前者解决“函数/变量在哪里”,后者解决“地址如何修正”。

1. 符号:链接的“接口”

在ELF中,符号(Symbol) 是函数、变量的“别名”,每个目标文件都有一张符号表(.symtab),记录了当前文件的符号(定义的符号)和引用的外部符号(未定义的符号)。

nm命令查看符号表:

nm SimpleSection.o
0000000000000000 T func1
000000000000002b T main
0000000000000004 D global_init_var
0000000000000000 D static_var
0000000000000004 B static_var2
0000000000000000 B global_uninit_varU printf

符号类型的含义(关键字母):

符号类型含义示例
T/t代码段符号(T:全局,t:局部)main()(T)、静态函数(t)
D/d已初始化数据段符号(D:全局,d:局部)global_init_var(D)
B/b未初始化数据段符号(B:全局,b:局部)global_uninit_var(B)
U未定义符号(引用外部符号)printf(在libc中定义)
W/w弱符号(W:全局,w:局部)库中的默认函数
强弱符号与强弱引用

符号分为“强符号”和“弱符号”,引用分为“强引用”和“弱引用”,这是解决符号冲突和可选依赖的关键:

  • 强符号:默认初始化的全局变量、函数(如int a = 1;void func());
  • 弱符号:未初始化的全局变量、用__attribute__((weak))声明的符号(如int a;__attribute__((weak)) void func());
  • 强引用:引用符号时,链接器必须找到定义(否则报错undefined reference);
  • 弱引用:引用符号时,找不到定义也不报错(符号值为0,程序可判断是否存在)。

链接规则

  1. 强符号不能重复定义(多个文件定义同名强符号,链接报错);
  2. 一个强符号 + 多个弱符号:选择强符号;
  3. 多个弱符号:选择占用空间最大的符号。

弱引用的典型场景是“插件化”——比如程序引用一个可选插件的函数,若插件未加载,弱引用不会报错,程序可通过判断符号是否为0来决定是否执行插件逻辑。

2. 重定位:修正符号地址

目标文件编译时,引用的外部符号(如printf)的地址是未知的(因为printf在libc中,还未链接),编译器会在.text段中留下一个“占位符”,而重定位表(.rela.text) 就记录了这些占位符的位置和修正方式。

readelf -r查看重定位表:

readelf -r SimpleSection.o
Relocation section '.rela.text' at offset 0x328 contains 6 entries:Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000000000001b  0000000600000001 R_X86_64_32    0000000000000000 .rodata + 0
0000000000000023  0000000700000002 R_X86_64_PC32  0000000000000000 printf - 4
...

这里的核心是“重定位类型(Type)”,以x86-64为例:

  • R_X86_64_PC32:PC相对地址重定位,用于函数调用(如printf)。修正公式为:最终地址 = 符号地址 - 重定位地址 - 4-4是因为PC指向当前指令的下一条,需要调整偏移);
  • R_X86_64_32:绝对地址重定位,用于引用只读数据(如.rodata中的字符串)。修正公式为:最终地址 = 符号地址 + 附加偏移

链接器的工作就是遍历重定位表,根据公式计算每个占位符的最终地址,替换.text段中的占位符,最终生成所有地址都确定的可执行文件。

四、C与C++的符号差异:名字修饰

为什么C++支持函数重载(如func(int)func(double)),而C不支持?核心是“符号修饰(Name Mangling)”——C++编译器会将函数的参数类型编码到符号名中,而C不会。

1. C语言的符号修饰

C语言的符号修饰非常简单,大多数编译器(如GCC)会直接使用函数名作为符号名(无额外修饰),比如void func(int)的符号是func。但早期Windows的MSVC会在符号前加_(如_func),不过这是编译器差异,不是标准。

2. C++的名字修饰

C++为了支持重载、命名空间、类继承等特性,会对符号名进行“编码”,不同编译器的修饰规则不同(主流是Itanium C++ ABI,GCC、Clang采用)。

例如:

  • void func(int) → 修饰后符号:_Z4funci_Z:前缀,4:函数名长度,func:函数名,i:参数类型int);
  • void func(double) → 修饰后符号:_Z4funcdd:参数类型double);
  • namespace A { void func(int) } → 修饰后符号:_ZN1A4funciN:命名空间开始,1A:命名空间A长度1);
  • class B { void func(int) } → 修饰后符号:_ZN1B4funci1B:类B长度1)。

正因为C++的符号修饰规则与C不同,若用C++编译的函数要给C代码调用,必须用extern "C"声明,强制编译器按C规则修饰符号:

// C++代码:用extern "C"声明,符号按C规则修饰为"add"
extern "C" int add(int a, int b) {return a + b;
}

C代码调用时,直接引用add符号即可,不会因修饰规则不同导致“undefined reference”错误。

五、实用工具:分析目标文件的“利器”

掌握以下工具,能帮你快速分析ELF目标文件的结构和问题:

工具核心用途常用参数
objdump反汇编、查看段信息、符号表-h(段信息)、-d(反汇编)、-t(符号表)、-C(demangle C++符号)
readelf查看ELF文件头、段表、重定位表、符号表-h(文件头)、-S(段表)、-r(重定位表)、-s(符号表)
nm查看符号表(简洁版)-A(显示文件名)、-u(只显示未定义符号)、-l(显示符号所在源码行)
size查看代码段、数据段、bss段的大小-A(显示文件名)、-t d(十进制显示)
strip去除目标文件中的调试信息(减小文件大小)-g(只去除调试信息)、-s(去除所有符号)

六、总结:理解目标文件的意义

目标文件是编译链接的“中间产物”,也是理解程序运行机制的“钥匙”。掌握ELF格式和目标文件结构,能帮你解决很多实际问题:

  1. 排查链接错误:比如undefined reference(未定义符号)、multiple definition(符号重复定义),通过nmreadelf定位符号来源;
  2. 优化程序性能:比如调整段对齐(提升CPU缓存命中率)、减少.bss段大小(降低内存占用);
  3. 逆向工程与安全:比如反汇编.text段分析代码逻辑、查看符号表了解程序结构;
  4. 深入理解动态链接:目标文件的符号和重定位机制,是动态库(.so)加载的基础。

参考:https://mp.weixin.qq.com/s/dWGQgqsp-7ZKqqNA67Ur2A

http://www.dtcms.com/a/414424.html

相关文章:

  • Java系列知识之 ~ Spring 与 Spring Boot 常用注解对比说明
  • 郫县建设局网站wordpress如何运行
  • LeetCode 114.二叉树展开为链表
  • 机器人中的电机与扭矩入门
  • 站长平台如何推广自己的网站怎么做网页版网站
  • 深入理解BFC:解决margin折叠和浮动高度塌陷的利器
  • Spec Kit - 规范驱动开发工具包
  • 具有价值的常州做网站免费进销存软件哪个简单好用
  • creo二次开发seo职位信息
  • Windows 10 环境下 Redis 编译与运行指南
  • 【编号206】房地产统计年鉴2002~2023
  • 某大型广告公司实习感受
  • 【Day 68】Zabbix-自动监控-Web检测-分布式监控
  • 企业网站建设公司公司网站开发客户挖掘
  • 天拓四方集团IOT平台:赋能电气设备制造商数智化转型新引擎
  • 【STM32项目开源】基于STM32的智能鱼缸养殖系统
  • 【小迪安全v2023】学习笔记集合 --- 持续更新
  • Django - DRF
  • Python全方位处理XML指南:解析、修改与重写实战
  • LabVIEW实现B样条曲线拟合
  • 门户网站系统建设招标文件中国建设教育协会网站培训中心
  • 常熟网站网站建设在线教育自助网站建设平台
  • 【Linux】深入探索多线程编程:从互斥锁到高性能线程池实战
  • 广州手机网站建设报价沧州市政务服务大厅
  • .net网站开发程序员深圳专业网站建设公司
  • DedeCMS命令执行复现研究 | CVE-2025-6335
  • BJDCTF2020
  • LeetCode:239. 滑动窗口最大值
  • 文件上传漏洞(二)iis6.0 CGI漏洞
  • PHP的json_encode()函数了解