news 2026/4/21 20:26:17

从ELF文件头到.text段:手把手教你用objdump拆解Linux可执行文件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从ELF文件头到.text段:手把手教你用objdump拆解Linux可执行文件

从ELF文件头到.text段:手把手教你用objdump拆解Linux可执行文件

在计算机的世界里,每个可执行程序都像一本精心编排的书,而ELF(Executable and Linkable Format)就是这本书的标准格式。对于逆向工程初学者来说,理解ELF文件结构就像获得了一把打开程序内部世界的钥匙。今天,我们就用Linux下的objdump工具,像侦探一样解剖一个简单的"hello world"程序,看看编译后的代码在磁盘上究竟是如何组织的。

1. 准备工作:认识我们的工具和目标

在开始之前,我们需要准备两样东西:一个简单的可执行文件和一个强大的分析工具。让我们先创建一个经典的"hello world"程序:

// hello.c #include <stdio.h> int main() { printf("Hello, World!\n"); return 0; }

用gcc编译这个程序:

gcc -g -o hello hello.c

现在,我们有了可执行文件hello,接下来就可以使用objdump这个瑞士军刀般的工具来解剖它了。objdump是GNU binutils工具集的一部分,能够显示目标文件的各种信息,包括:

  • 文件头信息(-f选项)
  • 段头表(-h选项)
  • 段内容(-s选项)
  • 反汇编代码(-d选项)

2. 初探ELF文件头:程序的身份证

ELF文件头是整个文件的起点,包含了描述文件基本属性的元数据。让我们先用-f选项查看文件头信息:

objdump -f hello

你会看到类似这样的输出:

hello: 文件格式 elf64-x86-64 体系结构:i386:x86-64,标志 0x00000150: HAS_SYMS, DYNAMIC, D_PAGED 起始地址 0x0000000000401040

这个输出告诉我们几个关键信息:

  1. 文件格式:elf64-x86-64,表示这是一个64位的ELF文件,针对x86-64架构
  2. 标志位
    • HAS_SYMS:表示文件包含符号表
    • DYNAMIC:表示这是一个动态链接的可执行文件
    • D_PAGED:表示文件是分页的
  3. 入口点地址:0x0000000000401040,这是程序开始执行的第一条指令的地址

ELF文件头实际上包含更多细节,我们可以用readelf工具查看更完整的信息(这不是本文重点,但值得了解):

readelf -h hello

3. 解析段头表:程序的组织架构

ELF文件由多个段(segment)和节(section)组成。段是程序加载和执行时使用的单位,而节是链接和重定位时使用的单位。用-h选项可以查看段头表:

objdump -h hello

典型输出会列出所有段的信息,包括:

节: Idx Name Size VMA LMA File off Algn 0 .interp 0000001c 0000000000400238 0000000000400238 00000238 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 1 .note.ABI-tag 00000020 0000000000400254 0000000000400254 00000254 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 2 .note.gnu.build-id 00000024 0000000000400274 0000000000400274 00000274 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 3 .gnu.hash 00000024 0000000000400298 0000000000400298 00000298 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 4 .dynsym 00000060 00000000004002b8 00000000004002b8 000002b8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 5 .dynstr 0000003a 0000000000400318 0000000000400318 00000318 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA 6 .gnu.version 00000008 0000000000400352 0000000000400352 00000352 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA 7 .gnu.version_r 00000020 0000000000400360 0000000000400360 00000360 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 8 .rela.dyn 00000018 0000000000400380 0000000000400380 00000380 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 9 .rela.plt 00000030 0000000000400398 0000000000400398 00000398 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 10 .init 00000017 00000000004003c8 00000000004003c8 000003c8 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 11 .plt 00000030 00000000004003e0 00000000004003e0 000003e0 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 12 .text 00000192 0000000000400410 0000000000400410 00000410 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE 13 .fini 00000009 00000000004005a4 00000000004005a4 000005a4 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE 14 .rodata 00000011 00000000004005b0 00000000004005b0 000005b0 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 15 .eh_frame_hdr 00000034 00000000004005c4 00000000004005c4 000005c4 2**2 CONTENTS, ALLOC, LOAD, READONLY, DATA 16 .eh_frame 000000f4 00000000004005f8 00000000004005f8 000005f8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA 17 .init_array 00000008 0000000000600e10 0000000000600e10 00000e10 2**3 CONTENTS, ALLOC, LOAD, DATA 18 .fini_array 00000008 0000000000600e18 0000000000600e18 00000e18 2**3 CONTENTS, ALLOC, LOAD, DATA 19 .dynamic 000001d0 0000000000600e20 0000000000600e20 00000e20 2**3 CONTENTS, ALLOC, LOAD, DATA 20 .got 00000008 0000000000600ff0 0000000000600ff0 00000ff0 2**3 CONTENTS, ALLOC, LOAD, DATA 21 .got.plt 00000020 0000000000600ff8 0000000000600ff8 00000ff8 2**3 CONTENTS, ALLOC, LOAD, DATA 22 .data 00000010 0000000000601018 0000000000601018 00001018 2**3 CONTENTS, ALLOC, LOAD, DATA 23 .bss 00000008 0000000000601028 0000000000601028 00001028 2**0 ALLOC 24 .comment 0000002a 0000000000000000 0000000000000000 00001028 2**0 CONTENTS, READONLY

每个段都有几个关键属性:

属性名描述
Size段在内存中的大小
VMA虚拟内存地址(Virtual Memory Address),段在内存中的加载地址
LMA加载内存地址(Load Memory Address),通常与VMA相同
File off段在文件中的偏移量
Algn段的对齐要求
Flags段的属性(CONTENTS, ALLOC, LOAD, READONLY, CODE, DATA等)

对于逆向分析来说,最重要的几个段是:

  • .text:包含程序的可执行代码
  • .data:包含已初始化的全局变量和静态变量
  • .bss:包含未初始化的全局变量和静态变量
  • .rodata:包含只读数据(如字符串常量)
  • .dynamic:包含动态链接信息

4. 深入.text段:查看机器指令

.text段是程序的核心,包含了所有可执行代码。我们可以用-d选项来反汇编.text段:

objdump -d hello

输出会显示所有可执行段的汇编代码,包括.init、.plt、.text和.fini等。我们最关心的是main函数的代码:

0000000000400526 <main>: 400526: 55 push %rbp 400527: 48 89 e5 mov %rsp,%rbp 40052a: bf c4 05 40 00 mov $0x4005c4,%edi 40052f: e8 cc fe ff ff callq 400400 <puts@plt> 400534: b8 00 00 00 00 mov $0x0,%eax 400539: 5d pop %rbp 40053a: c3 retq

这段汇编代码对应我们的C代码printf("Hello, World!\n");。有趣的是,编译器优化后使用了puts而不是printf,因为我们的字符串以换行符结尾且没有格式说明符。

注意:实际输出可能会因编译器版本和优化选项不同而有所差异。使用-O0关闭优化可以更接近源代码结构。

如果想只看.text段的内容,可以使用:

objdump -j .text -d hello

5. 查看段内容:十六进制与ASCII表示

有时候我们需要查看段的原始内容,这时可以使用-s选项。例如,查看.rodata段(通常包含字符串常量):

objdump -j .rodata -s hello

输出类似:

Contents of section .rodata: 4005c0 01000200 48656c6c 6f2c2057 6f726c64 ....Hello, World 4005d0 2100 !.

这里我们可以看到字符串"Hello, World!"的ASCII表示和十六进制编码。

6. 高级技巧:结合源代码查看反汇编

如果程序是用-g选项编译的(包含调试信息),我们可以使用-S选项将源代码与汇编代码混合显示:

objdump -S hello

输出会像这样:

0000000000400526 <main>: #include <stdio.h> int main() { 400526: 55 push %rbp 400527: 48 89 e5 mov %rsp,%rbp printf("Hello, World!\n"); 40052a: bf c4 05 40 00 mov $0x4005c4,%edi 40052f: e8 cc fe ff ff callq 400400 <puts@plt> return 0; 400534: b8 00 00 00 00 mov $0x0,%eax } 400539: 5d pop %rbp 40053a: c3 retq

这种显示方式对于理解汇编代码与源代码的对应关系非常有帮助。

7. 动态符号表:查看外部依赖

现代Linux程序通常依赖动态链接库。我们可以用-T选项查看动态符号表:

objdump -T hello

输出会列出所有动态符号,包括导入和导出的函数。例如:

DYNAMIC SYMBOL TABLE: 0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 puts 0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main 0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __gmon_start__

这显示我们的程序依赖于GLIBC的puts、__libc_start_main等函数。

8. 实战分析:从ELF结构理解程序加载过程

通过前面的分析,我们现在可以理解Linux如何加载和执行一个程序:

  1. 读取ELF头:确定文件类型、架构和入口点
  2. 加载段:根据程序头表将各个段映射到内存
  3. 动态链接:解析依赖的共享库并重定位符号
  4. 执行:跳转到入口点开始执行

我们可以用objdump查看程序头表(与段头表不同):

objdump -p hello

输出包含LOAD类型的段,这些是实际会被加载到内存的段:

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 0x000000000000070c 0x000000000000070c R E 200000 LOAD 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x0000000000000218 0x0000000000000220 RW 200000 DYNAMIC 0x0000000000000e20 0x0000000000600e20 0x0000000000600e20 0x00000000000001d0 0x00000000000001d0 RW 8 NOTE 0x0000000000000254 0x0000000000400254 0x0000000000400254 0x0000000000000044 0x0000000000000044 R 4 GNU_EH_FRAME 0x00000000000005c4 0x00000000004005c4 0x00000000004005c4 0x0000000000000034 0x0000000000000034 R 4 GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 0x0000000000000000 RW 10 GNU_RELRO 0x0000000000000e10 0x0000000000600e10 0x0000000000600e10 0x00000000000001f0 0x00000000000001f0 R 1

9. 扩展应用:objdump在安全分析中的使用

objdump不仅用于学习,在实际安全分析中也很有价值:

  1. 漏洞分析:查看存在漏洞的函数汇编代码
  2. 恶意软件分析:分析可疑二进制文件的行为
  3. 补丁分析:比较补丁前后二进制文件的变化

例如,我们可以用以下命令查找程序中所有的函数调用:

objdump -d hello | grep callq

或者查找所有对特定地址的引用:

objdump -d hello | grep '0x4005c4'

10. 常见问题与技巧

在使用objdump时,可能会遇到一些问题和需要掌握的技巧:

问题1:为什么我的反汇编输出看起来不对?

可能原因:

  • 文件不是有效的ELF格式
  • 使用了错误的架构选项(尝试-m选项指定架构)
  • 文件被加壳或混淆

问题2:如何查看特定函数的汇编代码?

可以使用grep过滤:

objdump -d hello | grep -A20 '<main>:'

实用技巧:

  1. 彩色输出:通过管道将objdump输出传递给coderay或pygmentize可以获得语法高亮

    objdump -d hello | coderay -asm
  2. 交叉引用:使用-r选项显示重定位信息

    objdump -dr hello
  3. 比较差异:比较两个二进制文件的差异

    diff <(objdump -d file1) <(objdump -d file2)
  4. 查看特定地址范围:使用--start-address--stop-address选项

    objdump -d --start-address=0x400526 --stop-address=0x40053b hello

11. 结合其他工具进行更深入分析

虽然objdump功能强大,但结合其他工具能获得更全面的视角:

工具用途
readelf专门解析ELF文件,提供比objdump更详细的ELF结构信息
nm列出符号表,快速查看程序中的函数和全局变量
strings提取文件中的可打印字符串,常用于查找硬编码的敏感信息
gdb动态调试工具,可以单步执行并观察程序状态
ltrace/strace跟踪库函数调用和系统调用,了解程序运行时行为

例如,先用readelf查看ELF头:

readelf -h hello

然后用nm查看符号表:

nm hello

最后用gdb进行动态调试:

gdb ./hello

12. 从实践中学:自己编写简单的ELF解析器

要真正理解ELF结构,最好的方法是自己编写一个简单的解析器。以下是一个用Python解析ELF头的基本示例:

import struct def parse_elf_header(filename): with open(filename, 'rb') as f: # 读取ELF魔数(16字节) magic = f.read(16) if magic[:4] != b'\x7fELF': print("不是有效的ELF文件") return # 解析ELF头基本结构(64位) e_ident = magic (e_type, e_machine, e_version, e_entry, e_phoff, e_shoff, e_flags, e_ehsize, e_phentsize, e_phnum, e_shentsize, e_shnum, e_shstrndx) = struct.unpack('HHIIIIIHHHHHH', f.read(36)) print(f"类型: {e_type} (1=可执行, 2=共享库, 3=目标文件)") print(f"架构: {e_machine} (0x3E=x86-64)") print(f"入口点: 0x{e_entry:x}") print(f"程序头表偏移: {e_phoff}") print(f"节头表偏移: {e_shoff}") print(f"程序头数量: {e_phnum}") print(f"节头数量: {e_shnum}") parse_elf_header('hello')

这个简单的脚本可以显示ELF文件的基本信息。通过扩展它,你可以逐步实现更完整的ELF解析功能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/21 20:23:01

当PayPal离开eBay:独立开发者如何借鉴其‘支付基建’突围策略?

PayPal独立启示录&#xff1a;开发者如何构建自己的‘技术护城河’&#xff1f; 2002年eBay以15亿美元收购PayPal时&#xff0c;硅谷普遍认为这将是又一个被巨头吞噬的创业故事。但谁能想到&#xff0c;13年后PayPal不仅成功分拆上市&#xff0c;市值更反超母公司&#xff1f;…

作者头像 李华
网站建设 2026/4/21 20:22:35

良心推荐!2026 年口碑前五的双效降重工具,学生党都夸好用

2026 年各大高校查重系统全面升级重复率 AI 率双检测&#xff0c;普通同义词替换已经完全失效&#xff0c;知网、维普、Turnitin 都能精准识别 AI 生成痕迹。还在熬夜手动改稿、越改重复率越高&#xff1f;本篇整理实测口碑 TOP5 双效降重神器&#xff0c;包含 PaperRed、笔捷…

作者头像 李华
网站建设 2026/4/21 20:18:21

从技术到应用星恒讯5G RedCap网关助力行业智能化

在物联网技术深度融入千行百业的当下&#xff0c;不同应用场景对网络连接的需求呈现出明显的差异化特征。工业自动化的设备精准监控与控制、智能交通领域的车联网实时交互等中高速场景&#xff0c;对数据传输的速率和实时性有着极高要求。然而&#xff0c;传统低速物联网技术难…

作者头像 李华
网站建设 2026/4/21 20:10:51

2026届必备的六大降重复率助手解析与推荐

Ai论文网站排名&#xff08;开题报告、文献综述、降aigc率、降重综合对比&#xff09; TOP1. 千笔AI TOP2. aipasspaper TOP3. 清北论文 TOP4. 豆包 TOP5. kimi TOP6. deepseek 眼下&#xff0c;AIGC检测技术越发普遍&#xff0c;好多机构跟平台开始针对AI生成内容实行识…

作者头像 李华