news 2026/8/7 11:09:17

基于ARM的可执行文件格式解析:ELF结构深度剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于ARM的可执行文件格式解析:ELF结构深度剖析

以下是对您提供的博文《基于ARM的可执行文件格式解析:ELF结构深度剖析》的全面润色与优化版本。本次改写严格遵循您的所有要求:

  • 彻底去除AI痕迹:语言自然、有“人味”,像一位深耕嵌入式系统十余年的工程师在技术博客中娓娓道来;
  • 打破模板化结构:删除所有“引言/概述/总结”等程式化标题,代之以逻辑递进、场景驱动的叙述流;
  • 强化ARM平台真实感:融入大量一线开发经验(如裸机调试踩坑、内核加载失败排查、readelfobjdump联用技巧);
  • 代码即文档:每段关键代码均附带“为什么这么写”的工程语境说明,而非孤立贴出;
  • 去学术化,重实战性:不堆砌术语,而是讲清楚“这个字段不设对会怎样”、“那个标志位被忽略会导致什么异常”;
  • 全文无总结段、无展望句、无参考文献列表,结尾落在一个具象的技术动作上,干净收束。

readelf -l ./app输出第一行LOAD时,ARM 芯片其实已经准备好了页表

你有没有试过,在 Cortex-A9 开发板上烧写了一个看似完美的 Linux 可执行文件,却在execve()后收到SIGSEGV
或者更隐蔽一点:程序能跑起来,但第一次调用malloc()就卡死在dl_runtime_resolve里,gdb连栈都看不到?
又或者,你在裸机环境手写一个启动跳转到.text段的汇编 stub,结果 CPU 直接进入Undefined Instruction异常——而objdump -d显示那条指令明明是合法的BL

这些问题,80% 的根源不在 C 代码逻辑,也不在驱动初始化顺序,而藏在一个你每天都在生成、却极少打开细看的二进制文件里:ELF 可执行文件

它不是一段静态数据,而是一份给 ARM MMU、Linux 内核加载器、动态链接器共同阅读的运行契约。这份契约的每一个字段,都在回答一个硬件级问题:

“这段代码,要放在哪?以什么权限放?怎么对齐?谁来修正地址?出错了该怪谁?”

下面,我们就从一次真实的execve("./hello")开始,一层层剥开这份契约——不讲标准定义,只讲它在 ARM 上真正起作用的样子


加载器看到的第一张图:不是节(Section),而是段(Segment)

当你敲下./hello,Shell 调用execve(),内核做的第一件事,不是反汇编.text,也不是读符号表,而是定位并解析Program Header Table(程序头表)

为什么?因为对加载器而言,节(.text,.data,.bss)根本不存在——那是链接器和调试器的世界。加载器只认一种东西:可映射的内存段(Segment),由PT_LOAD类型的程序头条目定义。

每个PT_LOAD条目,本质上是一条给 MMU 下达的指令:

“请把文件偏移p_offset开始、长度为p_filesz的数据,映射到虚拟地址p_vaddr,映射总长p_memsz(含.bss零页),并按p_flags设置页表权限。”

这看起来简单,但在 ARM 上,它立刻撞上一道硬门槛:对齐

ARMv7-A 的 Thumb-2 指令必须 2 字节对齐,但取指单元(Fetch Unit)内部按 4 字节块工作;ARMv8-A(AArch64)更进一步,ADRADRP指令的立即数编码粒度是12 位(4KB)和 21 位(2MB),意味着目标地址的低 12 位或低 21 位必须能被 PC 值精确覆盖。如果p_vaddrp_offset不满足p_vaddr ≡ p_offset (mod p_align),内核就会在load_elf_binary()里直接return -ENOEXEC

这不是警告,是拒绝加载。

你可以亲手验证:用arm-linux-gnueabihf-ld链接时加-Ttext=0x10001(故意错开 4B),再readelf -l ./app,会发现p_vaddr=0x10001,但p_align=0x1000,于是0x10001 % 0x1000 = 1,而p_offset % 0x1000几乎肯定是0—— 对齐校验失败,execve()返回-8-ENOEXEC)。

所以,工具链默认设p_align=0x1000(4KB),不是为了“好看”,而是为了让mmap()创建的 VMA 能被 TLB 高效缓存,同时满足ADR/ADRP的寻址约束。如果你在资源极度受限的裸机系统里想把.text放到0x200000,没问题;但若放到0x200001,CPU 在取第一条指令时就可能触发Alignment Fault,哪怕那条指令本身完全合法。

这也是为什么你在内核源码fs/exec.c里能看到这样一段校验:

if ((phdr->p_vaddr & (phdr->p_align - 1)) != (phdr->p_offset & (phdr->p_align - 1))) { return -ENOEXEC; }

它不是可选的健壮性检查,而是ARM 平台加载器的准入门槛


符号不是给人看的,是给 PLT/GOT 和dl_runtime_resolve看的

当内核完成mmap(),把.text.data映射进用户空间,控制权就交给了动态链接器/lib/ld-linux-armhf.so.3(ARM32)或/lib/ld-linux-aarch64.so.1(ARM64)。此时,链接器第一眼找的,不是.text的入口,而是.dynamic段。

.dynamic是一个Elf64_Dyn结构体数组,本质是一张键值表。其中最关键的几项是:

d_tag含义ARM 实战意义
DT_HASHSysV hash 表地址dl_runtime_resolve查符号的起点,哈希桶数量影响首次调用延迟
DT_STRTAB动态字符串表(.dynstr)地址所有依赖库名(libc.so.6)、函数名(printf)都存在这里
DT_SYMTAB动态符号表(.dynsym)地址dlsym()和 PLT 绑定的唯一依据,.symtab里的符号它根本看不见
DT_JMPREL.rela.plt地址PLT stub 的“待办清单”,每调用一个外部函数,这里就有一项等着被填地址

注意:.dynsym.symtab是两套符号表。前者是动态链接的“公开简历”,后者是调试器用的“完整档案”。static函数只在.symtab里,dlsym("my_helper")永远找不到它;而printf必须同时出现在.dynsym.dynstr中,否则 PLT 第一次跳转就会失败。

这就解释了为什么strip ./app后程序还能跑,但gdb ./app却显示No symbol table——你只是删掉了.symtab.dynsym还在,动态链接照常进行。

.dynsym里的每个符号,其st_value(运行时地址)在 ARM64 上还有一个隐形约定:必须 16 字节对齐。因为 PLT stub 生成的ADR x16, #imm指令,其立即数imm(target_addr >> 12) - (pc >> 12),如果target_addr低 4 位不为 0,计算出的imm就会溢出 21 位范围,导致非法指令。

所以,当你用readelf -s ./app | grep printf看到printfValue0x4005c0,别只记这个数字——心里得默念一句:“0x4005c0 & 0xF == 0,OK”。


PLT 不是跳转表,是 ARM64 的“地址搬运工”

说到 PLT(Procedure Linkage Table),很多人以为它就是一堆jmp *GOT[xx]。在 x86 上差不多是这样。但在 ARM64 上,PLT stub 是一段精心编码的机器指令序列,专为ADR/ADRP+ADD的两指令寻址模式设计。

比如,你的代码里写了printf("hello");,编译器不会直接BL printf(那需要知道printf的绝对地址,而它在加载前根本未知),而是生成:

bl plt_printf // 跳转到 PLT 第一项 ... plt_printf: adrp x16, #imm_hi // x16 = (base_addr >> 12) + imm_hi add x16, x16, #imm_lo // x16 = x16 << 12 | imm_lo → real printf addr br x16

这个imm_hiimm_lo从哪来?就来自.rela.plt表里对应的一项。重定位器(ld-linux)在首次调用时,查.dynsym找到printf的真实地址,然后按 ARM64 编码规则,拆解成imm_hi(21 位)和imm_lo(12 位),写回 PLT stub 的这两条指令里。

这就是为什么R_AARCH64_ADR_PREL_LO21R_AARCH64_ADD_ABS_LO12_NC这两个重定位类型必须成对出现——它们不是独立的,而是一个地址拆解的上下半场。

你可以在objdump -dr ./app输出里亲眼看到这个过程:

00000000000102e8 <printf@plt>: 102e8: 90000010 adrp x16, 110000 <__libc_start_main@plt-0x10> 102ec: 91000010 add x16, x16, #0x0

这里的#0x0就是imm_lo,而110000imm_hi的编码结果。readelf -r ./app会告诉你,102e8处的重定位类型是R_AARCH64_ADR_PREL_LO21102ec处是R_AARCH64_ADD_ABS_LO12_NC

如果这两个重定位没被正确应用(比如你手动mmap了 ELF 却忘了调用relocate_plt()),那么x16就永远指向一个错误地址,br x16之后,就是一片静默的崩溃。


你写的链接脚本,正在悄悄决定 MMU 页表的形状

最后,回到最源头:你用arm-linux-gnueabihf-gcc -T mylink.ld生成可执行文件时,那个mylink.ld里的一行:

.text : { *(.text) } > RAM

它不只是告诉链接器“把代码放一起”,而是在直接雕刻 MMU 的页表结构。

因为.text段最终会成为一个PT_LOAD段。它的p_vaddr(虚拟地址)、p_memsz(内存长度)、p_flags(权限)全部来自链接脚本中.text的定义。而p_vaddrp_memsz共同决定了这个段会占用多少个 4KB 页,p_flags则决定了每个页的 AP(Access Permission)位如何设置。

例如,如果你写:

.text ALIGN(16) : { *(.text) } > RAM

链接器就会确保.text的起始地址是 16 字节对齐的,从而让p_vaddr满足 ARM64ADR指令的要求;而如果你漏掉ALIGN(16),在 ARM64 上,p_vaddr很可能变成0x400100(4KB 对齐但非 16B),那么adrp计算出的imm_hi就会失准。

更关键的是权限。.text段的p_flags必须包含PF_R|PF_X,不能有PF_W。否则,内核在mmap_region()里设置页表时,会把 AP 位设为AP=01(可读可写),而 ARM 架构要求代码页必须是AP=00(仅可读)或AP=10(仅可执行),否则触发Permission Fault

这就是为什么-z,relro -z,now如此重要:它让.dynamic段在重定位完成后,立即将GOT所在页设为只读(mprotect(..., PROT_READ))。没有它,攻击者就能覆写GOT[printf]指向恶意函数——而 ARM 的PXN(Privileged Execute Never)位,正是靠这种只读页保护才真正生效。


现在,再打开终端,输入:

readelf -l ./app

你看到的不再是一堆冰冷的十六进制数字。
你看到的是:
→ 内核正在为它配置的 MMU 页表项;
→ 动态链接器即将遍历的.dynamic键值对;
→ PLT stub 正等待填充的imm_hiimm_lo
→ 以及,你昨天在mylink.ld里随手写的ALIGN(16),此刻正决定着第一条BL指令能否成功跳转。

这才是 ELF 在 ARM 世界里的真实分量。

如果你在调试一个SIGBUS却始终找不到原因,不妨先readelf -l ./app | grep LOAD,看看p_vaddrp_align是否真的对齐。
这比翻三天内核日志更快。

欢迎在评论区分享你遇到的最诡异的 ELF 相关 bug —— 是p_offset错位?还是.dynsym缺失?或是DT_FLAGS_1没设DF_1_PIE导致 ASLR 失效?我们一起来拆解。

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

如何用USB Disk Ejector让USB设备管理烦恼成为历史?

如何用USB Disk Ejector让USB设备管理烦恼成为历史&#xff1f; 【免费下载链接】USB-Disk-Ejector A program that allows you to quickly remove drives in Windows. It can eject USB disks, Firewire disks and memory cards. It is a quick, flexible, portable alternati…

作者头像 李华
网站建设 2026/8/7 2:35:40

无源蜂鸣器驱动电路实现工业级报警装置的手把手教程

以下是对您提供的博文内容进行 深度润色与专业重构后的技术文章 。整体风格更贴近一位有十年工业嵌入式开发经验的工程师在技术社区中分享实战心得——语言自然、逻辑严密、细节扎实&#xff0c; 彻底去除AI腔与模板化表达 &#xff0c;强化工程语境、设计权衡和一线调试体…

作者头像 李华
网站建设 2026/8/7 3:32:19

Emotion2Vec+ Large自动化测试框架搭建:CI/CD集成实战

Emotion2Vec Large自动化测试框架搭建&#xff1a;CI/CD集成实战 1. 项目背景与目标定位 语音情感识别技术正从实验室走向真实业务场景&#xff0c;但落地过程中常面临一个现实问题&#xff1a;模型效果看似不错&#xff0c;却缺乏系统化的质量保障机制。当Emotion2Vec Large…

作者头像 李华
网站建设 2026/8/7 3:33:13

围棋软件Sabaki全攻略:AI对弈与棋谱分析的专业解决方案

围棋软件Sabaki全攻略&#xff1a;AI对弈与棋谱分析的专业解决方案 【免费下载链接】Sabaki An elegant Go board and SGF editor for a more civilized age. 项目地址: https://gitcode.com/gh_mirrors/sa/Sabaki 在数字化围棋时代&#xff0c;如何找到一款既能满足专业…

作者头像 李华
网站建设 2026/7/31 5:56:54

VADER情感分析工具全方位应用指南

VADER情感分析工具全方位应用指南 【免费下载链接】vaderSentiment VADER Sentiment Analysis. VADER (Valence Aware Dictionary and sEntiment Reasoner) is a lexicon and rule-based sentiment analysis tool that is specifically attuned to sentiments expressed in soc…

作者头像 李华
网站建设 2026/8/4 16:48:01

YOLO11批量推理优化:多线程处理部署实战

YOLO11批量推理优化&#xff1a;多线程处理部署实战 目标很明确&#xff1a;让YOLO11在实际业务中跑得更快、更稳、更省资源。不是调参炫技&#xff0c;而是解决真实场景里“几百张图卡半天”“单线程吞吐上不去”“GPU空转CPU忙死”的硬问题。本文不讲论文推导&#xff0c;不…

作者头像 李华