十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式开发利器:用pyelftools解析ELF文件实现自动化固件分析

嵌入式开发利器:用pyelftools解析ELF文件实现自动化固件分析 1. 项目概述为什么嵌入式开发者需要关注ELF文件解析在嵌入式开发这个行当里尤其是跟Linux、RTOS或者裸机固件打交道时你迟早会碰到一个绕不开的文件格式ELF。无论是你从交叉编译工具链里产出的可执行文件还是从目标板上dump出来的内存镜像甚至是供应商提供的那个神秘的SDK库文件它们大概率都是ELF格式。对于很多刚入行的朋友来说ELF文件就像个黑盒子编译链接后生成一个.out或.bin文件烧录进去能跑就行里面的门道似乎不那么重要。但当你需要深究“为什么程序崩溃在了这个地址”、“这个库到底依赖了哪些符号”、“我代码里某个函数最终在镜像里占了多少空间”这类问题时打开这个黑盒子就变得至关重要。传统上我们依赖readelf、objdump、nm这类GNU Binutils工具它们在命令行下很强大但输出是文本想要编程式地提取、分析或集成到自动化流程里就得写一堆正则表达式去解析文本输出既脆弱又麻烦。这就是pyelftools的价值所在。它是一个纯Python库专门用来解析ELFExecutable and Linkable Format文件。简单说它把ELF文件这个复杂的二进制结构变成了Python里一堆直观的对象和属性。你可以像操作普通数据结构一样轻松读取文件头、程序头、节区Section、符号表、动态链接信息等等。对于嵌入式开发来说这意味着你可以将固件分析、内存布局检查、依赖审计、甚至定制化的链接后处理等任务用Python脚本自动化起来极大地提升了开发和调试效率。我最初接触它是因为需要批量分析一批芯片的SDK库文件确认其ABI兼容性。手动操作了十几个文件后我就崩溃了于是找到了pyelftools写了个不到一百行的脚本所有信息一目了然。它不是什么重型武器但绝对是嵌入式工程师工具箱里一把趁手的手术刀让你能精准地解剖你的程序看清内在的每一个细节。2. pyelftools核心能力与设计思路拆解2.1 ELF文件结构快速回顾要理解pyelftools能做什么首先得对ELF文件有个基本的概念。ELF文件是一种非常灵活的结构用来存储可执行代码、数据、调试信息以及链接和控制信息。它主要包含以下几个部分ELF Header文件头位于文件开头描述了整个文件的基本属性比如文件类型可执行文件、共享库、目标文件等、目标机器架构如ARM、x86、程序入口地址、以及程序头表和节区头表在文件中的位置和大小。Program Header Table程序头表或称段头表这个表对“加载器”Loader至关重要。它描述了系统如何将文件“段”Segment映射到进程的内存空间。每个“程序头”描述了一个“段”比如哪个是代码段可读可执行、哪个是数据段可读可写。一个段可能由多个“节区”组成。Section Header Table节区头表这个表对“链接器”Linker和调试器至关重要。它描述了文件中的所有“节区”Section比如.text代码、.data已初始化数据、.bss未初始化数据、.rodata只读数据、.symtab符号表、.strtab字符串表等。节区是链接和重定位的基本单位。pyelftools的设计核心就是为这些复杂的二进制结构提供面向对象的Python接口。它不尝试模拟readelf的所有功能而是提供底层数据访问能力把灵活性交给开发者。2.2 pyelftools的架构与核心类库的设计非常清晰主要围绕几个核心类展开ELFFile这是入口类。你通过ELFFile(stream)加载一个ELF文件stream可以是文件对象或BytesIO它就成为了你操作该文件的根对象。ELFHeader通过elffile.header访问。你可以获取e_type文件类型、e_machine机器架构如EM_ARM、e_entry入口地址等所有头信息。Segment通过迭代elffile.iter_segments()获取。每个Segment对象对应一个程序头你可以查询其类型如PT_LOAD可加载段、虚拟地址p_vaddr、文件偏移p_offset、内存大小p_memsz、文件大小p_filesz以及权限标志p_flags可读R、可写W、可执行X的组合。Section通过迭代elffile.iter_sections()或按名称获取elffile.get_section_by_name(‘.text’)。这是最常用的部分。你可以获取节区名称、类型、地址、大小、数据等。特别地对于符号表节区.symtab,.dynsympyelftools会将其解析为特殊的SymbolTableSection对象便于你直接遍历符号。这种设计的好处是直观。比如你想知道代码段是否被错误地标记为可写这是一个常见的安全隐患或bug用pyelftools只需要几行代码就能扫描所有LOAD段进行检查。2.3 为何选择Python和pyelftools你可能会问C/C不是有libelf这样的库吗为什么用Python在嵌入式开发中尤其是偏软件和系统层面Python的生态和快速原型能力优势巨大。快速原型与自动化Python脚本编写速度快适合编写一次性分析脚本或集成到CI/CD流水线中。比如在每次构建后自动分析生成固件的大小检查是否有符号冲突或者验证内存布局是否符合硬件限制。强大的生态集成分析结果可以轻松地用matplotlib画图用pandas做统计分析生成HTML报告或者与你的项目管理工具如Jira集成。这是纯C工具链难以比拟的。降低门槛对于嵌入式硬件工程师或刚接触底层软件的同学Python比C更友好。通过pyelftools这个清晰的接口去理解ELF比直接啃libelf的API或readelf的man手册要容易得多。跨平台一致性pyelftools是纯Python实现部分性能关键路径可能有C扩展在任何有Python的环境都能运行不需要处理不同系统上binutils版本差异的问题。当然对于性能极端敏感、需要直接修改ELF文件内容的场景可能仍需回归libelf。但对于绝大多数分析、检查和提取任务pyelftools绰绰有余。3. 环境搭建与基础使用实战3.1 安装与准备安装pyelftools非常简单通过pip即可。建议在虚拟环境中进行。pip install pyelftools为了演示我们需要一个ELF文件。最方便的就是用你自己的交叉编译工具链编译一个简单的程序。例如一个简单的“Hello World” C程序用ARM的GCC编译arm-linux-gnueabihf-gcc -o hello.elf hello.c -static # 静态链接方便分析如果没有现成的交叉环境也可以直接分析你Linux系统上的/bin/ls。但注意嵌入式环境下的ELF特征如架构、无动态链接器可能不同。3.2 第一个脚本读取ELF基本信息让我们写一个最简单的脚本打开一个ELF文件并打印其核心信息。#!/usr/bin/env python3 # -*- coding: utf-8 -*- import sys from elftools.elf.elffile import ELFFile def process_file(filename): print(f‘正在分析文件: {filename}‘) with open(filename, ‘rb‘) as f: elffile ELFFile(f) # 1. 打印ELF头信息 header elffile.header print(‘【ELF头信息】‘) print(f‘ 文件类型: {header[“e_type”]}‘) print(f‘ 机器架构: {header[“e_machine”]} ({ARM if header[“e_machine”] “EM_ARM” else “Other”})‘) print(f‘ Entry点地址: 0x{header[“e_entry”]:08x}‘) print(f‘ 程序头表偏移: {header[“e_phoff”]}‘) print(f‘ 节区头表偏移: {header[“e_shoff”]}‘) # 2. 遍历所有段Program Header print(‘\n【可加载段 (LOAD Segments)】‘) for segment in elffile.iter_segments(): if segment.header[‘p_type’] ‘PT_LOAD’: flags segment.header[‘p_flags’] # 将标志位转换为可读的字符串 flags_str ‘‘ if flags 0x4: flags_str ‘R‘ if flags 0x2: flags_str ‘W‘ if flags 0x1: flags_str ‘X‘ print(f‘ 虚拟地址: 0x{segment.header[“p_vaddr”]:08x}‘ f‘, 大小: 0x{segment.header[“p_memsz”]:x}‘ f‘, 权限: {flags_str}‘) # 3. 遍历所有节区Section Header print(‘\n【关键节区信息】‘) sections_of_interest [‘.text‘, ‘.data‘, ‘.rodata‘, ‘.bss‘, ‘.symtab‘] for section in elffile.iter_sections(): if section.name in sections_of_interest: print(f‘ {section.name}: 地址 0x{section[“sh_addr”]:08x}, 大小 {section[“sh_size”]} 字节‘) if __name__ ‘__main__‘: if len(sys.argv) ! 2: print(f‘用法: {sys.argv[0]} elf_file‘) sys.exit(1) process_file(sys.argv[1])运行这个脚本python3 elf_basic.py hello.elf。你会看到类似如下的输出它清晰地展示了程序的内存布局轮廓。注意pyelftools返回的某些字段值是整数枚举。比如e_type为1表示ET_REL可重定位文件2表示ET_EXEC可执行文件3表示ET_DYN共享目标文件。库中提供了常量定义如elftools.elf.constants.ENUM_E_TYPE但在快速脚本中直接打印数字有时更简单。你可以根据需求决定是否转换。3.3 深入节区提取代码与数据仅仅知道地址和大小还不够我们经常需要查看具体内容。例如提取整个代码段.text的二进制数据或者反汇编一小部分。#!/usr/bin/env python3 import sys from elftools.elf.elffile import ELFFile from capstone import Cs, CS_ARCH_ARM, CS_MODE_ARM # 需要安装capstone: pip install capstone def extract_and_disasm(filename): with open(filename, ‘rb‘) as f: elffile ELFFile(f) # 1. 找到.text节区并提取数据 text_section elffile.get_section_by_name(‘.text‘) if not text_section: print(‘未找到 .text 节区‘) return text_data text_section.data() # 返回的是bytes对象 text_addr text_section[‘sh_addr’] print(f‘.text节区加载地址: 0x{text_addr:08x}, 大小: {len(text_data)} 字节‘) # 2. 使用Capstone反汇编前64字节示例 # 注意这里需要根据目标架构设置。假设是ARM状态。 md Cs(CS_ARCH_ARM, CS_MODE_ARM) print(‘\n【.text节区起始部分反汇编】‘) for i in md.disasm(text_data[:64], text_addr): print(f‘ 0x{i.address:08x}: {i.mnemonic}\t{i.op_str}‘) # 3. 提取.rodata节区只读数据如字符串常量 rodata_section elffile.get_section_by_name(‘.rodata‘) if rodata_section: rodata_data rodata_section.data() # 尝试查找以null结尾的C字符串 print(‘\n【.rodata节区中的字符串示例】‘) current_str bytearray() for byte in rodata_data[:256]: # 只检查前256字节 if byte ! 0: current_str.append(byte) else: if len(current_str) 1: # 过滤掉单个字符或空字符串 try: print(f‘ “{current_str.decode(‘utf-8’, errors‘ignore’)}”‘) except: pass current_str bytearray() if __name__ ‘__main__‘: if len(sys.argv) ! 2: print(f‘用法: {sys.argv[0]} elf_file‘) sys.exit(1) extract_and_disasm(sys.argv[1])这个脚本演示了如何结合pyelftools和反汇编引擎Capstone进行基础静态分析。这对于逆向工程或验证编译器优化效果非常有用。实操心得section.data()返回的是该节区在文件中的原始数据。对于.bss这种在文件中不占空间sh_type为SHT_NOBITS的节区data()方法返回一个空的bytes对象。在计算固件大小时需要区分sh_size内存大小和实际文件大小。4. 高级应用场景与实战技巧4.1 符号表分析与依赖检查符号表是ELF文件的“字典”记录了函数和变量的名字、地址、大小和绑定信息。分析符号表可以解决很多实际问题。场景一查找未定义的符号潜在链接错误在分析一个静态库.a或动态库.so时你可能想快速知道它提供了哪些符号又依赖哪些外部符号。def analyze_symbols(filename): with open(filename, ‘rb‘) as f: elffile ELFFile(f) # 获取动态符号表.dynsym和静态符号表.symtab dynsym_section elffile.get_section_by_name(‘.dynsym‘) symtab_section elffile.get_section_by_name(‘.symtab‘) # 通常分析.symtab静态符号表信息更全 target_section symtab_section if symtab_section else dynsym_section if not target_section: print(‘未找到符号表‘) return print(f‘【符号表分析: {target_section.name}】‘) undefined_syms [] defined_syms [] # target_section是一个SymbolTableSection对象可以迭代 for symbol in target_section.iter_symbols(): name symbol.name if not name: # 有些符号可能没有名字 continue # ‘STB_GLOBAL‘ 或 ‘STB_WEAK‘ 绑定且类型是 ‘STT_FUNC‘ 或 ‘STT_OBJECT‘ if symbol[‘st_info’][‘bind’] in (‘STB_GLOBAL‘, ‘STB_WEAK‘): # 判断是否已定义节区索引不是SHN_UNDEF if symbol[‘st_shndx’] ! ‘SHN_UNDEF‘: defined_syms.append((name, hex(symbol[‘st_value’]), symbol[‘st_size’])) else: undefined_syms.append(name) print(f‘已定义全局/弱符号数量: {len(defined_syms)}‘) print(f‘未定义全局符号数量: {len(undefined_syms)}‘) if undefined_syms: print(‘未定义符号列表可能需要外部链接:‘) for sym in undefined_syms[:10]: # 只打印前10个 print(f‘ {sym}‘) if len(undefined_syms) 10: print(f‘ … 以及另外 {len(undefined_syms)-10} 个符号‘)场景二计算代码段中各个函数的大小这对于性能优化和内存受限的嵌入式系统非常关键。def analyze_function_sizes(filename): with open(filename, ‘rb‘) as f: elffile ELFFile(f) symtab elffile.get_section_by_name(‘.symtab‘) if not symtab: return # 获取.text节区的地址范围 text_section elffile.get_section_by_name(‘.text‘) if not text_section: return text_start text_section[‘sh_addr’] text_end text_start text_section[‘sh_size’] functions [] # 遍历符号表找出类型为STT_FUNC函数且位于.text节区内的符号 for symbol in symtab.iter_symbols(): if (symbol[‘st_info’][‘type’] ‘STT_FUNC‘ and symbol[‘st_shndx’] ! ‘SHN_UNDEF‘ and text_start symbol[‘st_value’] text_end): # 计算函数大小下一个符号的地址减当前地址。需要排序后处理。 functions.append((symbol.name, symbol[‘st_value’], symbol[‘st_size’])) # 按地址排序 functions.sort(keylambda x: x[1]) # 更精确地计算大小如果符号表本身没有大小信息st_size0则用下一个函数的起始地址作为结束地址 print(‘【.text节区内函数大小统计】‘) total_func_size 0 for i in range(len(functions)): name, addr, size functions[i] if size 0: # 估算大小下一个函数的地址减去当前地址 if i 1 len(functions): size functions[i1][1] - addr else: # 最后一个函数用.text节区结束地址估算 size text_end - addr if size 0: print(f‘ {name:30s} 地址: 0x{addr:08x}, 大小: {size:6d} 字节‘) total_func_size size print(f‘\n函数总大小估算: {total_func_size} 字节‘) print(f‘.text节区总大小: {text_section[“sh_size”]} 字节‘) print(f‘函数占比: {total_func_size/text_section[“sh_size”]*100:.1f}%‘)4.2 内存布局验证与链接脚本调试嵌入式开发中链接脚本.ld文件定义了内存区域的划分。有时你需要验证最终生成的ELF文件是否符合链接脚本的预期。def verify_memory_layout(filename, expected_layout): expected_layout: 一个字典定义预期的内存区域。 例如{‘FLASH‘: (0x08000000, 0x00100000), ‘RAM‘: (0x20000000, 0x00020000)} with open(filename, ‘rb‘) as f: elffile ELFFile(f) violations [] # 检查每个LOAD段是否落在预期的内存区域内 for segment in elffile.iter_segments(): if segment.header[‘p_type’] ‘PT_LOAD’: seg_vaddr segment.header[‘p_vaddr’] seg_memsz segment.header[‘p_memsz’] seg_end seg_vaddr seg_memsz - 1 segment_in_region False for region_name, (region_start, region_size) in expected_layout.items(): region_end region_start region_size - 1 if region_start seg_vaddr and seg_end region_end: segment_in_region True # 可选检查权限是否匹配区域如FLASH不应有W权限 flags segment.header[‘p_flags’] if region_name ‘FLASH‘ and (flags 0x2): # 可写 violations.append(f‘段(0x{seg_vaddr:x})在FLASH中但具有可写(W)权限‘) break if not segment_in_region: violations.append(f‘段(0x{seg_vaddr:x} - 0x{seg_end:x})未落在任何预期内存区域‘) if violations: print(‘【内存布局验证失败】‘) for v in violations: print(f‘ ! {v}‘) return False else: print(‘【内存布局验证通过】‘) return True这个脚本可以帮助你在硬件资源紧张时快速发现因链接脚本配置错误导致的数据段误放入只读存储器等问题。4.3 构建后自动化分析集成将上述检查集成到你的构建系统如Makefile、CMake或CI脚本中可以实现自动化的质量门禁。# 一个简单的Makefile集成示例 all: hello.elf echo “构建完成开始ELF分析...” python3 analyze_elf.py hello.elf analyze_elf.py: # 这个Python脚本可以包含上述所有检查 # 1. 检查关键段权限 # 2. 验证内存布局 # 3. 检查是否有未定义符号在可执行文件中不应存在 # 4. 输出代码/数据大小报告 # 如果任何检查失败以非零退出码退出使构建失败。在CI/CD管道如GitLab CI、Jenkins中你可以在编译步骤后添加一个分析步骤自动生成固件分析报告并作为构建产物的一部分存档。5. 常见问题、排查技巧与性能考量5.1 常见问题与解决AttributeError: ‘ELFFile’ object has no attribute ‘get_section_by_name’原因你使用的pyelftools版本可能较旧或者方法名记错。确保方法名正确。最可靠的方式是使用elffile.iter_sections()遍历。解决检查版本pip show pyelftools。建议使用最新版。也可以改用遍历方式for section in elffile.iter_sections(): if section.name ‘.text’: # 找到目标节区 break解析某些特殊编译器生成的ELF文件时出错原因一些编译器如某些版本的IAR、Keil ARMCC可能生成非标准或包含私有扩展的ELF文件。pyelftools主要遵循标准ELF规范。解决首先用标准的readelf工具检查文件是否能被正常识别。如果readelf可以pyelftools通常也可以。如果readelf也报错那可能是文件本身有问题。对于已知的编译器扩展可以查阅pyelftools的源码看是否支持或尝试忽略错误部分。符号表.symtab被剥离Stripped原因为了减少最终发布文件的大小经常使用strip命令移除调试信息和符号表。被剥离后.symtab节区将不存在。解决分析时使用未剥离not stripped的ELF文件通常是在调试版本中。动态符号表.dynsym通常不会被剥离但它只包含动态链接所需的符号如共享库的导出函数信息不全。如果只有剥离后的文件高级分析将非常困难可能需要结合反汇编和启发式方法。处理大文件时内存占用高原因pyelftools在解析时会按需将节区数据读入内存。对于几百MB的大型固件同时操作多个节区数据可能导致内存压力。解决使用elffile.stream和节区头的文件偏移sh_offset手动使用seek和read来流式读取特定部分的数据而不是一次性调用section.data()。专注于分析元数据头、表避免同时加载所有节区内容。5.2 性能优化技巧按需解析pyelftools是惰性加载的只在访问特定属性时才解析相关数据。编写脚本时也应遵循此原则避免不必要的遍历。例如如果你只关心段信息就不要去迭代所有节区。缓存重复访问如果你需要在脚本中多次访问同一个节区如.symtab最好将其保存到一个变量中而不是每次都调用get_section_by_name内部需要遍历。使用elftools.common中的工具pyelftools提供了一些实用函数例如elftools.common.utils中的struct_parse但直接使用高层API如ELFFile、Section在大多数情况下更方便且性能足够。5.3 与其他工具的结合pyelftools不是一个孤立的工具它最好与生态中的其他工具结合Capstone/Keystone如前所述用于反汇编和汇编。angr/radare2/Ghidra这些是更强大的二进制分析/逆向工程框架。pyelftools可以作为前端快速提取ELF的结构信息然后喂给这些框架进行更深层次的分析如控制流分析、污点分析。lief这是另一个强大的二进制文件操作库支持PE、ELF、Mach-O等多种格式。lief的API更现代化且支持修改ELF文件如添加节区、修改符号。pyelftools更侧重于解析和读取两者定位略有不同。如果你的需求是纯分析pyelftools的API可能更简洁直观如果需要打补丁或重构文件lief更合适。readelf/objdump作为验证工具。当你用pyelftools写了一个复杂分析脚本时可以用这些标准工具的输出交叉验证结果的正确性。5.4 嵌入式开发中的独特考量裸机Bare-metal与带OS的ELF裸机程序的ELF入口地址e_entry通常是物理地址直接指向复位向量或main函数。而Linux用户态程序的入口地址是虚拟地址。在分析时要注意上下文。静态链接与动态链接深度嵌入式系统常使用静态链接以减少依赖和体积。静态链接的ELF文件不包含.interp解释器和.dynamic节区符号解析也更直接。动态链接的ELF分析则需要关注重定位表.rel.dyn,.rel.plt和全局偏移表GOT。交叉架构你的分析主机x86和目标ARM架构不同。pyelftools能正确解析不同架构的ELF头但后续的内容分析如反汇编需要你使用对应的架构工具如ARM的objdump或Capstone的ARM模式。地址映射嵌入式系统中加载地址p_vaddr可能不是最终运行地址特别是在有MMU或复杂启动加载器的系统中。需要结合芯片手册和启动流程来理解。pyelftools本身不关心架构差异它只是忠实地解析文件格式。这给了你最大的灵活性但也要求你对目标系统有足够的了解。6. 实战案例构建一个简易的固件内存占用分析报告生成器让我们综合运用以上知识创建一个实用的脚本为嵌入式固件生成一份详细的、人类可读的内存占用分析报告。#!/usr/bin/env python3 固件内存分析报告生成器 用法: python3 firmware_analyzer.py firmware.elf import sys from elftools.elf.elffile import ELFFile from elftools.elf.constants import SH_FLAGS from collections import defaultdict def generate_memory_report(filename): report_lines [] with open(filename, ‘rb‘) as f: elffile ELFFile(f) # --- 1. 基本信息 --- header elffile.header report_lines.append(‘# 固件内存分析报告‘) report_lines.append(f‘**文件**: {filename}‘) report_lines.append(f‘**架构**: {header[“e_machine”]} ({get_arch_name(header[“e_machine”])})‘) report_lines.append(f‘**类型**: {header[“e_type”]} ({get_file_type(header[“e_type”])})‘) report_lines.append(f‘**入口点**: 0x{header[“e_entry”]:08x}‘) report_lines.append(‘‘) # --- 2. 内存段Segment分析 --- report_lines.append(‘## 内存段布局‘) seg_table [] seg_table.append(‘| 类型 | 虚拟地址 | 物理地址 | 内存大小 | 文件大小 | 权限 | 对齐 |‘) seg_table.append(‘| :--- | :--- | :--- | :--- | :--- | :--- | :--- |‘) total_load_size 0 for seg in elffile.iter_segments(): h seg.header if h[‘p_type’] ‘PT_LOAD’: perms ‘‘ if h[‘p_flags’] 0x4: perms ‘R‘ if h[‘p_flags’] 0x2: perms ‘W‘ if h[‘p_flags’] 0x1: perms ‘X‘ seg_table.append(f‘| LOAD | 0x{h[“p_vaddr”]:08x} | 0x{h[“p_paddr”]:08x} | ‘ f‘0x{h[“p_memsz”]:x} | 0x{h[“p_filesz”]:x} | {perms} | {h[“p_align”]} |‘) total_load_size h[‘p_memsz’] report_lines.extend(seg_table) report_lines.append(f‘\n**总计可加载内存大小**: 0x{total_load_size:x} ({total_load_size} 字节)‘) report_lines.append(‘‘) # --- 3. 节区Section详细分类统计 --- report_lines.append(‘## 节区详细分类‘) category_summary defaultdict(lambda: {‘size’: 0, ‘count’: 0}) section_details [] for sec in elffile.iter_sections(): sec_name sec.name sec_size sec[‘sh_size’] sec_addr sec[‘sh_addr’] sec_flags sec[‘sh_flags’] # 分类逻辑 category ‘其他‘ if sec_name ‘.text‘ or sec_name.startswith(‘.text.’): category ‘代码‘ elif sec_name in [‘.data‘, ‘.data*‘, ‘.sdata‘]: category ‘已初始化数据‘ elif sec_name ‘.rodata‘ or sec_name.startswith(‘.rodata.’): category ‘只读数据‘ elif sec_name ‘.bss‘ or sec_name.startswith(‘.bss.’) or sec_name ‘.sbss‘: category ‘未初始化数据 (BSS)‘ elif sec_name in [‘.stack‘, ‘.heap‘]: # 自定义区域 category ‘运行时内存‘ elif sec_name in [‘.init_array‘, ‘.fini_array‘]: category ‘构造/析构函数表‘ elif sec_name in [‘.symtab‘, ‘.strtab‘, ‘.shstrtab‘, ‘.comment‘]: category ‘调试与链接信息‘ # 通常会被strip掉 category_summary[category][‘size’] sec_size category_summary[category][‘count’] 1 # 记录详细信息只记录有实际地址和大小的节区 if sec_addr ! 0 and sec_size 0: flags_str ‘‘ if sec_flags SH_FLAGS.SHF_ALLOC: flags_str ‘A‘ # 分配 if sec_flags SH_FLAGS.SHF_WRITE: flags_str ‘W‘ if sec_flags SH_FLAGS.SHF_EXECINSTR: flags_str ‘X‘ section_details.append((sec_addr, sec_name, sec_size, category, flags_str)) # 输出分类汇总 cat_table [] cat_table.append(‘| 类别 | 节区数量 | 总大小 (字节) | 占比 |‘) cat_table.append(‘| :--- | :---: | :---: | :---: |‘) total_alloc_size sum(info[‘size’] for info in category_summary.values()) for cat, info in sorted(category_summary.items()): if info[‘size’] 0: percentage (info[‘size’] / total_alloc_size * 100) if total_alloc_size 0 else 0 cat_table.append(f‘| {cat} | {info[“count”]} | {info[“size”]:,} | {percentage:.1f}% |‘) report_lines.extend(cat_table) report_lines.append(‘‘) # 输出节区详情按地址排序 report_lines.append(‘### 节区详情按地址排序‘) sec_table [] sec_table.append(‘| 地址 | 节区名 | 大小 | 类别 | 标志 |‘) sec_table.append(‘| :--- | :--- | :--- | :--- | :--- |‘) for addr, name, size, cat, flags in sorted(section_details, keylambda x: x[0]): sec_table.append(f‘| 0x{addr:08x} | {name} | {size:,} | {cat} | {flags} |‘) report_lines.extend(sec_table) # --- 输出报告 --- report ‘\n‘.join(report_lines) print(report) # 也可以写入文件 report_filename filename.replace(‘.elf‘, ‘_report.md‘).replace(‘.out‘, ‘_report.md‘) with open(report_filename, ‘w‘) as f: f.write(report) print(f‘\n详细报告已生成: {report_filename}‘) # 辅助函数简化版 def get_arch_name(machine_num): from elftools.elf.constants import E_MACHINE arch_map {‘EM_ARM‘: ‘ARM‘, ‘EM_X86_64‘: ‘x86-64‘, ‘EM_386‘: ‘x86‘} for k, v in E_MACHINE.items(): if v machine_num: return arch_map.get(k, k) return f‘未知 ({machine_num})‘ def get_file_type(type_num): from elftools.elf.constants import E_TYPE type_map {‘ET_REL‘: ‘可重定位文件‘, ‘ET_EXEC‘: ‘可执行文件‘, ‘ET_DYN‘: ‘共享库‘} for k, v in E_TYPE.items(): if v type_num: return type_map.get(k, k) return f‘未知 ({type_num})‘ if __name__ ‘__main__‘: if len(sys.argv) ! 2: print(f‘用法: {sys.argv[0]} firmware.elf‘) sys.exit(1) generate_memory_report(sys.argv[1])这个脚本生成的Markdown报告可以直接在代码仓库或文档系统中查看清晰地展示了固件的内存分布帮助开发者优化代码体积和内存使用。你可以根据需要扩展它比如添加对特定符号大小的分析、检查是否存在重复代码段、或者与上一次构建的结果进行对比。通过pyelftools我们将原本需要手动执行多条命令并拼接信息的繁琐过程变成了一个一键执行的自动化脚本。这正是工具带来的效率提升。它可能不会每天都用但当你需要深入理解或验证你的嵌入式程序时它会是你最可靠的伙伴。
返回列表