第3章 ARM处理器的指令系统
3.1 ARM微处理器的寻址方式
寻址方式是在指令中设置的用于处理器定位操作数位置的操作数表达方式。掌握寻址方式是编写和阅读汇编语言程序必须掌握的知识。
通常在高级语言的编程中是不需要关心程序语句所涉及数据的具体存放地点的,编程员只需要针对具体的变量或数据进行编程。而汇编语言与高级语言在编程方式上的一个重要不同点在于:程序语句所操作的数据许多情况下并不是数据本身,而是存放该数据的载体,如CPU内部的寄存器或外部的存储单元,所以汇编语言编程员必须清楚程序语句中操作数的存放地点,并且能够利用编译系统提供的多种操作数定位方式(寻址方式)灵活地寻址位于不同数据存放点的数据。
汇编指令可寻址的数据存放点有4个,如下所述。
1)包含于指令中
这种方式的数据也称为立即数,是直接书写于指令中的数据。
2)包含于处理器内部的寄存器中
这种方式的数据称为寄存器数,指令中对数据的操作不是针对数据,而是针对含有数据的寄存器。
3)包含于处理器外部的存储器中
这种方式的数据称为存储器数据。尽管最终的操作数是存于存储器中的数据,但指令中书写的却是代表数据的存储单元地址。位于存储单元中的操作数将由编译程序按照汇编指令中给出的存储器地址提取出来。
4)包含于外设端口中
在计算机系统的体系结构中,处理器与各种外设的的联系都是通过外设及其接口电路中的各种信息存储单元实现的,包括数据寄存器、控制寄存器、状态寄存器等类型。尽管它们散落于不同的外设及其接口电路中,但处理器都必须为它们分配不同的地址加以区分,这些地址的总和称为外设地址空间或I/O空间。在ARM处理器中,对外设端口数据的访问没有像X86系列处理器那样用专门的IN/OUT指令,而是采用与访问存储器数据相同的指令格式,但严格地将I/O空间与存储空间划分在统一地址空间的不同区域内。
对于不同的处理器,其指令系统所能够支持的寻址方式是不完全相同的。因为不同的处理器从编程员的角度来看,它们的主要区别在于不同CPU内部具有的寄存器类型和数量不同,另外访问存储器和外设端口的方式也不同,所以掌握一种处理器的汇编语言编程的首要问题是搞清楚该处理器所支持的寻址方式,以便在书写程序指令时能够正确地理解和运用操作数。下面将讲述ARM指令系统支持的寻址方式。
3.1.1 立即寻址
立即寻址是将操作数直接书写在汇编指令中的操作数指定方式,汇编指令被编译为机器码后,操作数将成为整个机器码的组成部分,CPU在执行该指令时就可立即获得所需的操作数,故包含的操作数也称为立即数。
在ARM指令集中,有许多指令可以采用立即数(立即寻址),但由于RISC结构指令长度的限制,立即数在机器码中只能占有少量的位空间,不是任意的数值都可以作为立即数,立即数必须符合一定的生成规则才会有效,这将在后续内容介绍。
采用立即数的简单指令示例如下:
MOV R0,#0x55 ;#0x55为十六进制立即数,指令功能为:R0=0x55 ADD R0,R1,#5 ;#5为十进制立即数,指令功能为:R0=R1+5 ADD R0,R0,#0x5d ;#0x5d为十六进制立即数,指令功能为:R0←R0+0x5d
说明:汇编指令中用“#”号标示一个立即数,其后用“0x”或“&”标示立即数为十六进制,如果默认则标示一个十进制立即数。
3.1.2 寄存器寻址
寄存器寻址就是事先将需要操作的数据放置在某个寄存器中,然后在指令中指定该寄存器(携带操作数)参与运算或操作。由于寄存器是距离处理器核心最近的存储单元,具有很快的读写操作速度,所以是汇编指令中频繁使用的一类寻址方式。指令示例如下。
例1:ADD R0,R1,R2 ;寄存器R1和R2内容相加,结果送R0 例2:ADD R0,R1,R2,ROR #5 ;R0=R1+(R2循环右移5位) 例3:MOV R0,R1,LSL R3 ;R0=R1逻辑左移R3内容指定的位数
例1是一条加法运算指令,加数和被加数都预先存于R1和R2中。指令实现将R1和R2的内容相加,结果存于另外的寄存器R0中。其中R1和R2也称为源操作数寄存器,R0称为目的操作数寄存器。指令中源和目的操作数都采用了寄存器寻址方式。
例2和例3中源操作数和目的操作数同样采用了寄存器寻址方式。与例1不同的是,源操作数的一个寄存器内容在运算或操作之前进行了移位操作。由于ARM处理器内置了桶形移位寄存器,可以实现在一条指令中进行数据移位和运算的双重操作,所以各种采用寄存器寻址的运算或操作指令可以在一条指令内先对寄存器内容进行移位后再进行运算或操作。在ARM指令中没有专门的移位指令,移位操作都是融合在各种采用寄存器寻址的运算或操作指令内一并完成的。有效的移位方式有逻辑左移(LSL)、逻辑右移(LSR)、算术右移(ASR)、循环右移(ROR)、带扩展的循环右移(RRX)共5种,详细内容请参见3.3.5节的内容。
3.1.3 单存储器数据寻址(位于存储器中的单字节、单字、半字等单个数据的寻址)
寄存器尽管具有高的读写速度,但数量有限,大量的数据只能存放在CPU外部的存储器中,所以指令必须提供能够对外部存储器数据进行访问的寻址方式。在ARM的指令系统内,对存储器单元的寻址必须通过一个通用寄存器进行。有以下几种可选的方式。
- 寄存器间接寻址:以一个通用寄存器的内容作为存储器单元的地址。
- 基址变址寻址:以一个通用寄存器的内容作为基地址,再以一个合法的立即数或者另外一个寄存器(甚至一个移位后的寄存器)的内容为偏移地址相加/减的结果作为存储器单元的地址。
1.寄存器间接寻址
寄存器间接寻址以一个通用寄存器的内容作为存储器地址,并从所寻址的存储单元(字节、字、半字等)读取一个数据或者向内写入一个数据。例如以下指令:
ADD R0,R1,[R2] ;R0←R1+[R2] LDR R0,[R1] ;R0←[R1] STR R0,[R1] ;[R1]←R0
第一条指令中寄存器R2的内容将作为存储器的地址并从其指向的存储单元读取一个字数据,然后与R1寄存器内的数据相加,结果送入寄存器R0中。
第二条指令直接以R1的内容为地址寻址存储器,并将读出的字数据送入R0内。
第三条指令是将R0的内容传送到以R1的值为地址的存储器字单元内。
2.基址变址寻址
基址变址寻址是另外一种对存储器数据的寻址方式,其存储器地址是由两部分内容合成的。一个部分为某个通用寄存器,其内容称为基地址。另一个部分为一个立即数或一个寄存器(或者一个带移位操作的寄存器),称为变址(偏移地址)。最终的存储器地址是由包含基地址的寄存器(称为基址寄存器)内容与另外的变址值(立即数或另一个寄存器内容)相加/减而得到的。变址寻址方式常用于访问以某个固定地址(基地址)为中心的前后地址单元。采用变址寻址方式的指令常见有以下几种形式:
LDR R0,[R1,#4] ;R0←[R1+4] LDR R0,[R1,#4]! ;R0←[R1+4],R1←R1+4 LDR R0,[R1],#4 ;R0←[R1],R1←R1+4 LDR R0,[R1,-R2] ;R0←[R1-R2], LDR R0,[R1,R2,LSL #4] ;R0←[R1+R2内容左移4位]
第一条指令以寄存器R1内容为基地址再加上立即数4形成最终的存储器地址,然后从该存储单元读出一个字数据送到寄存器R0内,操作完成后R1的值不变。
第二条指令同样以寄存器R1内容为基地址再加上立即数4形成最终的存储器地址,然后从该存储单元读出一个字数据送到寄存器R0内。但不同的是操作完成后基址寄存器R1的值将变为:R1=R1+4。这种在操作进行前改变基地址值的方式称为立即数前变址寻址(Immediated pre-indexed)方式。
第三条指令先以R1的内容为存储器地址寻址存储单元,并在将读到的数据传送给寄存器R0后增值,R1的内容为:R1=R1+4。这种在操作完成后改变基地址值的方式称为立即数后变址寻址(Immediated post-indexed)方式。
第四条指令以寄存器R1的内容为基地址,以R2的内容为变址,并以R1-R2为存储器地址寻址存储单元,最后将读出的数据传送给寄存器R0,操作完成后R1的值不变。
第五条指令以寄存器R1的内容为基地址,以寄存器R2的内容左移4位后为变址,以R1+(R2左移4位)为存储器地址寻址存储单元,最后将读出的数据传送给寄存器R0,操作完成后R1的值不变。
3.1.4 多寄存器寻址
多寄存器寻址是RISC处理器中实现寄存器与存储器之间进行批量数据传输的一种特殊方式。由于RISC处理器内部设置有较多的寄存器,这些寄存器内容在进行程序转移或异常/中断操作时需要批量地保存起来。而当程序返回时又需要批量地将它们恢复到原来的寄存器内。为了快捷方便地实现这类操作,ARM处理器设立了一种仅需一条指令就可以实现多个寄存器与一个连续的存储器区之间数据传送的方式,最多可以针对16个通用寄存器进行操作。这类指令的基本格式为:
LDMXX/STMXX Rn! ,(寄存器列表)
其中的Rn是作为存储区地址指针的寄存器,称为基地址寄存器。寄存器列表是参与数据传输的寄存器组合。LDM和STM指令后面的XX代表了IA、IB、DA、DB等4个后缀选项,分别对应在多个寄存器与存储器之间进行数据读写操作时,存储器的地址指针是按递增还是递减的方向变化,以及是先读写数据后变化地址指针还是先变化地址指针后读写数据。这4个后缀选项的具体含义如下所述。
1)IA(Increment After):先读写数据后递增存储器地址值
各寄存器与存储单元的对应情况及储器地址值在数据传输过程中的变化情况为:
- 序号最小的寄存器在存储器的对应地址=基址寄存器Rn的值;
- 序号最大的寄存器在存储器的对应地址=Rn的值+参与传输的寄存器数量×4;
- 操作结束后的Rn值(若允许Rn变化)=Rn起始值+参与传输的寄存器数×4。
2)IB(Increment Before):先递增存储器地址值后读写数据
各寄存器与存储单元的对应情况及储器地址值在数据传输过程中的变化情况为:
- 序号最小的寄存器在存储器的对应地址=基址寄存器Rn的值+4;
- 序号最大的寄存器在存储器的对应地址=Rn的值+参与传输的寄存器数量×4;
- 操作结束后的Rn值(若允许Rn变化)=Rn起始值+参与传输的寄存器数×4。
3)DA(Decrement After):先读写数据后递减存储器地址值
各寄存器与存储单元的对应情况及储器地址值在数据传输过程中的变化情况为:
- 序号最大的寄存器在存储器的对应地址=基址寄存器Rn值;
- 序号最小的寄存器在存储器的对应地址=Rn值–(参与传输的寄存器数量×4)+4;
- 操作结束后的Rn值(若允许Rn变化)=Rn起始值+参与传输的寄存器数×4。
注意:本方式可以理解为存储器地址递减变化过程中是先读写序号大的寄存器。
4)DB(Decrement Before):先递减存储器地址值,后读写数据
各寄存器与存储单元的对应情况及储器地址值在数据传输过程中的变化情况为:
- 序号最大的寄存器在存储器的对应地址=基址寄存器Rn值-4;
- 序号最小的寄存器在存储器的对应地址=Rn值-参与传输的寄存器数量×4;
- 操作结束后的Rn值(若允许Rn变化)=Rn值-参与传输的寄存器数×4。
注意:本方式可以理解为存储器地址递减变化过程中是先读写序号大的寄存器。
地址递增方式示例如图3-1所示,假设操作前R0的初值为0x0010。
图3-1 两种地址递增方式下的多寄存器寻址指令执行示例图
地址递减方式示例如图3-2所示,假设操作前R0的初值为0x0020。
图3-2 两种地址递减方式下的多寄存器寻址指令执行示例图
注意:LDM/STM指令与LD/STR指令格式的以下不同点:
①LDM/STM指令中提供存储器地址的寄存器没有方括号[]。
②LDM/STM指令中前面操作数都是提供存储器地址的寄存器,后面是寄存器列表。
LDM/STM指令的机器码格式如图3-3所示。
图3-3 LDM/STM指令的机器码格式
说明:P=0对应先变化Rn的值再读写数据方式,且与U值相关,U=0时Rn的值指向本次操作数据最高地址加4的地址;U=1时Rn的值指向本次操作数据最低地址减4的地址。
P=1对应先读写数据再变化Rn值的方式,且与U值相关,U=0时Rn的值指向本次操作数据最高地址;U=1时Rn的值指向本次操作数据最低地址。
3.1.5 堆栈寻址及其若干模式
堆栈是定义于存储区内的一块特殊区域,专用于保存当程序发生转移运行(如异常、中断、调用子程序、任务切换等)时的原场景信息,如处理器内部的各类寄存器值。这些保存在堆栈中的寄存器值在程序返回时必须准确地恢复到它们原来的寄存器内。为了准确地对应寄存器与它们内容所存入的堆栈单元,规定了一种特殊的堆栈操作方式:向堆栈存数(入栈)和从堆栈取数(出栈)严格遵循先进后出(First In Last Out,FILO)的顺序操作原则(堆栈区数据的操作就类同冲锋枪弹夹中的子弹装填)。入栈操作结束时堆栈指针指向的单元是最后一个寄存器内容存入的地址,这样就能够在出栈操作时以第一个出栈内容恢复到原来的寄存器内。但是必须保证入栈操作完成后到出栈操作前的时间段内保持堆栈指针值不变。
堆栈寻址是一组寄存器与若干连续存储单元间的操作,也可以看成多寄存器寻址的一种特殊运用方式。由于被标识为堆栈区的存储单元处理器必须按照FILO的方式进行存取操作,而且要求在完成入栈操作后保持堆栈指针不变,始终指向栈顶的位置,这样才能保证在出栈操作时正确地将堆栈内容恢复到原有寄存器内。但是与入栈操作STM指令配合的出栈指令LDM必须采用与入栈相反的顺序从堆栈取数,这就要求从堆栈区读取数据的LDM操作要与向堆栈区写入数据的STM操作配合成对运用。
在ARM指令模式下每执行一次堆栈操作堆栈指针可自动递增或递减4,以指向栈顶位置。在遵循这样的基本操作原则的前提下,在不同的计算机系统中,有的采用堆栈数据按照增地址方式增长(存入数据后堆栈指针递增),而有的则采用堆栈数据按照减地址方式增长(存入数据后堆栈指针递减);另外,在堆栈指针的变化上,有些采用先存入数据再变化堆栈指针,有些则采用先变化堆栈指针再存入数据的方式,这就造成了堆栈操作的多样性。ARM处理器为了适应各种软件环境的需要,设置了多种堆栈操作模式供程序选择。按照堆栈的生长方向和堆栈指针的变化方式设置了四种堆栈方式并以指令后缀形式供用户选择。它们分别为:
- FD(Full Decending Stack),满递减堆栈操作(STMFD=压栈,LDMFD=出栈);
- FA(Full Ascending Stack),满递增堆栈操作(STMFA=压栈,LDMFA=出栈);
- ED(Empty Decending Stack),空递减堆栈操作(STMED=压栈,LDMED=出栈);
- EA(Empty Ascending Stack),空递减堆栈操作(STMEA=压栈,LDMEA=出栈)。
满堆栈的操作特点是:先移动4字节的偏移地址(减4或加4,即先变址)再写入4字节的字数据。由于堆栈操作结束后的当前堆栈指针所指向的存储单元存有最后一个压入堆栈的数据,故称为满堆栈(Full Stack)。
空堆栈的操作特点是:先写入字数据,然后移动4字节的偏移地址(减4或加4,即后变址)。由于堆栈操作结束后的当前堆栈指针所指向的存储单元没有本次存入的数据(距最后一个压入堆栈的数据相差4字节),故称为空堆栈(Empty Stack)。
同时,根据堆栈的生长方式又可以分为递增堆栈(Ascending Stack)和递减堆栈(Decending Stack),若堆栈指针随着数据的存入不断由低地址向高地址增长,称为递增堆栈;反之则称为递减堆栈。这样就有四种类型的堆栈工作方式,ARM微处理器支持这四种类型的堆栈工作方式。
- 满递增堆栈:堆栈指针指向最后压入的数据,堆栈由低地址向高地址增长;
- 满递减堆栈:堆栈指针指向最后压入的数据,堆栈由高地址向低地址增长;
- 空递增堆栈:堆栈指针指向下一个将放入数据的空位置,且由低地址向高地址增长;
- 空递减堆栈:堆栈指针指向下一个将放入数据的空位置,且由高地址向低地址增长;
将字数据0x12345678压入堆栈后的满堆栈和空堆栈(小端格式)分别如图3-4和图3-5所示。
由于堆栈操作必须按照先进后出,以及压栈时的源寄存器必须是出栈时的目的寄存器的原则,所以递减堆栈在进行压栈操作时堆栈指针按递减进行,而对应的出栈操作堆栈指针则必须按递增进行。例如,满递减堆栈的出栈操作LDMFD堆栈指针实际上是递增的,而不是递减,FD只表明它所配合的压栈操作是FD。LDMFD等效于LDMIA。
图3-4 满堆栈示意图
图3-5 空堆栈示意图
多寄存器寻址与堆栈寻址的对应关系如表3-1所示。
表3-1 多寄存器寻址与堆栈寻址的对应关系
关于堆栈寻址的应用例子请参见3.4.2节。