ARM汇编指令学习

1 ARM介绍

安卓支持三类处理器(CPU):ARM、Intel和MIPS。ARM无疑被使用得最为广泛。移动设备非常复杂,其中的处理器需要执行数百万行指令才能完成人们希望这些设备去做的事。速度和功耗对处理器来说至关重要。速度影响用户体验,功耗影响电池寿命。完美的移动设备必须有好性能以及低功耗。这就是为什么选择什么样的处理器很重要。一个超级耗电、反应迟钝的处理器会很快吸干你的电池,而一个考究的、高效的处理器给你带来高性能和长久的电池寿命。ARM和Intel处理器的第一个区别是,前者使用精简指令集(RISC),而后者使用复杂指令集(CISC)。通俗而言,精简指令集规模较小,更接近原子操作,而复杂指令集规模较大,更加复杂。所谓原子操作,是指每条指令的工作大都可以由处理器在一个操作内完成,例如对两个寄存器做加法。另外ARM是采用Big-Endian设计(ARM3之后)。

ARM代码文件使用.s文件,里面存放汇编代码。汇编代码需要经过编译和链接才能生存可执行文件,编译命令为as,链接命令为ld。

gif-assembly-to-machine-code.gif

图1  编译和链接过程

在我的树莓派上直接编写s文件,然后编译链接之后,使用gdb进行调试。另外借助gef插件,调试的时候更方便。

gef.png

图2  编译和链接过程

2 数据类型和寄存器

2.1 数据类型

跟其他高级语言一样,ARM支持操作不同的数据类型。这些数据类型包括有符号和无符号的bytes、halfwords、words。某些内存操作指令,如果如果以h或者sh结尾,表明操作一个halfword,如果以b或者sb结尾,表明操作一个byte,如果无后缀,表明操作一个word。

data-types-1.png

图3  数据类型

表1  指令表
指令 作用
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

2.2 字节序(Endianness)

在内存视图中,通常有两种字节序列Little-Endian(小端)或者Big-Endian(大端),他们的不同之处在于字节的排列方式不同。小端存储中:低对低,高对高(低位存储在低地址,高位存储在高地址)。大端存储中低对高,高对低(低位存储在高地址,高位存储在低地址)。

例如32bit宽的数0x12345678在Little-endian模式CPU内存中的存放方式(假设从地址0x4000开始存放)为:

表2  字节序列
类型 0x4000 0x4001 0x4002 0x4003
小端 0x78 0x56 0x34 0x12
大端 0x12 0x34 0x56 0x78

big-little-endian-1.png

图4  Endianness

2.3 寄存器

ARM中有30个通用寄存器,在用户模式下可见的为前16个。这十六个寄存器,根据使用目的不同可以分为常规寄存器和特殊寄存器,其功能和用途以及和x86的对比可以参考以下表格。

表3  寄存器
ARM 作用 x86
R0 General Purpose EAX
R1-R5 General Purpose EBX, ECX, EDX, ESI, EDI
R6-R10 General Purpose -
R11 (FP) Frame Pointer EBP
R12 Intra Procedural Call -
R13 (SP) Stack Pointer ESP
R14 (LR) Link Register -
R15 (PC) Program Counter / Instruction Pointer EIP
CPSR Current Program State Register/Flags EFLAGS

这里面比较重要的有指令指针寄存器R15、函数栈帧寄存器R11和R13、状态寄存器CPSR。

  • 指令指针寄存器 为指令指针寄存器,它指示了CPU当前要读取指令的地址。
  • 函数栈帧寄存器 sp寄存器在任意时刻会保存我们栈顶的地址。fp寄存器在某些时刻我们利用它保存栈底的地址。
  • 状态寄存器 CPSR和其他寄存器不一样,其他寄存器是用来存放数据的,都是整个寄存器具有一个含义。而CPSR寄存器是按位起作用的,也就是说,它的每一位都有专门的含义,记录特定的信息。通常一些比较指令会根据状态寄存器的位来做判断。

3 ARM指令集

ARM指令基本格式为:

MNEMONIC{S}{condition} {Rd}, Operand1, Operand2
MNEMONIC     - 指令助记符
{S}          - 可选后缀,如果指定了后缀,在指令执行完毕后自动更新CPSR中的条件码标志位的值
{condition}  - 条件码,描述指令执行条件
{Rd}         - 用来储存指令直接结果的目标寄存器
Operand1     - 第一操作数,可以是寄存器或者立即数
Operand2     - 第二操作数,不仅可以是寄存器,还能是立即数,而且能够使用经过位移运算的寄存器和立即数

指令举例:

ADD   R0, R1, R2         // R1寄存器的值加上R2寄存器的值,存入RO寄存器
ADD   R0, R1, #2         // R1寄存器的值加上立即数2,存入RO寄存器
MOVLE R0, #5             // 当condition LE满足的时候(状态码(Z==1) or (N!=V))为R0寄存器赋值为立即数5
MOV   R0, R1, LSL #1     // R1寄存器逻辑左移一位,并存入R0寄存器

ARM常用指令集表

表4  常用指令
指令 描述 指令 描述
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

4 内存操作指令

4.1 数据装载和传输指令load和store

ARM使用储存-装载模型来访问内存,意味着只有加载/存储(LDR和STR)指令才可以访问内存。在X86中,大多数指令允许直接操作内存中的数据,而在ARM中,在操作数据之前,必须把数据从内存移动到寄存器中。

4.2 寻址方式

4.2.1 立即寻址

也称为立即数寻址,这种寻址方式指令中就已经给出了操作数。也就是在执行指令的过程中,处理器取得指令的同时也取得了操作数,因此称为立即数寻址。

ADD  R0, #1                 //R0+1->R0

4.2.2 寄存器寻址

即将寄存器中的数值作为操作数,是各类微处理器常用的寻址方式,也是效率较高的寻址方式。

ADD  R0, R1, R2             //R1+R2->R0

4.2.3 寄存器间接寻址

寄存器间接寻址是以寄存器中的值作为操作数的地址,操作数本身存放在寄存器中。

ADD  R0, R1, [R2]           //R1+[R2]->R0
LDR   R0, [R1]              //[R1]->R0

4.2.4 基址加变址(偏移)寻址

  1. 立即数作为偏移
    STR    Ra, [Rb, imm]       // Ra->[Rb+imm]
    LDR    Ra, [Rc, imm]       // [Rc+imm]->Ra
    
  2. 寄存器作为偏移
    STR    Ra, [Rb, Rc]       // Ra->[Rb+Rc]
    LDR    Ra, [Rb, Rc]       // [Rb+Rc]->Ra
    
  3. 寄存器位移结果作为偏移
    STR    Ra, [Rb, Rc, <shifter>]       // Ra->[Rb+Rc的位移结果]
    LDR    Ra, [Rb, Rc, <shifter>]       // [Rb+Rc的位移结果]->Ra
    

4.3 数据传输指令

有时,一次性加载(或存储)多个值更有效率。因此,我们需要使用LDM(载入多个值)和STM(存储多个值)。来看一段简单的代码和注释,是不是对ia、ib、da、db比较疑惑,i和d对应加和减,a和b对应after和before,表示自增(自减)操作在数据传输之前还是之后:

.data

array_buff:
 .word 0x00000000             /* array_buff[0] */
 .word 0x00000000             /* array_buff[1] */
 .word 0x00000000             /* array_buff[2]. This element has a relative address of array_buff+8 */
 .word 0x00000000             /* array_buff[3] */
 .word 0x00000000             /* array_buff[4] */

.text
.global _start

_start:
 adr r0, words+12             /* address of words[3] -> r0 */
 ldr r1, array_buff_bridge    /* address of array_buff[0] -> r1 */
 ldr r2, array_buff_bridge+4  /* address of 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             /* address of array_buff, or in other words - array_buff[0] */
 .word array_buff+8           /* address of array_buff[2] */

5 条件执行和分支

5.0.1 条件执行

ARM有很多条件执行指令,具体表格如下:

表5  条件码
条件码 描述 状态码
EQ Equal Z==1
NE Not Equal Z==0
GT Signed Greater Than (Z==0) and (N==V)
LT Signed Less Than N!=V
GE Signed Greater Than or Equal N==V
LE Signed Less Than or Equal (Z==1) or (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) and (Z==0)
LS Unsigned Lower or same (C==0) or (Z==0)

5.0.2 THUMB模式下的条件执行

  1. 语法:IT{x{y{z}}} cond
    • cond 指定为IT块中第一条指令使用
    • x 指定IT块中第二条指令的是否执行的开关
    • y 指定IT块中第三条指令的是否执行的开关
    • z 指定IT块中第四条指令的是否执行的开关
  2. 解释:IT指令是由“IF-Then-(Else)”的语法组成,包含T和E两个字母
    • IT 全称 If-Then (下一条指令是条件执行的)
    • ITT 全称 If-Then-Then (下两条指令是条件执行的)
    • ITE 全称 If-Then-Else (下两条指令是条件执行的)
    • ITTE 全称 If-Then-Then-Else (下三条指令是条件执行的)
    • ITTEE 全称 If-Then-Then-Else-Else (下四条指令是条件执行的)
  3. 例子
    ITTE   NE           // 以下三条语句条件执行,为if-then-then-else,也就是ne满足的时候执行后面两条语句,不满足执行第三条语句。
    ANDNE  R0, R0, R1   
    ADDSNE R2, R2, #1   
    MOVEQ  R2, R3       
    

5.0.3 分支

ARM中有三类分支指令:

  1. B 最简单的跳转指令。一旦遇到一个B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址。
  2. BL 带链接的跳转。 首先将当前指令的下一条指令地址保存在LR寄存器,然后跳转的lable。通常用于调用子程序,可通过在子程序的尾部添加mov pc, lr 返回。
  3. BX和BLX 和B以及BL一样,但是带了状态切换。BX Rm和BLX Rm可从Rm的位[0]推算出目标状态,最低位为1时,切换到Thumb指令执行,为0时,解释为ARM指令执行。

6 栈和函数

6.0.1 示例

参考如下函数做一个堆栈示例演示,在max函数中下断点。可以看到如下堆栈,注意在gef中配置一下context gef config context.layout "regs stack code"

int main()
{
  int res = 0;
  int a = 1;
  int b = 2;
  res = max(a, b);
  return res;
}

int max(int a,int b)
{
  do_nothing();
  if(a<b)
    {
      return b;
    }
  else
    {
      return a;
    }
}
int do_nothing()
{
  return 0;
}

stack.jpg

图5  堆栈图片

6.0.2 堆栈平衡

要了解ARM中的函数,需要理解一下三个部分 Prologue 、Body、Epilogue 。我们以上个例子中的max函数为例,查看一下汇编代码,图中标红的为Prologue和Epilogue。Prologue的目的是为了保存程序的执行状态(保存返回地址LR寄存器和堆栈FP寄存器R11),Epilogue的目的是在执行函数体之后恢复到之前的执行状态(跳转到之前存储的返回地址以及恢复之前保存FP寄存器)。

diassmax.png

图6  反汇编max函数