ARM 汇编基础教程
——数据类型和寄存器
原文链接: https://azeria-labs.com/arm-data-types-and-registers-part-2/
翻译: ljcnaix
数据类型 这是 ARM 汇编基础教程的第二篇,包含了数据类型和寄存器的相关知识。
和高级语言一样, ARM 汇编语言支持对不同数据类型的操作。我们可以 load (或 store )的数据类型包括 signed/unsigned words , halfwords 或者 bytes 。我们用“ -h ”或“ -sh ”后缀表示 half words ,用“ -b ”或“ -sb ”表示 bytes ,无后缀默认表示 words 。有符号和无符号数据类型之间的区别有:
§ 有符号数可以表示正直和负值,所以范围较小;
§ 无符号数只能表示正值,所以范围更大。
下面是使用 load 和 store 指令操作不同类型数据的示例:
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 字节序
在内存中有两种存储多字节数据的方式,大端序和小端序。这两种方式的差异是数据存储时的字节顺序不同。在以小端序存储数据的设备中(如 x86 ),位权低(个位的位权比十位低)的字节存储在低地址(地址值小的地址)。在以大端序存储数据的设备上,位权高的字节存储在低地址。上一篇我们提到过, ARM 架构在 ARMv3 之前是小端序的,在那之后, ARM 处理器可以通过硬件配置在大小端之间切换。以 ARMv6 为例,指令是固定的以小端序存储的,而内存数据的读取方式可以通过控制程序状态寄存器 CPSR 的第 9 位实现在大端和小端之间切换。
ARM 寄存器 ARM 处理器的寄存器个数与 ARM 指令集版本有关。根据 ARM 手册,除了基于 ARMv6-M 和 ARMv7-M 的处理器,其它的 ARM 处理器都有 30 个 32 bit 的通用寄存器。前 17 个(原文是 16 个,我觉的可能是作者犯的 off-by-one 错误,如果理解有误,请高手指正)寄存器是在用户模式下可访问的,其它的寄存器只有在特定的运行模式下才可以访问( ARMv6-M 和 ARMv7-M 除外,它们的架构有一些差异,有兴趣的话可以单独去学习)。在这篇教程中,我们将关注那些可以在任何运行模式下被访问的寄存器: r0~r15 还有 CPSR 。这 16 个寄存器可以被分为两组:通用寄存器和专用寄存器。
寄存器
别名
作用
R0
-
通用
R1
-
通用
R2
-
通用
R3
-
通用
R4
-
通用
R5
-
通用
R6
-
通用
R7
-
常用于保存系统调用号
R8
-
通用
R9
-
通用
R10
-
通用
R11
FP
用于保存栈帧
专用寄存器
R12
IP
内部调用暂存寄存器
R13
SP
栈顶指针
R14
LR
用于保存函数返回地址
R15
PC
用于保存下一条指令的地址
CPSR
-
当前程序状态寄存器
下面这张表将 ARM 的寄存器和 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
R0~R12 ( R12 的使用要慎重) :R0~R12 是通用寄存器( R12 已经不完全是了),它们可以在常规操作中使用,来存储临时变量或地址。习惯上, R0 常在算数运算中作为累加器,或者存储函数的返回地址。 R7 常用于存储系统调用号。 R11 常作为栈帧指针来标记函数栈帧的边界。此外, ARM 的函数调用约定规定,函数的前四个参数存储在寄存器 r0~r3 中。
R13 : R13 是堆栈指针( SP , Stack Point )。它指向堆栈的顶部。堆栈是用来存储函数局部存储的一段内存,在函数返回时回收。堆栈指针通过减去我们要分配的空间大小,来分配堆栈上的空间。比如,我们要分配一个 32 bit 的空间,那么就令 R13 减 4 。
R14 : R14 是链接寄存器( LR , Link Register )。当进行函数调用时,链接寄存器被更新为调用函数指令的下一条指令的地址。这样做可以使程序在执行完子函数之后得以返回父函数。
R15 : R15 是程序计数器( PC , Program Counter )。在执行指令时, PC 总是自动的增加,增加的大小等于正在执行指令的长度。这个长度在 ARM 架构下是固定的, ARM 模式是 4 字节, Thumb 模式是 2 字节。当执行分支指令时, PC 被更新为目的地址。需要注意的是,由于 RISC CPU 流水线优化的原因,在执行期间, ARM 模式下 PC 等于当前指令地址加 8 , Thumb 模式下等于当前指令地址加 4 ,也就是后移两条指令。这不同于 x86 的 EIP 寄存器,总是指向当前指令的下一条指令。
下面我们通过调试器来看看 PC 的行为。我们使用下面的程序,先将 PC 保存在寄存器 r0 中,然后随意执行两条指令。让我们看看会发生什么:
.section .text
.global _start
.global _main
_start:
b _main
_main:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt
我们使用 gdb 远程调试(作者在这里使用了 gef 增强脚本,可以在 https://github.com/hugsy/gef 找到,关于 gdb 远程调试环境搭建以及 gef 的配置使用,后面会单独写文章介绍)在 _main 标号处设置断点,然后执行:
gef ➤ b _main
Breakpoint 1 at 0x10058: file src/0x00pc/pc.s, line 9.
gef ➤ c
Continuing.
你应该会看到类似下面的输出:
$r0 : 0x00000000
$r1 : 0x00000000
$r2 : 0x00000000
$r3 : 0x00000000
$r4 : 0x00000000
$r5 : 0x00000000
$r6 : 0x00000000
$r7 : 0x00000000
$r8 : 0x00000000
$r9 : 0x00000000
$r10 : 0x00000000
$r11 : 0x00000000
$r12 : 0x00000000
$sp : 0xbefff740 → 0x00000001
$lr : 0x00000000
$pc : 0x00010058 → <_main+0> mov r0, pc
$cpsr: [thumb fast interrupt overflow carry zero negative]
────────────── [ source:src/0x00pc/pc.s+9 ] ────────────
5 _start:
6 b _main
7
8 _main:
-> 9 mov r0, pc
10 mov r1, #2
11 add r2, r1, r1
12 bkpt0x8070 andeq r0, r0, r11
我们可以看到, PC 中存储的地址为( 0x10058 ),也就是下一条即将执行的指令地址。我们使用 si 命令单步执行,下一条指令中 PC 将被存储到寄存器 r0 中,届时寄存器 r0 的值将会是 0x10058 ,是这样吗?
$r0 : 0x00010060 → <_main+8> add r2, r1, r1
$r1 : 0x00000000
$r2 : 0x00000000
$r3 : 0x00000000
$r4 : 0x00000000
$r5 : 0x00000000
$r6 : 0x00000000
$r7 : 0x00000000
$r8 : 0x00000000
$r9 : 0x00000000
$r10 : 0x00000000
$r11 : 0x00000000
$r12 : 0x00000000
$sp : 0xbefff740 → 0x00000001
$lr : 0x00000000
$pc : 0x0001005c → <_main+4> mov r1, #2
$cpsr: [thumb fast interrupt overflow carry zero negative]
───────────── [ source:src/0x00pc/pc.s+10 ] ────────────
6 b _main
7
8 _main:
9 mov r0, pc
-> 10 mov r1, #2
11 add r2, r1, r1
12 bkpt
然而,事实并非如此。我们来看寄存器 r0 ,我们期待的结果是调试器显示的 PC 的值 0x10058 ,然而指令执行的结果表明,在指令执行时 PC 指向的是 0x10060 ,相当于向后偏移两条指令的位置。产生这种差异的原因其实很简单,调试器显示的 PC 寄存器的值是经过处理的。下面我简单解释一下,当 0x10058 处的指令被执行时, PC 寄存器已经指向了 0x10058+0x8 处的指令,这是由于 CPU 的流水线机制导致的。 CPU 取指令,解码指令和执行指令时使用的是不同的硬件部件,因此,这几个操作(实际的 CPU 可能更复杂,有更多的操作步骤)是可以并行执行的。因为 RISC CPU 的指令长度一定,所以 CPU 可以在解码指令之前就知道下一条指令的长度,从而在解码指令时取下一条指令,在执行指令时,对下一条指令进行解码,并取下下一条指令,这称为三级流水线。所以 ARM 当我们在执行 0x10058 处的指令时, PC 已经指向 0x10060 处进行取指令操作了。这是硬件中真实发生的情况,而调试器为了令展示更有逻辑性,所以 PC 寄存器显示了当前执行指令的地址,当我们真实调试时不要受此影响。
当前程序状态寄存器 当你调试一个 ARM 二进制文件时,你会关心 Flags 。
如果你查看 gef 显示的寄存器信息,会发现一个特殊的寄存器 $cpsr ( Current Program Status Register ,当前程序状态寄存器)。你可以看到其中存储了 thumb 、 fast 、 interrupt 、 overflow 、 carry 、 zero 和 negative 这些 Flags 标志位。
$sp : 0xbefff740 → 0x00000001
$lr : 0x00000000
$pc : 0x0001005c → <_main+4> mov r1, #2
$cpsr : [thumb fast interrupt overflow carry zero negative] thumb 、 fast 、 interrupt 、 overflow 、 carry 、 zero 、 negative 这些标志位,由寄存器 $cpsr 中特定的比特位表示。当 $cpsr 寄存器的某个比特位被置位时, gef 中的标志位会显示为粗体。 N , Z , C 和 V 位与 x86 上 EFLAG 寄存器中的 SF , ZF , CF 和 OF 位相同。这些标志位被用于实现汇编语言层变的条件分支和循环,我们将在第六篇:条件分支中对它们进行详细介绍。
上图显示了 32 bit 寄存器 $cpsr 的布局,左边是高位,右边是低位。每个单元格(除了 GE 、 MM 和空白单元格)都是 1 bit 。这些 1 bit 的标志位定义了程序当前状态的各种属性。
标记
含义
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 字节码
假设我们用 cmp 指令来比较 1 和 2 ,结果将为负, Negative 标志位被置 1 。因为 cmp 指令执行一次隐式的减法操作, 1-2=-1 。然而,如果我们比较 2 和 1 (和刚才相反),减法操作不借位, Carry 标志位被置 1 。如果我们比较两个相同的数,比如 2 和 2 ,那么 2-2=0 ,在 Carry 标志位置 1 的同时, Zero 标志位也被置 1 。可以使用如下示例程序检验上述分析。
.section .text
.global _start
.global _main
_start:
b _main
_main:
mov r1, #1
mov r2, #2
cmp r1, r2
cmp r2, r1
cmp r2, r2
bkpt
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
上传的附件: