《嵌入式工程师自我修养/C语言》系列——由浅入深夯实ARM汇编基础,汇编指令及寻址方式梳理(附示例)!
- 一、引言
- 二、ARM汇编语言
- 2.1 ARM汇编的特点
- 2.2 ARM指令集格式标准
- 2.2.1 机器指令格式
- 2.2.2 汇编指令格式
- 三、ARM寻址方式
- 3.1 立即数寻址
- 3.2 寄存器寻址
- 3.2.1 基本方式(寄存器寻址)
- 3.2.2 对第二操作数寄存器的移位操作(寄存器偏移寻址)
- 3.2.3 寄存器间接寻址
- 3.2.4 基址加偏移地址寻址(基址寻址)
- 3.3 多寄存器及块拷贝寻址
- 3.4 堆栈寻址
- 3.5 相对寻址
- ——为什么B指令的前后跳转范围为[0,32MB]?
- 四、ARM汇编指令
- 4.1 存储访问指令
- 4.2 数据处理类指令
- 4.2.1 数据传送指令
- 4.2.2 算数逻辑运算指令
- 4.2.3 比较指令
- 4.3 跳转指令
- 五、伪指令
- 5.1 什么是ARM伪指令?
- 5.2 LDR伪指令
- 5.3 ADR伪指令
- 六、结语
快速学习嵌入式开发其他基础知识?>>>>>>>>> 返回专栏总目录 《嵌入式工程师自我修养/C语言》<<<<<<<<<
Tip📌:鼠标悬停双虚线关键词/句,可获得更详细的描述
一、引言
阅读《到底什么是指令集?什么是微架构?他们是什么关系?》后,我们知道指令集是编译器开发者以及CPU开发者研发时的重要参考标准,并且任何复杂的运算都可以分解为指令集中的基本指令。
这种由基本指令组成的不同组合,就是存储在内存中的程序(他们以二进制形式存储)。比如为了实现3+4-5运算,在ARM平台下对应的二进制机器指令如下。
Tip📌:ARM指令集一条指令占 32 位,4字节,Thumb指令集一条指令占 16 位 ,2字节。
11100011101000000000000000000011
11100010100000000000000000000100
11100010010000000000000000000101
肉眼可见,这串二进制数字非常不好记,可读性差,更别提用它直接编程了。于是为了编程方便,我们给每个二进制指令起一个别名,使用一个助记符表示,这些助记符就是汇编语言,由助记符组成的指令序列就是汇编程序。汇编程序经过汇编器的翻译,才能变成CPU真正能识别、译码和运行的二进制指令。上面的二进制机器指令,使用ARM汇编指令表示如下(读完本文,你将知道是如何转换的)。
MOV R0, #3
ADD R0, R0, #4
SUB R0, R0, #5
Tip📌:通常我们使用更易于理解和编写的高级语言编写程序,比如C语言,编译器中会集成汇编功能,直接将高级语言翻译成可执行的机器指令。
二、ARM汇编语言
2.1 ARM汇编的特点
ARM采用RISC架构,CPU本身不能直接读取内存,而需要先将内存中内容加载入CPU中通用寄存器才能被CPU处理。使用LDR/STR指令组合来实现 ARM CPU和内存数据的交换:
- LDR(load register)指令将内存内容加载入通用寄存器。
- STR(store register)指令将寄存器内容存入内存空间中。
Tip📌:ARM官方的指令风格:指令一般用大写,一般用于Windows的开发环境(ADS,MDK等)如: LDR R0, [R1]。GNU风格:指令一般用小写字母、linux中常用。如:ldr r0, [r1]。
2.2 ARM指令集格式标准
2.2.1 机器指令格式
虽然我们不用编写机器指令完成某项功能,但了解机器指令的格式,对汇编指令的学习以及汇编内部逻辑的理解是非常有帮助的!这也是众多汇编语言教程中只顾及指令用法介绍但往往忽略的重要扩展知识点。
以32位ARM指令集为例,其机器指令的格式如下所示:
条件码:
在阅读《ARM处理器有哪些工作模式和寄存器?各寄存器作用是什么?ARM异常中断处理流程?》后,我们知道CPSR寄存器中存放着一些当前程序的运行状态,比如条件标志位的N、Z、C、V等。一条机器指令是否会执行就是根据这前四位条件码与CPSR寄存器中指示的当前程序状态是否匹配做判断的,条件码的取值情况、对应的汇编指令后缀、与CPSR中关联的标志位及相应含义如下表所示:
——举个例子:
如果将要执行的某条机器指令的前四位为0000,则表示需要先判断CPSR寄存器的Z标志位是否为1(即当前比较结果是否相等),如果Z为1,才会执行这条机器指令,否则忽略。大多数情况下,在实际程序中很多操作都是无条件执行的,条件码都是1110。读到这里你也许知道引言部分三条机器指令前四位的意思了,没错,那三条指令都是无条件执行的。
指令码: 占8位,即存在256种情况,也就是说这种格式下,支持指令集中最多存在256种指令,远远够用,基于RISC架构的ARM指令集也就几十条指令而已。
目的寄存器和操作数1寄存器: 都占用4位,即支持16种寄存器情况,虽然ARM总共37个寄存器,但每种模式下最多只能访问18个,再具体到指令中这两处可能出现的情况,也足够使用。
操作数2: 这个比较有意思,可以看到总共占用12位,但32位的ARM机器中,肯定有32位的立即数(就是一个数,后文立即寻址会讲到)或者32位的地址需要载入寄存器中。但是这里又限制死了机器指令中操作数2只有12位,那么如何在机器指令中使用12位的操作数2完成32位数据的相关操作呢?别急寻找答案,不太懂也没关系,耐心往下看,在立即数寻址一节会有示例说明。
2.2.2 汇编指令格式
每一条ARM汇编指令都需要遵循下图所示的格式:
具体来说:
指令单元 | 功能简述 |
---|---|
opcode | 二进制机器指令的操作码助记符,如MOV、ADD这些汇编指令都是操作码的指令助记符 |
cond | 执行条件,ARM为减少分支跳转指令个数,允许类似BEQ、BNE等形式的组合指令, B是跳转,EQ(相等时)、NE(不等时)就是这里说的执行条件 |
S | 表示是否影响CPSR寄存器中的标志位,如SUBS指令会影响CPSR寄存器中的N、Z、C、V标志位,而SUB指令不会 |
Rd | 目标寄存器 |
Rn | 第一个操作数的寄存器 |
operand2 | 第二个可选操作数,灵活使用第二个操作数可以提高代码效率(下文将专门详细介绍它) |
三、ARM寻址方式
像大多数教程或者书籍一样,在学习具体的汇编指令前,我们一般都先学习ARM的寻址方式,但学习寻址方式必须与具体汇编指令结合,因此这部分遇到看不懂的指令时可以先跳转到下一节简单学习下指令用法。
——首先,到底什么是寻址?
寻址(Addressing)是指计算机在进行内存读取或写入操作时,通过指定内存单元的地址来访问内存数据的过程。在计算机中,所有的数据都保存在内存中,并且每个内存单元都有一个唯一的地址,这个地址可以被用来访问这个内存单元中的数据。在程序执行过程中,CPU通过指令中的地址来访问内存中的数据,根据不同的寻址方式可以实现不同的内存访问方式,如寄存器寻址、立即寻址、寄存器偏移寻址、寄存器间接寻址、基址寻址、多寄存器寻址、相对寻址等。
3.1 立即数寻址
在立即数寻址中,ARM指令中的操作数为一个常数。立即数以#为前缀,0x前缀表示该立即数为十六进制,不加前缀默认是十进制。这个立即数直接包含在32位机器指令的编码中,即指令中包含了要操作的数据的具体数值,而不是该数据所在的地址。这种方式可以直接将数据值嵌入到指令中,而不需要额外的寻址操作。这也是为什么叫做立即寻址的原因。
ADD R1, R1, #1 ;将R1寄存器中值加1,并将结果保存到R1中
MOV R1, #0xFF ;将十六进制常数0xFF写到R1寄存器中
MOV R1, #12 ;将十进制常数12放到R1寄存器中
——问题
上述示例中的立即数最多是8位的0xFF,是合法的,甚至12位的0xFFF也是合法的,但32位长立即数该如何处理?在汇编指令中,立即数作为操作数2出现,对应的机器指令编码格式中仅安排12位空间,32位立即数显然不能直接编码进去!
——解决方案
一种常见的或者比较简单的处理方案是:机器指令中的12位操作数2的编码由8位常数和4位循环右移值构成,由8位常数循环右移那另外4位取值的2倍得到最后的32位立即数。
——示例详解
需要将32位立即数0x0000F200放到寄存器R0中,汇编指令为:MOV R0, #0x0000F200
,该指令经过汇编后对应的机器指令码为:E3A00CF2
。结合前文所述机器指令的格式,其解释如下:
32位原立即数获取方法:0xF2循环右移12×2=24位得到原32位数值
移位前:0000 0000 0000 0000 0000 0000 1111 0010
移位后:0000 0000 0000 0000 1111 0010 0000 0000 (十六进制:#0x0000F200)
Tip📌:上面只是说明了一种最简单的编码处理方式,并不是所有32位立即数都可以这样编码操作!
如0x1010,0xFF10,0x00102等,如果你也按上述方法做,会发现行不通,移位后并不能得到原始数据。这时候需要在使用立即数前做合法性判断,如果不合法做诸如0xFFFFFF00与0x000000FF之间取反之类的操作。这样的话,具体哪个立即数可以哪个不可以岂不是都要试试才知道?
值得庆幸的是我们没必要一个一个数的算,编译系统为我们提供了伪指令LDR,实现任意立即数直接载入寄存器!!ldr r1, =0x12345678
…既如此上面不都白学了?直接用这个指令不就可以了…其实不然,上述示例可以帮助我们更深刻的理解汇编指令和机器指令之间的关系,机器指令中各种二进制取值到具体含义的实现正是在cpu内部通过各种译码电路实现的(比如1110属于无条件执行,在cpu内部一定有对应的译码电路),了解了这种最底层的关系,对深入理解嵌入式系统以及未来代码调试能力的深度提升有很大的帮助。
3.2 寄存器寻址
在众多汇编语言教程中,对寻址方式做了大量的分类,寄存器基址寻址、变址寻址等等,实际上他们都属于寄存器寻址。本文将他们都归类在这种寻址方式下:
3.2.1 基本方式(寄存器寻址)
也就是常说的寄存器寻址,操作数保存在寄存器中,通过寄存器名就可以直接对寄存器中的数据进行读写。
MOV R1, R2 ;将寄存器R2中的值传递到R1
ADD R1, R2, R3 ;执行加法运算R2+R3,并将结果保存到R1中
3.2.2 对第二操作数寄存器的移位操作(寄存器偏移寻址)
通过第二个操作数operand2的灵活配置,我们可以将第二个操作数做各种左移和右移操作,作为新的操作数使用。
MOV R2, R1, LSL #3 ;R2 = R1<<3
ADD R3, R2, R1, LSL #3 ;R3 = R2 + R1<<3
ADD R3, R2, R1, LSL R0 ;R3 = R2 + R1<<R0
Tip📌:
常见的移位操作有逻辑移位和算术移位,两者的区别是:逻辑移位无论是左移还是右移,空缺位一律补0;而算术移位则不同,左移时空缺位补0,右移时空缺位使用符号位填充。
第二个操作数为寄存器时方可进行移位操作,移位数可以是五位立即数或某个寄存器内的数值,执行完毕后第二操作数寄存器中的数值并不改变;
常见的移位方式有以下几种:
移位方式 | 含义 |
---|---|
LSL | 逻辑左移(乘) |
LSR | 逻辑右移(除) |
ASL | 算数左移,和LSL一样 |
ASR | 算数右移,分正负来填充右移后的空余位,正0负1 |
ROR | 循环右移 |
RRX | 带扩展的循环右移,循环右移1位后左端用C填充,这种方式只移位1位,所以无须指定移位位数 |
3.2.3 寄存器间接寻址
寄存器间接寻址主要用来在内存和寄存器之间传输数据。寄存器中保存的是数据在内存中的存储地址,我们通过这个地址就可以在寄存器和内存之间传输数据。C语言中的指针操作,在汇编层次其实就是使用寄存器间接寻址实现的,数据传送类的load/store类指令也都是使用寄存器间接寻址方式;
LDR R1, [R2] ;将R2中的值作为地址,取该内存地址上的数据,保存到R1
STR R1, [R2] ;将R2中的值作为地址,将R1寄存器中的值写入该内存地址
3.2.4 基址加偏移地址寻址(基址寻址)
基址寻址其实也属于寄存器间接寻址。两者的不同之处在于,基址寻址将寄存器中的地址与一个偏移量相加,生成一个新地址,然后基于这个新地址去访问内存。(变种较多,需要仔细阅读下面的示例)
LDR R1, [R0, #2] ;将R0中的值加2作为新地址,取该内存地址上的数据,保存到R1
LDR R1, [R0, #2]! ;与上一条的不同是:在加载完数据后,R0寄存器的值会增加2。即感叹号表示在执行LDR指令后,更新基址寄存器。
LDR R1, [R0, R2] ;将R0+R2作为新地址,取该内存地址上的数据,保存到R1
LDR R1, [R0, R2, LSL #2] ;将R0+R2<<2作为新地址,取该内存地址上的数据,保存到R1
LDR R1, [R0], #2 ;将R0中的值作为地址,取该内存地址上的数据,保存到R1,然后R0寄存器值加2,与第二条的不同在于这里取数据用的是R0地址,最后再更新R0。第二条是先用R0+2作为地址取数据,再更新R0。
STR R1, [R0, #-2] ;将R0中的值减2作为新地址,将R1中的值写入该地址
STR R1, [R0], #-2 ;将R0中的值作为地址,将R1中的值写入该地址,然后R0寄存器值减2
Tip📌:
基址寻址一般用在查表、数组访问、函数的栈帧管理等场合。根据偏移量的正负,基址寻址又可以分为向前索引寻址和向后索引寻址,如上面的第1条和第3条指令,就是向后索引寻址,而第6条指令则为向前索引寻址。
3.3 多寄存器及块拷贝寻址
一条指令完成多字数据或数据块的传送,STM/LDM指令就属于多寄存器寻址,一次可以传输多个寄存器的值。LDM/STM指令一般和IA、IB、DA、DB组合使用,用来表示指令执行对基址寄存器的影响:
指令后缀 | 含义 |
---|---|
IA (Increment after Operating) | 操作完后地址递增 |
IB (Increment before Operating) | 地址先增后完成操作 |
DA (Decrement after Operating) | 操作完后地址递减 |
DB (Decrement before Operating) | 地址先减后完成操作 |
LDMIA R0, {R1-R4, R6} ;R1=[R0],R2=[R0+4],……R6=[R0+16],R0=R0+20。(R0作为基址寄存器,其值可自动更新)
Tip📌:
在多寄存器寻址中,用大括号{}括起来的是寄存器列表,寄存器之间用逗号隔开,如果是连续的寄存器,还可以使用连接符-连接,如R0-R3,就表示R0、R1、R2、R3这4个寄存器。
LDM指令与STM指令配合实现数据块拷贝:
LDMIA R0, {R1-R5} ;以R0为基址读取五字存储单元数据加载至R1-R5
STMIA R6, {R1-R5} ;将R1-R5中数据依次存入R6为起始地址的存储单元
3.4 堆栈寻址
——堆栈有以下几种:
堆栈格式 | 含义 |
---|---|
FA(Full Ascending) | 满递增堆栈 |
FD(Full Descending) | 满递减堆栈 |
EA(Empty Ascending) | 空递增堆栈 |
ED(Empty Descending) | 空递减堆栈 |
——具体如何区分呢?
如下图所示,在一个堆栈内存结构中,如果堆栈指针SP总是指向栈顶元素,那么这个栈就是满栈;如果堆栈指针SP指向的是栈顶元素的下一个空闲的存储单元,那么这个栈就是空栈。(即通过sp指向的位置是否有数据判断是空栈还是满栈)
每入栈一个元素,栈指针SP都会往栈增长的方向移动一个存储单元。如果栈指针SP从高地址往低地址移动,那么这个栈就是递减栈;如果栈指针SP从低地址往高地址移动,那么这个栈就是递增栈,ARM处理器使用的一般都是满递减堆栈。
LDM/STM指令可以和FD、ED、FA、EA组合使用,用于堆栈操作,完成存储空间中的数据栈与寄存器组之间的批量数据传输,采用R13(SP)作为堆栈指针,采用FILO(先进后出)的方式工作,SP指向栈顶。
LDMFD SP!, {R0-R2,R14} ;将内存栈中的数据依次弹出到R14, R2, R1, R0
STMFD SP!, {R0-R2,R14} ;将R0, R1, R2, R14依次压入内存栈
栈的特点是先入后出(First In Last Out,FILO),栈元素在入栈操作时,STMFD会根据大括号{}中寄存器列表中各个寄存器的顺序,从左往右依次压入堆栈。在上面的例子中,R0会先入栈,接着R1、R2入栈,最后R14入栈,入栈操作完成后,栈指针SP在内存中的位置如下图左侧所示。栈元素在出栈操作时,顺序刚好相反,栈中的元素先弹出到R14寄存器中,接着是R2、R1、R0。将栈中的元素依次弹出到R14、R2寄存器后,堆栈指针在内存中的位置如下图右侧所示。
Tip📌:ARM中的PUSH和POP指令其实就是LDM/STM的同义词,是LDMFD和STMFD组合指令的助记符。PUSH指令和POP指令的使用示例如下:
STMFD SP!, {R0-R2,R14} ;将R0、R1、R2、R14依次压入栈
LDMFD SP!, {R0-R2,R14} ;将栈中的数据依次弹出到R14、R2、R1、R0
PUSH {R0-R2,R14} ;将R0、R1、R2、R14依次压入栈
POP {R0-R2,R14} ;将栈中的数据依次弹出到R14、R2、R1、R0
3.5 相对寻址
相对寻址其实也属于基址寻址,只不过它是基址寻址的一种特殊情况。特殊在什么地方呢?它是以PC指针作为基地址进行寻址的,以指令中的地址差作为偏移,两者相加后得到的就是一个新地址,然后可以对这个地址进行读写操作。ARM中的B、BL、ADR指令其实都是采用相对寻址的。
...
B LOOP
...
LOOP MOV R0, #1
MOV R1,R0
...
在上面的示例代码中,B LOOP指令其实就等价于:
ADD PC, PC, #OFFSET
其中OFFSET为B LOOP这条当前正在执行的指令地址与地址标号LOOP之间的地址偏移。B指令的前后跳转范围为[0,32MB],如果你编写的程序生成的二进制文件小于32MB,基本上就可以随意地使用B指令跳转了。
除此之外,很多与位置无关的代码,如动态链接共享库,其在汇编代码层次的实现其实也是采用相对寻址的。程序中使用相对寻址访问的好处是不需要重定位,将代码加载到内存中的任何地址都可以直接运行。
——为什么B指令的前后跳转范围为[0,32MB]?
首先跳转指令B的指令编码是这样的:
bit31 - bit28:4位条件码
bit27 - bit25:101,指令码
bit24:是否链接标志
bit23 - bit0:跳转的地址
也就是说跳转的地址位共24位,常规理解来说,其中一位为符号位也就只有正负8M的跳转地址而已。但实际上,因为ARM中要求指令是字对齐的,也就是32位对齐,因为每条指令占4字节,也就是说每条指令地址都间隔至少4(假设当前指令地址二进制表示为…0100,那么下一条应该是…1000,再下一条应该是…1100),可见每条指令地址的末尾两位都为00,而寄存器R15程序计数器PC每次跳转地址都要加4,保证了跳转的指令末两位为00,但B指令中的23:0并不保证这个规律,所以再其转换为PC指向的地址的时候会将这24位左移2位,也就是在末尾加两个0变成了26位,此时其中一位为符号位,那么所能表示的跳转范围就变成了25个bit,为正负32M了。
四、ARM汇编指令
4.1 存储访问指令
在ARM存储访问指令中,我们经常使用的是LDR/STR、LDM/STM这两对指令。LDR/STR指令是ARM汇编程序中使用频率最高的一对指令,每一次数据的处理基本上都离不开它们。
LDR R1, [R0] ;将R0中的值作为地址,将该地址上的数据保存到R1
STR R1, [R0] ;将R0中的值作为地址,将R1中的值存储到这个内存地址
LDR/STR默认每次读写4字节,还可以使用LDRB/STRB指令,他们每次读写一字节:
LDRB R3, [R2], #1 ;以R2为地址读取一字节数据至R3,R2=R2+1
STRH R1, [R0, #2]! ;半字传送,传送R1中低两字节数据至R0+2为地址的存储单元,R0更新
LDM/STM指令常用来加载或存储一组寄存器到一片连续的内存,通过和堆栈格式符组合使用,LDM/STM指令还可以用来模拟堆栈操作,在堆栈寻址一节有详细描述。此外,涉及到这种多寄存器的操作,在非用户或系统模式下,可出现“^”后缀,若LDM指令寄存器列表中包含PC,则会额外将SPSR拷贝至CPSR,这种指令通常用于函数返回时,将之前保存在栈中的寄存器值恢复,以及恢复程序状态寄存器的值,一般用于异常处理的返回:
LDMFD SP!, {R0-R7, PC}^ ;将SP指向的一片连续内存空间内的数据依次载入R0至R7以及PC寄存器;^符号表示在加载完成后,要将SPSR的值加载到CPSR中。
4.2 数据处理类指令
4.2.1 数据传送指令
LDR/STR指令用来在寄存器和内存之间输送数据。如果我们想要在寄存器之间传送数据,则可以使用MOV指令。MOV指令的格式如下。
MOV R1, #1 ;将立即数1传送到寄存器R1中
MOV R1, R0 ;将R0寄存器中的值传送到R1寄存器中
MOV PC, LR ;子程序返回
MVN指令用来将操作数operand2按位取反后传送到目标寄存器。操作数operand2可以是一个立即数,也可以是一个寄存器。
MVN R0, #0 ;立即数0取反传送至R0,R0=-1
MVN R0, #0xFF ;将立即数0xFF取反后赋值给R0
MVN R0, R1 ;将R1寄存器的值取反后赋值给R0
4.2.2 算数逻辑运算指令
算术运算指令包括基本的加、减、乘、除。
ADD R2, R1, #1 ;R2=R1+1
ADC R1, R1, #1 ;R1=R1+1+C(其中C为CPSR寄存器中进位)
SUB R1, R1, R2 ;R1=R1-R2
SBC R1, R1, R2 ;R1=R1-R2-C
继续举个例子,实现64位整数加法,用R0/R1与R2/R3分别存放两个加数的低/高32位,R4/R5存放结果的低/高32位:
ADDS R4,R0,R2 ;带S后缀结果影响CPSR中的标志位C
ADC R5,R1,R3 ;带进位的加法,C标志位参与运算
逻辑运算指令包括与、或、非、异或、清除等。
AND R0, R0, #3 ;保留R0的bit0和1,其余位清除
ORR R0, R0, #3 ;置位R0的bit0和bit1
EOR R0, R0, #3 ;反转R0中的bit0和bit1
BIC R0, R0, #3 ;清除R0中的bit0和bit1
4.2.3 比较指令
比较指令用来比较两个数的大小,或比较两个数是否相等。比较指令的运算结果会影响CPSR寄存器的N、Z、C、V标志位,具体的标志位说明可参考前面的CPSR寄存器介绍。
CMP R1, #10 ;R1-10,运算结果会影响N、Z、C、V位,不会保留运算结果
CMP R1, R2 ;R1-R2,比较结果会影响N、Z、C、V位
CMN R0, #1 ;R0 -(-1)将立即数取负,然后比较大小
比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;N=1表示运算结果为负,N=0表示运算结果为非负,即运算结果为正或者为零。
4.3 跳转指令
跳转指令用于控制程序的走向,可完成从当前指令向前或向后的32MB的地址空间跳转,包括基本跳转指令B,带返回的跳转指令BL,带状态切换(ARM与Thumb之间)的跳转指令BX,带返回和状态切换的跳转指令BLX。
无条件跳转指令B主要用在循环、分支结构的汇编程序中:
CMP R2, #0
BEQ label ;若R2=0,则跳转到label处执行
...
label
...
BL跳转指令表示带链接的跳转。在跳转之前,BL指令会先将当前指令的下一条指令地址(即返回地址)保存到LR寄存器中,然后跳转到label处执行。BL指令一般用在函数调用的场合,主函数在跳转到子函数执行之前,会先将返回地址,即当前跳转指令的下一条指令地址保存到LR寄存器中;子函数执行结束后,LR寄存器中的地址被赋值给PC,处理器就可以返回到原来的主函数中继续运行了。
;主程序
...
BL subfunc ;跳到subfunc执行,在跳之前将返回地址保存在LR
... ;子程序返回后接着从此处继续执行
subfunc ;子程序
...
MOV PC, LR ;子程序执行完,将返回地址赋值给PC,返回到主函数
BX表示带状态切换的跳转。Rm寄存器中保存的是跳转地址,要跳转的目标地址处可能是ARM指令,也可能是Thumb指令。处理器根据Rm[0]位决定是切换到ARM状态还是切换到Thumb状态。
- Rm[0]位值为0:表示目标地址处是ARM指令,在跳转之前要先切换至ARM状态。
- Rm[0]位值为1:表示目标地址处是Thumb指令,在跳转之前要先切换至Thumb状态。
BLX指令是BL指令和BX指令的综合,表示带链接和状态切换的跳转,使用方法和上面相同,不再赘述。
五、伪指令
5.1 什么是ARM伪指令?
顾名思义,ARM伪指令并不是ARM指令集中定义的标准指令,而是为了编程方便,各家编译器厂商自定义的一些辅助指令。伪指令有点类似C语言中的预处理命令,在程序编译时,这些伪指令会被翻译为一条或多条ARM标准指令。常见的ARM伪指令主要有4个:ADR、ADRL、LDR、NOP,它们的使用示例如下。
ADR R0, LOOP ;将标号LOOP的地址保存到R0寄存器中
ADRL R0, LOOP ;中等范围的地址读取
LDR R0, =0x30008000 ;将内存地址0x30008000赋值给R0
NOP ;空操作,用于延时或插入流水线中暂停指令的运行
NOP伪指令比较简单,其实就相当于MOV R0,R0。在以后的学习和工作中,大家在ARM汇编程序中经常看到的就是LDR伪指令。
5.2 LDR伪指令
LDR伪指令的主要用途是将一个32位的立即数保存到寄存器中,在立即寻址一节做出了详细的解释。LDR伪指令通常会让很多朋友感到迷惑,容易和加载指令LDR混淆。
LDR R0, =0×30008000 ;有=号的就是伪指令,将立即数0x3000800送到R0
LDR RO, =LOOP ;将标号LOOP表示的地址送到R0
LDR R0, [R1] ;R1中的值作为地址,将该地址上的值送到R0
LDR RO, LOOP ;将标号LOOP表示的内存地址上的数据送到R0
5.3 ADR伪指令
ADR伪指令的功能与LDR伪指令类似,将基于PC相对偏移的地址值读取到寄存器中。ADR为小范围的地址读取伪指令,底层使用相对寻址来实现,因此可以做到代码与位置无关。ADR伪指令的使用示例代码如下。
ADR R0, LOOP
...
...
LOOP
b LOOP
在上面的示例代码中,ADR伪指令的作用是将标号LOOP表征的内存地址送到寄存器R0中。编译器在编译ADR伪指令时,会首先计算出当前正在执行的ADR伪指令地址与标号LOOP之间的地址偏移OFFSET,然后使用ARM指令集中的一条标准指令代替之,如使用ADD指令将标号表征的地址送到寄存器R0中。
OFFSET = LOOP-(PC-8)
ADD R0, PC, #OFFSET
ADR伪指令和LDR伪指令的相似之处在于:两者都是为了加载一个地址到指定的寄存器中。两者的不同之处在于:LDR伪指令通常被翻译为ARM指令集中的LDR或MOV指令,而ADR伪指令则通常会被ADD或SUB指令代替。在用途上,LDR伪指令主要用来操作外部设备的寄存器,而ADR伪指令主要用来通过相对寻址,生成与位置无关的代码。在一个程序中,只要各个标号之间的相对位置不变,使用ADR伪指令就可以做到与位置无关,将指令代码加载到内存中的任何位置都可以正常运行。
在寻址方式上,LDR使用绝对地址,而ADR则使用相对地址,LDR和ADR伪指令的地址适用范围也不同,LDR伪指令适用的地址范围为[0,32GB],而ADR伪指令则要求当前指令和标号必须在同一个段中,地址偏移范围也较小,地址对齐时偏移范围为[0,1020],地址未对齐时偏移范围为[0,4096]。
六、结语
以上就是所有内容了,汇编指令、寻址方式这部分内容比较多,仅靠一篇文章就想完全掌握是绝对不可能的,我也记不住所有的细节。但我们只要知道遇到问题时候怎么查就行,(个人储备的知识有两种,一种叫我会,另一种叫我知道并且清楚在哪儿能查到)。掌握汇编需要不断的在实际工作中分析汇编、查阅资料、再分析、再查阅,往复循环,这样终有一天你会发现神功大成,遇到汇编代码分析不再望而却步!
>>>>>>>>> 返回专栏总目录 《嵌入式工程师自我修养/C语言》<<<<<<<<<