2026/4/18 11:41:18
网站建设
项目流程
怎么做一个公司网站,长春火车站是南站还是北站,免费做外贸的网站建设,科技团队网站以下是对您提供的博文《基于ARM的可执行文件格式解析#xff1a;ELF结构深度剖析》的 全面润色与优化版本 。本次改写严格遵循您的所有要求#xff1a; ✅ 彻底去除AI痕迹 #xff1a;语言自然、有“人味”#xff0c;像一位深耕嵌入式系统十余年的工程师在技术博客中…以下是对您提供的博文《基于ARM的可执行文件格式解析ELF结构深度剖析》的全面润色与优化版本。本次改写严格遵循您的所有要求✅彻底去除AI痕迹语言自然、有“人味”像一位深耕嵌入式系统十余年的工程师在技术博客中娓娓道来✅打破模板化结构删除所有“引言/概述/总结”等程式化标题代之以逻辑递进、场景驱动的叙述流✅强化ARM平台真实感融入大量一线开发经验如裸机调试踩坑、内核加载失败排查、readelf与objdump联用技巧✅代码即文档每段关键代码均附带“为什么这么写”的工程语境说明而非孤立贴出✅去学术化重实战性不堆砌术语而是讲清楚“这个字段不设对会怎样”、“那个标志位被忽略会导致什么异常”✅全文无总结段、无展望句、无参考文献列表结尾落在一个具象的技术动作上干净收束。当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当你敲下./helloShell 调用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-AAArch64更进一步ADR和ADRP指令的立即数编码粒度是12 位4KB和 21 位2MB意味着目标地址的低 12 位或低 21 位必须能被 PC 值精确覆盖。如果p_vaddr和p_offset不满足p_vaddr ≡ p_offset (mod p_align)内核就会在load_elf_binary()里直接return -ENOEXEC。这不是警告是拒绝加载。你可以亲手验证用arm-linux-gnueabihf-ld链接时加-Ttext0x10001故意错开 4B再readelf -l ./app会发现p_vaddr0x10001但p_align0x1000于是0x10001 % 0x1000 1而p_offset % 0x1000几乎肯定是0—— 对齐校验失败execve()返回-8-ENOEXEC。所以工具链默认设p_align0x10004KB不是为了“好看”而是为了让mmap()创建的 VMA 能被 TLB 高效缓存同时满足ADR/ADRP的寻址约束。如果你在资源极度受限的裸机系统里想把.text放到0x200000没问题但若放到0x200001CPU 在取第一条指令时就可能触发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.3ARM32或/lib/ld-linux-aarch64.so.1ARM64。此时链接器第一眼找的不是.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看到printf的Value是0x4005c0别只记这个数字——心里得默念一句“0x4005c0 0xF 0OK”。PLT 不是跳转表是 ARM64 的“地址搬运工”说到 PLTProcedure Linkage Table很多人以为它就是一堆jmp *GOT[xx]。在 x86 上差不多是这样。但在 ARM64 上PLT stub 是一段精心编码的机器指令序列专为ADR/ADRPADD的两指令寻址模式设计。比如你的代码里写了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_hi和imm_lo从哪来就来自.rela.plt表里对应的一项。重定位器ld-linux在首次调用时查.dynsym找到printf的真实地址然后按 ARM64 编码规则拆解成imm_hi21 位和imm_lo12 位写回 PLT stub 的这两条指令里。这就是为什么R_AARCH64_ADR_PREL_LO21和R_AARCH64_ADD_ABS_LO12_NC这两个重定位类型必须成对出现——它们不是独立的而是一个地址拆解的上下半场。你可以在objdump -dr ./app输出里亲眼看到这个过程00000000000102e8 printfplt: 102e8: 90000010 adrp x16, 110000 __libc_start_mainplt-0x10 102ec: 91000010 add x16, x16, #0x0这里的#0x0就是imm_lo而110000是imm_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_vaddr和p_memsz共同决定了这个段会占用多少个 4KB 页p_flags则决定了每个页的 APAccess Permission位如何设置。例如如果你写.text ALIGN(16) : { *(.text) } RAM链接器就会确保.text的起始地址是 16 字节对齐的从而让p_vaddr满足 ARM64ADR指令的要求而如果你漏掉ALIGN(16)在 ARM64 上p_vaddr很可能变成0x4001004KB 对齐但非 16B那么adrp计算出的imm_hi就会失准。更关键的是权限。.text段的p_flags必须包含PF_R|PF_X不能有PF_W。否则内核在mmap_region()里设置页表时会把 AP 位设为AP01可读可写而 ARM 架构要求代码页必须是AP00仅可读或AP10仅可执行否则触发Permission Fault。这就是为什么-z,relro -z,now如此重要它让.dynamic段在重定位完成后立即将GOT所在页设为只读mprotect(..., PROT_READ)。没有它攻击者就能覆写GOT[printf]指向恶意函数——而 ARM 的PXNPrivileged Execute Never位正是靠这种只读页保护才真正生效。现在再打开终端输入readelf -l ./app你看到的不再是一堆冰冷的十六进制数字。你看到的是→ 内核正在为它配置的 MMU 页表项→ 动态链接器即将遍历的.dynamic键值对→ PLT stub 正等待填充的imm_hi和imm_lo→ 以及你昨天在mylink.ld里随手写的ALIGN(16)此刻正决定着第一条BL指令能否成功跳转。这才是 ELF 在 ARM 世界里的真实分量。如果你在调试一个SIGBUS却始终找不到原因不妨先readelf -l ./app | grep LOAD看看p_vaddr和p_align是否真的对齐。这比翻三天内核日志更快。欢迎在评论区分享你遇到的最诡异的 ELF 相关 bug —— 是p_offset错位还是.dynsym缺失或是DT_FLAGS_1没设DF_1_PIE导致 ASLR 失效我们一起来拆解。