汇编指令(重点|常用)

创建时间:2022/1/11 14:50
更新时间:2022/5/6 13:34
作者:gi51wa2j
标签:bingo, 汇编, 正文

一、ARM汇编概述

一开始,ARM公司发布两类指令集:

① ARM指令集,这是32位的,每条指令占据32位,高效,但是太占空间

② Thumb指令集,这是16位的,每条指令占据16位,节省空间

要节省空间时用Thumb指令,要效率时用ARM指令。


一个CPU既可以运行Thumb指令,也能运行ARM指令。

怎么区分当前指令是Thumb还是ARM指令呢?

程序状态寄存器中有一位,名为“T”,它等于1时表示当前运行的是Thumb指令。


假设函数A是使用Thumb指令写的,函数B是使用ARM指令写的,怎么调用A/B?

我们可以往PC寄存器里写入函数A或B的地址,就可以调用A或B,

但是怎么让CPU在执行A函数是进入Thumb状态,在执行B函数时进入ARM状态?


做个手脚:

调用函数A时,让PC寄存器的BIT0等于1,即:PC=函数A地址+(1<<0);

调用函数B时,让PC寄存器的BIT0等于0:,即:PC=函数B地址


为了解决这类麻烦的事情,引入Thumb2指令集

它支持16位指令、32位指令混合编程。


问:三种指令集,我们是否需要全部会?如何区分?
ARM公司推出了: Unified Assembly Language

UAL,统一汇编语言,你不需要去区分这些指令集。

在程序前面用CODE32/CODE16/THUMB表示指令集:

ARM/Thumb/Thumb2

1.1 如何学习汇编

日常工作中,

只需要这么几条汇编指令,从名字就可以猜出含义:

常用汇编指令格式及使用在此列出

有需要再学习即可,无需花费大量时间去学些这类知识,需要即可百度
若很感兴趣,参考《ARM Cortex-M3与Cortex-M4权威指南.pdf》

二、汇编指令的格式

汇编指令可以分为几大类:数据处理、内存访问、跳转、饱和运算、其他指令

1.1 指令格式(1)

汇编指令的格式,如下:
label:                 
    instruction @ comment
label,即标签,表示地址位置,可以通过label得到指令/数据地址
instruction,即指令,表示汇编指令或伪指令
@ comment,@表示后面是注释,comment表示注释内容
比如:
addnum:                                    mov r0, #0 @ 将R0寄存器设置成0
上面汇编代码中,addnum表示标签,mov r0, #0表示指令,@ 将R0寄存器设置成0 表示  注释

1.2 指令格式(2)

Operation{cond} {S}  Rd,  Rn,  Operand2
Operation:表示各类汇编指令,比如ADD,MOV;
cond:表示condition,即该指令执行的条件;
S表示该指令执行后,会去修改程序状态寄存器;

Rd为目的寄存器,用来存储运算的结果;

Rn、Operand2是两个源操作数

1.2.1 Operation

1.2.2 cond取值

三、汇编指令学习(基础)

03_ARM汇编.pptx04_ARM汇编模拟器VisUAL.pptx05_内存访问指令.pptx06_数据处理指令.pptx07_跳转指令.pptx

2.1 内存访问指令

读内存指令LDR/LDM:参考《DEN0013D_cortex_a_series_PG.pdf》P340、P341

写内存指令STR/STM:参考《DEN0013D_cortex_a_series_PG.pdf》P377、P378

LDR:Load Register;

LDM:Load Multiple Register;

STR:Store Register;

STM:Store Multiple Register。

2.1.1 LDR指令

LDR:L表示LOAD,LOAD的含义应该理解为:Load from memory into register。下面这条语句就说明的很清楚:

  LDR   R1,     [R2]

  R1<——[R2]

  就是把R2所指向的存储单元的内容的值(一个memory地址内的值),读取到R1中(一个register)

2.1.2 STR指令

STR:S表示STORE,STORE的含义应该理解为:Store from a register into memory。下面这条语句表示的很清楚:

  STR    R1,     [R2]

  R1——>[R2]

  就是把寄存器R1中的内容“保存”到R2所指向的存储的单元中(一个memory地址)

STR与LDR的区别

这两条语句都有个特点,就是寄存器写在前面(左边)而内存地址写在后面(右边),数据传送的方向则是恰好相反的。

2.1.3 LDM指令

LDM:L的含义仍然是LOAD,即是Load from memory into register。

  虽然貌似是LDR的升级,但是,千万要注意,这个指令运行的方向和LDR是不一样的,是从左到右运行的。该指令是将内存中堆栈内的数据,批量的赋值给寄存器,即是出栈操作;其中堆栈指针一般对应于SP,注意SP是寄存器R13,实际用到的却是R13中的内存地址,只是该指令没有写为[R13],同时,LDM指令中寄存器和内存地址的位置相对于前面两条指令改变了,下面的例子:

  LDMFD     SP! ,   {R0, R1, R2}

  实际上可以理解为:    LDMFD     [SP]!,    {R0, R1, R2}

  意思为:把sp指向的3个连续地址段(应该是3*4=12字节(因为为r0,r1,r2都是32位))中的数据拷贝到r0,r1,r2这3个寄存器中去;

2.1.4 STM指令

STM:S的含义仍然是STORE,与LDM是配对使用的,其指令格式上也相似,即区别于STR,是将堆栈指针写在左边,而把寄存器组写在右边。

  STMFD  SP!,   {R0}

  该指令也可理解为:  STMFD  [SP]!,  {R0}

  意思是:把R0保存到堆栈(sp指向的地址)中。

LDM与STM的区别
这两个堆栈操作指令也有个特点,就是寄存器组写在后面(右边)而堆栈指针写在前面(左边),而且实际上使用的是堆栈指针中的内存地址,这一点与前面两条指令是有区别的。

(补充:sp后面的!的作用:R0的值在ldm过程中发生的增加或减少,最后写回到R0中去,也就是ldm时会改变R0的值)

2.1.5 LDM与STM的地址模式

addr_mode:

2.2 分支跳转指令

跳转指令用于实现程序流程的跳转,在 ARM 程序中有两种方法可以实现程序流程的跳转:

(1) 直接向程序计数器 PC 写入跳转地址值。

通过向程序计数器 PC 写入跳转地址值,可以实现在 4GB 的地址空间中的任意跳转,在跳转之前结合使用

MOV LR , PC

(2) 使用专门的跳转指令。

参考《DEN0013D_cortex_a_series_PG.pdf》P327、P328、P329

核心指令是B、BL:

B:Branch,跳转

BL:Branch with Link,跳转前先把返回地址保持在LR寄存器中

BX:Branch and eXchange,根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)

BLX:Branch with Link and eXchange

         根据跳转地址的BIT0切换为ARM或Thumb状态(0:ARM状态,1:Thumb状态)

2.2.1 B指令

B 指令的格式为:

B{条件} 目标地址

B{条件} 目标地址
B 指令是最简单的跳转指令。一旦遇到一个 B 指令,ARM 处理器将立即跳转到给定的目标地址,从那里继续执行。
注意存储在跳转指令中的实际值是相对当前PC 值的一个偏移量,而不是一个绝对地址,它的值由汇编器来计算(参考寻址方式中的相对寻址)。它是 24 位有符号数,左移两位后有符号扩展为 32 位,表示的有效偏移为 26 位(前后32MB 的地址空间)。以下指令:


B Label ;程序无条件跳转到标号 Label 处执行

CMP R1 ,# 0 ;当 CPSR 寄存器中的 Z 条件码置位时,程序跳转到标号 Label 处执行

BEQ Label

.W是一个可选的指令宽度说明符,用于强制要求在 Thumb-2 中使用 32 位 B 指令。

2.2.2 BL指令

BL 指令的格式为:

BL{条件} 目标地址

       BL 是另一个跳转指令,但跳转之前,会在寄存器R14 中保存PC 的当前内容,因此,可以通过将R14 的内容重新加载到PC 中,来返回到跳转指令之后的那个指令处执行。该指令是实现子程序调用的一个基本但常用的手段。以下指令:

BL Label ;当程序无条件跳转到标号 Label 处执行时,同时将当前的 PC 值保存到 R14 中

2.2.3 BX指

BX 指令的格式为:

BX{条件} 目标地址

         BX 指令跳转到指令中所指定的目标地址,目标地址处的指令既可以是ARM 指令,也可以是Thumb指令。

2.2.4 BLX指令

BLX 指令的格式为:

BLX 目标地址

       BLX 指令从ARM 指令集跳转到指令中所指定的目标地址,并将处理器的工作状态有ARM 状态切换到Thumb 状态,该指令同时将PC 的当前内容保存到寄存器R14 中。因此,当子程序使用Thumb 指令集,而调用者使用ARM 指令集时,可以通过BLX 指令实现子程序的调用和处理器工作状态的切换。

同时,子程序的返回可以通过将寄存器R14 值复制到PC 中来完成。

2.3 立即数

这样一条指令:

    MOV   R0, #VAL

意图是把VAL这个值存入R0寄存器。

问:VAL可以是任意值吗?

答:不可以,必须是立即数。假设VAL可以是任意数,”MOV  R0, #VAL”本身是16位或32位,哪来的空间保存任意数值的VAL?

        所以,VAL必须符合某些规定。

2.4 伪指令(常用)

2.4.1 LDR伪指令

想把任意数值赋给R0,可以使用伪指令:

            LDR   R0,  =VAL

“伪指令”,就是假的、不存在的指令。

注意LDR作为“伪指令”时,指令中有一个“=”,否则它就是真实的LDR(load regisgter)指令了。

编译器读取伪指令时的操作:

编译器会把“伪指令”替换成真实的指令,比如:

LDR  R0,  =0x12   

0x12是立即数,那么替换为:MOV  R0,  #0x12

LDR  R0, =0x12345678

0x12345678不是立即数,那么替换为:

LDR  R0, [PC, #offset]          // 2. 使用Load Register读内存指令读出值,offset是链接程序时确定的

……

Label  DCD  0x12345678    // 1. 编译器在程序某个地方保存有这个值

如果一定需要赋值一个数,但是又不符合立即数的规则,那么就可以使用伪指令

2.4.2 ADR伪指令

    语法格式: ADR {cond}  reg,expr

    其中:cond——可选的指令执行条件。

                reg——目标寄存器。

                expr——基于PC或基于寄存器的地址表达式。

 ADR伪指令将基于PC相对偏移的地址值或基于寄存器相对偏移的地址值读取到寄存器中。在汇编编译器编译源程序时,ADR伪指令被编译器替换成一条合适的指令。通常,编译器用一条ADD指令或SUB指令来实现该ADR伪指令的功能,若不能用一条指令实现,则产生错误,编译失败。

    ADR伪指令格式 :ADR{cond}   register, expr  

      地址表达式expr的取值范围:

          当地址值是字节对齐时,其取指范围为: +255 ~ 255B;

          当地址值是字对齐时,其取指范围为:   -1020 ~ 1020B;

字对齐和字节对齐


ARM编译器与GCC编译器语法差别

四、汇编指令学习(常用)


常用的汇编指令一般有mov、bl/b、add/sub、ldm/stm、push/pop等等,下面一一介绍。

4.1 MOV指令

Move register or constant,把某个寄存器的值移给另一个寄存器,或是把一个常数移入寄存器
mov r1, #10  @ 将10赋值给寄存器r1,即r1=10
  指令执行过程,如下:
1. 取址
假设从内存的addrA地址取机器码e3a0100a(即mov r1, #10指令)
2. 译码
原来是MOV指令
3. 执行
CPU内部寄存器R1等于10
其中,机器码e3a0100a,MOV指令各位的解析如下:
[31:28]位是条件码0xe;[15:12]位是寄存器R1,即0x1;[12:0]位是立即数10,即0x00a

4.2 BL指令

Branch with Link,跳转并把返回地址记录在LR寄存器里。
bl test_tag mov r1, #10 test_tag: mov r3, #0  mov pc, lr
第1行,跳转到test_tag标签处执行“mov r3, #0”指令,并且将下一条指令即“mov  r1, #10”指令的地址存储到 LR 寄存器。
第6行,返回到“mov r1, #10”指令地址,并且执行“mov r1, #10”指令
指令执行过程,如下:

1. CPU从内存的addrA地址取机器码eb000000(即“bl test_tag”指令),执行后,PC跳转到test_tag标签位置,即内存的addrA+8地址,从上图可知,其实test_tag标签的地址是“mov r3, #0”指令的地址。同时自动将内存的addrA+4地址存储在寄存器LR中。
2. CPU从内存的addrA+8地址取机器码e3a03000(即“mov r3, #0”指令),执行,CPU内部寄存器R3等于0。
3. CPU从内存的addrA+12地址取机器码e1a0f00e(即“mov pc, lr”指令),执行,PC跳转到内存的addrA+4地址。
4. CPU从内存的addrA+4地址取机器码e3a0100a(即“mov r1, #10”指令),执行,CPU内部寄存器R1等于10
其中,机器码eb000000,BL指令各位的解析如下:
imm[23:0]是PC值与标签的偏移值除以4,但是此处的偏移值是0,为什么呢?这是因为ARM采用三级流水线的方法,即取指、译码、执行指令。所以当ARM执行addrA地址的“bl  test_tag”指令时,但是PC已经指向addrA+8地址进行取“mov r3, #0”指令,也就是PC值等于当前指令地址加8,所以要跳去执行“addrA+8”的指令,偏移值就要设置为0:执行这条bl指令后,新的PC=PC+偏移,刚好就是“addrA+8”。

4.2 B指令

Branch,跳转指令。相比于BL指令,它并不保存下一条指令的地址到LR寄存器。
b test_tag mov r1, #10 test_tag: mov r3, #0
第1行,只是跳转到test_tag标签处执行“mov r3, #0”指令,没有保存返回地址。
指令B与指令BL,大同小异,此处就不一一分析了,可以参数指令BL,它们的区别:BL指令的下一条指令的地址会被存储到LR寄存器,而B指令不会存储。

4.3 ADD/SUB指令

加法、减法指令。
mov r1, #10 add r2, r1, #4 sub r2, r1, #4
第2行,r2 = r1 + 4。
第3行,r2 = r1 - 4。

指令执行过程,如下:
CPU从内存的addrA+4地址取机器码e2812004(即add r2, r1, #4指令),执行后,CPU内部寄存器R2等于14
CPU从内存的addrA+8地址取机器码e2412004(即sub r2, r1, #4指令),执行后,CPU内部寄存器R2等于6
其中,机器码e2812004,ADD指令各位的解析如下:
[19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

其中,机器码e2412004,SUB指令各位的解析如下:
[19: 16]位是源寄存器R1,即1;[15: 12]位是目标寄存器R2,即2;[11: 0]位是立即数4,即0x004;

4.4 LDR/STR指令

Store register to memory/Load register from memory,前者是把寄存器的值写入内存,后者是读内存到寄存器。
mov r0, #400H @ 0x400 mov r1, #aH   @ 0xa str r1, [r0] ldr r2, [r0]
第3行,将寄存器R1的值0xa存储到寄存器R0指向的地址0x400,即内存0x400地址的值为0xa
第4行,将寄存器R0指向地址0x400的数据赋值给寄存器R2,即CPU内部寄存器R2等于0xa

指令执行过程,如下:



1. CPU从内存的addrA 地址取机器码e3a00b01(即mov r0, #400H指令),执行后,CPU内部寄存器R0等于0x400
2. CPU从内存的addrA+4地址取机器码e3a0100a(即mov r1, #aH指令),执行后,CPU内部寄存器R1等于0xa
3. CPU从内存的addrA+8地址取机器码e5801000(即str r1, [r0]指令),执行后,寄存器R1的0xa数据存储到寄存器R0指向的地址0x400,即内存的0x400地址的值为0xa(结论正确,但是前文描述不准确)
4. CPU从内存的addrA+12地址取机器码e5902000(即ldr r2, [r0]指令),执行后,寄存器R0指向的地址0x400的数据存储到CPU内部寄存器R2,即CPU内部寄存器R2等于0xa(结论正确,但是前文描述不准确)
  注意源寄存器和目标寄存器位置
其中,机器码e5801000,STR指令各位的解析如下:
STR语法中是:源寄存器+目标寄存器
[19: 16]位是目标寄存器R0,即0;[15: 12]位是源寄存器R1,即1;

其中,机器码e5902000,LDR指令各位的解析如下:
LDR语法中是:目标寄存器+源寄存器
[19: 16]位是源寄存器R0,即0;[15: 12]位是目标寄存器R2,即2;

4.5 LDR伪指令

所谓“伪指令”就是假的、不真实存在的指令。编译器会用真正的指令来代替它。
Ldr指令的参数中,有“=”时,它就是伪指令,不再是上小节讲的“读内存”指令了。
ldr sp,=0x80200000
这个是一条伪指令,即实际中并不存在这个指令,它会被拆分成几个真正的ARM指令,实现一样的效果,将0x80200000赋值给寄存器sp,即sp=0x80200000。
指令执行过程,如下:
  
ldr sp,=0x80200000这条伪指令,被翻译成一条指令来执行: ldr sp, [pc, #-4]。它是一条读内存指令,显然,在“pc-4”这个地址对应的内存里存有0x80200000这个数值。这是编译器预先把0x80200000设置在这个地址的。

4.6 LDM/STM指令

ldm,Load multiple registers,从内存里把值加载进多个寄存器。
stm,Store Multiple,把多个寄存器的值存储到内存。
格式: 
ldm{cond} Rn{!}, reglist stm{cond} Rn{!}, reglist
参数说明: 
cond:如下所示,下面列出的前四个条件是用于数据块操作,后四个条件是用于堆栈操作:
IA : Increment After,先传输再增加地址
IB : Increment Before,先增加地址再传输
DA : Decrement After,先传输再减小地址
DB : Decrement Before,先减小地址再传输
FD : 满递减堆栈
FA : 满递增堆栈
ED : 空递减堆栈
EA : 空递增堆栈
Rn:基址寄存器,里面含有内存的起始地址。
!:表示最后的地址写回到Rn中。
reglist:里面列出多个寄存器,如{R1,R2,R6-R9}。
注意:传输数据时,低号寄存器对应低地址,高号寄存器对应高地址。
示例: 
ldr r1,=0x10000000     ldmib r1!, {r0,r4-r6} stmda r1!, {r0,r4-r6}
第1行,将起始地址0x10000000赋值给r1
第3行,因为使用ib,所以每次传送前地址加4,具体操作如下(低地址的值存入低号寄存器):
将0X10000004地址的内容赋值给R0
将0X10000008地址的内容赋值给R4
将0X1000000C地址的内容赋值给R5
将0X10000010地址的内容赋值给R6
“!”表示,最后的地址写回到R1中,R1=0X10000010
第4行,因为使用da,所以每次传送后地址减4,具体操作如下(低号寄存器的值存入低地址):
将R6存储到0X10000010地址
将R5存储到0X1000000C地址
将R4存储到0X10000008地址
将R0存储到0X10000004地址
“!”表示,最后的地址写回到R1中,R1=0X10000000
如下图所示:

4.6.1 堆栈操作:满递减堆栈 

ldr sp,=0x80200000 stmfd sp!, {r0-r2} @ 入栈 ldmfd sp!, {r0-r2} @ 出栈
第1行,将0x80200000赋值给sp,作为堆栈的起始地址
第3行,入栈,具体操作如下:
将R2存储到0X80200000地址
将R1存储到0X801FFFFC地址
将R0存储到0X801FFFF8地址
第4行,出栈,具体操作如下:
将0X801FFFF8地址的内容赋值给R0
将0X801FFFFC地址的内容赋值给R1
将0X80200000地址的内容赋值给R2
 
如下图所示:
上述第3,4行汇编代码,就是所谓的入栈,出栈。也可以用push,pop指令完成入栈,出栈,如下
ldr sp,=0x80200000 push {r0-r2} @ 入栈 pop {r0-r2}  @ 出栈LDR

4.6.2 参考资料

http://c.biancheng.net/view/3534.html

4.7 ALIGN 指令

语法如下:
ALIGN bound
     Bound 可取值有:1、2、4、8、16。当取值为 1 时,则下一个变量对齐于 1 字节边界(默认情况)。当取值为 2 时,则下一个变量对齐于偶数地址。当取值为 4 时,则下一个变量地址为 4 的倍数。当取值为 16 时,则下一个变量地址为 16 的倍数,即一个段落的边界。
     为了满足对齐要求,汇编器会在变量前插入一个或多个空字节。为什么要对齐数据?因为,对于存储于偶地址和奇地址的数据来说,CPU 处理偶地址数据的速度要快得多。
     下述例子中,bVal 处于任意位置,但其偏移量为 0040 4000。在 wVal 之前插入 ALIGN 2 伪指令,这使得 wVal 对齐于偶地址偏移量:
bVal BYTE ?           ;00404000h ALIGN 2 wVal WORD ?           ;00404002h bVal2 BYTE ?          ;00404004h ALIGN 4 dVal DWORD ?          ;00404008h dVal2 DWORD ?         ;0040400Ch
请注意,dVal 的偏移量原本是 0040 4005,但是 ALIGN 4 伪指令使它的偏移量成为 0040 4008。