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

张开发
2026/4/21 20:26:23 15 分钟阅读

分享文章

从ELF文件头到.text段:手把手教你用objdump拆解Linux可执行文件
从ELF文件头到.text段手把手教你用objdump拆解Linux可执行文件在计算机的世界里每个可执行程序都像一本精心编排的书而ELFExecutable 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这个输出告诉我们几个关键信息文件格式elf64-x86-64表示这是一个64位的ELF文件针对x86-64架构标志位HAS_SYMS表示文件包含符号表DYNAMIC表示这是一个动态链接的可执行文件D_PAGED表示文件是分页的入口点地址0x0000000000401040这是程序开始执行的第一条指令的地址ELF文件头实际上包含更多细节我们可以用readelf工具查看更完整的信息这不是本文重点但值得了解readelf -h hello3. 解析段头表程序的组织架构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 putsplt 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 hello5. 查看段内容十六进制与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 putsplt 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如何加载和执行一个程序读取ELF头确定文件类型、架构和入口点加载段根据程序头表将各个段映射到内存动态链接解析依赖的共享库并重定位符号执行跳转到入口点开始执行我们可以用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 19. 扩展应用objdump在安全分析中的使用objdump不仅用于学习在实际安全分析中也很有价值漏洞分析查看存在漏洞的函数汇编代码恶意软件分析分析可疑二进制文件的行为补丁分析比较补丁前后二进制文件的变化例如我们可以用以下命令查找程序中所有的函数调用objdump -d hello | grep callq或者查找所有对特定地址的引用objdump -d hello | grep 0x4005c410. 常见问题与技巧在使用objdump时可能会遇到一些问题和需要掌握的技巧问题1为什么我的反汇编输出看起来不对可能原因文件不是有效的ELF格式使用了错误的架构选项尝试-m选项指定架构文件被加壳或混淆问题2如何查看特定函数的汇编代码可以使用grep过滤objdump -d hello | grep -A20 main:实用技巧彩色输出通过管道将objdump输出传递给coderay或pygmentize可以获得语法高亮objdump -d hello | coderay -asm交叉引用使用-r选项显示重定位信息objdump -dr hello比较差异比较两个二进制文件的差异diff (objdump -d file1) (objdump -d file2)查看特定地址范围使用--start-address和--stop-address选项objdump -d --start-address0x400526 --stop-address0x40053b hello11. 结合其他工具进行更深入分析虽然objdump功能强大但结合其他工具能获得更全面的视角工具用途readelf专门解析ELF文件提供比objdump更详细的ELF结构信息nm列出符号表快速查看程序中的函数和全局变量strings提取文件中的可打印字符串常用于查找硬编码的敏感信息gdb动态调试工具可以单步执行并观察程序状态ltrace/strace跟踪库函数调用和系统调用了解程序运行时行为例如先用readelf查看ELF头readelf -h hello然后用nm查看符号表nm hello最后用gdb进行动态调试gdb ./hello12. 从实践中学自己编写简单的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} (0x3Ex86-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解析功能。

更多文章