ARM汇编编程基础(五) -- 其它常见寻址模式与常见指令

来源:互联网 发布:京东刷单软件 编辑:程序博客网 时间:2024/05/02 02:03

本系列文章节选自本人所著《深入浅出嵌入式底层软件开发》



现在我们已经掌握了所有知识,可以编写简单的ARM汇编程序,但如果要编写较为复杂的ARM程序,就必须掌握更多的寻址模式和指令,这就是本节的重点所在。

我们在“基本寻址模式与基本指令”中学习了最常用的3种寻址方式。下面介绍其它寻址方式。

1.6.1 其它常见寻址模式

1. 基址寻址

基址寻址就是将基址寄存器的内容与指令中给出的偏移量相加,形成操作数的有效地址。基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。基址寻址指令举例如下:

LDR R1,[R2,#0x0C]

R2的值+0x0C形成内存地址,读取内存中该地址上的内容,放入R1

其它额外需要了解的内容:

l        零偏移。 如:LDR R0,[R1]

l        前索引偏移。 如:LDR R0,[R1,#0x04]!,表示将R1的值加上4后作为内存地址,将该内存处存放的数读出送给R0;并且指令执行结束时,R1本身的值也要加4。这里!表示要回写R1

l        程序相对偏移。

如:LDR R0,labe1,表示将标号label所代表的内存地址处存放的内容放入R0,相当于
LDR R0, [PC, #某个常数]

       ldr r0, label

       ......

label   DCD 0x12345678

l        后索引偏移。 如:LDR R0,[R1],#0x04,表示将R1的值作为内存地址,将该内存处存放的数读出送给R0;并且指令执行结束时,R1本身的值要加4

2. 多寄存器寻址

多寄存器寻址一次可传送几个寄存器值,允许一条指令传送16个寄存器的任何子集或所有寄存器。多寄存器寻址指令举例如下:

LDMIA R1!,{R2-R4,R6} ,它是ldr的多寄存器版本,将内存中连续存放的4个字加载到寄存器R2,R3,R4,R6中。R1中存放的是内存地址。

                       


                                    图1 - 64  LDMIA指令执行前                                                                                                   图1 - 65  LDMIA指令执行后

两点说明:

(1)R1!中的!号表示在指令执行完成后,要改变(回写)基址寄存器(R1)的值

(2)寄存器列表{R2-R4, R6}中的顺序并不要紧。最终寄存器与内存地址的对应关系是:编号小的寄存器与内存的低地址相对应

两点问题:

(1)为什么内存起地址是0x40000000,而不是0x40000004

(2)为什么内存地址是从0x40000000 ---- 0x4000000C,而不是从0x3FFFFFF4 ----  0x40000000

要解释上面2个问题,其实也很简单。其实多寄存加载指令ldm总共有4个:ldmia、ldmib、 ldmda、ldmdb。ia的意思是increaseafter,ib的意思是increasebefore,da的意思是decreaseafter,db的意思是decreasebefore。以LDMIA R1!, {R2-R4, R6}为例子,这里的ia是指办事(将内存中的数加载到寄存器)之后增加基址寄存器(R1)的值。这条指令的执行过程从逻辑(实际上一条指令肯定是原子操作,所以从物理上看,下面8个步骤其实是同时完成的)上看,如下:

(1)先办事:将R1的值(0x40000000)作为内存地址,到该地址处取得数(0x01),加载到寄存器R2中

(2)后增加:将R1的值从0x40000000增加为0x40000004

再重复上面的操作3次,分别将内存中的数0x02、0x03、0x04放到寄存器中R3、R4、R6中,最后R1的值变为0x40000010。

这个例子中,如果将ldmia改为ldmib,则R2、R3、R4、R6中存放的是0x02、0x03、0x04、内存0x40000010处的内容,最后R1的值为0x40000010。

除了4条多寄存器加载指令外,还有4条类似的多寄存器存储指令,分别是stmia、 stmib、 stmda、 stmdb

3. 堆栈寻址

由于ARM指令集没有专门的出栈和入栈指令,所以ARM汇编程序是采用SP作为栈指针,以stm指令完成入栈操作,以ldm指令完成出栈操作。

以入栈后SP的值是增加还是减少为依据,可将堆栈类型划分为递增堆栈(向上生长)和递减堆栈(向下生长);


图1 - 66 递增堆栈与递减堆栈

以SP所指向的内存是栈顶元素所在位置,还是下一次要入栈的元素的位置,可将堆栈类型划分为满堆栈和空堆栈


图1 - 67 满堆栈与空堆栈

那么当堆栈类型为空递减堆栈时候,入栈操作应该使用什么指令?出栈操作应该使用什么指令?进一步,如果堆栈类型为空递增、满递增、满递减堆栈,又将如何呢? 如果你不看下面的答案,我相信你一定会让这几个问题折磨得做很多的脑力体操,然后感叹ARM指令集的设计者太不为你这样的程序员考虑了,给了你本不应该由你承担的负荷。但事实上正相反,ARM指令集的设计者充分理解了你作为程序员的苦恼,请看下面的答案。

 

表1 - 8 堆栈类型与堆栈操作

 

 

数据块传送(存储)

堆栈操作(入栈)

说明

STMDA

STMED

空递减

STMIA

STMEA

空递增

STMDB

STMFD

满递减

STMIB

STMFA

满递增

数据块传送(加载)

堆栈操作(出栈)

说明

LDMDA

LDMFA

满递增

LDMIA

LDMFD

满递减

LDMDB

LDMEA

空递增

LDMIB

LDMED

空递减

 

这张表的第一、三列回答了前面你绞尽脑汁回答的问题。而第二列则体现了ARM指令集的设计者对作为程序员的你的充分体贴。第二列中的ED、EA、FD、 FA分别表示empty descend(空递减)、 empty ascend(空递增)、 full descend(满递减)、 full ascend(满递增),其含义是说,如果你采用的是空递减(空递增、满递减、满递增)堆栈的话,入栈操作则使用指令STMED(STMEA、 STMFD、STMFA),出栈操作则使用指令LDMED(LDMEA、LDMFD、LDMFA)。从此你再也不会为你应该使用ia、ib、da还是db 来实现出、入栈操作而苦恼了。

STMED、STMEA、STMFD、STMFA和LDMED、LDMEA、LDMFD、LDMFA就是所谓的堆栈寻址指令。由此可见:为了对程序员体贴入微,ARM指令集的设计者设计了堆栈寻址指令,其实质就是多寄存寻址指令的快捷方式。

4. 寄存器移位寻址

寄存器移位寻址是ARM指令集特有的寻址方式。当第2个操作数是寄存器移位方式时,第2个寄存器操作数在与第1个操作数结合之前,选择进行移位操作。例如:

MOV R0,R2,LSL #3 表示将R2的值逻辑左移3位,结果放入R0,即是R0=R2×8。

移位的方式有以下几种:


图1 - 68 移位操作类型

LSL(logic shift left):逻辑左移

LSR(logic shift right):逻辑右移

ASR(arithmetic shift right):算术右移

ROR(rotate shift right):循环右移

RRX(rotate shift right with extend):带扩展的循环右移。其中的C指的是CPSR的C位

5. 相对寻址

相对寻址是基址寻址的一种变通。由程序计数器PC提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到的地址即为操作数的有效地址。例如:

   B    LOOP
    ...
LOOP    MOV    R6,#1

该条B指令的意思是要跳转到标号LOOP所代表的指令处,其含义相当明显,但你要明白CPU根本不明白标号是个什么东西(事实上在指令的机器码中根本就没有标号这种东西),那么b loop这条指令的机器码会是什么呢?答案是:高8bit是操作码相关内容,低24bit是一个常数,表示从b指令到mov指令之间的内存地址的差值(如果不考虑流水线的影响的话)。由此可见,b loop这条指令相当于add pc, pc, #偏移量常数,典型的相对于PC(当前指令地址)的相对寻址。由于是相对于当前指令地址进行相对寻址,所以无论程序最终运行在内存的何处(即使运行的地址不是它预期的位置),这条B指令都能正确运行。关于相对寻址、程序期望的运行地址等等,将在“ARM汇编伪指令”中详细描述。

随便说一下,前面学到b指令的跳转范围是当前指令的前后32M,为什么是这个范围呢?因为24bit常数用1个比特区别正负,还剩23bit,同时由于ARM指令在内存中的地址的最低2bit一定是0(为什么?请自行思考一下),因此23bit中可以不必表示这2个0,所以23bit可以表示的范围是0 ---- 2^25,即:0 ---- 32M。 

关于指令的机器码编码格式,请参阅:光盘中提供的技术文档“ARM Architecture Reference Manual.pdf”(位于\docs目录)



1.6.2 其它常见指令(访存指令、数据处理指令、乘法指令)

我们在“基本寻址模式与基本指令”中学习了最常用的指令。下面介绍其它较为常用的指令。

1. 访存指令

LDRH(半字加载);

LDRSH (有符号半字加载);

STRH(半字存储);

交换指令

表1 - 9 2个交换指令

 

助记符

说明

操作

SWP Rd,Rm,[Rn]

寄存器和存储器字进行数据交换

同时完成Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm)

SWPB Rd,Rm,[Rn]

寄存器和存储器字节进行数据交换

同时完成Rd←[Rn],[Rn]←Rm (Rn≠Rd或Rm)

 

2. 数据处理指令

表1 - 10 数据传送指令

 

助记符

说明

操作

MVN Rd,operand2

数据非传送

Rd←(~operand2)

 

 

 表1 - 11  算术运算指令

 

助记符

说明

操作

RSB Rd, Rn, operand2

逆向减法指令

Rd←operand2-Rn

ADC Rd, Rn, operand2

带进位加法

Rd←Rn+operand2+Carry

SBC Rd, Rn, operand2

带进位减法指令

Rd←Rn-operand2-(NOT)Carry

RSC Rd, Rn, operand2

带进位逆向减法指令

Rd←operand2-Rn-(NOT)Carry

 

这里要特别提到,ADC指令结合CPSR,可以实现64位整数加法。

 

表1 - 12 逻辑运算指令

 

助记符

说明

操作

BIC Rd, Rn, operand2

按位清除指令

Rd←Rn & (~operand2)

 

其实现功能是:将Rn中对应于operand2中为1的bit位全部清0,其它bit位保持不变,然后将结果保存到Rd中

表1 - 13 比较指令

 

助记符

说明

操作

CMN Rn, operand2

负数比较指令

标志N、Z、C、V←Rn+operand2

TST Rn, operand2

位测试指令

标志N、Z、C←Rn & operand2

TEQ Rn, operand2

相等测试指令

标志N、Z、C←Rn ^ operand2

 

TST指令测试的是:Rn中所有指定bit位是否全为0(指定的bit位是operand2中为1的所有位);

TEQ指令测试的是:Rn和operand2是否相等。这点上与CMP指令一样,区别在于CMP指令除了可以比较2个数是否相等外,也可以比较2个数谁大谁小,但TEQ不行。

 

3. 乘法指令

表1 - 14 乘法指令

 

助记符

说明

操作

MUL Rd,Rm,Rs

32位乘法指令

Rd←Rm*Rs (Rd≠Rm)

MLA Rd,Rm,Rs,Rn

32位乘加指令

Rd←Rm*Rs+Rn (Rd≠Rm)

UMULL RdLo,RdHi,Rm,Rs

64位无符号乘法指令

(RdLo,RdHi) ←Rm*Rs

UMLAL RdLo,RdHi,Rm,Rs

64位无符号乘加指令

(RdLo,RdHi) ←Rm*Rs+(RdLo,RdHi)

SMULL RdLo,RdHi,Rm,Rs

64位有符号乘法指令

(RdLo,RdHi) ←Rm*Rs

SMLAL RdLo,RdHi,Rm,Rs

64位有符号乘加指令

(RdLo,RdHi) ←Rm*Rs+(RdLo,RdHi)

 

 

4. 协处理器指令

参见“MMU与内存保护的实现”

5. 杂项指令

SWI:软中断指令,参见“swi与systemcall的实现”

MRS、MSR:程序状态寄存器操作指令,参见“ARM异常处理”

原创粉丝点击