说明:本系列文章将主要以ARMv7和ARMv8架构为例,介绍ARM汇编语言的一些基础知识。关于ARM汇编语言的学习,这里我要推荐一本书和一个网站,其中书是由宋岩翻译的《Cortex-M3权威指南》,其文笔风趣幽默,引人入胜,网站则是azeria-labs。当然,ARM官方的Architecture Reference Manual更是重要的参考。
说起与系统结构相关的汇编语言,自然要先介绍该体系结构的寄存器组成。ARMv7相较于同为32位的x86,寄存器的数量要多一些,名称和配置也不尽相同,但两者还是有一个基本的对照关系:
ARMv7-A在设计之初,就有和之前系列的处理器(比如以ARM9系列为代表的ARMv5)兼容的七种处理器模式,后来在向ARMv8过渡的过程中,又增加了"MON"和"HYP"。
为了减少模式切换时的寄存器保存和恢复,同名寄存器在多种模式下各有一份,称为bank register。某些模式会有自己专有的寄存器,比如FIQ就比IRQ多一些寄存器(R8到R12),这样FIQ在进入和退出中断的时候,所需要做的寄存器保存和恢复就可以减少,这也是它比IRQ更"Fast"的原因。
自从ARMv8出现以后,ARM的寄存器就全面进入了64位时代,通用寄存器的数量从13个(R0-R12)变成了30个(X0-X29) ,其名称中的"R"也被"X"所取代了,但为了保持和32位系统的兼容性,每个ARMv8/ARM64通用寄存器都可被当做2个32位寄存器来使用,这样的32位寄存器用"Wn"来表示。
ARMv8支持两种执行状态(execution state),分别是AArch64和AArch32,在AArch64状态下执行的是A64指令集,在AArch32状态下执行的是与ARMv7前向兼容的A32/T32指令集。
A64指令集看起来和前代的指令集差别不大,但其具有更高的编码效率,别看它叫A64就以为它的指令长度是64位的,依然是32位,也就是4个字节。通常一条指令不会占据太多字节,而为了方便流水线的操作,ARM中指令的字节数通常是保持一致的(最多就是T32/Thumb-2这种2字节和4字节混合的指令),都设计成8个字节话的确挺浪费代码空间。
ARMv8中的A32/T32指令集也不是和ARMv7中A32/T32一模一样的,它做了一些改进和增强,如果你使用了这些强化的特性,当然可以获得更好的性能,但是就不能和ARMv7完全兼容了。如果你希望同样的一套汇编代码在ARMv7和ARMv8中都能直接运行,就不能使用这部分额外的特性。
ARM虽说是RISC架构的,但RISC和CISC并不是泾渭分明的,双方都在互相学习,取长补短。现在ARM支持的指令也是越来越多,本系列文章将仅介绍其中的一部分指令。
【数据传送指令】
基础的LDR/STR
在x86架构中,不管是寄存器之间,还是寄存器和内存之间,都可以使用MOV指令,并且直接操作内存单元上的数据是被允许的。
在ARM架构中,寄存器间传送数据的指令依然是MOV,比如"MOV Ra Rb" 就是把Rb里存放的数据传送给Ra,但内存单元上的数据不允许被直接操作,而是必须先放到寄存器中,为此就有了把内存的内容传送到寄存器的指令LDR(Load),以及把寄存器的内容传送回内存的指令STR(Store)。
传送的时候,内存单元的地址存放在一个寄存器中(比如R1),用[R1]表示,"[]"在这里就对应C语言里的"*",表示取地址里的内容。假设R1里存放的是0x200,内存中地址0x200处的内容是0x5,那么"ldr r0, [r1]"就是将0x5放入r0中。
通用寄存器的数量一共就那么多,直接用寄存器的值来获取内存地址的数量实在太有限了,更多的时候,是通过寄存器的值(基址)加上一个偏移/索引(offset/index)来指向内存对应的单元,索引的大小可以由立即数提供,也可以由寄存器存储的值提供:
STR R0,[R1, #12] // R0 --> [R1+12]
LDR R4,[R5, R6] // R4 <-- [R5+R6]
如果索引对基址的更改发生在数据传输之前,则称为"预索引"(pre-index),传输前后寄存器R1的值都不会改变。
如果索引对基址的更改发生在数据传输之后(注意下图"[]"位置的改变),则称为"后索引"(post-index),传输后寄存器R1的内容将变为加上其原来的值加上索引后的值。"后索引"其实算是一种二合一的指令,比如"str r0, [r1], #12"就等同于"str r0, [r1]"加上"r1 = r1+12"。
好像缺了点什么,没有既更改R1的值为R1+12,同时把*(R1+12)的值送入R0的?不急,这将在本文的后半段给出答案。
LDR和STR后面可以接一些后缀,比如"B", "H"和"W"分别表示从给定的内存地址取1个字节,2个字节和4个字节。
如果一次传送的不是8个字节,那么64位的寄存器是填不满的,为了保持负数的数值不变,这剩余的字节可能就需要进行符号位扩展(Signed),由后缀"S"表示,其配合"W"使用表示只进行低32位空余字节的扩展,配合"X"则表示进行整个64位的符号位扩展:
LDM/STM与三个问题
一个字节一个字节的传送那是“蚂蚁搬家”,如果要复制大批量的数据,效率实在不高,为此ARMv7还提供了用于批量传输的LDM和STM指令,"M"在这里代表Multiple。STM是把多个寄存器的值传送到内存相邻的位置,LDM反之。多个寄存器在ARM汇编语言中用"{}"圈起来,表示待传送的寄存器列表。
比如"STM R0, {R4,R5}" 就表示将R4的值传送到R0指向的内存单元,R5的值传送到下一个内存单元。批量传输其实是存在一个方向问题的,为了区分下一个内存单元是在上一个单元的后面还是前面(地址更大还是更小),需要加上后缀"I"和"D"来分别表示"Increase"和"Decrease"。
还有一个问题,要将R5的值传送到下一个内存单元,需要首先获得这“下一个”内存单元的地址,这就涉及到地址的增减。假设R0的值是0,如果先增加"0"这个值(在32位系统中,一次增加的值是4),再传送R4,那么就是[0x4]=R4, [0x8]=R5;如果是传送完R4后再增加"0"这个值,那么就是[0x0]=R4, [0x4]=R5。所以还需要加上后缀"A"和"B"来分别表示"After"(传送后增加)和"Before"(传送前增加)。
因此,LDM/STM家族一共有"IA", "IB", "DA"和"DB"四个变种(variant),"LDM"和"STM"什么后缀都不接也可以直接使用,但它其实包含一个隐式规则,即默认为"IA",也就是说"LDM"和"STM"其实分别等同于"LDMIA"和"STMIA"。
在函数调用中,进入子函数的时候要用"PUSH"指令,把存储在CPU寄存器中的局部变量/上下文保存到内存的栈中,退出子函数的时候要用"POP"指令,将栈中保存的内容恢复到对应的寄存器中,因为栈通常是自顶向下生长的,所以"PUSH"和"POP"其实可以分别用"STMDB"和"LDMIA"来替代。
STMDB SP!, {R0-R3, LR} <--> PUSH {R0-R3, LR}
LDMIA SP!, {R0-R3, PC} <--> POP {R0-R3, PC}
这里出现了一个"!"符号,那就是我们要解决的第三个问题:在增加/减少"SP"表示的这个数值(比如前面假设的"0")的时候,"SP"本身存储的内容是否跟着一起变化?加上"!"就表示在传送过程中"SP"会自增/自减,传送完成后"SP"的值已经不再是传送前的那个值了,不加"!"就是在传送前后保持"SP"的内容不变。"SP"作为stack pointer,在入栈和出栈的时候自然是要移动的,所以这里用了"!"。
"!"是表示寄存器自增/自减的,所以它并不局限于配合LDM/STM使用,如果它用在STR指令中,比如"str r0, [r1, #12]!",就相当于"str r0 [r1, #12]"加上"r1 = r1+12",这也解决了本文前半段介绍LDR/STR指令时留下的那个问题。
新一代的LDP/STP
在ARMv8中,LDM/STM被新一代的指令LDP(Load Pair)和STP(Store Pair)所取代了,LDM/STM对寄存器列表里包含的寄存器数量并没有什么限制,而LDP/STP要求和内存之间传送数据的寄存器不超过2个。因为"PUSH"和"POP"完全可以用LDM/STM表示,所以他俩也被一并干掉了。两代指令的对应关系大概是这样的:
小结一下,本文主要介绍了ARMv7和ARMv8的数据传送指令,并在其中穿插了ARM汇编语言中"[]", "{}", "!"符号的含义和用法。下文将介绍移位、序转和位操作等数据处理指令。
上一篇:ARM汇编进阶
下一篇:ARM常用的22个概念!
推荐阅读最新更新时间:2024-11-10 10:21