可能是比较详细的ARM汇编文章

由于CSDN上传图片比较麻烦为了更好的观看体验请移步

https://flowus.cn/share/7c2f0202-6d48-4b02-b9d9-1cc152a0d06a【FlowUs 息流】ARM汇编

1. ARM汇编

ARM是32位架构的,也就是说一次可以处理 32 位(单位: bit )二进制

1.1 汇编与C语言的区别

汇编

每条汇编都会唯一对应一条机器码,且CPU能直接识别和执行

即汇编中所有的指令都是CPU能够识别和执行的

汇编中寄存器的使用、栈的分配与使用、程序的调用、参数的传递都需要自己维护

C语言

每条C语句都要被编译器编译成若干条汇编指令才能被CPU识别和执行

即C语句中的指令CPU不一定能直接识别,需要编译器进行“翻译”

C中寄存器的使用、栈的分配与使用、程序的调用、参数的传递等都是编译器来分配和维护

1.2 简单汇编例子


.text       @ 表示当前为代码段
.global _start  @ _start定义为全局符号
_start:     @ 汇编的入口

    MOV R1, #1  @ 汇编指令
    MOV R2, #2
    MOV R3, #3

.end        @ 汇编的结束
   

运行MOV R1 #1时,成功将R1的值置1,PC的值是下一个指令的地址

2. 组成

2.1 指令

能够编译生成一段32bit的机器码,并且能被cpu识别和执行

2.1.1 数据处理指令

进行数学运算、逻辑运算

2.1.2 跳转指令

实现程序的跳转

原理上就是修改PC寄存器

2.1.3 Load/Store 指令

访问(读写)内存

2.1.4 状态寄存器传送指令

用于访问(读写)CPSR寄存器

2.1.5 软中断指令

触发软中断

2.1.6 协处理器指令

什么是协处理器?顾名思义就是协助arm处理器进行操作的处理器,比如我们之前讲到arm处理器不能进行浮点运算,如果我们要进行浮点运算我们可以加一个协处理器,常见的GPU就是协处理器

2.1.7 隔离指令

2.2 伪指令

本身不是指令,CPU不能够直接识别,但是可以通过编译器编译成CPU能够识别的指令

一条伪指令可能编译成多条指令

2.3 伪操作

不会生成指令,只是在编译阶段告诉编译器怎么编译

3. 数据处理指令

数据运算指令的格式

 , ,

操作码:MOV ADD…

目标寄存器:用于存储运算的结果

第一操作寄存器:存储第一个参与运算的数据 (只能是寄存器)

第二操作数:第二个参与运算的数据 (可以是寄存器也可以是立即数)

3.1 数据搬移指令MOV

语法 MOV , <#值||寄存器>

3.1.1 搬移一个值到寄存器

MOV R1, #1

3.1.2 将一个寄存器的值搬移到另外一个寄存器

MOV R3, R1

3.1.3 对PC进行MOV的时需要注意的地方

之前我们讲过,ARM模式下指令在内存的起始地址必须是4的整数倍,如果[1:0]位不是0的话cpu会自动将其置零,现在让我们来实验一下

运行后,我们的PC变成了4,也就是0100

3.2 按位取反数据搬移指令MVN

MVN和MOV的使用方法一样

不同点是MVN会将数据按位取反后再搬移

3.3 立即数

立即数的本质是包含在指令当中的数,属于指令的一部分

CPU通过MOV指令搬移一个立即数的时候,在从内存拿指令的时候,这个指令就包含了MOV和立即数

优点:相比于变量来说,立即数更快

缺点:立即数范围有限,不能是任意的32位数字

如果MOV的数不是立即数,那么编译器会报错

但是奇怪的是我把要MOV的数改成0xFFFFFFFF的时候竟然编译成功了,按理说这个数肯定不是立即数

进入debug后发现,原来编译器将他优化成了MVN指令

3.4 加法指令ADD

语法: ADD , , <#值||寄存器>

注意:

根据我们前面说的数据运算指令的格式,第一操作寄存器只能是寄存器

3.5 减法指令SUB

语法:SUB , <第一操作寄存器(被减数)>, <第二操作数(减数)>

缺点:当我想要让立即数-寄存器时无法使用这个指令进行实现

3.6 逆向减法指令RSB

为了实现让立即数-寄存器,引入了逆向减法指令RSB

语法:RSB , <第一操作寄存器(减数)>, <第二操作数(被减数)>

3.7 乘法指令MUL

语法:MUL , ,

注意:乘法指令只能是两个寄存器相乘

3.8 按位与指令 AND

语法:ADD , ,

3.9 按位或指令 ORR

语法:ADD , ,

3.10 按位异或指令 EOR

语法:EOR, ,

3.11 左移指令 LSL (Logical Shift Left)

语法:LSL, ,

3.12 右移指令 LSR

语法:LSR, ,

3.13 位清零指令 BIC (BIT CLEAR)

语法:BIC, ,

这个指令会将第一操作寄存器中,第二操作寄存器对应1的位置清零

例如我要清除后五位那么就是11111 = 0x1f

3.14 数据运算指令的格式扩展

MOV R1, R2, LSL #1

这个指令是什么意思?

之前我们讲过数据运算指令的格式,所以MOV是操作码,R1是目标寄存器,(R2, LSL #1) 是第一操作数,MOV指令没有第二操作数

那么(R2, LSL #1)代表什么?

经过实验,R2, LSL #1 就是 R2左移一位的立即数

4. 通过数据处理指令验证CPSR寄存器

我们已经学习了汇编常用的数据处理指令,接下来我们可以测试数据运算指令对CPSR的影响

4.1 回顾CPSR

进位的意思是产生的结果寄存器不够放

错位的意思是被减数不够减去减数

4.2 数据运算对N的影响

尝试 R0 = 3, R1 = 5 并让R2 = R0-R1

结果出乎意料,为什么CPSR的N不是1,我明明出现了负数

注意:

默认情况下数据运算不会对条件位产生影响,在指令后加后缀”S“才可以影响

4.2 数据运算对Z/C的影响

4.3 数据运算对C的影响

4.3.1 进位

4.3.2 错位

4.4 数据运算对V的影响

产生符号位进位,V置1

5. 为什么要引入CPSR

前面我们说过ARM是32位架构的,一次可以处理 32 位(单位: bit )二进制

如果我们要处理64位的数据应该如何来进行

例如:两个64位数据做加法运算

第一个数的低32位放在R1

第一个数的高32位放在R2

第二个数的低32位放在R3

第二个数的高32位放在R4

但是这样算的话是非常危险的,没有考虑到进位问题

5.1 带进位的加法指令 ADC

本质是 R6 = R2+R4+CPSR的C[29]位

执行流程:

R5 = R1+ R3 = 0x04

由于进位所以CPSR C置为0x01

R6 = R3+R4+C = 0x04

5.1 带借位的减法指令SBC

本质是 R6 = R2-R4-(~C)

执行流程:

R5 = R1 - R3 = 0xFFFFFFFC

由于减法错位CPSR C置为0

R6 = R2-R4-(~C) = 0x00

6. 联系C语言和汇编

6.1 32位数据加法

以一个简单的C语言程序为例

之前我们说过一个c语句可能对应多条汇编语句,这个例子就很好的证明了这一点

简单的解释下上面的汇编指令

分别读取两个地址的值到R2和R3,然后通过ADD指令相加

6.2 64位数据加法

在ARM64的Linux操作系统中,C语言的long类型是8字节,对于64位数据的加法,我们在前面说过了,C语言反汇编得到的汇编指令和我们前面说的64位数据加法的处理方法是一样的

7. 跳转指令

7.1 为什么要用到跳转指令

情景一:

之前我们讲过,每执行一次取址PC就会+4(ARM指令模式)

但是有没有想过,我们的地址是有限的,如果他一直加下去肯定是不行的

情景二:

一般来说,C语言中一个函数中编译生成的汇编指令在内存中是连续存储的

如果我们在一个函数中调用另外一个函数,就需要实现跳转

7.2 跳转实现

7.2.1 直接修改PC值

7.2.2 通过跳转指令 B

在ARM体系结构中我们讲过LR这个寄存器

他是在执行跳转指令(BL/BLX)时,会自动保存跳转指令下一条指令的地址

8. ARM指令的条件码

我们C语言中有判断语句,那么判断语句在汇编中要如何实现呢?

8.1 比较指令 CMP

语法: CMP 

CMP得到的结果会保存在CPSR寄存器的NZCV

CMP的本质是一条减法指令(SUB),只是没有将运算的结果存入寄存器

例如:CMP R1, R2

当R1 = R2 的时候 Z=1

当R1 !=R2 的时候 Z=0

当R1 > R2 的时候 C=1且Z=0

当R1≥ R2的时候 C=1

当R1 < R2 的时候 C=0

当R1 ≤R2 的时候 C=0或Z=1

CMP比较完之后数据是存在CPSR里面的,那么我们要怎么拿出来用?

通过在指令后面加助记符,当匹配到对应的NZCV值的时候就会执行指令

8.2 一个CMP的练习

9. 内存读写指令

9.1 写内存 STR

语法:STR  <[要存的地址]>

可以发现,ARM是小端字节序

注意:写内存时,地址必须是4的整数倍

9.2 读内存 LDR

语法:LDR  <[要读取的地址]>

9.3 按字节/半字/字 读写数据到内存

byte对应的后缀是B

halfword对应的后缀是H

word 不加后缀默认

10. ARM指令的寻址方式

10.1 什么是寻址方式

寻址方式就是CPU去寻找一个操作数的方式

10.2 有哪些寻址方式(9种)

立即寻址

如果操作数是个立即数,那么寻找操作数的方式就是立即寻址

MOV R1, #1

寄存器寻址

操作数如果是寄存器的值那么这个寻址方式就是寄存器寻址

ADD R1, R1, R2

寄存器移位寻址

操作数是寄存器进行移位操作

MOV R1, R2, LSL #1

寄存器间接寻址

操作数所在内存单元的地址通过存储器间接给出。

STR R1, [R2]

基址加变址寻址

MOV R1, #0xFFFFFFFF

MOV R2, #0x40000000

MOV R3, #4

STR R1, [R2, R3]

将 R1寄存器中的数据写入到R2+R3指向的空间

基址加变址的索引方式
  1. 前索引

@ R1的数据写入到R2+4所指向的空间
MOV R1, #0xFFFFFFFF
MOV R2, #0x40000000
STR R1, [R2, #4]

  1. 后索引

@ R1的数据写入到R2所指向的空间然后R2自增4
MOV R1, #0xFFFFFFFF
MOV R2, #0x40000000
STR R1, [R2], #4

  1. 自动索引

@ R1的数据写入到R2+4所指向的空间然后R2自增4
MOV R1, #0xFFFFFFFF
MOV R2, #0x40000000
STR R1, [R2, #4]!

11. 多寄存器内存访问(读写)指令

可以读写多个寄存器

11.1 多寄存器写入指令 STM

语法:STM , {寄存器}

注意:如果多寄存器是连续的那么寄存器中间可以用’-’来连接

比如 {R1-R4}

如果寄存器不是连续的那么中间用逗号隔开

比如 {R1, R3, R9}

不管寄存器列表中的顺序如何,存储时永远是低地址存储小编号的寄存器数据

    MOV R1, #1
    MOV R2, #2
    MOV R3, #3
    MOV R4, #4
    MOV R11, #0x40000020
    @ R1-R4寄存器中的数据存储到以R11指向的地址起始的内存中
    STM R11, {R1-R4}

11.2 多寄存器读取指令 LDM

语法:LDM , {寄存器}

    MOV R1, #1
    MOV R2, #2
    MOV R3, #3
    MOV R4, #4
    MOV R11, #0x40000020
    STM R11, {R1-R4}
    @ R11所指向的地址为起始地址数据读取到R5-R7
    LDM R11, {R5-R8}

11.3 多寄存器内存写入指令的寻址方式(读取指令同样能加)

IA:increase after:先存储再移动R11

IB:increase before:先移动R11再存储

DA:decrease after:先存储再移动R11

DB:decrease before:先移动R11再存储

12.

栈的本质就是一段内存,程序运行时用于保存一些临时数据

如局部变量、函数的参数、返回值、以及程序跳转时需要保护的寄存器等

SP寄存器指向的就是栈的地址

12.1 栈的分类

12.1.1 增栈和减栈

增栈:每次压栈的位置是上一次压栈的地址+4,每次出栈的位置是上次出栈位置-4

压栈时栈指针越来越大,出栈时栈指针越来越小

减栈:压栈时栈指针越来越大,出栈时栈指针越来越小

12.1.2 满栈和空栈

满栈:栈指针指向最后一次压入到栈中的数据,压栈时需要先移动栈指针到相邻位置然后再压栈

空栈:栈指针指向最后一次压入到栈中的数据的相邻位置,压栈时可直接压栈,之后需要将栈指针移动到相邻位置

ARM处理器一般使用满减栈

12.2 满减栈使用那个指令来压栈

在多寄存器存储写入指令的寻址方式中我们讲过四种寻址方式

分别是IA IB DA DB

满减栈,每次压栈都要先移动一次SP指针,然后再压栈

显然,满减栈与DB寻址方式相符

所以,对于满减栈,我们压栈的使用可以使用STMDB,出栈的时候使用LDMIA

但是每次都要区分过于麻烦,编译器设计了几个四个后缀用来代表不同的栈类型

空增(EA)、空减(ED)、满增(FA)、满减(FD)

所以我们压栈可以使用 STMFD,出栈可以使用LDMFD

编译器会自动将他们替换成STMDB和LDMIA

12.3 栈的应用

MAIN:
    MOV R1, #3
    MOV R2, #5
    BL FUNC
    ADD R3, R1, R2
    B STOP
   
FUNC:
    MOV R1, #10
    MOV R2, #20
    SUB R3, R2, R1
    MOV PC, LR

STOP:
    B STOP

.end

对于上面的程序,从MAIN进入FUNC之后R1, R2寄存器的数据会被覆盖,如果我想在MAIN中正常计算3+5是无法做到的,这个时候我们就可以通过压栈来将数据暂时存起来

注意:对于非叶子函数,当他跳转到别的函数的时候,要把LR指针压栈

如果我们将非叶子函数的LR寄存器压栈就可以解决这个问题

12.4 局部变量为什么初始化是随机值

通过前面的实验我们发现,栈里面的数据虽然出栈了,但是内存里面的值还在,如果我要初始化一个局部变量,那么他就会在栈指针指向的下一个位置申请一块内存,但是我之前出栈的数据还在,所以这个局部变量的值就是之前出栈的数据

13. 状态寄存器传送指令

读写CPSR寄存器,因为CPSR寄存器是非常重要的寄存器,所以不允许其他指令对其读写

需要使用专用指令进行读写

13.1 读CPSR指令 MRS

Move from State register to general Register(加载状态寄存器的值到通用寄存器)

语法:MRS  CPSR

补充:

我们在程序一开始运行的时候发现CPSR寄存器不是0x00而其他寄存器都是0x00

这个0xD3代表什么呢?

0xD3 = 1101 0011

代表禁止IRQ 禁止 FIQ ARM状态 SVC

因为CPU刚上电的时候需要初始化系统,所以禁止IRQ和FIQ

13.1 写CPSR指令 MSR

Move from general Register to State register(加载通用寄存器的值到状态寄存器)

接下来,如果我想要开启FIQ IRQ 和USER模式

0001 0000

0x10

实验发现,第一个MSR有效果,但是第二个MSR没有效果,这是为啥呢

原因是,不同模式拥有的权限不同

14. 软中断指令

在ARM的8个基本工作模式中有5个属于异常模式,即ARM遇到异常后会切

换成对应的异常模式

软中断源Reset SWI对应的异常模式为SVC模式

异常中断的整个过程中,只有异常返回需要我们自己编写,其他都由CPU自己完成

14.1 一个软中断实现例子

这段程序可以在发生软中断的时候跳到中断向量表,再从中断向量表跳到SWI_HANDLER

接下来实现一下中断返回

要实现中断返回主要有两步,一是MOV CPSR, SPSR 二是MOV PC, LR

一般我们不会用两个MOV,将LR压栈然后出栈的时候给到PC这样就可以省去一条指令

同时在LDM的最后加上’^’可以起到把SPSR传递给CPSR的作用

@ **************************************************************************
@ * *
@ * Written by flose *
@ * *
@ * A example to make a soft interrupt
@ * *
@ **************************************************************************
.text
.global _start
_start:

    @ 异常向量表
    B MAIN
    B .
    B SWI_HANDLER
    B .
    B .
   
MAIN:
    @ 初始化栈顶指针 注意!不同模式下的栈顶指针是不同的,下面的这个SPSVC模式下的SP
    MOV SP, #0x40000020
    @ 切换USER模式
    MSR CPSR, #0x10
    @ USER模式代码
    MOV R1, #1
    MOV R2, #2
    @ 发生软件中断,切换到SVC模式 0x08
    SWI #1
    ADD R3, R2, R1
    B STOP
   
SWI_HANDLER:
    STMFD SP!, {R1-R2, LR}  @ PUSH 保护现场
    MOV R1, #10
    MOV R2, #20
    SUB R3, R2, R1
    LDM SP!, {R1-R2, PC}^   @ LR压栈之后出栈到PC,然后将SPSR寄存器的数据搬移到CPSR,把状态切换回USER
   
STOP:
    B STOP
   
.end

15. 协处理器指令

当我们要进行一些ARM处理器无法处理的运算的时候我们需要将这些运算交给协处理器

在 arm 的协处理器设计中,最多可以支持 16 个协处理器,通常被命名为 cp0~cp15,其中:

  • cp15 提供一些系统控制功能,主要针对内存管理部分(MMU)
  • cp14 主要提供 debug 系统的控制
  • cp10、cp11 两个协处理器一起提供了浮点运算(FPU)和向量操作,以及高级的 SIMD 指令扩展。
  • 其它作为保留

15.1 协处理器指令分类

  1. 数据运算指令

CPD

  1. 存储器访问指令

STC 将协处理器中的数据存储在存储器

LDC 将存储器中的数据读取到协处理器中

  1. 寄存器传送指令

MRC 将协处理器中寄存器的数据传送到Arm处理器中的寄存器

MCR 将ARM处理器中寄存器的数据传送到协处理器中的寄存器

16. 隔离指令

16.1数据存储器隔离DMB

DMB 指令保证: 仅当所有在它前面的存储器访问操作都执行完毕后,才提交(commit)在它后面的存储器访问操作。

16.2数据同步隔离DSB

比 DMB 严格: 仅当所有在它前面的存储器访问操作都执行完毕后,才执行在它后面的指令(亦即任何指令都要等待存储器访问操作——译者注)

16.3指令同步隔离ISB

指令同步隔离。最严格:它会清洗流水线,以保证所有它前面的指令都执行完毕之后,才执行它后面的指令。

17.伪指令

伪指令本身不是指令,编译器可以将他替换成若干条指令

17.1 NOP指令

可以看到NOP指令与MOV R0, R0 的数据是一样的

也就是说NOP指令会被编译器替换为MOV R0, R0

17.2 LDR指令

之前我们说过LDR是读取内存到寄存器的指令

那么为什么在这里又说他是伪指令呢

其实根据LDR语法的不同,他有时候是指令,有时又是伪指令

17.2.1 LDR是指令的时候

LDR R0, [R2]

17.2.2 LDR是伪指令的时候

LDR R1, = 0x12345678

可以将任意一个32位的数据放在寄存器中

18. 伪操作

不会生成代码,只在编译阶段告诉编译器怎么编译

伪操作一般都以 ‘.’ 开头

18.1 global

.global 声明成全局

例如,我有两个汇编文件,其中一个汇编文件调用了另外一个汇编文件的symbol,那么就需要先在symbol前面加 ‘.’

例如 .global _start

18.2 local

.local 声明成局部的

18.3 equ

类似于c语言的define,我们可以使用equ关键字来定义一个符号常量

例如 .equ Data 0xff

18.4 macro封装汇编指令

当调用macro的时候就只会运行macro 和endm之间指令

例如
.macro
  MOV R1, #1
  MOV R2, #2
.endm

18.5 if 条件编译

和c语言的条件编译类似,只不过要用’.’开头

.if 1
  MOV R1, #1
  MOV R2, #2
.endif

18.6 rept 重复编译

有时程序会连续地重复完成相同或几乎相同的一组语句,当出现这种情况时,可考虑用重复伪指令定义的重复块,以简化源程序。

.rept 3
  MOV R1, #1
  MOV R2, #2
.endr

18.7 weak 弱化符号

用来告诉编译器,即便用到的句子没有进行定义也不要去报错,没有定义的句子会被替换成NOP

 .weak func
 B func

上面代码中的func会被替换成NOP

18.8 word

word伪操作会在当前地址申请一个字的空间

18.9 byte

word伪操作会在当前地址申请一个字节的空间

为什么会出现报错呢?原因显而易见,ARM每个汇编指令在内存的起始地址必须是4的整数倍,所以MOV R2, #2这个指令会在地址为0x05的位置

所以这里我们引入伪操作 align

18.10 align

.align

这里的N代表是2的多少次方字节对齐,如下图代表的是4字节对齐

18.11 .arm

代表后续的指令都是arm指令

18.12 .thumb

代表后续的指令都是thumb指令

18.13 .space

在当前地址生成任意字节的数据

.space ,

19. C和汇编混合编程

在那种语言的环境下就符合那种语言的语法规则

  1. 在汇编中将C语言中的函数当作标号来处理
  2. 在C语言中将标号当作函数来处理

19.1 汇编语言调用C语言

void func_c (void)
{
  int a;
  a++;
}

MOV R1, #1
B func_c
MOV R2, #2

19.2 C语言调用汇编

void
func_c (void)
{
    int a = 0;
    a ++;
    FUNC_ASM();
    a --;
}

19.3 C内联汇编

20.ATPCS协议

ARM-Thumb Produce Call Standard(ARM程序和Thumb程序中子程序调用的基本规则)

协议主要内容

栈的种类

使用满减栈

寄存器的使用

R15:程序计数器,只能用于存放程序的指针

R14:链接寄存器,只能用于存放返回地址不能用作其他用途

R13:SP指针,只能用于存储栈指针不能作其他用途

R0-R3:当函数传参少于4个的时候用R0-R3传递,当传参多于4个的时候将多余的压栈

函数返回值用R0传递

其余的寄存器主要用于存储局部变量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值