![嵌入式C语言自我修养:从芯片、编译器到操作系统](https://wfqqreader-1252317822.image.myqcloud.com/cover/248/37669248/b_37669248.jpg)
3.2 ARM汇编指令
接下来的几节我们将从实用角度出发,学习ARM常用的一些汇编指令,如存储器访问指令、数据传送指令、算术逻辑运算指令、跳转指令等。一个完整的ARM指令通常由操作码+操作数组成,指令的编码格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_110_2.jpg?sign=1739344107-jGaw9IboAsPXgzy1kyMoVR1V1uhnVoQ7-0-78d36fb13a02af6e6a3fb6c0d13452b9)
这是一个完整的ARM指令需要遵循的格式规则,指令格式的具体说明如下。
● 使用<>标起来的是必选项,使用{}标起来的是可选项。
● <opcode>是二进制机器指令的操作码助记符,如MOV、ADD这些汇编指令都是操作码的指令助记符。
● cond:执行条件,ARM为减少分支跳转指令个数,允许类似BEQ、BNE等形式的组合指令。
● S:是否影响CPSR寄存器中的标志位,如SUBS指令会影响CPSR寄存器中的N、Z、C、V标志位,而SUB指令不会。
● Rd:目标寄存器。
● Rn:第一个操作数的寄存器。
● operand2:第二个可选操作数,灵活使用第二个操作数可以提高代码效率。
在熟悉了ARM指令的基本格式后,我们接下来就开始学习ARM常用的一些汇编指令。
3.2.1 存储访问指令
ARM指令集属于RISC指令集,RISC处理器采用典型的加载/存储体系结构,CPU无法对内存里的数据直接操作,只能通过Load/Store指令来实现:当我们需要对内存中的数据进行操作时,要首先将这个数据从内存加载到寄存器,然后在寄存器中对数据进行处理,最后将结果重新存储到内存中。ARM处理器属于冯·诺依曼架构,程序和数据都存储在同一存储器上,内存空间和I/O空间统一编址,ARM处理器对程序指令、数据、I/O空间中外设寄存器的访问都要通过Load/Store指令来完成。ARM处理器中经常使用的Load/Store指令的使用方法如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_111_1.jpg?sign=1739344107-7NJfx2jvZIZoOZtk3hsqyCItI01hzrGK-0-242436a71fb88f7d787ec6e8e70cf783)
在ARM存储访问指令中,我们经常使用的是LDR/STR、LDM/STM这两对指令。LDR/STR指令是ARM汇编程序中使用频率最高的一对指令,每一次数据的处理基本上都离不开它们。LDM/STM指令常用来加载或存储一组寄存器到一片连续的内存,通过和堆栈格式符组合使用,LDM/STM指令还可以用来模拟堆栈操作。LDM/STM指令常和表3-2的堆栈格式组合使用。
表3-2 不同类型的堆栈
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_111_2.jpg?sign=1739344107-i9FWlJhgmMZoJdYD1pjE4iVlLfeWOsGL-0-7cd7a7032ff048fa1e26fae5bc918869)
如图3-3所示,在一个堆栈内存结构中,如果堆栈指针SP总是指向栈顶元素,那么这个栈就是满栈;如果堆栈指针SP指向的是栈顶元素的下一个空闲的存储单元,那么这个栈就是空栈。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_112_1.jpg?sign=1739344107-txacqlbhV9SIgDPVmLIC53MZQamX8wPg-0-f75bcb8e94f7677c777faafe9317567b)
图3-3 满栈与空栈的区别
每入栈一个元素,栈指针SP都会往栈增长的方向移动一个存储单元。如果栈指针SP从高地址往低地址移动,那么这个栈就是递减栈;如果栈指针SP从低地址往高地址移动,那么这个栈就是递增栈。ARM处理器使用的一般都是满递减堆栈,在将一组寄存器入栈,或者从栈中弹出一组寄存器时,我们可以使用下面的指令。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_112_2.jpg?sign=1739344107-FAryC2MAyAUoTlqYUVsAd3EmEIZY1XgQ-0-de0c424dd223b721d285b349e82f5ab5)
这里需要注意的一个细节是,在入栈和出栈过程中要留意栈中各个元素的入栈出栈顺序。栈的特点是先入后出(First In Last Out,FILO),栈元素在入栈操作时,STMFD会根据大括号{}中寄存器列表中各个寄存器的顺序,从左往右依次压入堆栈。在上面的例子中,R0会先入栈,接着R1、R2入栈,最后R14入栈,入栈操作完成后,栈指针SP在内存中的位置如图3-4左侧所示。栈元素在出栈操作时,顺序刚好相反,栈中的元素先弹出到R14寄存器中,接着是R2、R1、R0。将栈中的元素依次弹出到R14、R2寄存器后,堆栈指针在内存中的位置如图3-4右侧所示。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_112_3.jpg?sign=1739344107-RrcmBUvROJrdzREN8ZSvb4dBVzPYrskD-0-22d07d3318db6e03ae6bbdd27c3208dc)
图3-4 入栈与出栈
除此之外,ARM还专门提供了PUSH和POP指令来执行栈元素的入栈和出栈操作。PUSH和POP指令的使用方法如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_113_1.jpg?sign=1739344107-wVYPb9qu44Y6s2xoulyRMAlYJJ9n9wFY-0-68553b1e1051d57220622e5a0f46542a)
3.2.2 数据传送指令
LDR/STR指令用来在寄存器和内存之间输送数据。如果我们想要在寄存器之间传送数据,则可以使用MOV指令。MOV指令的格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_113_2.jpg?sign=1739344107-2ZsqzhVuAEbhMoF9YxbRQHY33Zr7YFoK-0-2c3ce1f86ad41afcec746b44b8151dcb)
其中,{cond}为条件指令可选项,{S}用来表示是否影响CPSR寄存器的值,如MOVS指令就会影响寄存器CPSR的值,而MOV则不会。MVN指令用来将操作数operand2按位取反后传送到目标寄存器Rd。操作数operand2可以是一个立即数,也可以是一个寄存器。
MOV和MVN指令的一般使用方法如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_113_3.jpg?sign=1739344107-MiWyc30F6vU6eWOfPp9lHvaVeEM73HPo-0-0bed8ee6289656d587874ea7a3f6a06e)
3.2.3 算术逻辑运算指令
算术运算指令包括基本的加、减、乘、除,逻辑运算指令包括与、或、非、异或、清除等。指令格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_113_4.jpg?sign=1739344107-USVFSjopc8DPEnnJM5U5qkwh2b4iZBL7-0-50e82eabe7d6d0e32f3c77b0b6c7bc02)
算术逻辑运算指令的基本使用方法及说明如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_113_5.jpg?sign=1739344107-3x9WWqVqEWXxMHAlKF3KrXwZHJcBtEaH-0-79753aebc2785a492b5881f0b312468d)
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_1.jpg?sign=1739344107-wqObelAfoALvtWS6AhZ4j7hJPLLmk44s-0-64ecc71dbee93dca180ad067bc7871b9)
3.2.4 操作数:operand2详解
ARM指令的可选项很多,操作数也很灵活。很多ARM指令会使用第二个参数operand2:可以是一个常数,也可以是寄存器+偏移的形式。操作数operand2在汇编程序中经常出现的两种格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_2.jpg?sign=1739344107-fz2dKTgBYmHsxu8RzegYY8v8j7DW1291-0-cb053162e3720f7ce6779c1c3b898184)
第一种格式比较简单,操作数是一个立即数,第二种格式可以直接使用寄存器的值作为操作数。在3.2.3节中的ADD、SUB、AND指令示例中,第二个操作数要么是一个常数,要么是一个寄存器。在第二种格式中,通过{,shift}可选项,我们还可以通过多种移位或循环移位的方式,构建更加灵活的操作数。可选项{,shift}可以选择的移位方式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_3.jpg?sign=1739344107-WjMAC8bGxvPJDz0HQ6MUVyxFvCChTwwb-0-d811e89b55cc69f59156c18c88cd75eb)
可选性指令的使用示例及说明如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_4.jpg?sign=1739344107-TB1ZV9pgyPF9XIMXU3FkP4k6Dvbs9bNd-0-eca3e10741cdd1e2fc1e388e2769b6ae)
3.2.5 比较指令
比较指令用来比较两个数的大小,或比较两个数是否相等。比较指令的运算结果会影响CPSR寄存器的N、Z、C、V标志位,具体的标志位说明可参考前面的CPSR寄存器介绍。比较指令的格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_5.jpg?sign=1739344107-ABzFkPsUkyBQKQjNQi02HVY4NXx8bU4K-0-f67633098fd216b9f75fd4af0437656f)
比较指令的使用示例及说明如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_114_6.jpg?sign=1739344107-PDxGLcowYSrcMW4jF2wrNGtxS7qPhQEi-0-e64152ba358a4734a509556a48adb0a4)
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_115_1.jpg?sign=1739344107-oTGS4fS5oClsTLVDuMiq1FGecCYFklxX-0-6d992fbe9c597b4292e85409b9531a82)
比较指令的运行结果Z=1时,表示运算结果为零,两个数相等;N=1表示运算结果为负,N=0表示运算结果为非负,即运算结果为正或者为零。
3.2.6 条件执行指令
为了提高代码密度,减少ARM指令的数量,几乎所有的ARM指令都可以根据CPSR寄存器中的标志位,通过指令组合实现条件执行。如无条件跳转指令B,我们可以在后面加上条件码组成BEQ、BNE组合指令。BEQ指令表示两个数比较,结果相等时跳转;BNE指令则表示结果不相等时跳转。CPSR寄存器中的标志位根据需要可以任意搭配成不同的条件码,和ARM指令一起组合使用。ARM指令的条件码如表3-3所示。
表3-3 ARM指令的条件码
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_115_2.jpg?sign=1739344107-X3et0F0eu7CMJljKFu0uTJwZV0kJbJk7-0-733f4f0c66fe57080ba7b532874816f6)
条件执行经常出现在跳转或循环的程序结构中。如下面的汇编程序,通过循环结构,我们可以实现数据块的搬运功能。我们可以将无条件跳转指令B和条件码NE组合在一起使用,构成一个循环程序结构。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_115_3.jpg?sign=1739344107-qIm6gT9wwzYv1ZYwkH7HYhFP5ZKRSmbM-0-1e8b11c2ca56013aee7b1dda6b8b4b8a)
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_116_1.jpg?sign=1739344107-CR8xoNSW4n3F5rdvrgypmn7wMztavcII-0-29aa03ad53f602701c3ac3b0869bfd1e)
3.2.7 跳转指令
在函数调用的场合,以及循环结构、分支结构的程序中经常会用到跳转指令。ARM指令集提供了B、BL、BX、BLX等跳转指令,每个指令都有各自的用武之地和使用场景。跳转指令的格式如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_116_2.jpg?sign=1739344107-RTmQsVj35Ouvc9OuxEGFm2XHeEojF8SX-0-9c3f6f1b961a1e8bcd6adce439d22f85)
1.B label
跳转到标号label处,B跳转指令的跳转范围大小为[0,32MB],可以往前跳,也可以往后跳。无条件跳转指令B主要用在循环、分支结构的汇编程序中,使用示例如下。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_116_3.jpg?sign=1739344107-qlZFb5h5zkMyaYyuVjRlBDWBsPLugq2u-0-cd02c047aa7e836af3f65591aae56fbe)
2.BL label
BL跳转指令表示带链接的跳转。在跳转之前,BL指令会先将当前指令的下一条指令地址(即返回地址)保存到LR寄存器中,然后跳转到label处执行。BL指令一般用在函数调用的场合,主函数在跳转到子函数执行之前,会先将返回地址,即当前跳转指令的下一条指令地址保存到LR寄存器中;子函数执行结束后,LR寄存器中的地址被赋值给PC,处理器就可以返回到原来的主函数中继续运行了。
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_116_4.jpg?sign=1739344107-Kbd6MEdye2qeJp1tzjuMEvAzlLAnZewL-0-0cbaee737816417337d71003b4a517df)
![](https://epubservercos.yuewen.com/1B291C/19938711108152406/epubprivate/OEBPS/Images/40856_117_1.jpg?sign=1739344107-G7SKvXLSEvRJzuF0ruW4f2DXuk3VUyhC-0-707e91cb30e981ff36b6e5f98b359a1d)
3.BX Rm
BX表示带状态切换的跳转。Rm寄存器中保存的是跳转地址,要跳转的目标地址处可能是ARM指令,也可能是Thumb指令。处理器根据Rm[0]位决定是切换到ARM状态还是切换到Thumb状态。
● 0:表示目标地址处是ARM指令,在跳转之前要先切换至ARM状态。
● 1:表示目标地址处是Thumb指令,在跳转之前要先切换至Thumb状态。
BLX指令是BL指令和BX指令的综合,表示带链接和状态切换的跳转,使用方法和上面相同,不再赘述。