文章目录
参考文章:
- https://bbs.pediy.com/thread-220461.htm
- 《逆向工程权威指南》
- https://developer.arm.com/documentation/ddi0595/latest
- https://bbs.pediy.com/thread-220907.htm
- https://www.cnblogs.com/viiv/p/15705846.html
1. ARM指令集架构
ARM属于RISC cpu,最初尽力保持各指令长度为4字节。
Intel是CISC指令集,内容很庞大。
最初的arm模式指令集,指令长4字节。后来的thumb模式指令集,指令长2字节。
ARM模式和Thumb模式,可类比x86的实模式和保护模式。
ARM v7引入 thumb-2指令集,指令长2或4字节。后来还有ThumbEE:在Thumb-2基础上包含了针对动态代码生成(代码在执行前或执行期间编译代码)的一些变更和补充。
64位ARM使用ARM 64指令集,指令长4字节。
截至2016年,xcode使用thumb-2指令集。后来也引入了ARVv8和ARM64。
总结三种ARM模式指令集(Jazelle先不考虑):
- arm模式
- thumb模式(包括thumb-2)
- arm64模式(需要单独学习)
ARM和Thumb的区别:
- 条件执行:ARM状态下的所有指令都支持条件执行。某些ARM处理器版本允许使用IT指令在Thumb中进行条件执行。条件执行提高了代码密度,因为它减少了要执行的指令数量,并节省了昂贵的分支指令;
- 32位Thumb指令具有.w后缀;
- 桶形移位器,ARM模式特有的功能,可以将多个指令合并成一个,例如
Mov R1,R0,LSL#1; R1 = R0 * 2
; - 如果当前程序状态寄存器中的T位置1,我们知道我们处于Thumb模式。
mips, powerpc, alphaAXP等处理器都是固定4字节指令。
arm系列从arm11开始,以后的就命名为cortex,并且性能上大幅度提升。从cortex开始,分为三个系列,a系列,r系列,m系列:
- a系列应用在人机互动要求较高的场景,类似于cpu,用于运行os;
- r系列,用于实时控制;
- m系列,可以理解为高级的单片机。
ARM 处理器家族 | ARM指令集架构 |
---|---|
ARM7 | ARM v4 |
ARM9 | ARM v5 |
ARM11 | ARM v6 |
Cortex-A | ARM v7-A |
Cortex-R | ARM v7-R |
Cortex-M | ARM v7-M |
… | … |
Intel处理器的差异
ARM指令只处理寄存器中的数据,也就是说只有load/store指令可以访问存储器。所以如果我们要增加某个内存地址中保存的值,至少需要三种类型的指令(load指令、加法指令和store指令)。
ARM和x86/x64之间更多的区别还包括:
- 在ARM中大多数指令可以用于分支跳转的条件判断。
- Intel的x86/x64系列CPU是小端序的。
- ARM架构在ARMv3之前是小端序的,在那之后,ARM处理器提供一个配置项,可以通过配置在大端和小端之间切换。
2. 编写
$ as program.s –o program.o
$ ld program.o –o program
3. 数据类型和寄存器
可以load(或store)的数据类型包括signed/unsigned words,halfwords或者bytes。默认表示word。
ldr = Load Word
ldrh = Load unsigned Half Word
ldrsh = Load signed Half Word
ldrb = Load unsigned Byte
ldrsb = Load signed Bytes
str = Store Word
strh = Store unsigned Half Word
strsh = Store signed Half Word
strb = Store unsigned Byte
strsb = Store signed Byte
字节序
v3之后,ARM处理器可以通过硬件配置在大小端之间切换
以ARMv6为例,指令是固定的以小端序存储的,而内存数据的读取方式可以通过控制程序状态寄存器CPSR的第9位实现在大端和小端之间切换。
寄存器
https://developer.arm.com/documentation/ddi0595/2021-12?lang=en
除了基于ARMv6-M和ARMv7-M的处理器,其它的ARM处理器都有30个32 bit的通用寄存器。
我们关注17个可以在任何运行模式下被访问的寄存器:r0~r15还有CPSR。它们被分为被分为两组:通用寄存器和专用寄存器。
寄存器 | 别名 | 作用 |
---|---|---|
R0 | - | 通用 |
R1 | - | 通用 |
R2 | - | 通用 |
R3 | - | 通用 |
R4 | - | 通用 |
R5 | - | 通用 |
R6 | - | 通用 |
R7 | - | 常用于保存系统调用号 |
R8 | - | 通用 |
R9 | - | 通用 |
R10 | - | 通用 |
R11 | FP | 用于保存栈帧 |
专用寄存器 | ||
R12 | IP | 内部调用暂存寄存器 |
R13 | SP | 栈顶指针 |
R14 | LR | 用于保存函数返回地址 |
R15 | PC | 用于保存下一条指令的地址 |
CPSR | - | 当前程序状态寄存器 |
和x86寄存器做一个简单类比:
ARM | 简述 | X86 |
---|---|---|
R0 | 通用寄存器 | EAX |
R1~R5 | 通用寄存器 | EBX,ECX,EDX,ESI,EDI |
R6~R10 | 通用寄存器 | - |
R11(FP) | 栈帧寄存器 | EBP |
R12 | 内部调用暂存寄存器 | - |
R13(SP) | 堆栈寄存器 | ESP |
R14(LR) | 链接寄存器 | - |
R15(PC) | 程序计数器 | EIP |
CPSR | 当前程序状态寄存器 | EFLAGS |
ARM的函数调用约定规定,函数的前四个参数存储在寄存器r0~r3中。
r12要慎重使用.
CPU的流水线机制
CPU取指令,解码指令和执行指令时使用的是不同的硬件部件,可以并行执行。
因为RISC CPU的指令长度一定,所以CPU可以在解码指令之前就知道下一条指令的长度,从而在解码指令时取下一条指令,在执行指令时,对下一条指令进行解码,并取下下一条指令,这称为三级流水线。
可以用这么一条汇编来测试:
mov r0, pc
执行完这一句时,pc+4(或2)== r0。
执行这句指令时,PC已经指向下一处进行取指令操作了。这是硬件中真实发生的情况,而调试器为了令展示更有逻辑性,所以PC寄存器显示了当前执行指令的地址,当我们真实调试时不要受此影响。
CPSR
CPSR: Current Program Status Register
标记 | 含义 |
---|---|
N(Negative) | 指令执行结果为负时置1 |
Z(Zero) | 指令执行结果为0时置1 |
C(Carry) | 加法有进位则置1否则置0,减法有借位则置0否则置1 |
V(oVerflow) | 指令执行结果超出32位补码存储范围时置1 |
E(Endian-bit) | 置0时使用小端序,置1时使用大端序 |
T(Thumb-bit) | 置1时使用Thumb模式,置0时使用ARM模式 |
M(Mode-bit) | 共5位表示处理器运行模式 |
J(Jazelle) | 对于有的处理器,置位表示允许以硬件执行java字节码 |
4. Opcode
ARM指令通常跟一到两个操作数, 模板如下:
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
MNEMONIC
- 指令的助记符如ADD
{S} - 可选的扩展位
- 如果指令后加了S,将依据计算结果更新CPSR寄存器中相应的FLAG
{condition}
- 执行条件,如果没有指定,默认为AL(无条件执行)
{Rd}
- 目的寄存器,存储指令计算结果
Operand1
- 第一个操作数,可以是一个寄存器或一个立即数
Operand2
- 第二个(可变)操作数,可以是一个立即数或寄存器甚至带移位操作的寄存器
补充解释一下执行条件和第二个操作数。
- 设置了执行条件的指令需要在执行指令前先校验CPSR寄存器中的标志位。
- 第二个操作数被称为可变操作数,因为它可以被设置为多种形式,包括立即数、寄存器、带移位操作的寄存器,如下
#123 - 立即数
Rx - 寄存器比如R1
Rx, ASR n - 对寄存器中的值进行算术右移n位后的值
Rx, LSL n - 对寄存器中的值进行逻辑左移n位后的值
Rx, LSR n - 对寄存器中的值进行逻辑右移n位后的值
Rx, ROR n - 对寄存器中的值进行循环右移n位后的值
Rx, RRX - 对寄存器中的值进行带扩展的循环右移1位后的值
5. 交叉编译环境
ubuntu 18.04
搜索交叉编译器:
$ apt search gcc-5-arm-linux
可以看到gcc-5-arm-linux-gnueabi和gcc-5-arm-linux-gnueabihf,后者对浮点数的编译选项做了特殊处理。安装前者即可。另外需要安装qemu-arm模拟器:
$ sudo apt install gcc-5-arm-linux-gnueabi
$ sudo apt-get install qemu qemu-system qemu-user
用法和gcc一样:
$ arm-linux-gnueabi-as [source file] –o [object file]
$ arm-linux-gnueabi-ld [object file] –o [executable file]
$ arm-linux-gnueabi-gcc-5 hello.c –g –o hello -static
其它工具还有arm-linux-gnueabi-objdump, arm-linux-gnueabi-elf
等。
设置ld-linux.so的加载路径, 否则执行会报错/lib/ld-linux.so.3: No such file or directory
:
$ export QEMU_LD_PREFIX=/usr/arm-linux-gnueabi
$ qemu-arm -L /usr/arm-linux-gnueabi
5.1 arm架构模拟器
$ sudo apt install qemu-user-static
执行 hello 程序:
$ qemu-arm-static hello
启动 gdbserver 等待 gdb 连接:
$ qemu-arm-static -g 1234 ./hello
5.2 虚拟 Raspberry
略 有时间实践后再补充。
5.3 调试环境
gdb-multiarch
gdb 支持多种硬件体系架构的版本。
$ sudo apt install gdb-multiarch
$ gdb-multiarch
(gdb) set architecture arm 非必须
(gdb) target remote localhost:1234
(gdb) b main
(gdb) c
编译 gdbserver
分析 IoT 设备时,往往需要上传 gdbserver 进行远程调试。在我们的实验环境中(如果没有gdb的话 ),可以模拟搭建一个远程调试环境。首先,获取与本地gdb 版本一致的 gdb 源码(包含了 gdb 源码和 gdbserver 源码),因为 gdbserver 需要与 gdb 版本保持一致。
http://ftp.gnu.org/gnu/gdb/
$ CC="arm-linux-gnueabi-gcc-5" CXX="arm-linux-gnueabi-g++-5" ./configure --target=arm-linux-gnueabi --host="arm-linux-gnueabi" --prefix="setup-directory"
$ make install
编译完成后用scp 将 gdbserver 上传到我们的虚拟arm环境中并启动:
$ ln -s arm-linux-gnueabi-gdbserver gdbserver
$ gdbserver 0.0.0.0:2333 hello
Process hello created; pid = 702
Listening on port 2333
gef
支持多种硬件体系结构的 gdb 插件。
https://github.com/hugsy/gef
注意keystone-engine 需要提前手动安装
$ wget -O ~/.gdbinit-gef.py -q https://gef.blah.cat/py
$ echo source ~/.gdbinit-gef.py >> ~/.gdbinit
安装成功后:
gef> set architecture arm
gef> gef-remote –q 127.0.0.1:1234