3、ARM汇编实战:编写第一个ARM汇编程序、使用GCC交叉编译、反汇编工具(objdump, readelf)、分析简单C程序的汇编输出。

好,咱们直接动手。前面两章讲了理论,这一章咱们来点实在的。我个人习惯是,学汇编一定要先跑通一个最小的例子,哪怕它只是把寄存器里的值加一。你想想看,如果连个能跑的程序都写不出来,后面那些高级分析全是空中楼阁。

3.1 第一个ARM汇编程序:点亮一盏“灯”

这里说的“灯”,不是真的LED,而是让程序在某个寄存器里写个值,然后我们能通过调试器看到它。说白了,就是验证我们的工具链和环境是通的。

先看代码。我写了一个最简单的ARM汇编,功能是把立即数 0x1234 加载到寄存器 r0 里,然后返回。

@ first.S - 第一个ARM汇编程序
.section .text
.global _start

_start:
    mov r0, #0x1234      @ 将立即数 0x1234 加载到 r0
    mov r7, #1           @ Linux syscall number for exit
    swi 0                @ 触发软中断,退出程序

嗯,这里要注意。这个程序是给Linux用户态用的,不是裸机程序。为什么?因为我用了 swi 0 这个系统调用。在ARM Linux里,r7 存放系统调用号,1 就是 exit。这样程序跑完能正常退出,不会崩溃。

个人经验: 我刚开始学ARM汇编时,总喜欢写死循环或者直接让PC跑飞。结果每次都要重启开发板,特别浪费时间。后来我养成了习惯,每个汇编程序末尾都加一个干净的退出机制。哪怕只是模拟器,也这么做。

3.2 使用GCC交叉编译

写好了源码,下一步就是编译。我们用的是交叉编译器,因为目标平台是ARM,而你的电脑很可能是x86。我个人习惯用 arm-linux-gnueabihf-gcc 这个工具链。

编译命令很简单:

arm-linux-gnueabihf-gcc -o first first.S -nostdlib -static

解释一下参数:

  • -nostdlib:不链接标准C库。因为我们没调用任何C函数,链接了反而会报错。
  • -static:静态链接。这样生成的可执行文件不依赖动态库,方便我们后续分析。

编译完成后,用 file 命令看一眼:

$ file first
first: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, not stripped

看到 ARM32-bit 了没?说明编译成功了。如果这里显示的是 x86-64,那说明你忘了用交叉编译器,或者环境变量没配好。我曾经在这上面栽过跟头,折腾了半小时才发现是路径问题。

3.3 反汇编工具:objdump 和 readelf

有了可执行文件,我们就可以用反汇编工具来“解剖”它了。这两个工具是逆向工程师的左膀右臂。

3.3.1 objdump:看汇编代码

objdump 最常用的功能就是把二进制文件里的机器码反汇编成汇编指令。命令如下:

arm-linux-gnueabihf-objdump -d first

输出结果大概长这样:

first:     file format elf32-littlearm

Disassembly of section .text:

00008054 <_start>:
    8054:   e3a00123    mov r0, #0x1234
    8058:   e3a07001    mov r7, #1
    805c:   ef000000    svc 0

看到左边那列 e3a00123 了吗?那就是机器码。右边是反汇编出来的指令。你会发现,我们写的 swi 0 被反汇编成了 svc 0。其实是一回事,svcSupervisor Call 的缩写,是 swi 的新名字。嗯,这里要注意,不同版本的GCC可能输出不同的助记符,但意思一样。

避坑指南: 我曾经在分析一个固件时,发现反汇编出来的指令和手册对不上。后来才发现,我用的objdump版本太老,不认识新的指令编码。所以,我建议你始终使用和目标平台匹配的最新版工具链。

3.3.2 readelf:看文件结构

readelf 用来查看ELF文件的头部信息、段表、符号表等。对于逆向分析来说,段表信息特别重要。

arm-linux-gnueabihf-readelf -S first

输出节选:

There are 6 section headers, starting at offset 0x20f8:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00008054 000054 000010 00  AX  0   0  4
  [ 2] .ARM.attributes   ARM_ATTRIBUTES  00000000 000064 00002d 00      0   0  1
  ...

注意看 .text 段:它的地址是 0x8054,大小是 0x10(16字节)。正好对应我们那三条指令(每条4字节)。

为什么地址不是从0开始?因为Linux可执行文件默认有一个加载地址偏移。这个偏移量由链接脚本决定。你想想看,如果地址从0开始,那和空指针冲突了,操作系统可不允许。

3.4 分析简单C程序的汇编输出

光看汇编程序还不够,我们得看看C语言编译出来是什么样。这样才能把高级语言和底层指令对应起来。

写一个简单的C程序:

// simple.c
int add(int a, int b) {
    return a + b;
}

int main() {
    int x = 3;
    int y = 5;
    int z = add(x, y);
    return z;
}

用GCC编译并反汇编:

arm-linux-gnueabihf-gcc -O0 -o simple simple.c
arm-linux-gnueabihf-objdump -d simple

我们重点关注 add 函数的反汇编:

000083f4 <add>:
    83f4:   e52db004    push {fp}        ; 保存帧指针
    83f8:   e28db000    add fp, sp, #0   ; 设置帧指针
    83fc:   e24dd00c    sub sp, sp, #12  ; 分配栈空间
    8400:   e50b0008    str r0, [fp, #-8] ; 存储第一个参数
    8404:   e50b100c    str r1, [fp, #-12]; 存储第二个参数
    8408:   e51b2008    ldr r2, [fp, #-8] ; 加载第一个参数
    840c:   e51b300c    ldr r3, [fp, #-12]; 加载第二个参数
    8410:   e0823003    add r3, r2, r3   ; 执行加法
    8414:   e1a00003    mov r0, r3       ; 返回值放入 r0
    8418:   e24bd000    sub sp, fp, #0   ; 恢复栈指针
    841c:   e49db004    pop {fp}         ; 恢复帧指针
    8420:   e12fff1e    bx lr            ; 返回

你看,即使是最简单的加法,在 -O0(不优化)的情况下,编译器也生成了大量指令。它把参数从寄存器 r0r1 存到栈上,然后又读回来,再计算。这明显是多余的。为什么?因为 -O0 是为了方便调试,所有变量都放在内存里,这样调试器可以随时修改它们的值。

如果我们用 -O2 优化一下:

arm-linux-gnueabihf-gcc -O2 -o simple_opt simple.c
arm-linux-gnueabihf-objdump -d simple_opt

再看 add 函数:

000083f4 <add>:
    83f4:   e0800001    add r0, r0, r1   ; 直接相加
    83f8:   e12fff1e    bx lr            ; 返回

只有两条指令!这才是我们期望的ARM汇编。优化后的代码直接把参数从 r0r1 里取出来相加,结果放回 r0,然后返回。干净利落。

重要提醒: 分析固件时,你几乎不可能遇到 -O0 编译的代码。因为嵌入式设备对代码大小和执行效率要求极高,厂商通常会用 -Os(优化大小)或 -O2。所以,如果你在反汇编中看到大量冗余的压栈、弹栈操作,那多半是调试版固件,或者你分析的是未优化的代码。

3.5 本章知识体系图

下面这张SVG图,把本章的核心逻辑串起来了。从源码到可执行文件,再到反汇编分析,每一步都有对应的工具和输出。

ARM汇编实战:从源码到反汇编 汇编源码 first.S C源码 simple.c 链接脚本 默认/自定义.ld 交叉编译 arm-linux-gnueabihf-gcc -O0/-O2 -static ELF可执行文件 first / simple / simple_opt objdump -d 反汇编:查看汇编指令 readelf -S 查看段表、节区信息 file / strings 文件类型识别/字符串提取 图3-1:ARM汇编实战流程与工具链

这张图把整个流程串起来了。你从左边或中间开始都行,最终都会汇聚到ELF文件,然后通过不同的工具去分析它。我个人习惯是先跑 file 确认架构,再用 readelf 看段表,最后用 objdump 看具体指令。这个顺序能帮你快速定位问题。

好了,这一章的内容就到这里。你动手试一下,把代码敲进去,跑一遍流程。遇到问题很正常,多看看错误信息,多查查手册。嗯,就这样。


专注资料整理