ARM基础学习笔记

发布于 2022-11-22  43 次阅读


ARM

RISC(精简指令集计算)处理器,因此具有简化的指令集(100条指令或更少)和比CISC更多的通用寄存器

ARM指令集可以分为以下六种

  • 跳转指令
  • 数据处理指令
  • 程序状态寄存器传输指令
  • Load/Store指令
  • 协处理器指令
  • 异常中断指令

基础知识

数据类型可以是signed and unsigned words, halfwords, or bytes.

-h(unsigned) -sh(signed) 表示 halfwords 2byte

-b -sb 表示 byte

其余表示word 4byte

则对应的加载和存储指令就是

ldr ldrh ldrsh ldrb ldrsb

str strh strsh strb strsb

并且支持双端序,实际中默认都是使用小端序。

关于arm寄存器,除了基于 ARMv6-M 和 ARMv7-M 的处理器外,有30个通用寄存器。前 16 个寄存器可在用户级模式下访问。

r0-r10 通用寄存器,r7保留syscall,r11帧指针(FP)栈底指针,r12内部过程调用,r13(栈顶),r14(LR),r15(PC)EIP

在x86中的寄存器的对应关系

PC总是在当前执行的指令之前获取两条指令

当前程序状态寄存器CPSR

如果设置了当前程序状态寄存器中的 T 位,我们就处于Tumble模式。

指令集

ARM处理器有两种主要状态,它们可以运行(我们在这里不计算Jazelle),ARM和Thumb。

如前所述,有不同的拇指版本。不同的命名只是为了将它们彼此区分开来(处理器本身将始终将其称为 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 模式功能。它可用于将多个指令缩小为一条。例如,您可以使用左移 1 -> Mov R1、R0、LSL #1 将乘法包含在 MOV 指令中,而不是使用两个指令进行乘法(将寄存器乘以 2 并使用 MOV 将结果存储到另一个寄存器中);R1 = R0 * 2(相当于位移指令可以和操作数2合并)

所以操作数2也可以称为灵活操作数

MOV R0, R1, LSL #1

MOVLE R0, #5

ADD R0, R1, #2

ADD R0, R1, R2

内存指令

汇编器默认把文字池放在每一个代码节的末尾处。代码节的末尾的确定或者是由汇编源文件尾部的。文本池是同一部分中的内存区域(因为文本池是代码的一部分),用于存储常量、字符串或偏移量

arm使用加载存储模型进行内存访问,也就是说,只有LDR和STR指令能够访问内存。数据必须在操作之前从内存移到寄存器中。所以,如果需要增加特定的内存地址处的值,需要“加载,递增,存储”三个类型的指令。

LDR R2, [R0] 将R0地址处的值加载到R2

STR R2, [R1]将 R2 中的值存储到 R1 中地址

实际上的指令是:

#取地址保存到寄存器
ldr r0, adr_var1

ldr r1, adr_var2
#进行实际存储操作
ldr r2, [r0]

str r2, [r1]

相对寻址

ldr r0, [pc, #12]

缩放寄存器作为偏移

LDR &nbsp; &nbsp;Ra, [Rb,Rc, <shifter>]
STR &nbsp; &nbsp;Ra, [Rb,Rc, <shifter>]

此偏移形式的一个示例用法是循环遍历数组

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

取r2的值,向左(LSL)位移2位(#2),然后与r1寄存器地址相加,取偏移后的[R1],将r2的值送入[r1]地址中去

用于PC相对寻址的LDR

LDR可以将数据从存储加载到寄存器中,还可以引用word池中的数据

.section .text
.global _start

_start:
   ldr r0, =jump
   ldr r1, =0x68DB00AD
jump:
   ldr r2, =511 
   bkpt

ldr r0, =jump 将函数标签为jump的地址存放入r0

ldr r1, =0x68DB00AD0x68DB00AD放入r1寄存器中

jump函数中ldr r2, =511则是将511放入r2寄存器

因为arm一次性只能加载一个8位值,即1byte,所以需要这个语法来进行32位常量的移动。这个机制由于arm的处理即时值的机制。

arm机立即数机制

每个立即数都是由一个8位的常循环右移偶数位得到。

v = n ror 2*r

所以存在有效立即数和无效立即数,为了能够一次性加载完32位地址,可以用下面两个方法绕过限制

  1. 使用ADD,将两个有效立即数相加
  2. 使用负载结构LDR r1 ,=511

LDM STM

对多个值的加载

如果未另行指定,LDM 和 STM 将按字的步长(32 位 = 4 字节)进行操作。(WORD)

IA(之后增加)、-IB(增加之前)、-DA(之后减少)、-DB(之前减少)

例如:LDMIB 指令首先将源地址增加 4 个字节(一个字值),然后执行第一次加载。

条件EQual condition

例如

MOV R0, #2
CMP R0, #3
ADDLT RO, RO, #1
CMP R0, #3
ADDLT RO, RO ,#1

2-3=-1,cmp将negative位置设为1,没有发生溢出,V为0,满足ADDLT的N≠V,则R0=R0+1=#3

再将r0和#3比较,得到标志Z=0,N=0,未发生溢出,V=0,则不进行ADD,

Thumb版本

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

  • 它指的是 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条指令是有条件的)

注意到IF的条件和then的指令条件必须是一致的,else的条件必须是if条件的逆

跳转指令

简单跳转:B

分支链接:BL,在LR中保存(PC+4),并跳转(有点类似jmp)

分支交换:BX和分支链路交换:BLX 会转换到Thumb模式(需要寄存器作为操作数)

堆栈及函数

前面提到的LDM和STM用于多个数据的存取,在arm中是实现堆栈存取的一种方法

在x86中使用的完全降序的栈结构,在这里如何实现的

MOV R0, #2
PUSH {R0}  ; 此时SP-=4,R0的值入栈,存在sp所在的地址,也就是SP地址所指向的内存地址,内存地址存储了2
MOV R0, #3 ; 此时R0的值被破坏
POP {R0}   ;将SP处的值出栈,SP所指向的的地址的存储的值2,保存到R0中,SP+=4      

而FP帧指针设置为堆栈帧的底部,在FP的下面是返回地址LR

在进入函数前,首先需要进入准备工作,将LR和R11(即EBP和返回地址)存储到stack上,并让局部参数入栈

PUSH {R11, LR} 
ADD R11,SP,#0  ;将sp初始化到栈顶(底)
sub sp,sp,16

函数的结果是通过R0返回的,在长度超过32的情况下,可以将r1和r0结合返回

函数的最后,将程序状态恢复。

SUB SP,R11,#0 ;相当mov esp,ebp
pop {r11,pc}

叶与非叶函数

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

非叶函数在函数的准备工作时,需要将返回地址入栈

push {r11,lr}

而叶函数的准备工作,不需要将准备工作入栈

push {r11}

因为非叶函数在进入更深的函数调用时,返回地址LR会被修改,所以LR的值是需要被保留的,而叶函数不需要进行更深的函数调用,返回地址LR不会被修改,所以没有必要保留。

同样的,在函数的结束工作,非叶函数需要

pop {r11, pc}

而叶函数需要

pop {r11}

在使用BL跳转指令到叶函数的时候,标签将会被替换成内存地址,LR同时会被保存(隐式的)

而离开叶函数使用的BX会将LR作为参数,

BX 指令可以在分支操作期间在 ARM/Thumb 模式之间进行扩展交换。在这种情况下,它是通过检查 LR 寄存器的最后一位来完成的:如果该位设置为 1,CPU 会将模式更改为(或保持)为 thumb,如果设置为 0,则模式将更改为(或保留)为 ARM。这是一个很好的设计功能,允许从不同的模式调用函数。

接下来用两个函数进行详细解释:

;非叶函数
MAIN:
    PUSH {R11, LR} ;将返回地址和栈底地址入栈
    ADD R11, SP , #0 ;将栈顶指针初始化
    SUB, SP, SP, #16 ;开辟栈空间
    MOV R0, #1  ;寄存器作为参数
    MOV R1, #2
    BL MAX ;max标签将会转换为函数的内存地址,同时将LR入栈
    SUB SP, R11, #0 ;将栈顶指针恢复到栈底
    POP {R11 ,PC}  ;结束栈的工作

;叶函数
MAX:
    PUSH {R11} ;将栈底入栈
    ADD R11, SP, #0 ;将栈顶指针初始化
    SUB SP , SP, #12 ;开辟栈空间
    CMP R0, R1 ;比较大小
    MOVLT R0, R1 ;1-2=-1,标志位N=1,V=0,所以此时r0=r1=2
    ADD SP, R11, #0 ;恢复栈顶到栈底
    POP {R11} ;恢复栈底,结束栈的工作
    BX LR ;跳转到BL所保存的LR

间桐桜のお菓子屋さん