ARM 汇编学习

ARM 汇编学习

ARM 处理器已经在生活中有了广泛应用,随之而来也有许多的安全问题,所以对于二进制方向,学习ARM 汇编是必须且有必要的。

ARM 汇编基础知识

ARM 与 Intel 的区别

​ Intel 和 ARM之间区别有很多,最主要的区别在于指令集。

​ Intel 是一个CISC(复杂指令集计算)处理器,具有更大,功能更丰富的指令集,并允许许多复杂指令访问内存。因此,它比ARM具有更多的操作,寻址模式,但寄存器更少。CISC处理器主要用于普通PC,工作站和服务器。

​ ARM 是RISC(简化指令集计算)处理器,因此具有简化的指令集(100条或更少)比CISC更通用的寄存器。与英特尔不同,ARM 使用仅在寄存器上运行的指令,并使用加载/存储内存模型进行内存访问,这意味着只有加载/存储指令才能访问内存。这意味着在 ARM 上增加特定内存地址的 32 位值需要三种类型的指令(加载、递增和存储)首先将特定地址处的值加载到寄存器中,在寄存器中递增该值,然后将其从寄存器存储回存储器。

简化的指令集有其优点和缺点。其中一个优点是指令可以更快地执行,可能允许更高的速度(RISC系统通过减少每条指令的时钟周期来缩短执行时间)。缺点是更少的指令意味着更加强调使用有限的可用指令有效地编写软件。另外需要注意的是ARM有两种模式,ARM模式Thumb模式。 Thumb 指令可以是 2 个或 4 个字节。

ARM 和 x86 的更多区别:

  • 在 ARM 中,大多数指令都可用于条件执行。
  • Intel x86 和 x86-64 系列处理器使用小端格式,而在版本 3 之前,ARM 体系结构是小端序的。从版本3开始,ARM处理器成为BI-Endian,具有允许可切换字节序的设置。

理解汇编语言

​ 电路上存在着信号,也就是电压的高低,我们可视地将高低点位转换成1和0去编写,通过01序列来形成机器代码指令来操控机器。这就是机器语言。而对于我们来说,我们没办法向机器一样快速理解这些0,1序列的含义,所以创造了一种与机器语言高度对应的助记符来代表机器指令。早些时候,人们通过使用汇编语言作为指令编写程序,称为汇编语言程序。要记住,汇编语言是人类用来对计算机进行编程的最低级别,指令的操作数位于助记符之后。

​ 汇编语言程序实际上只是一个文本程序,我们需要将其转换为机器代码让计算机执行。在 ARM 汇编的情况下,GNU Binutils项目为我们提供了一个名为 **as **的工具,使用汇编器将(ARM)汇编语言转换为(ARM)机器语言,该过程称为汇编。

数据类型与寄存器

数据类型

​ 与高级语言类似,ARM 支持对不同数据类型的操作,可以是有符号、无符号的字(Word,无扩展名),半字(Half Word,扩展名为 -h 或 -sh )和字节(Byte,扩展名为 -b 或 -sb )。这里的字是32位,半字是16位,字节是8位,无符号数比有符号数能够表示数的范围更大,因为符号位不参与数值大小的计算。

以下是如何将这些数据类型与“加载和存储”说明一起使用的一些示例:(帮助记忆)

1
2
3
4
5
6
7
8
9
10
11
12
## 加载
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

字节序

​ 查看内存中的字节有两种基本方法:小端 (Little-Endian) 或大端 (Big-Endian)。不同之处在于对象的每个字节存储在内存中的字节顺序。在像 Intel x86 这样的小端机器上,最低有效字节存储在最低地址(最接近零的地址),例如0x12345678用小端存储则为78 56 34 12(地址从低到高)。在大端机器上,最高有效字节存储在最低地址(最接近零的地址),例如0x1245678用大端存储则为12 34 56 78(地址从低到高)。在版本3之前,ARM架构是小端的,从那个版本起它是双端的,这意味着它具有允许可切换字节序的设置。例如,在 ARMv6 上,指令是固定的小端,数据访问可以是小端或大端,由程序状态寄存器 (CPSR) 的位 9(E 位)控制。

寄存器

​ 寄存器的数量取决于 ARM 版本。根据 ARM 参考手册,有 30 个通用 32 位寄存器,但基于 ARMv6-M 和 ARMv7-M 的处理器除外。前 16 个寄存器可在用户级模式下访问,其他寄存器可在特权软件执行中使用(ARMv6-M 和 ARMv7-M 除外)。

这 16 个寄存器可以分为两组:通用寄存器和专用寄存器:

# 别名 用途
R0 General purpose(相当于 x86 的eax)
R1 General purpose(相当于 x86 的ebx)
R2 General purpose(相当于 x86 的ecx)
R3 General purpose(相当于 x86 的edx)
R4 General purpose(相当于 x86 的esi)
R5 General purpose(相当于 x86 的edi)
R6 General purpose
R7 Holds Syscall Number(系统调用号)
R8 General purpose
R9 General purpose
R10 General purpose
R11 FP Frame Pointer(帧指针寄存器,相当于 x86 的 ebp )
Special Purpose Registers
R12 IP Intra Procedural Call(过程内调用)
R13 SP Stack Pointer(栈指针寄存器,相当于 x86 的 esp )
R14 LR Link Register(链接寄存器)
R15 PC Program Counter(程序计数器,相当于 x86 的 eip )
CPSR Current Program Status Register(当前程序状态寄存器,相当于 x86 的 EFLAGS )

R0-R12:可在常见操作期间用于存储临时值、指针(内存地址)等。R0 在算术运算期间可称为累加器或用于存储先前调用函数的结果。 R7 存储了系统调用号,在处理系统调用时相当有用。R11 帧指针指向当前栈帧底部。 此外,ARM 上的函数调用约定指定函数的前四个参数存储在寄存器 r0-r3 中。

R13:SP(栈指针)。 栈指针指向当前栈帧的顶部,也是整个栈的顶部。 堆栈是用于特定于函数的存储的内存区域,当函数返回时会被回收。 堆栈指针用于在堆栈上分配空间,方法是从堆栈指针中减去我们想要分配的值(以字节为单位)。 换句话说,如果我们要分配一个 32 位的值,我们从堆栈指针中减去 4。

R14:LR(链接寄存器)。 进行函数调用时,链接寄存器会更新为引用下一条指令的内存地址。简单来说就是存储着函数的返回地址,相当于存储着 x86 汇编中 call 指令入栈的值。

R15:PC(程序计数器)。 程序计数器根据所执行指令的大小自动递增。 此大小在 ARM 状态下始终为 4 个字节,在 THUMB 模式下为 2 个字节。 当执行分支指令时,PC 保存目标地址。 在执行过程中,PC 将当前指令地址加 8(两条 ARM 指令)存放在 ARM 状态,将当前指令地址加 4(两条 Thumb 指令)存放在 Thumb(v1) 状态。 这与 x86 不同,x86中 PC 总是指向下一条要执行的指令。

$cpsr 显示当前程序状态寄存器 (CPSR) 的值,在其下您可以看到标志拇指、快速、中断、溢出、进位、零和负。 这些标志代表 CPSR 寄存器中的某些位,并根据 CPSR 的值设置,并在激活时变为粗体。 N、Z、C 和 V 位与 x86 上 EFLAG 寄存器中的 SF、ZF、CF 和 OF 位相同。 这些位用于支持汇编级别的条件和循环中的条件执行。 我们将介绍第 6 部分中使用的条件代码:条件执行和分支。

Flag Description
N (Negative) Enabled if result of the instruction yields a negative number.
Z (Zero) Enabled if result of the instruction yields a zero value.
C (Carry) Enabled if result of the instruction yields a value that requires a 33rd bit to be fully represented.
V (Overflow) Enabled if result of the instruction yields a value that cannot be represented in 32 bit two’s complement.
E (Endian-bit) ARM can operate either in little endian, or big endian. This bit is set to 0 for little endian, or 1 for big endian mode.
T (Thumb-bit) This bit is set if you are in Thumb state and is disabled when you are in ARM state.
M (Mode-bits) These bits specify the current privilege mode (USR, SVC, etc.).
J (Jazelle) Third execution state that allows some ARM processors to execute Java bytecode in hardware.

ARM 指令集

ARM & Thumb

​ ARM 处理器有两个可以运行的主要状态(这里不计算 Jazelle),ARMThumb。这些状态与特权级别无关。这两种状态的主要区别在于指令集,其中 ARM 状态的指令始终是 32 位的,而 Thumb 状态的指令是 16 位的(但也可以是 32 位的)。

​ Thumb有不同的版本:

  • Thumb-1(16 位指令):用于 ARMv6 和更早的架构。
  • Thumb-2(16 位和 32 位指令):通过添加更多指令并允许它们为 16 位或 32 位宽(ARMv6T2、ARMv7)来扩展 Thumb-1。
  • ThumbEE:包括针对动态生成的代码(在执行前不久或执行期间在设备上编译的代码)的一些更改和添加。

​ ARM 和 Thumb 的区别:

  • 条件执行:ARM 状态下的所有指令都支持条件执行。某些 ARM 处理器版本允许使用 IT 指令在 Thumb 中进行条件执行。条件执行导致更高的代码密度,因为它减少了要执行的指令数量并减少了昂贵的分支指令的数量。
  • 32 位 ARM 和 Thumb 指令:32 位 Thumb 指令具有 .w 后缀。
  • 桶形移位器是另一个独特的 ARM 模式功能。它可用于将多条指令压缩为一条。例如,不要使用两条指令进行乘法(将寄存器乘以 2 并使用 MOV 将结果存储到另一个寄存器中),可以使用(左移) 1 -> MOV R1、R0、LSL 将乘法包含在 MOV 指令中#1 ; R1 = R0 * 2

要切换处理器执行的状态,必须满足以下两个条件之一:

  • 我们可以使用分支指令 BX(分支和交换)或 BLX(分支、链接和交换)并将目标寄存器的最低有效位设置为 1,CPSR寄存器会根据最低有效位的值去设置T位来切换模式来解释分支跳转后的地址后的内容。这可以通过在偏移量上加 1 来实现,例如 0x5530 + 1。这不会导致偏移问题,因为处理器将忽略最低有效位。
  • 如果当前程序状态寄存器($cpsr)中的 T 位被置位,我们就知道我们处于 Thumb 模式。

ARM指令介绍

ARM 指令通常为指令后跟一到两个操作数这样的形式。模板大致为:

MNEMONIC{S}{condition} {Rd}, Operand1, Operand2

(由于 ARM 指令集的灵活性,并非所有指令都使用模板中提供的所有字段。)

1
2
3
4
5
6
MNEMONIC     - 指令的简称(助记符)
{S} - 可选后缀。 如果指定了 S,则条件标志会根据操作结果更新
{condition} - 执行指令需要满足的条件
{Rd} - 用于存储指令结果的寄存器(目标)
Operand1 - 第一个操作数。 寄存器或立即数
Operand2 - 第二个(灵活的)操作数。 可以是立即数(数字)或带有可选移位的寄存器

{condition} 与 CPSR 寄存器的值密切相关,或者更准确地说,是寄存器中特定位的值。

Operand2 被称为灵活操作数,因为可以以各种形式使用它——作为立即值(具有有限的值集)、寄存器或移位寄存器,如:

1
2
3
4
5
6
7
#123                    - Immediate value (with limited set of values). 
Rx - 寄存器 x(如 R1、R2、R3 ...)
Rx, ASR n - 寄存器 x 算术右移 n 位 (1 = n = 32)
Rx, LSL n - 寄存器 x 逻辑左移 n 位 (0 = n = 31)
Rx, LSR n - 寄存器 x 逻辑右移 n 位 (1 = n = 32)
Rx, ROR n - 寄存器 x 逻辑右移 n 位 (1 = n = 32)
Rx, RRX - 寄存器 x 右移一位,扩展

下面是指令使用示例:(’#’ 用于立即数的前缀)

1
2
3
4
ADD R0, R1, R2 - 将 R1 (Operand1) 和 R2 (Operand2 的寄存器形式) 的内容相加并将结果存储到 R0 (Rd)
ADD R0, R1, #2 - 将 R1 (Operand1) 的内容与值 2 (Operand2 的立即数形式) 相加并将结果存储到 R0 (Rd)
MOVLE R0, #5 - 仅当满足条件 LE(小于或等于)时,将数字 5(操作数 2,因为编译器将其视为 MOVLE R0,R0,#5)移动到 R0(Rd)
MOV R0, R1, LSL #1 - 将 R1 的内容(操作数 2 采用逻辑左移的寄存器形式)左移一位到 R0 (Rd)。 因此,如果 R1 的值为 2,它会左移一位并变为 4。然后将 4 移至 R0。

接下来是一些常用指令(助记符):

Instruction Description Instruction Description
MOV Move data EOR Bitwise XOR
MVN Move and negate LDR Load
ADD Addition STR Store
SUB Subtraction LDM Load Multiple
MUL Multiplication STM Store Multiple
LSL Logical Shift Left PUSH Push on Stack
LSR Logical Shift Right POP Pop off Stack
ASR Arithmetic Shift Right B Branch
ROR Rotate Right BL Branch with Link
CMP Compare BX Branch and eXchange
AND Bitwise AND BLX Branch with Link and eXchange
ORR Bitwise OR SWI/SVC System Call

内存操作

LDR/STR

LDR(load),唯二可对内存操作的指令,通常用于将内容从内存加载到寄存器中。

指令格式为:LDR Ra, [Rb]

将数据从Rb所记录的内存位置加载到寄存器Ra中,Rb可以是立即数,也可以是寄存器。

STR(store),唯二可对内存操作的指令,通常用于将内容从寄存器加载到内存中。

指令格式为:STR RA, [Rb]

将数据从Ra寄存器存储到Rb所记录的内存位置,Rb可以是立即数,也可以是寄存器。

下面对一些用法用示例做说明:

  1. ldr r0, [pc, #2]

​ 将 PC + 2 的地址处的数据加载到 r0 寄存器中。同样这里的 #2 的立即数位置换成寄存器也没有问题。下同。

  1. ldr r3, [r1], #2

​ 将 r1 + 2 的地址处的数据加载到 r3 寄存器中,并将 r1 寄存器的值修改为 r1 + 2。

  1. ldr r3, [r1], r2, LSL#2

​ 将 r1 的地址处的数据加载到 r3 寄存器中,并将 r1 寄存器的值修改为 r1 + (r2 LSL #2)。

  1. str r2, [r1, #2]

​ 将 r2 寄存器中的数据存储到 r1 + 2 的地址处。

  1. str r2, [r1, #2]!

​ 将 r2 寄存器中的数据存储到 r1 + 2 的地址处,并将 r1 寄存器的值修改为 r1 + 2。

  1. str r2, [r1, r2, LSL#2]

​ 将 r2 寄存器中的数据存储到 r1 + (r2 LSL #2) 的地址处。

  1. str r2, [r1, r2, LSL#2]!

​ 将 r2 寄存器中的数据存储到 r1 + (r2 LSL #2) 的地址处,并将 r1 寄存器的值修改为 r1 + (r2 LSL #2)。

用于 PC 相对寻址的 LDR

这里列举两种用法:

ldr r0, =jump

ldr r1, =0x68DB00AD

首先,LDR指令既可以是大范围的地址读取伪指令,也可以内存访问指令。当它的第二个参数前面有“=”时,表示伪指令,否则表示内存访问指令。

我们可以使用伪指令来引用文字池中的数据。 文字池是同一节中的一个内存区域(因为文字池是代码的一部分),用于存储常量、字符串或偏移量。 在上面的示例中,我们使用这些伪指令来引用函数的偏移量,并在一条指令中将 32 位常量移动到寄存器中。

在 ARM 上使用立即数

​ 我们知道每条 ARM 指令都是 32 位长的,并且所有的指令都是有条件的。我们可以使用 16 个条件代码,一个条件代码占用指令的 4 位。然后我们需要 2 位作为目标寄存器。 2 位用于第一个操作数寄存器,1 位用于设置状态标志,以及用于其他事项(如实际操作码)的各种位数。所以,这里的重点在于,在为指令类型、寄存器和其他字段分配位之后,只剩下 12 位用于立即数,所以在 ARM 中使用 MOV 指令将只能使用有限范围的立即数。实际上,ARM 并不是直接使用这12位去引用立即数,而是将这 12 位拆分为一个 8 位数字,即能够加载 0-255 范围内的任何 8 位值,以及一个 4 位的循环位移字段 ,在 0 到 30 之间以2的倍数为次数循环右移。所以完整的立即数的公式为:v = n ror 2 * r,所以这里的立即数是有要求的,立即数可以由一个8位的常数循环右移偶数位得到。

​ 在这里对于超过 0 - 255 范围的32位立即数,有如下几种引用方式:

  1. 将该数拆分然后相加

例:我们想要实现 MOV r0, #511

​ 则可以使用: MOV r0, #256 ADD r0, #255

  1. 使用 LDR 伪指令:

LDR 伪指令用于加载32位的立即数或一个地址值到指定寄存器。在汇编编译源程序时,LDR伪指令被编译器替换成一条合适的指令。若加载的常数未超出 MOV 的范围,则使用 MOV 或指令代替该 LDR 伪指令,否则汇编器将常量放入文字池,并使用一条程序相对偏移的ldr指令从文字池读出常量。

ldr r1, =511

存储多个数据

LOAD/STORE MULTIPLE

​ ARM 中使用 LDMSTM 指令对多个数据进行加载或存储的操作。这里要注意 LDM/STM 与 LDR/STR 的操作数顺序是反的。

LDM Ra Rb 将Ra加载到Rb

STM Ra Rb 将Rb存储到Ra

​ LDM 与 STM 在不做指定的情况下一次操作的数据都是四个字节。也就是一个 word。

​ 这两个指令可以伴随一些后缀一起使用,使用的后缀有:-IA(之后增加)、-IB(之前增加)、-DA(之后减少)、-DB(之前减少)。 这些变体的不同之处在于它们访问由第一个操作数(存储源地址或目标地址的寄存器)指定的内存的方式。

这里举例子说明:

(r0 -> word[3], r1 -> word[0], r2 -> word[2])

-IA:

1
2
ldmia r0, {r4-r6} /* words[3] -> r4, words[4] -> r5; words[5] -> r6; */ 
stmia r1, {r4-r6} /* r4 -> array_buff[0]; r5 -> array_buff[1]; r6 -> array_buff[2] */

-IB:

1
2
ldmib r0, {r4-r6}            /* words[4] -> r4 = 0x04; words[5] -> r5 = 0x05; words[6] -> r6 = 0x06 */
stmib r1, {r4-r6} /* r4 -> array_buff[1] = 0x04; r5 -> array_buff[2] = 0x05; r6 -> array_buff[3] = 0x06 */

-DA:

1
2
ldmda r0, {r4-r6} /* words[3] -> r6; words[2] -> r5; words[1] -> r4 */
stmda r2, {r4-r6} /* r6 -> array_buff[2]; r5 -> array_buff[1]; r4 -> array_buff[0] */

-DB:

1
2
ldmdb r0, {r4-r6} /* words[2] -> r6; words[1] -> r5; words[0] -> r4 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1]; r4 -> array_buff[0] */

总结一下就是D和I的区别在于寄存器的执行方向,I 是从左到右,D 是从右到左。A 和 B 的区别在于是先操作还是先变化地址。

PUSH AND POP

​ 在 x86 中,通过POP PUSH 指令对栈中的元素进行修改,在 ARM 中同样也可以使用这两个指令:

1
2
push {r0, r1}
pop {r2, r3}

由于栈的数据结构是后进先出,所以存储数据会先减少栈指针,然后先存储低位寄存器再存储高位寄存器(地址由低到高增长),同样的从栈中弹出数据就是先将低地址数据弹出到高位寄存器,再增加栈指针。这就与 STRDB 和 LDMIA 十分相像。实际上这两种表示方法是一样的,可以将 pop,push 写成:

1
2
stmdb sp!, {r0, r1}
ldmia sp!, {r2, r3}

在汇编器对我们写的汇编语言进行编译时,也会将如上的指令翻译成 push 和 pop。

条件执行和分支

条件执行

在程序运行的时候我们通过条件来控制程序的流程,在 ARM 中,条件被描述为了 CPSR 寄存器中特定位的状态。

下表列出了可用的条件代码、它们的含义以及测试的标志的状态:

Condition Code Meaning (for cmp or subs) Status of Flags
EQ Equal Z==1
NE Not Equal Z==0
GT Signed Greater Than (Z==0) && (N==V)
LT Signed Less Than N!=V
GE Signed Greater Than or Equal N==V
LE Signed Less Than or Equal (Z==1) || (N!=V)
CS or HS Unsigned Higher or Same (or Carry Set) C==1
CC or LO Unsigned Lower (or Carry Clear) C==0
MI Negative (or Minus) N==1
PL Positive (or Plus) N==0
AL Always executed
NV Never executed
VS Signed Overflow V==1
VC No signed Overflow V==0
HI Unsigned Higher (C==1) && (Z==0)
LS Unsigned Lower or same (C==0) || (Z==0)

这里再贴一遍CPSR寄存器的各位详情方便对照理解:

Flag Description
N (Negative) Enabled if result of the instruction yields a negative number.
Z (Zero) Enabled if result of the instruction yields a zero value.
C (Carry) Enabled if result of the instruction yields a value that requires a 33rd bit to be fully represented.
V (Overflow) Enabled if result of the instruction yields a value that cannot be represented in 32 bit two’s complement.
E (Endian-bit) ARM can operate either in little endian, or big endian. This bit is set to 0 for little endian, or 1 for big endian mode.
T (Thumb-bit) This bit is set if you are in Thumb state and is disabled when you are in ARM state.
M (Mode-bits) These bits specify the current privilege mode (USR, SVC, etc.).
J (Jazelle) Third execution state that allows some ARM processors to execute Java bytecode in hardware.

​ 之前提到过,存在不同版本的 Thumb,如允许条件执行的 Thumb 版本 (Thumb-2)。 某些 ARM 处理器版本支持“IT”指令,该指令允许在 Thumb 状态下有条件地执行多达 4 条指令。

语法结构:IT{x{y{z}}} cond

  • cond 指定 IT 块中第一条指令的条件
  • x 指定 IT 块中第二条指令的条件切换
  • y 指定 IT 块中第三条指令的条件切换
  • z 指定 IT 块中第四条指令的条件切换

IT 指令的结构是“IF-Then-(Else)”,语法是两个字母 T 和 E 的结构:

  • IT 指 If-Then(下一条指令是有条件的)
  • ITT 指 If-Then-Then(接下来的 2 条指令是有条件的)
  • ITE 指 If-Then-Else(接下来的 2 条指令是有条件的)
  • ITTE 指 If-Then-Then-Else(接下来的 3 条指令是有条件的)
  • ITTEE 指 If-Then-Then-Else-Else(接下来的 4 条指令是有条件的)

讲白了 x y z 的可选值就是T(Then)或者E(Else)。I后面有几个字母,接下来的几条指令就属于此条件操作。

举个例子:

1
2
3
4
cmp r0, #10      
ite eq @ if R0 is equal 10...
addeq r1, #2 @ ... then R1 = R1 + 2
addne r1, #3 @ ... else R1 = R1 + 3

那么第一个add代表then的执行命令,第二个add代表else的执行命令。

IT 块内的每条指令都必须指定一个条件后缀,该条件后缀可以是相同的,也可以是逻辑逆的。 这意味着如果您使用 ITE,则第一条和第二条指令 (If-Then) 必须具有相同的条件后缀,而第三条 (Else) 必须具有前两条的逻辑逆。

错误示例:

1
2
3
cmp r0, #2
IT NE ; Next instruction is conditional
ADD R0, R0, R1 ; Syntax error: no condition code used in IT block.

以下是条件代码及其对立面:

Condition Code Opposite
Code Meaning Code Meaning
EQ Equal NE Not Equal
HS (or CS) Unsigned higher or same (or carry set) LO (or CC) Unsigned lower (or carry clear)
MI Negative PL Positive or Zero
VS Signed Overflow VC No Signed Overflow
HI Unsigned Higher LS Unsigned Lower or Same
GE Signed Greater Than or Equal LT Signed Less Than
GT Signed Greater Than LE Signed Less Than or Equal
AL (or omitted) Always Executed There is no opposite to AL

分支

​ 分支(又名跳转)允许我们跳转到另一个代码段。 当我们需要跳过(或重复)代码块或跳转到特定函数时,就会用到分支。 经常配合条件语句和循环使用。

条件

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.global main

main:
mov r1, #2 /* setting up initial variable a */
mov r2, #3 /* setting up initial variable b */
cmp r1, r2 /* comparing variables to determine which is bigger */
blt r1_lower /* jump to r1_lower in case r2 is bigger (N==1) */
mov r0, r1 /* if branching/jumping did not occur, r1 is bigger (or the same) so store r1 into r0 */
b end /* proceed to the end */
r1_lower:
mov r0, r2 /* We ended up here because r1 was smaller than r2, so move r2 into r0 */
b end /* proceed to the end */
end:
bx lr /* THE END */

这个例子就是比较两个整数的大小并且返回较大数。这里我们可以看到 blt 即小于跳转。

这里单独说一下最后的bx lr,其作用等同于 mov pc,lr

lr 就是连接寄存器(Link Register, LR),在ARM体系结构中LR的特殊用途有两种:一是用来保存子程序返回地址;二是当异常发生时,LR中保存的值等于异常发生时PC的值减4(或者减2),因此在各种异常模式下可以根据LR的值返回到异常发生前的相应位置继续执行。  

当通过BL或BLX指令调用子程序时,硬件自动将子程序返回地址保存在R14(LR)寄存器中。在子程序返回时,把LR的值复制到程序计数器PC即可实现子程序返回。

循环

示例:

1
2
3
4
5
6
7
8
9
10
11
.global main

main:
mov r0, #0 /* setting up initial variable a */
loop:
cmp r0, #4 /* checking if a==4 */
beq end /* proceeding to the end if a==4 */
add r0, r0, #1 /* increasing a by 1 if the jump to the end did not occur */
b loop /* repeating the loop */
end:
bx lr /* THE END */

通过不断向前跳转实现数据的累加。

不同的分支指令

B:简单跳转到函数

BL:在 LR 中保存 (PC+4) 并跳转到函数

BX/BLX:需要一个寄存器作为第一个操作数,跳转并且交换指令集(ARM/Thumb)

堆栈和函数

堆栈

​ 一般来说,堆栈是程序/进程内的一个内存区域。这部分内存是在创建进程时分配的。我们使用堆栈来存储临时数据,例如某些函数的局部变量、帮助我们在函数之间转换的环境变量等。我们使用 PUSH 和 POP 指令与堆栈交互。PUSH 和 POP 是其他一些与内存相关的指令的别名,而不是真正的指令,但出于简单的原因,我们使用 PUSH 和 POP。

​ 存储器堆栈可分为两种:
​ 向高地址方向生长,称为递增堆栈
​ 向低地址方向生长,称为递减堆栈

​ 栈指针指向最后压入的堆栈的有效数据项,称为满堆栈

​ 堆栈指针指向下一个要放入的空位置,称为空堆栈

这样就有4中类型的堆栈表示递增和递减的满堆栈和空堆栈的各种组合。

满递增:堆栈通过增大存储器的地址向上增长,堆栈指针指向内含有效数据项的最高地址。指令如LDMFASTMFA等。(Ascending 递增)

空递增:堆栈通过增大存储器的地址向上增长,堆栈指针指向堆栈上的第一个空位置。指令如LDMEASTMEA等。

满递减:堆栈通过减小存储器的地址向下增长,堆栈指针指向内含有效数据项的最低地址。指令如LDMFDSTMFD等。(Descending 递减)

空递减:堆栈通过减小存储器的地址向下增长,堆栈指针指向堆栈下的第一个空位置。指令如LDMEDSTMED等。

Stack Type Store Load
Full descending STMFD (STMDB, Decrement Before) LDMFD (LDM, Increment after)
Full ascending STMFA (STMIB, Increment Before) LDMFA (LDMDA, Decrement After)
Empty descending STMED (STMDA, Decrement After) LDMED (LDMIB, Increment Before)
Empty ascending STMEA (STM, Increment after) LDMEA (LDMDB, Decrement Before)

函数

这里其实和 x86 中十分类似,理解起来可以去类比函数栈帧。

在ARM中函数由如下三个部分组成:

  • Prologue 为函数设置环境
  • Body 实现函数的逻辑并将结果存储到 R0
  • Epilogue 恢复状态,以便程序可以在调用函数之前从其离开的位置恢复

ARM 中,无论函数内部如何调用,如何跳转,最后的返回值都会存储在 r0 寄存器中,若返回64位结果,则将同时使用 r0 和 r1 去记录返回值。

关于函数的另一个关键点是它们的类型:叶子函数非叶函数

  • 叶函数是一种不会从自身调用/分支到另一个函数的函数。

  • 非叶函数是一种函数,除了它自己的逻辑之外,它还调用/分支到另一个函数。

​ 简而言之,他俩的主要区别是会不会调用其他函数。就如我们的 main 函数一般都要调用一些子函数,那 main 函数无疑就是非叶函数,而一般较为简单的子函数由于不调用其他函数,其就为叶子函数。

另一个关键区别是 Prologue 和 Epilogue 的实现方式,例如:

1
2
3
4
5
6
7
8
9
/* A prologue of a non-leaf function */
push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */
add r11, sp, #0 /* Setting up the bottom of the stack frame */
sub sp, sp, #16 /* End of the prologue. Allocating some buffer on the stack */

/* A prologue of a leaf function */
push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */
add r11, sp, #0 /* Setting up the bottom of the stack frame */
sub sp, sp, #12 /* End of the prologue. Allocating some buffer on the stack */

​ 这里的主要区别在于非叶函数中的 Prologue 条目将更多的寄存器保存到堆栈中。 这背后的原因是,由于非叶函数的性质,LR 在执行此类函数期间会被修改,因此需要保留该寄存器的值,以便以后可以恢复。

1
2
3
4
5
6
7
8
 /* An epilogue of a leaf function */
add sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */
pop {r11} /* restoring frame pointer */
bx lr /* End of the epilogue. Jumping back to main via LR register */

/* An epilogue of a non-leaf function */
sub sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */
pop {r11, pc} /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */

叶子函数直接跳转到 lr 寄存器存储的返回地址返回,而非叶函数则是将栈上的返回地址弹出到 PC 寄存器实现返回。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2022-2023 Syclover.Kama
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信