arm汇编基础

Posted by nop on 2020-12-27
Words 9.2k In Total

本篇是继上篇环境配置之后的第二篇,算是对arm指令集的学习笔记,原文参见【系列分享】ARM 汇编基础速成1:ARM汇编以及汇编语言基础介绍

ARM汇编概述

ARM和INTEL

ARM的处理器与Intel处理器有许多不同,其中最主要的区别就在于指令集。相对与Intel采用的复杂指令集(CISC),ARM属于精简指令集(RISC),它指令集少寄存器多。ARM的指令集仅仅操作寄存器或者用于从内存中的加载、存储过程,比如要对特定地址中的32位值加一,只需要将其从内存中加载到寄存器,然后做加一操作 ,接着再从寄存器存储到内存。相比而言,精简指令集可以使代码执行变快,但是代价是指令集变少了,编写代码时要更加注意指令间使用关系以及约束。

ARM架构有两种模式:ARM模式和Thumb模式,后者的代码只有2字节或者4字节。

此外,ARM和x86还有其他的不同点:

  1. ARM中的很多指令都可以用来做为条件执行的判断依据
  2. x86和x64的机器码使用小端格式
  3. ARM机器码在v3之前是小端,之后默认采用大端模式,但可以切换到小端

ARM家族

ARM family ARM architecture
ARM7 ARM v4
ARM9 ARM v5
ARM11 ARM v6
Cortex-A ARM v7
Cortex-R ARM v7-R
Cortex-M ARM v7-M

汇编语言的本质

在最底层,只有电路的电信号,信号被格式化成可变化的高低电平0V(off)或者5V(on)。但是通过电压变化来表述电路状态是繁琐的,所以用0和1来代替高低电平,也就有了二进制格式。由二进制序列组成的组合便是最小的计算机处理器工作单元了:
1110 0001 1010 0000 0010 0000 0000 0001

但是这些组合是不可能记住的,所以就需要助记符来帮助我们记忆这些二进制组合,这些助记符一般是连续的三个字母,用这些助记符编写的程序就是汇编语言程序,用以代表一种计算机的机器码的助记符集合就称之为汇编语言。所以,汇编语言是用来编写程序的最底层的语言。

ARM的两种模式:ARM和Thumb

常见的ARM是32位,其中thumb模式是16位模式,标准32位模式下,可以切换到Thumb模式下用以压缩代码提高空间利用率,程序可以通过对应的指令在ARM模式和Thumb模式之间切换。
Thumb指令可以看作是ARM指令压缩形式的子集,是针对代码密度的问题而提出的,它具有16位的代码密度,但Thumb并不是完整的体系结构,也即是说程序不能只执行Thumb指令而不支持ARM指令集,所以Thumb指令只支持通用功能,必要时借助ARM指令集。

编写Thumb指令时,需先使用伪指令CODE16声明,ARM指令中使用BX指令跳转到Thumb指令以切换处理器状态;编写ARM指令时,可使用伪指令CODE32声明。

Thumb指令集没有协处理指令、信号量指令以及访问CPSR、SPSR的指令,没有乘加指令及64位乘法指令,并且指令的第二操作数受到限制;除跳转指令B有条件执行功能外,其他指令均为无条件执行;大多数Thumb数据处理指令采用2地址格式。

编写ARM汇编的程序

通过编译工具链中的as程序可以将我们写的存有汇编代码的“.s”文件编译成机器码(目标文件“.o”),然后再使用ld链接程序生成可执行文件:

1
2
as program.s -o program.o
ld program.o -o program

arm汇编中的数据类型

ARM汇编数据类型基础

加载和存储的数据类型可以是无符号(有符号)的字(word)、半字(halfword)、字节(bytes),arm汇编中扩展后缀-sh-h对应半字、-sb-b对应字节,相关汇编指令:

1
2
3
4
5
6
7
8
9
10
ldr     @加载字
ldrh @加载无符号半字
ldrsh @加载半字
ldrb @加载无符号字节
ldrsb @加载字节
str @存储字
strh @存储无符号半字
strsh @存储半字
strb @存储无符号字节
strsb @存储字节

字节序

前面提到过,版本3之前,ARM使用小端序,之后采用大端序同时允许切换回小端序。ARM指令里面,访问数据时采用大端序还是小端序由程序状态寄存器(CPSR)的第九个比特位来决定。

ARM寄存器

寄存器的数量由ARM版本决定,在ARMv6到ARMv7-M的处理器中有30个32bit位宽度的通用寄存器。前16个寄存器是用户层能访问控制的,其他寄存器在高权限进程中可以访问(ARMv6-M与ARMv7-M除外)。任何权限下能访问的16个寄存器可分为两组,即通用寄存器和特特殊含义的寄存器

R0–R10:为通用寄存器,其中R7一般用来存放系统调用号
其余6个寄存器的特殊含义分别为:

寄存器 别名 用途
R11 FP 栈帧指针
R12 IP 内部程序调用
R13 SP 栈指针
R14 LR 链接寄存器(一般存放函数的返回地址)
R15 PC 程序计数寄存器
CPSR - 当前程序状态寄存器

ARM架构和Intel架构寄存器间对应关系:

ARM 描述 x86
R0 通用寄存器 EAX
R1-R5 通用寄存器 EBX、ECX、EDX、ESI、EDI
R6-R10 通用寄存器 -
R11 栈帧指针 EBP
R12 内部程序调用 -
R13 栈指针 ESP
R14 链接寄存器 -
R15 程序计数寄存器 EIP
CPSR 程序状态寄存器 FLAGS

R0-R12(R12的使用需谨慎):
1. R0-R3:用作传入函数参数,传出函数返回值。子程序调用间,可以作为任意用途,被调函数返回前如果调用函数不使用R0-R3的值可以不恢复R0-R3
2. R4-R11: 用来存放函数的局部变量,如果被调函数使用这些寄存器需在函数返回前恢复这些值。其中,R11用于指明函数栈帧边界
3. R12:如果汇编代码中存在bl指令,而r12又被用来作为通用寄存器,那么r12的值就很有可能会被链接器插入的veneer程序修改掉;过程调用之间,可以用于任意用途,被调函数返回前也不必恢复R12
4. R13:栈指针寄存器,不能被用于其他用途
5. R14:函数调用发送时,链接寄存器就会被用来记录函数调用发生位置的下一条指令的地址
6. R15:程序执行时自增的计数器,ARM模式下总是4字节对齐,Thumb模式下总是两字节对齐。执行分支指令时,PC存储目的地址。程序执行时,ARM模式下PC存储当前指令加8(两条ARM指令后)的位置,Thumb(v1)模式下PC存储这当前指令加4(两条Thumb指令后)的位置

调试下面的代码(小端序):

1
2
3
4
5
6
7
.section .text
.global _start
_start:
mov r0, pc
mov r1, #2
add r2, r1, r1
bkpt

开启调试之后,可以看到程序断下的位置在将要执行的第一条语句处:
Alt

在继续执行下一条指令时,r0的值并不是0x10054,而是0x1005c:
Alt

这也就说明了在ARM模式下PC存储当前指令加8(两条arm指令后)的位置,至于为什么有些不明觉厉。原文如下:

在执行0x8054(0x10054)这条位置的机器码时,PC已经读到了两条指令后的位置也就是0x805c(0x1005c(见R0寄存器)。所以我们以为直接读取PC寄存器的值时,它指向的是下一条指令的位置。但是调试器告诉我们,PC指向当前指令向后两条机器码的位置。这是因为早期的ARM处理器总是会先获取当前位置后两条的机器码。这么做的原因也是确保与早期处理器的兼容性。

当前程序状态寄存器(CPSR)

使用gef插件时,可以看到当前寄存器CPSR的值(或者输入命令flag):
Alt

其中,thumb,fast,interrupt,overflow,carry,zero,negative就是CSPR寄存器中对应比特位的值。ARM架构的N,Z,C,V与X86架构EFLAG中的SF,ZF,CF,OF相对应:
Alt

相关比特位的含义:

标志位 含义
N 指令结果为负值时置一
Z 指令结果为零时置一
C 对于加法有进位则置一,对于减法有借位则置零
V 指令结果不能用32位的二进制补码存储,溢出时置一
E 小端序置0,大端序置1
T Thumb模式置一,ARM模式置零
M 当前的权限模式(用户态和内核态)
J 允许ARM处理器去以硬件执行java字节码的状态标识

注意:gef插件在显示CPSR时,为1的比特位会加粗
Alt

ARM模式和Thumb模式

ARM模式(32位)和Thumb模式(16位也可以是32位)是ARM处理器的两个主要操作状态,编写ARM的shellcode时要尽可能的少使用NULL以及使用16位宽度的Thumb指令以精简代码。

不同版本ARM,其调用约定不完全相同,而且支持的Thumb指令集也不完全相同。Thumb指令不同的名字用来区分不同版本:

  • Thumb-1(16位宽指令集):ARMv6以及更早期的版本中使用
  • Thumb-2(16位\32位宽指令集):在Thumb-1的基础上拓展了更多的指令集(ARMv6T2、ARMv7以及很多32位Android手机所支持的架构上使用)
  • Thumb-EE:包括一些改变以及对于动态生成代码的补充(在设备上执行钱或者允许时编译的代码)

ARM与Thumb的区别

条件执行指令(不是条件跳转)

所有的ARM模式指令都支持条件执行,一些版本的ARM处理器上允许Thumb模式下通过IT汇编指令进行条件执行,条件执行减少了要被执行的指令数量,以及用来做分支跳转的语句,所以具有更高的代码密度。

条件执行和条件跳转:
1 条件执行:指令可以根据状态位来决定是否执行,即只有当某个特定条件满足时(条件标志位),指令才会执行(可以减少分支指令的数目,改善性能,提高代码密度
2 条件跳转:跳转指令又叫分支指令,是指能够迫使PC指针指向一个新地址,改变程序的执行流程。跳转指令使得子程序调用、if-then-else结构、循环结构变为可能

条件执行必须满足以下两个条件:

  • 条件码:编码在指令的机器码中,说明该指令的执行条件的执行条件。ARM中,AL(always execute)为缺省的执行条件
  • 条件标志:位于CPU的某个通用寄存器中

ARM模式与Thumb的32位指令

桶形移位是ARM模式的特性,它可以被用来减少指令数量,比如mov R1,R0,LSL #1就表示将R0的值乘以2在赋给R1,如果使用乘法指令的话实现同样的功能就需要两条指令。

ARM模式和Thumb模式之间的切换:使用分支跳转指令BX(branch and exchange)或者分支链接跳转指令BLX(branch,link and exchange)时,需将目的寄存器的最低位置一,之后的代码执行就会在Thumb模式下运行。(此处并不会造成地址对齐问题,因为处理器会直接忽略最低比特位的标识)

ARM指令集规律

ARM指令的模板如下:

1
2
MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
; 助记符{是否使用CPSR}{是否条件执行以及条件} {目的寄存器}, 操作符1, 操作符2

上述模板满足大部分的ARM指令,其具体含义为:

  • MNEMONIC: 指令助记符,如ADD
  • {S}: 可选的扩展位,如果指令后面加了s则需要依据计算结果更新CPSR寄存器中的条件跳转相关的FLAG
  • {condition}: 如果机器码要被条件执行,那它需要满足的条件标识
  • {Rd}: 存储结果的目的寄存器
  • Operand1: 第一个操作数,寄存器或者立即数
  • Operand2: 第二个操作数(可变),可以是立即数、寄存器、偏移量的寄存器

当助记符、S、目的寄存器以及第一个操作数都被声明的时候,条件执行以及第二操作数需要一些声明,因为条件执行时依赖与CPSR寄存器中一些比特位的。第二操作数的使用形式:

1
2
3
4
5
6
7
#123        @ 立即数
Rx @ 寄存器,如R1
Rx, ASR n @ 对寄存器中的值进行算术右移n位后的值
Rx, LSL n @ 对寄存器中的值进行算数左移n位后的值
Rx, LSR n @ 对寄存器中的值进行逻辑右移n位后的值
Rx, ROR n @ 对寄存器中的值进行逻辑左移n位后的值
Rx, RRX @ 对寄存器中的值进行带扩展的循环右移1位后的值

使用示例:

1
2
3
4
ADD R0, R1, R2     @ 将第一操作数R1的内容与第二操作数的R2的内容相加,将结果存入R0
ADD R0, R1, #2 @ 将第一操作数R1的内容与第二操作数的立即数2相加,将结果存入R0
MOVLE R0, #5 @ 满足条件LE(Less and Equal)时,将第二操作数的立即数5移动到R0中,等同于MOVELE R0, R0, #5V
MOV R0, R1, LSL #1 @ 将第二操作数R1寄存器的值逻辑左移1位后存入R0

满足上述模板的同样ARM指令集及其含义:

指令 含义 指令 含义
MOV 移动数据 EOR 异或
MVN 取反码移动数据 LDR 加载数据
ADD 数据相加 STR 存储数据
SUB 数据相减 LDM 多次加载
MUL 数据相乘 STM 多次存储
LSL 逻辑左移 PUSH 压栈
LSR 逻辑右移 POP 出栈
ASR 算术右移 B 分支跳转
ROR 循环右移 BL 链接分支跳转
CMP 比较操作 BX 分支跳转切换
AND 比特位与 BLX 链接分支跳转切换
ORR 比特位或 SWI/SVC 系统调用

ARM汇编内存访问相关指令

ARM使用加载-存储模式控制对内存的访问,即只有加载/存储指令才能访问内存。所以ARM下,需要在操作数据前,必须先从内存中取出来放到寄存器再进行相关操作。(x86中允许一些指令直接操作内存中的数据)

ARM架构中的加载和存储有三种形式,区别在于偏移量的形式:立即数作为便宜、寄存器作为偏移、寄存器缩放值作为偏移

通常,LDR用于从内存中加载数据到寄存器,STR用于将寄存器中的数据存放到内存中:

1
2
LDR R2, [R0] @ 从R0指向的内存地址取出数据存放到R2中
STR R2, [R1] @ 把R2的数据存放到R1指向的内存地址中

参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
.data          /* 数据段是在内存中动态创建的,所以它的在内存中的地址不可预测*/
var1: .word 3 /* 内存中的第一个变量 */
var2: .word 4 /* 内存中的第二个变量 */
.text /* 代码段开始 */
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1] @ 将R2中的值0x3存放到R1做指向的地址
bkpt
adr_var1: .word var1 /* var1的地址助记符 */
adr_var2: .word var2 /* var2的地址助记符 */

代码底部为文字标识池(在代码中用来存储常量、字符串、偏移等,可以通过位置无关的方式引用),分别用adr_var1和adr_var2存储变量var1和var2的内存地址。

在调试器中调试时,发现代码似乎有些出入:
Alt

前两条指令变成了[PC+#12]的形式,这种形式称为PC相对地址,是因为在汇编代码中使用的只是数据的标签,所以在汇编的时候,汇编器会计算出与我们想要访问的文字标识池的相对偏移。这其中,立即数(#12)必须是4字节对齐的,因为ARM指令长度是4字节。

继续调试,可以发现R0和R1分别存储了变量的地址,且对应的值为3、4,R2则存储了R0指向的内存对应的值(3):
Alt

再继续调试时可以发现,R1指向的内存空间已经变成了3

立即数做偏移的情况

1
2
STR Ra, [Rb, imm]
LDR Ra, [Rc, imm]

立即数做偏移的情况类似于与高级语言里面的数组下标,通过一个立即数加寄存器的形式访问对应地址/数据,参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, #2] @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加2所指向地址处。
str r2, [r1, #4]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加4所指向地址处,之后R1寄存器中存储的值加4,也就是R1=R1+4。
ldr r3, [r1], #4 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中存储的值加4,也就是R1=R1+4。
bkpt
adr_var1: .word var1
adr_var2: .word var2

首先记录一下GDB的调试指令:
Alt

再看代码,实际内容并不复杂,这里主要记录三种取址模式:

  • 基于偏移量:[r1, #2]
    Alt
    Alt
    可以看到,基于偏移量的寻址方式其实就是寄存器值加立即数

  • 基于索引前置修改:[r1, #4]!,这种方式的特点时感叹号,操作后的改变是将值赋给r2之后,还会更新r1的值,就好比r2 = addr[r1+4];r1+=4;
    Alt

  • 基于索引后置修改:r3, [r1], #4,就好比r3 = addr[r1]; r1+=4,即从r1指向的地址取值后,在将这个地址加4
    Alt

基于索引前置修改和基于索引后置修改的区别在于前者相当于用r1保存偏移后的地址,而后者则相当于先取值后再做偏移

寄存器做偏移的情况

这种情况与立即数做偏移实际上是类似的,只是其中的立即数变成了寄存器,参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, r2] @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处。R1寄存器不会被修改。
str r2, [r1, r2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加R2寄存器的值所指向地址处,之后R1寄存器中的值被更新,也就是R1=R1+R2。
ldr r3, [r1], r2 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1=R1+R2。
bx lr
adr_var1: .word var1
adr_var2: .word var2

同样的,寄存器偏移的情况也分三种:基于偏移量、基于索引前置修改、基于索引后置修改,这部分与前面一至,这里不在赘述。

寄存器缩放值做偏移

1
2
LDR    Ra, [Rb, Rc, <shifter>]
STR Ra, [Rb, Rc, <shifter>]

这种情况下,相较之前多了一个操作数(移位操作),参考如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.data
var1: .word 3
var2: .word 4
.text
.global _start
_start:
ldr r0, adr_var1 @ 将存放var1值的地址adr_var1加载到寄存器R0中
ldr r1, adr_var2 @ 将存放var2值的地址adr_var2加载到寄存器R1中
ldr r2, [r0] @ 将R0所指向地址中存放的0x3加载到寄存器R2中
str r2, [r1, r2, LSL#2] @ 取址模式:基于偏移量。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处。R1寄存器不会被修改。
str r2, [r1, r2, LSL#2]! @ 取址模式:基于索引前置修改。R2寄存器中的值0x3被存放到R1寄存器的值加(左移两位后的R2寄存器的值)所指向地址处,之后R1寄存器中的值被更新,也就R1 = R1 + R2<<2。
ldr r3, [r1], r2, LSL#2 @ 取址模式:基于索引后置修改。R3寄存器中的值是从R1寄存器的值所指向的地址中加载的,加载之后R1寄存器中的值被更新也就是R1 = R1 + R2<<2。
bkpt
adr_var1: .word var1
adr_var2: .word var2

这种方式有些向前两种的结合体,即[基址寄存器+偏移寄存器+立即数],其余与前面的情况基本无异。

关于PC相对取址的LDR指令

1
2
3
4
5
6
7
8
.section .text
.global _start
_start:
ldr r0, =jump /* 加载jump标签所在的内存位置到R0 */
ldr r1, =0x68DB00AD /* 加载立即数0x68DB00AD到R1 */
jump:
ldr r2, =511 /* 加载立即数511到R2 */
bkpt

上述指令被称作伪指令,编写ARM汇编时可以使用这种格式的指令去引用文字标识池中的数据,比如上述例子中用一条指令将一个32位的常量值放到一个寄存器中,而这么写的原因是因为ARM每次仅能加载8位的值

在ARM中使用立即数的规律

在ARM下不能像x86那样直接将立即数加载到寄存器中,因为使用的立即数是受限的。我们知道的是ARM指令的宽度是32位,并且所有的指令都是可以条件执行的。这里一共有16种条件可以使用,并且每个条件在机器码中的占位是4位,然后还需要2位来作为目的寄存器、2位作为第一操作寄存器、1位用作设置状态的标记位、还有操作码(opcode)等的占位。到最后,每条指令剩下的用于存放立即数的空间只有12位宽,即4096个不同的值。换句话说,ARM下使用MOV指令时所操作的立即数的数值范围是有限的,如果是大数就只能拆分成多个部分外加移位操作拼接了。

因为还要外加移位操作,所以剩下的12位中需要有4位用作0-30位的循环右移,8位用作加载0-255中的任意值,所以有公式:v = n ror 2*r

有效的立即数(例):

1
2
3
4
5
6
7
8
9
10
#256        // 1 循环右移 24位 --> 256
#384 // 6 循环右移 26位 --> 384
#484 // 121 循环右移 30位 --> 484
#16384 // 1 循环右移 18位 --> 16384
#2030043136 // 121 循环右移 8位 --> 2030043136
#0x06000000 // 6 循环右移 8位 --> 100663296 (十六进制值0x06000000)
Invalid values:
#370 // 185 循环右移 31位 --> 31不在范围内 (0 – 30)
#511 // 1 1111 1111 --> 比特模型不符合
#0x06010000 // 1 1000 0001.. --> 比特模型不符合

但是这样并不一次性加载所有32位值,两种解决办法如下:

  • 用小部分去组成更大的值:

比如MOV r0, #511会把511拆分成两部分即MOV r0,#256; ADD r0, #255

  • 使用加载指令构造ldr r1, =value的形式

这种情况下编译器会自动转换成MOV的形式,如果失败就转换成通过从数据段中加载的形式即PC加偏移量。

验证立即数的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
from __future__ import print_function   # PEP 3105
import sys

# Rotate right: 0b1001 --> 0b1100
ror = lambda val, r_bits, max_bits: \
((val & (2**max_bits-1)) >> r_bits%max_bits) | \
(val << (max_bits-(r_bits%max_bits)) & (2**max_bits-1))

max_bits = 32

input = int(raw_input("Enter the value you want to check: "))

print()
for n in xrange(1, 256):

for i in xrange(0, 31, 2):

rotated = ror(n, i, max_bits)

if(rotated == input):
print("The number %i can be used as a valid immediate number." % input)
print("%i ror %x --> %s" % (n, int(str(i), 16), rotated))
print()
sys.exit()

else:
print("Sorry, %i cannot be used as an immediate number and has to be split." % input)

连续存取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.data
array_buff:
.word 0x00000000 /* array_buff[0] */
.word 0x00000000 /* array_buff[1] */
.word 0x00000000 /* array_buff[2]. 这一项存的是指向array_buff+8的指针 */
.word 0x00000000 /* array_buff[3] */
.word 0x00000000 /* array_buff[4] */
.text
.global main
main:
adr r0, words+12 /* words[3]的地址 -> r0 ,adr指令用于将基于PC相对偏移的地址值读取到寄存器中,ldr与adr功能一致,区别在于ldr主要用于远端的地址*/
ldr r1, array_buff_bridge /* array_buff[0]的地址 -> r1 */
ldr r2, array_buff_bridge+4 /* array_buff[2]的地址 -> r2 */
ldm r0, {r4,r5} /* words[3] -> r4 = 0x03; words[4] -> r5 = 0x04 */
stm r1, {r4,r5} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04 */
ldmia r0, {r4-r6} /* words[3] -> r4 = 0x03, words[4] -> r5 = 0x04; words[5] -> r6 = 0x05; */
stmia r1, {r4-r6} /* r4 -> array_buff[0] = 0x03; r5 -> array_buff[1] = 0x04; r6 -> array_buff[2] = 0x05 */
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 */
ldmda r0, {r4-r6} /* words[3] -> r6 = 0x03; words[2] -> r5 = 0x02; words[1] -> r4 = 0x01 */
ldmdb r0, {r4-r6} /* words[2] -> r6 = 0x02; words[1] -> r5 = 0x01; words[0] -> r4 = 0x00 */
stmda r2, {r4-r6} /* r6 -> array_buff[2] = 0x02; r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00 */
stmdb r2, {r4-r5} /* r5 -> array_buff[1] = 0x01; r4 -> array_buff[0] = 0x00; */
bx lr
words:
.word 0x00000000 /* words[0] */
.word 0x00000001 /* words[1] */
.word 0x00000002 /* words[2] */
.word 0x00000003 /* words[3] */
.word 0x00000004 /* words[4] */
.word 0x00000005 /* words[5] */
.word 0x00000006 /* words[6] */
array_buff_bridge:
.word array_buff /* array_buff的地址*/
.word array_buff+8 /* array_buff[2]的地址 */

ldm用于加载,stm用于存储。实际的操作就是将从某个地址开始连续读取n个字节的数据或向某个地址连续写入n个字节的数据:
Alt
Alt

对于上述的汇编代码,.word标识用于对内存中长度为32位的数据块做引用,所以程序中由.data段组成的数据,在内存中会申请一个长度为5的4字节数组array_buff。

LDM和STM的多种形式:

1
2
3
4
IA(increase after)
IB(increase before)
DA(decrease after)
DB(decrease before)

划分依据为作为源地址或者目的地址的指针是在访问内存前增减,还是访问内存后增减。值得一提的是,LDM与LDMIA功能相同,都是在加载操作完成后访问对地址增加的。通过这种方式,可以序列化的向前或者向后从一个指针指向的内存加载数据到寄存器,或者存放数据到内存。

1
2
ldmia r0, {r4-r6}
stmia r1, {r4-46}

上述两行代码执行后结果为:
Alt

而LDMIB指令会首先对指向的地址先加4,然后再加载数据到寄存器中。所以第一次加载的时候也会对指针加4,所以存入寄存器的是0X4(words[4])而不是0x3(words[3]):

1
2
ldmib r0, {r4-r6}
stmib r1, {r4-r6}

Alt

当用LDMDA指令时,执行一个反序的操作即R0指向words[3],当加载数据时数据的加载方向变成加载words[3],words[2],words[1]的值到R6,R5,R4中。也即是说在加载操作完成后,会将指针做递减的操作。需要注意的是在做减法模式下的寄存器的操作是反向的,这么设定的原因为了保持让编号大的寄存器访问高地址的内存的原则:
Alt

依次类推,其余指令都大相径庭。

PUSH和POP

PUSH/POP和LDMIA/STMDB:

1
2
3
4
5
6
7
8
9
10
.text
.global _start
_start:
mov r0, #3
mov r1, #4
push {r0, r1}
pop {r2, r3}
stmdb sp!, {r0, r1} @ sp之后没有"!"时,指令执行之后sp指针会回到原来的位置,带有"!"时,执行之后,sp执行之后位置改变,效果等同于pop、push
ldmia sp!, {r4, r5}
bkpt

通过objdump查看上述代码生成的文件:
Alt

可以看出,实际上LDMIA/STMDB直接就被翻译成了PUSH和POP,原因很简单,LDMIA功能是取址后递增,这一行为刚好和POP功能相似,同样的,STMDB是先递减后再存值,这又刚好和PUSH的行为相似。

条件执行与分支

条件含义及状态位:

条件码 含义 状态寄存器
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 Equal(有符号小于等于) (Z==1)
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 Nerver executed(不执行) -
VS Signed Overflow(符号溢出) V==1
VC No Signed Overflow(无符号溢出) V==0
HI Unsigned Higher(无符号大于) (C==1)&&(Z==0)
LS Usigned Lower or same(无符号小于等于) (C==0

条件执行

示例代码:

1
2
3
4
5
6
7
8
.global main
main:
mov r0, #2 /* 初始化值 */
cmp r0, #3 /* 将R0和3相比做差,负数产生则N位置1 */
addlt r0, r0, #1 /* 如果小于等于3,则R0加一 */
cmp r0, #3 /* 将R0和3相比做差,零结果产生则Z位置一,N位置恢复为0 */
addlt r0, r0, #1 /* 如果小于等于3,则R0加一*/
bx lr

Thumb模式中的条件执行

指令格式:Syntax: IT{x{y{z}}} cond,其中:

  • cond:代表IT指令后第一条执行指令需要满足的条件
  • x:代表第二条条件执行的指令要满足的条件逻辑相同还是相反
  • y:代表第三条条件执行的指令要满足的条件逻辑相同还是相反
  • z:代表第四条条件执行的指令要满足的条件逻辑相同还是相反

IT指令的含义是if-then-(else),所以:

  • IT:if-then,接下来的一条指令条件执行
  • ITT:if-then-then,接下来的两条指令条件执行
  • ITE:if-then-then-else,接下来的三条指令条件执行
  • ITTEE:if-then-then-else-else,接下来的四条指令条件执行

在IT块中的每一条条件指令必须是相同的逻辑条件或相反的逻辑条件,如ITE指令,第一条和第二天指令必须使用相同的逻辑条件,而第三条必须是和前两条逻辑上相反的条件:

1
2
3
4
5
6
7
8
9
10
11
12
ITTE   NE           ; 后三条指令条件执行
ANDNE R0, R0, R1 ; ANDNE不更新条件执行相关flags
ADDSNE R2, R2, #1 ; ADDSNE更新条件执行相关flags
MOVEQ R2, R3 ; 条件执行的move
ITE GT ; 后两条指令条件执行
ADDGT R1, R0, #55 ; GT条件满足时执行加
ADDLE R1, R0, #48 ; GT条件不满足时执行加
ITTEE EQ ; 后两条指令条件执行
MOVEQ R0, R1 ; 条件执行MOV
ADDEQ R2, R2, #10 ; 条件执行ADD
ANDNE R3, R3, #1 ; 条件执行AND
BNE.W dloop ; 分支指令只能在IT块的最后一条指令中使用

错误的格式:

1
2
IT     NE           ; 下一条指令条件执行
ADD R0, R0, R1 ; 格式错误:没有条件指令

条件指令的逻辑关系:

指令 逻辑相反
EQ NE
HS(or CS) LO(or CC)
MI PL
VS VC
HI LS
GE LT
GT LE

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
.syntax unified    @ 这很重要!
.text
.global _start
_start:
.code 32
add r3, pc, #1 @ R3=pc+1
bx r3 @ 分支跳转到R3并且切换到Thumb模式下(因为R3此时最低比特位为1)
.code 16 @ Thumb模式
cmp r0, #10
ite eq @ if R0 == 10
addeq r1, #2 @ then R1 = R1 + 2
addne r1, #3 @ else R1 = R1 + 3
bkpt

这里存在一个ARM模式到Thumb模式的转换,指令add r3, PC, #1执行后,r3的最低位刚好为1,所以执行bx r3指令时就会切换到Thumb模式:
Alt

分支指令

分支指令(分支跳转)允许在代码中跳转到别的段,比如要跳到某个函数执行或者跳过一段代码块。常用于条件跳转和循环语句,示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@ 条件分支
.global main
main:
mov r1, #2 @ 初始化 a
mov r2, #3 @ 初始化 b
cmp r1, r2 @ 比较谁更大些
blt r1_lower @ 如果R2更大跳转到r1_lower
mov r0, r1 @ 如果分支跳转没有发生,将R1的值放到到R0
b end @ 跳转到结束
r1_lower:
mov r0, r2 @ 将R2的值放到R0
b end @ 跳转到结束
end:
bx lr @ lr是链接寄存器,此处相当于main函数执行完后返回

上述代码就类似于如下代码:

1
2
3
4
5
int a(2), b(3);
if(a>b)
return a;
else
return b;

循环分支的例子:

1
2
3
4
5
6
7
8
9
10
.global main
main:
mov r0, #0 @ 初始化 a
loop:
cmp r0, #4 @ 检查 a==4
beq end @ 如果是则结束
add r0, r0, #1 @ 如果不是则加1
b loop @ 重复循环
end:
bx lr

功能相似的c代码:

1
2
3
int a(0);
while(a < 4>)
a += 1

B/BL/BX

  • B:Branch,简单的跳转到一个函数
  • BL:Branch link,将下一条指令的入口PC+4保存到LR,然后跳转到函数
  • Branch exchange、Branch link exchange:同B/BL,只是外加了执行模式的切换,且此处需要寄存器作为第一操作数

栈与函数

一般来讲,栈用于存储一些如函数的局部变量、环境变量等临时数据。

栈相关

关于栈的增长,栈可以向上增长(栈的实现是负向增长时)也可以向下增长(栈的实现是正向增长时)。具体区别在于下一个要存放到栈中的数据要存放到哪里,而决定存放位置的是SP指针。如果SP当前指向上一次存放数据的位置(满栈),SP将会递减(降序栈)或递增(升序栈),然后再对指向的内容进行操作;如果SP指向的是下一次要操作数据的空闲位置(空栈实现),数据会先被存放,而SP会被递减(降序栈)或递增(升序栈)。

不同栈实现,可以使用不同的多次连续存取指令来表示:

栈类型 压栈 弹栈
满栈降序(FD,Full descending) STMFD(等价于STMDB,操作之前递减) LDMFD(等价于LDM,操作之后递增)
满栈增序(FA,Full ascending) STMFA(等价于STMIB,操作之前递增) LDMFA(等价于LDMDA,操作之后递减)
空栈降序(ED,Empty descending) STMED(等价于STMDA,操作之后递减) LDMED(等价于LDMIB,操作之后递增)
空栈增序(EA,Empty ascending) STMEA(等价于STM,操作之后递增) LDMEA(等价于LDMDB,操作之前递减)

函数栈帧

ARM下函数体结构:

  • 开辟栈帧:
1
2
3
push   {r11, lr}    /* 保存R11与LR */
add r11, sp, #4 /* 设置栈帧底部,PUSH两个寄存器,SP加4后指向栈帧底部元素 */
sub sp, sp, #16 /* 在栈上申请相应空间 */
  • 栈帧销毁
1
2
sub    sp, r11, #4  /* 收尾操作开始,调整栈指针,有两个寄存器要POP,所以从栈帧底部元素再减4 */
pop {r11, pc} /* 收尾操作结束。恢复之前函数的栈帧指针,以及通过之前保存的LR来恢复PC。 */

实际上,ARM下的函数栈帧和x86下的栈帧基本无异


You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.