ARM汇编基本指令

常用指令的积累,ARM寻址方式,ARM64和ARM32的不同,一小段ARM64汇编指令的翻译。

1.前言

前期在学习《逆向工程实战》的过程中学习了ARM汇编(32位)相关基础知识和指令。在这篇博客中主要是对实际过程中碰到的一些指令含义的记录,以及对ARM64和ARM32的不同之处的学习,同时在最后的实战部分,将一小段ARM64汇编指令翻译成对应的c语言程序。

2.指令积累

A.MSR与MRS

MRS
MRS{条件} 通用寄存器,程序状态寄存器(CPSR 或SPSR)

MRS指令用于将程序状态寄存器的内容传送到通用寄存器中。该指令一般用于以下两种情况:

(1)当需要改变程序状态寄存器的内容时,可用MRS将程序状态寄存器的内容读入通用寄存器,修改后再写回程序状态寄存器。
(2)当在异常处理或进程切换时,需要保存程序状态寄存器的值,可先用该指令读出程序状态寄存器的值,然后保存。

指令示例:
MRS R0,CPSR  //传送CPSR的内容到R0
MRS R0,SPSR  //传送SPSR的内容到R0

MSR
MSR{条件} 程序状态寄存器(CPSR或SPSR)_<域>,操作数

MSR指令用于将操作数的内容传送到程序状态寄存器的特定域中。其中,操作数可以为通用寄存器或立即数。<域>用于设置程序状态寄存器中需要操作的位,32位的程序状态寄存器可分为4个域:

(1)位[31:24]为条件标志位域,用f表示;
(2)位[23:16]为状态位域,用s表示;
(3)位[15:8]为扩展位域,用x表示;
(4)位[7:0]为控制位域,用c表示;

该指令通常用于恢复或改变程序状态寄存器的内容,在使用时,一般要在MSR指令中指明将要操作的域。

指令示例:
MSR CPSR,R0//传送R0的内容到CPSR
MSR SPSR,R0//传送R0的内容到SPSR
MSR CPSR_c,R0   //传送R0的内容到SPSR,但仅仅修改CPSR中的控制位域

B.BIC

BIC
BIC{条件}{S} 目的寄存器,操作数1,操作数2 //注意指令后面加了S表示运算结果会影响CPSR寄存器的条件位,也就是说指令执行完后更新flag标志位。

BIC指令用于清除操作数1的某些位,并把结果放置到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器、被移位的寄存器、或一个立即数。操作数2为32位的掩码,如果在掩码中置了某一位1,则清除这一位。未设置的掩码位保持不变。

bic r0,r0,#0x1f
//0x1f=11111b
//其含义:清除r0的bit[4:0]位。

C.LDR与STR

LDR
LDR{条件} 目的寄存器,<存储器地址>

LDR指令用于从存储器中将一个32位的字数据传送到目的寄存器中。该指令通常用于从存储器中读取32位的字数据到通用寄存器,然后对数据进行处理。当程序计数器PC作为目的寄存器时,指令从存储器中读取的字数据被当作目的地址,从而可以实现程序流程的跳转。

指令示例:
LDR   R0,[R1]  ;将存储器地址为R1的字数据读入寄存器R0。
LDR   R0,[R1,R2] ;将存储器地址为R1+R2的字数据读入寄存器R0。
LDR   R0,[R1,#8] ;将存储器地址为R1+8的字数据读入寄存器R0。
LDR   R0,[R1,R2] !   ;将存储器地址为R1+R2的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR   R0,[R1,#8] !  ;将存储器地址为R1+8的字数据读入寄存器R0,并将新地址R1+8写入R1。
LDR   R0,[R1],R2  ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2写入R1。
LDR   R0,[R1,R2,LSL#2]!   ;将存储器地址为R1+R2×4的字数据读入寄存器R0,并将新地址R1+R2×4写入R1。
LDR   R0,[R1],R2,LSL#2 ;将存储器地址为R1的字数据读入寄存器R0,并将新地址R1+R2×4写入R1

注意(1):ldr和ldr.w之间的区别——.W是可选的指令宽度说明符.它不会影响指令的行为,它只是确保生成32位Thumb指令,我们看到.w后缀就可以判断当前指令为Thumb指令。

注意(2):ldr、ldrb、ldrh的区别——依次为读一个32位的字数据,读一个8位的字节数据,读一个16位的半字数据。

STR
STR{条件} 源寄存器,<存储器地址>

STR指令用于从源寄存器中将一个32位的字数据传送到存储器中。该指令在程序设计中比较常用,且寻址方式灵活多样,使用方式可参考指令LDR。

指令示例:
STR   R0,[R1],#8;将R0中的字数据写入以R1为地址的存储器中,并将新地址R1+8写入R1。
STR   R0,[R1,#8];将R0中的字数据写入以R1+8为地址的存储器中。

3.ARM64与ARM32

二者之间的区别主要体现在以下几个方面——

A.架构

  • Arm32位是ARMV7架构;
  • ARM64位采用ARMv8架构。

B.指令编码长度

  • A32模式(ARM instruction sets),指令固定的编码长度为32bit;
  • T32模式(Thumb instruction sets),指令可以编码成16bit长,也可编码成32bit长;
  • A64模式(AArch64 instruction sets),指令固定的编码长度为32bit

C.当前指令的地址(PC)

由于ARM指令的三级流水线执行(ARM处理器使用流水线来增加处理器指令流的速度,这样可使几个操作同时进行,并使处理与存储器系统之间的操作更加流畅连续),程序计数器R15(PC)总是指向“正在取指”的指令,如下图,而不是指向“正在执行”的指令或者正在“译码”的指令。

  • 在ARM32状态下,当前执行指令的地址通常是pc-8,而在Thumb状态下通常是pc-4。
  • 在AARCH64状态下,当前执行指令的地址通常是pc。

Program counter
The current Program Counter (PC) cannot be referred to by number as if part of the general register file and therefore cannot be used as the source or destination of arithmetic instructions, or as the base, index or transfer register of load and store instructions.
The only instructions that read the PC are those whose function it is to compute a PC-relative address (ADR, ADRP, literal load, and direct branches), and the branch-and-link instructions that store a return address in the link register (BL and BLR). The only way to modify the program counter is using branch, exception generation and exception return instructions.
Where the PC is read by an instruction to compute a PC-relative address, then its value is the address of that instruction. Unlike A32 and T32, there is no implied offset of 4 or 8 bytes.

在ARM64下,PC的值只能间接读取或改变——

  1. 64位可读取PC值的情况有:计算相对地址,如adr,adrp,文字池加载以及直接分支;子程序返回地址,比如bl,blr
  2. 可修改pc的方式为:使用控制流指令,如条件跳转、无条件跳转、异常生成和异常返回指令。

D.参数的传递和访问

  • arm32下,前4个参数是通过r0~r3传递,大于4个的参数通过栈来传递(即第4个参数需要通过sp访问,第5个参数需要通过sp + 4 访问,第n个参数需要通过sp + 4*(n-4)访问)。

  • arm64下,前8个参数是通过x0~x7传递,大于8个的参数通过栈来传递(第8个参数需要通过sp访问,第9个参数需要通过sp + 8 访问,第n个参数需要通过sp + 8*(n-8)访问)。

E.寄存器和寻址

(1)寻址

ARM微处理器采用的是精简指令集,指令间的组合灵活。ARM微处理器支持九种寻址方式,分别是:立即寻址、寄存器寻址、寄存器间接寻址、寄存器移位寻址、基址寻址、多寄存器寻址、堆栈寻址、块拷贝寻址、相对寻址。

立即寻址

指令的地址字段指出的不是操作数的地址,而是操作数本身。注意:立即数只能作为源操作数,不能作为目的操作数。

MOV R0 0x10;

寄存器寻址

当操作数不放在内存中,而是放在CPU的通用寄存器中时,可采用寄存器寻址方式。

MOV R0 R1;

寄存器间接寻址

寄存器间接寻址方式与寄存器寻址方式的区别在于:指令格式中的寄存器内容不是操作数,而是操作数的地址,该地址指明的操作数在内存中。

MOV R0 R1;   //此时R1中存放的是操作数的地址

寄存器移位寻址(ARM指令集特有的寻址方式)

在操作前对源寄存器操作数进行移位操作。支持以下5种移位操作:

LSL: 逻辑左移,移位后寄存器空出的低位补0。
LSR: 逻辑右移,移位后寄存器空出的高位补0。
ASR: 算术右移,移位过程中符号位保持不变,如果源操作数为正数,则移位后寄存器空出的高位补0;否则补1。
ROR: 循环右移,移位后移出的低位填入移位空出的高位。
RRX: 带扩展的循环右移,操作数右移一位,移位后寄存器空出的高位用C标志的值填充。

MOV R0, R1, LSL #2   //R0=R1*4(逻辑左移2位)

基址寻址

基址寻址是将基址寄存器与偏移量相加,形成操作数的有效地址,所需的操作数保存在有效地址所指向的存储单元中。基址寻址多用于查表、数据访问等操作。

LDR R0, [R1, #-4]   //R0=[R1-4]

多寄存器寻址

多寄存器寻址一条指令最多可以完成16个通用寄存器值的传送。比如LDMIA和LDMIB指令,LDM是数据加载指令,指令的后缀IA表示每次执行完加载操作后寄存器的值自增1个字;指令的后缀IB表示每次执行加载操作前寄存器的值自增1个字;还有两条指令后缀DA和DB,分别表示在指令操作后和操作前寄存器的值自减1个字。ARM32指令集中,字表示一个32位的数字,注意:该条指令的源寄存器与目的寄存器位置。

LDMIA R0, {R1, R2, R3, R4}   //R1=[R0], R2=[R0+4], R3=[R0+8], R4=[R0+12]

堆栈寻址(ARM32位指令集特有的寻址方式)

注意:这里列出的指令是ARM32支持的批量压栈和出栈指令,thumb指令模式下的出栈和压栈指令是POP/PUSH。在ARM64下对于栈操作限制严格,这两种栈操作指令都不再支持,只有LDP/STP这一对双寄存器栈操作指令。

堆栈寻址是ARM指令集特有的一种寻址方式,堆栈寻址需要使用特定的指令来完成。堆栈寻址的指令有LDMFA/STMFA、LDMEA/STMEA、LDMFD/STMFD、LDMED/STMED。LDM和STM为指令前缀,表示多寄存器寻址。FA(Full Ascending stack)、FD(Full Descending stack)、EA、ED为指令后缀,其中:FA表示满递增堆栈,堆栈向高地址生长,堆栈指针指向下一个要放入的空地址;FD表示满递减堆栈,堆栈向低地址生长,堆栈指针指向最后一个入栈的有效数据数据项; EA表示空递增堆栈,堆栈向高地址生长;ED空递减堆栈,堆栈向低地址生长。

STMFD  SP!, {R1-R7, LR}   //将R1-R7, LR入栈,多用于保护子程序现场
LDMFD  SP!, {R1-R7, LR}   //将数据出栈,放入R1-R7, LR寄存器。多用于恢复子程序现场

块拷贝寻址

块拷贝寻址可实现连续地址数据从存储器的某一位置拷贝到另一位置。块拷贝寻址的指令有LDMIA/STMIA、LDMDA/STMDA、LDMIB/STMIB、LDMDB/STMDB。指令前缀和指令后缀前面已经介绍了。

STMIA  R0!, {R1-R3}   //从R0寄存器指向的存储单元中读取3个字到R1-R3寄存器
LDMIA  R0!, {R1-R3}   //存储R1-R3寄存器的内容到R0寄存器指向的存储单元

相对寻址

相对寻址以程序计数器PC的当前值为基地址,指令中的地址标作为偏移量,将两者相加之后得到操作数的有效地址。例如——

BL NEXT
   ....
NEXT:
   ....

(2)ARM32寄存器

ARM32微处理器有两种工作状态:ARM32状态与Thumb状态。处理器可以在两种状态之间随意切换,当处理器处于ARM状态时,会执行32位对齐的ARM指令;当处于Thumb状态时,会执行16位对齐的Thumb指令。ARM32指令集与Thumb指令集切换方法,是在BX和BLX指令跳转时,判断目标地址最低位是否为1。

  • 如果为1,跳转时将CPSR寄存器标志T置位,并将目标地址处的代码解释位Thumb代码,处理器切换到Thumb状态;
  • 如果为0,跳转时将CPSR寄存器标志T复位,并将目标地址处的代码解释位ARM32代码,处理器切换到ARM32状态。

Thumb状态下对寄存器的命名与ARM32有部分差异,它们的关系如下图所示。

(3)ARM64寄存器

ARM64位参数调用规则遵循AAPCS64,规定堆栈为满递减堆栈。对于ARM64架构的CPU, 以 X 开头的就是64位的寄存器, 以 W 开头的就是32位的寄存器, 其中32位寄存器就是64位寄存器的低32位部分。

  • X0~X7:用于传递子程序参数和结果,使用时不需要保存,多余参数采用堆栈传递,64位返回结果采用X0表示,128位返回结果采用X1:X0表示。
  • X8:用于保存子程序返回地址, 尽量不要使用 。
  • X9~X15:临时寄存器,使用时不需要保存。
  • X16~X17:子程序内部调用寄存器,使用时不需要保存,尽量不要使用。
  • X18:平台寄存器,它的使用与平台相关,尽量不要使用。
  • X19~X28:临时寄存器,使用时必须保存。
  • X29:帧指针寄存器FP(栈底指针),用于连接栈帧,使用时需要保存。
  • X30:链接寄存器LR
  • X31:堆栈指针寄存器SP或零寄存器ZXR

F.栈操作

ARM32中的LDM、STM、PUSH、POP指令,在ARM64中并不存在。取而代之的是LDP、STP指令,例如,用IDA Pro逆向的某个ARM64 SO库函数的开头和结尾:

STP  X24, X23, [SP,#var_40]!
STP  X22, X21, [SP,#0x40+var_30]
STP  X20, X19, [SP,#0x40+var_20]
STP  X29, X30, [SP,#0x40+var_10]
ADD  X29, SP, #0x40+var_10
....
SUB  SP, X29, #0x30
LDP  X29, X30, [SP,#0x150+var_120]
LDP  X20, X19, [SP,#0x150+var_130]
LDP  X22, X21, [SP,#0x150+var_140]
LDP  X24, X23, [SP+0x150+var_150],#0x40
RET

G.软中断进行系统调用

关于软中断

软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和”信号”有些类似。软中断是实现系统API函数调用的手段。

软中断调用时将返回地址和CPU状态寄存器内容压栈,修改特权级,根据中断号查找中断向量表,找到ISR中断服务例程地址,跳转执行。函数调用和软中断调用的区别是,软中断多了修改特权级和查找中断向量表的功能,其他部分完全一样。

有了软中断,就可以实现应用程序的动态加载。就像WINDOWS/Linux那样,应用程序和系统程序分别开发,不在一起编译连接,应用程序通过软中断调用系统提供的功能。

(1)ARM32

在使用软中断进行系统调时,系统调用号通过R7寄存器传递,用SWI指令产生软中断,实现从用户模式到管理模式的切换。例如,调用exit(0)的汇编代码如下:

MOV R0, #0  //参数0
MOv R7, #1  //系统功能号1为 exit
SWI #0  //执行 exit(0)

(2)ARM64

在使用软中断进行系统调时,系统调用号通过x8寄存器传递,用svc指令产生软中断,实现从用户模式到管理模式的切换。例如:

mov x0, 123 // exit code
mov x8, 93  // sys_exit() is at index 93 in kernel functions table
svc #0  // generate kernel call sys_exit(123);

4.ARM64常用指令

ARM指令所有指令都是带有条件的,默认是AL即无条件执行,当指令带有默认条件时不需要明确写出。ARM指令包含4位的条件码列表如下——

这里对ARM64下的栈操作指令进行补充说明——

由于在ARM64下,对于栈的操作都是要16个字节对齐的,所以都是双寄存器读写操作。

stp 写入指令

将数据从两个寄存器中读出来, 写入到栈中

ldp 读取指令

将数据从栈中读取出来, 存到两个寄存器中

//汇编代码--堆栈操作练习
//使用32字节空间作为这段程序的栈空间, 然后利用栈将x0, x1的值进行交换

sub sp, sp, #0x20 ; 拉伸栈空间32(20 = 2*16)个字节
stp x0 , x1, [sp, #0x10] ; sp往上加16(10 = 1 * 16)个字节,存放x0 和 x1
ldp x1 , x0, [sp, #0x10] ; 将sp偏移16个字节的值取出来,放入x1 和 x0

//注意: 拉伸栈空间是往低地址拉伸, 拉伸的字节数只能是16的倍数, 否则会崩溃(对照上面 16字节对齐理解 )
//第二,三句代码: sp指向的地址不会变, 只是将x0, x1的值从相应地址存入或读取.

5.简单翻译

汇编代码如下——我们可以从寄存器判断这是一段ARM64汇编程序

cbz x2, flag2
mov w8, #0x41
mov x9, x2

flag1:
ldrb w10, [x1], #1
subs x9, x9, #1
eor w10, w10, w8
strb w10, [x0], #1
b.ne flag1

flag2:
mov w0, w2
ret

翻译如下——

//3个变量:x2(int a)、x9=x2(int i)、起始地址为x1的字符串/字符数组str

int j = 0;
if(a!=0){
    for(int i=a; i>0; i--){
       str[j++] = str[j++] ^ 'A';
    }
}
return a;

6.参考

【arm】ARM32和AARCH64的几点区别
【arm】arm32位和arm64位架构、寄存器和指令差异分析总结
【arm】ARM寄存器以及使用说明
ARM32 汇编语言
ARM64 汇编语言
ARM64 汇编——寄存器和指令
初识汇编(三)