ARM汇编的必知必会
无论是体系结构还是指令集,大家或多或少都应该对X86汇编有些了解,而对于嵌入式领域已被广泛采用的ARM 处理器,了解的可能并不多。如果你有兴趣从事嵌入式方面的开发,那么了解一些RISC 体系结构和ARM汇编的知识还是有必要的。这里,我们找出了这两种体系结构最明显的不同之处,并对此进行介绍,让大家对于RISC体系结构的汇编有一个基本的了解。首先,我们就来看一看基于RISC的ARM的体系结构。
%A
%A 基于RISC 的ARM CPU
%A ARM是一种RISC体系结构的处理器芯片。和传统的CISC体系结构不同,RISC 有以下的几个特点:
%A ◆ 简洁的指令集――为了保证CPU可以在高时钟频率下单周期执行指令,RISC指令集只提供很有限的操作(例如add,sub,mul等),而复杂的操作都需要由这些简单的指令来组合进行模拟。并且,每一条指令不仅执行时间固定,其指令长度也是固定的,这样,在译码阶段就可以对下一条指令进行预取。
%A ◆ Load-Store 结构――这个应该是RISC 设计中比较有特点的一部分。在RISC 中,CPU并不会对内存中的数据进行操作,所有的计算都要求在寄存器中完成。而寄存器和内存的通信则由单独的指令来完成。而在CSIC中,CPU是可以直接对内存进行操作的,这也是一个比较特别的地方。
%A ◆ 更多的寄存器――和CISC 相比,基于RISC的处理器有更多的通用寄存器可以使用,且每个寄存器都可以进行数据存储或者寻址。
%A 当然,作为RISC 领域最成功的处理器,ARM也遵从上面的特点。这里,我们不妨来看一看在user 模式下,ARM处理器的体系结构,这对于我们了解其汇编语言是有好处的。而其它模式下只是有一些寄存器分组略有不同,大家可以在ARM的手册上查到。这里要说明的是,尽管ARM处理器也支持16位指令,不过在下文中,我们都假定ARM处理器在32 位模式下工作。
%A
%A 图1:user模式下ARM处理器体系结构
%A 从图1中我们看到,在user 模式下,ARM CPU 有16个数据寄存器,被命名为r0~r15(这个要比x86的多一些)。r13~r15有特殊用途,其中:
%A ◆ r13 - 指向当前栈顶,相当于x86的esp,这个东西在汇编指令中要用sp 表示
%A ◆ r14 - 称作链接寄存器,指向函数的返回地址。用lr表示,这和x86将返回地址保存在栈中是不同的
%A ◆ r15 - 类似于x86的eip,其值等于当前正在执行的指令的地址+8(因为在取址和执行之间多了一个译码的阶段),这个用pc表示
%A 另外,ARM处理器还有一个名为cspr的寄存器,用来监视和控制内部操作,这点和x86 的状态寄存器是类似的。具体的内容就用到再说了。
%A
%A ARM 指令集
%A ARM处理器可以支持3种指令集――ARM,Thumb和Jazelle。
%A 采用那种指令集,由cspr中的标志位来决定。大体说来:
%A ◆ ARM――这是ARM自身的32 位指令集
%A ◆ Thumb ――这是一个全16 位的指令集,在16 位外部数据总线宽度下,这个指令集的效率要比32 位的ARM指令高一些。
%A ◆ Jazelle ――这是一个8位指令集,用来加速Java字节码的执行
%A 整个ARM指令集由数据处理指令、分支指令、Load-Store指令、程序中断指令和一些系统控制指令构成,除了Load-Store指令外,其他部分和x86指令集是比较类似的。但和x86相比,ARM指令最显著的特点它们都是32-bit 定长的。另外,由于arm是基于RISC指令集的,所以CPU只处理在寄存器中的数据并通过独立的load-store指令在内存和寄存器之间进行数据的传递。
%A 在使用方面,ARM指令的格式也要比Intel的复杂些。一般说来,一条ARM指令有如下的形式:
%A <Instruction> {S} [Rd], [Rn], [Rm]
%A 其中:
%A * {S} ―― 加上这个后缀的指令会更新cpsr 寄存器
%A * [Rd] ―― 目的寄存器
%A * [Rn]/[Rm] ―― 源寄存器
%A 一般来说,arm 指令有3个操作数,其中Rm寄存器在执行指令前可以进入桶形移位器进行移位操作,而Rn则会直接进入ALU 单元。如果一条arm 指令只有2 个操作数,那么源寄存器按照Rm 来处理。例如,一条加法指令:
%A add r0, r1, #1
%A 就会把r1+1的结果存放到r0中。
%A 在熟悉了基本的汇编格式后,读者就可以自行去查询基本的ARM汇编指令了,下面,我们找出ARM中比较有特色部分――Load-Store指令结构,它是CPU 和内存进行通信的一个重要媒介。
%A
%A Load-Store 指令体系
%A 由于ARM CPU并不直接处理内存中的数据,这个指令体系就担起了在寄存器和内存之间交换数据的重要媒介。它要比x86 的内存访问机制复杂一些。该指令体系分成3 类:
%A ◆ 单寄存器传输(这是与x86 最为相像的)
%A ◆ 多寄存器传输
%A ◆ 交换指令
%A
%A 单寄存器传输
%A 先看第一个,很简单:把单一的数据传入(LDR) 或传出(STR)寄存器,对内存的访问可以是DWORD(32-bit), WORD(16-bit)和BYTE(8-bit)。指令的格式如下:
%A DWORD:
%A <LDR | STR> Rd, addressing1
%A WORD:
%A <LDR | STR> H Rd, addressing2 无符号版
%A <LDR> SH Rd, addressing2 有符号版
%A BYTE:
%A <LDR | STR> B Rd, addressing1 无符号版
%A <LDR> SB Rd, addressing2 有符号版
%A addressing1 和addressing2 的分类下面再说,现在理解成某种寻址方式就可以了。
%A 在单寄存器传输方面,还有以下三种变址模式,他们是:
%A ◆ preindex
%A 这种变址方式和x86的寻址机制是很类似的,先对寄存器进行运算,然后寻址,但是在寻之后,基址寄存器的内容并不发生改变,例如:
%A ldr r0, [r1, #4]
%A 的含义就是把r1+4 这个地址处的DOWRD 加载到r0,而寻址后,r1 的内容并不改变。
%A ◆ preindex with writeback
%A 这种变址方式有点类似于++i的含义,寻址前先对基地址寄存器进行运算,然后寻址. 其基本的语法是在寻址符[]后面加上一个"!" 来表示.例如:
%A ldr r0, [r1, #4]!
%A 就可以分解成:
%A add r1, r1, #4
%A ldr r0, [r1, #0]
%A ◆ postindex
%A 自然这种变址方式和i++的方式就很类似了,先利用基址寄存器进行寻址,然后对基址寄存器进行运算,其基本语法是把offset 部分放到[]外面,例如:
%A ldr r0, [r1], #4
%A 就可以分解成:
%A ldr r0, [r1, #0]
%A add r1, r1, #4
%A 如果你还记得x86 的SIB 操作的话,那么你一定想ARM是否也有,答案是有也没有。在ss上面提到的addressing1 和addressing2的区别就是比例寄存器的使用,addressing1可以使用[base, scale, 桶形移位器]来实现SB 的效果,或者通过[base,offset](这里的offset 可以是立即数或者寄存器)来实现SI 的效果,而addressing2则只能用后者了。于是每一种变址方式最多可以有3 种寻址方式,这样一来,最多可以有9种用来寻址的指令形式。例如:
%A ldr r0, [r1, r2, LSR #0x04]!
%A ldr r0, [r1, -#0x04]
%A ldr r0, [r1], LSR #0x04
%A 每样找了一种,大概就是这个意思。到此,单寄存器传输就结束了,掌握这些足够应付差事了。下面来看看多寄存器传输吧。
%A
%A 多寄存器传输
%A 说得很明白,意思就是通过一条指令同时把多个寄存器的内容写到内存或者从内存把数据写到寄存器中,效率高的代价是会增加系统的延迟,所以armcc 提供了一个编译器选项来控制寄存器的个数。指令的格式有些复杂:
%A <LDM | STM> <寻址模式> Rn{!}, <Registers>{r^}
%A 我们先来搞明白寻址模式,多寄存器传输模式有4 种:
%A 也就是说以A开头的都是在Rn的原地开始操作,而B开头的都是以Rn的下一个位置开始操作。如果你仍然感到困惑,我们不妨看个例子。
%A 所有的示例指令执行前:
%A mem32[0x1000C] = 0x04
%A mem32[0x10008] = 0x03
%A mem32[0x10004] = 0x02
%A mem32[0x10000] = 0x01
%A r0 = 0x00010010
%A r1 = 0x00000000
%A r3 = 0x00000000
%A r4 = 0x00000000
%A 1) ldmia r0!, {r1-r3} 2) ldmib r0!, {r1-r3}
%A 执行后: 执行后:
%A r0 = 0x0010001C r0 = 0x0010001C
%A r1 = 0x01 r1 = 0x02
%A r2 = 0x02 r2 = 0x03
%A r3 = 0x03 r3 = 0x04
%A 至于DA 和DB 的模式,和IA / IB 是类似的,不多说了。
%A 最后要说的是,使用ldm 和stm指令对进行寄存器组的保护是很常见和有效的功能。配对方案:
%A stmia / ldmdb
%A stmib / ldmda
%A stmda / ldmib
%A stmdb / ldmia
%A 继续来看两个例子:
%A 执行前:
%A r0 = 0x00001000
%A r1 = 0x00000003
%A r2 = 0x00000002
%A r3 = 0x00000001
%A 执行的指令:
%A stmib r0!, {r1-r3}
%A mov r1, #1 ; These regs have been modified
%A mov r2, #2
%A mov r3, #3
%A 当前寄存器状态:
%A r0 = 0x0000100C
%A r1 = 0x00000001
%A r2 = 0x00000002
%A r3 = 0x00000003
%A ldmia r0!, {r1-r3}
%A 最后的结果:
%A r0 = 0x00001000
%A r1 = 0x00000003
%A r2 = 0x00000002
%A r3 = 0x00000001
%A 另外,我们还可以利用这个指令对完成内存块的高效copy:
%A loop
%A ldmia r9!, {r0-r7}
%A stmia r10!, {r0-r7}
%A cmp r9, r11
%A bne loop
%A 说到这里,读者应该对RISC的Load-Store体系结构有一个大概的了解了,能够正确配对使用指令,是很重要的。
%A
%A%A
%A
*博客内容为网友个人发布,仅代表博主个人观点,如有侵权请联系工作人员删除。