(一) -- ARM CPU寄存器
本系列文章,所需代码请从以下地址下载:
ARM的汇编编程,本质上就是针对CPU寄存器的编程,所以我们首先要弄清楚ARM有哪些寄存器?这些寄存器都是如何使用的?
ARM寄存器分为2类,普通寄存器和状态寄存器,如表1-1所列。
表1 - 1 ARM寄存器
寄存器类别
寄存器在汇编中的名称
各模式下实际访问的寄存器
用户
系统
管理
中止
未定义
中断
快中断
通用寄存器和程序计数器
R0(a1)
R0
R1(a2)
R1
R2(a3)
R2
R3(a4)
R3
R4(v1)
R4
R5(v2)
R5
R6(v3)
R6
R7(v4)
R7
R8(v5)
R8
R8_fiq
R9(SB,v6)
R9
R9_fiq
R10(SL,v7)
R10
R10_fiq
R11(FP,v8)
R11
R11_fiq
R12(IP)
R12
R12_fiq
R13(SP)
R13
R13_svc
R13_abt
R13_und
R13_irq
R13_fiq
R14(LR)
R14
R14_svc
R14_abt
R14_und
R14_irq
R14_fiq
R15(PC)
R15
状态寄存器
CPSR
CPSR
SPSR
无
SPSR_abt
SPSR_abt
SPSR_und
SPSR_irq
SPSR_fiq
请看表1-1的第2列,普通寄存器总共16个,分别为R0-R15;状态寄存器共2个,分别为CPSR和SPSR。
1.1.1
普通寄存器R0 – R15
普通寄存器中特别要提出来的是R13、R14、R15。
R15 别名PC(program counter),中文称为程序计数器,它的值是当前正在执行的指令在内存中的位置(不考虑流水线的影响,参见“流水线对PC值的影响”),而当指令执行结束后,CPU硬件会自动将PC的值加上一个单位,从而使得PC的值为下一条即将执行的指令在内存中的位置,这样CPU硬件就可以根据PC的值自动完成取指的操作。正是由于有PC的存在,以及CPU硬件会自动增加PC的值,并根据PC的值完成取指操作,才使得CPU一旦上电就永不停歇地运转,由此可见PC寄存器对于计算机的重要性。对于我们进行汇编程序编写而言,PC寄存器亦是十分重要,因为当程序员通过汇编指令完成了对PC寄存器的赋值操作的时候,其实就是完成了一次无条件跳转,这一点非常重要,请务必要牢记。
R14别名LR(linked register),中文称为链接寄存器,它与子程序调用密切相关,用于存放子程序的返回地址,它是ARM程序实现子程序调用的关键所在。下面我们用C语言中对子程序调用的实现细节来说明LR是如何被使用的。
1 int main(void)
2 {
3 int k, i = 1, j = 2;
4 addsub(i, j);
5 k = 3;
6 }
7 int addsub(inta, int b)
8 {
9 int c;
10 c = a+ b;
11 returnc;
12 }
对于上面的程序,编译器会将第4行编译为指令:BL addsub,将第11行编译为指令:MOV pc, lr。(关于BL和MOV指令详见“基本寻址模式与基本指令”)
在这里,关键指令BL addsub会完成2件事情:
1. 将子程序的返回地址(也就是第5行代码在内存中的位置)保存到寄存器LR中;
2. 跳转到子程序addsub的第1条指令处。
这样就完成了子程序的调用。
而指令MOV pc, lr则将保存在lr中的返回地址赋给pc,这样就完成了从子程序的返回。
由此可见,lr是专门用于存放子程序的返回地址的。
另外一个要引起注意的问题是,如果子程序又调用了孙子程序,那么根据前面的分析,在调用孙子程序时,lr寄存器中的值将从子程序的返回地址变为孙子程序的返回地址,这将导致从孙子程序返回子程序没有问题,但从子程序返回父程序则会出错。那么这个问题如何解决呢?其实,如果我们编写的是C程序,那么我们一点也不用担心,因为编译器会为我们考虑一切,针对这个问题,编译器会在子程序的入口处增加入栈操作将lr的值入栈,然后在子程序即将返回前增加出栈操作,将lr的值恢复(一般情况下而言,是将该值恢复到PC,从而完成返回到父程序),从而解决这个难题。不过我们一定要保持头脑的清醒,因为你要知道,我们现在是在编写汇编子程序,此时编译器已经不能在这方面给我们提供保障,所以当你在编写汇编子程序的时候,发现该子程序还要再调用孙子程序,那么请你务必记住,一定要在子程序的入口处保存lr寄存器的值。
好了,现在轮到寄存器R13了,R13又名SP(stack pointer),中文名称栈指针寄存器。顾名思义,它是用于存放堆栈的栈顶地址的。也就是说,每次当我们进行出栈和入栈的时候,都将根据该寄存器的值来决定访问内存的位置(即:出入栈的内存位置),同时在出栈和入栈操作完成后,SP寄存器的值也应该相应增加或减少。这里要特别说明的是,其实在32位的
ARM指令集中没有专门的入栈指令和出栈指令,所以并不是一定要用SP来作为栈指针寄存器,除了PC外,任何普通寄存器均可作为栈指针寄存器,只不过约定俗成,都使用SP罢了。我们将在“其它寻址模式与其它指令”中见到ARM中使用SP作为栈指针寄存器来实现出入栈的汇编指令。
寄存器R0-R12是普通的数据寄存器,可用于任何地方。在不涉及ATPCS规则(在“ATPCS与混合编程”中详细介绍)的情况下,它们并没有什么特别的用法。
1.1.2状态寄存器CPSR与SPSR
1. 状态寄存器CPSR(current program status register)
中文名称:当前程序状态寄存器,顾名思义它是用于保存程序的当前状态的。那么,程序的哪些状态是需要保存的呢?
图1
-1 CPSR寄存器
上图是CPSR寄存器的内容,主要由以下部分组成:
(1)条件代码标志位。它们是ARM指令条件执行的依据。
l N:运算结果的最高位反映在该标志位。对于有符号二进制补码,结果为负数时N=1,结果为正数或零时N=0;
l Z:指令结果为0时Z=1(通常表示比较结果“相等”),否则Z=0;
l C:当进行加法运算(包括CMN指令),并且最高位产生进位时C=1,否则C=0。当进行减法运算(包括CMP 指令),并且最高位产生借位时C=0,否则C=1。对于结合移位操作的非加法/减法指令,C为从最高位最后移出的值,其它指令C通常不变。
l V:当进行加/减法运算,并且发生有符号溢出时V=1,否则V=0,其它指令V通常不变
(2)控制位。它们将控制CPU是否响应中断。
I:中断禁止位,当I位置位时,IRQ中断被禁止。
F:快中断禁止位,当F位置位时,FIQ中断被禁止。
T:反映了CPU当前的状态。当T位置位时,处理器正在Thumb状态下运行;当T位清零时,处理器正在ARM状态下运行。
(3)模式位
包括M4、M3、M2、M1和M0,这些位决定了处理器的模式(关于处理器模式详见“ARM处理器模式与异常初步”)。
总共有7种模式:用户、快中断、中断、管理、中止、未定义、系统,分别会用于不同的情况和异常。由此可见,不是所有模式位的组合都定义了有效的处理器模式,如果使用了错误的设置,将引起一个无法恢复的错误。
2. SPSR(saved program status register)
中文名称:保存的程序状态寄存器
该寄存器的结构与CPSR完全一样,在异常发生时(关于异常,请参见“ARM处理器模式与异常初步”),由硬件自动将异常发生前的CPSR的值存放到SPSR中,以便将来在异常处理结束后,程序能恢复原来CPSR的值。
1.1.3流水线对PC值的影响
图1 - 2 CPU内部结构框图
从上图中我们看到CPU内部有3个主要组成部分:指令寄存器,指令译码器,指令执行单元(包括ALU和通用寄存器组)。
CPU 在执行一条指令的时候,主要有3个步骤:取指(将指令从内存或指令cache中取入指令寄存器);译码(指令译码器对指令寄存器中的指令进行译码操作,从而辨识出该指令是要执行add,或是sub,或是其它操作,从而产生各种时序控制信号);执行(指令执行单元根据译码的结果进行运算并保存结果)
现在我们假设一下:CPU串行执行程序(即:执行完1条指令后,再执行下一条指令);指令执行的3个步骤中每个步骤都耗时1秒;整个程序共10条指令。那么,这个程序总的执行时间是多少呢?显然,是30秒。但这个结果令我们非常不满意,因为它太慢了。有没有办法让它座上京津高铁提速3倍呢?当然有!仔细观察上图,我们发现:取指阶段占用的CPU硬件是指令通路和指令寄存器;译码阶段占用的CPU硬件是指令译码器;执行阶段占用的CPU硬件是指令执行单元和数据通路。三者占用的CPU硬件完全不同,这样就使得如下的操作得以同时进行:在对第一条指令进行译码的时候,可以同时对第二条指令进行取指操作;在对第一条指令进行执行的时候,可以同时对第2条指令进行译码操作,对第三条指令进行取指操作。显然,这样就可以将该程序的运行总时间从30秒缩减为12秒,提速近
3倍。上面所述并行运行指令的方式就被称为流水线操作。可见:流水线操作的本质是利用指令运行的不同阶段使用的CPU硬件互不相同,并发的运行多条指令,从而提高时间效率。
图1 - 3 流水线指令执行图
流水线的引入,的确提高了CPU运行指令的时间效率,但却为我们的汇编程序编写引入了新的问题。请看下面的分析:
寄存器PC的值是即将被取指的指令的地址,正常情况下,在该条指令被取入CPU后执行期间,PC的值保持不变,在该条指令执行完成的时间点上,硬件会自动将 PC的值增加一个单位的大小,这样PC就指向了下一条将被取指和执行的指令。而在引入流水线后,PC值的情况发生了变化,假定第1条指令的内存地址为X, 则在时刻T,PC的值变为X,并在时刻T至时刻T+1期间维持不变;在时刻T+1,PC的值变为X+1个单位,并在时刻T+1至时刻T+2期间维持不变;在时刻T+2,PC的值变为X+2个单位,并在时刻T+2至时刻T+3期间维持不变;在时刻T+3,PC的值将变为X+3个单位。由此可见,在第1条指令的执行阶段,PC的值不再是该指令在内存中的位置,而是该指令在内存中的位置+2个单元。对于ARM指令集而言,每条指令的长度为32bit,占4byte,所以1条指令在内存中需要4byte存储。因此,我们的结论是:
指令执行时,PC的值 = 当前正在执行指令在内存中的地址 + 8
请牢记以上结论。虽然目前我们并不明白这个结论有何作用,但在后续的课程中,特别是通过查看反汇编代码的方式理解伪指令和编译器行为的时候,这个结论将会很有帮助。
最后说明一点:其实ARM现在的CPU的流水线级数早已经突破了3级。但我仍然以3级流水线来进行讲解,是因为:1、较之多级流水线,3级流水线最简单,因此也最便于初学者理解;2、虽然存在多种级别的流水线,但ARM出于统一和前后兼容的考虑,PC的值 = 当前正在执行指令在内存中的地址 + 8 这个结论在所有的流水线级别上都是相同的。作为编程人员而言,我们只需要知道这个结论即可。
要想进行ARM的汇编编程,首当其冲要知道最基本、最常用的指令,而要了解指令则必须要了解寻址方式。所以这里将聚焦在——基本寻址方式和基本指令。
首先,来看一看我们已经见过的2条指令:MOV pc, lr和BL addsub
最简单的汇编指令格式是操作码(例如:MOV、BL)和操作数(例如:pc, lr, addsub)。操作码易于理解,例如MOV表示将某个值从一处传送到另一处,BL表示跳转到某处;而操作数则表示一处和另一处到底是哪里(是在寄存器中还是内存中),要跳转的位置在哪里(或者是绝对地址或者是相对地址)。
操作数部分要解决的问题是:到哪里去获得操作数?因此就有了寻址方式的分类。基本上来讲,ARM共有8种寻址方式,这里我们先了解其中最基本的3种寻址方式:寄存器寻址、立即数寻址、寄存器间接寻址。
1.2.1最常见寻址方式精解
1. 寄存器寻址
MOV pc, lr 表示操作数来源于寄存器(pc和lr)。对于这种寻址方式而言,在指令的32位机器码中的地址码部分,存放的是寄存器(pc和lr)的编号,故称之为寄存器寻址。
2. 立即数寻址
MOV pc, #64 表示将常数64放入寄存器pc,其中常数64被称为立即数。立即数寻址指令中的地址码部分就是操作数本身,也就是说,数据就包含在指令当中,取出指令也就取出了可以立即使用的操作数(故称为立即数)。
这里,可能大家会看出一个问题:由于立即数是位于32位机器码中的,而32位机器码中除了操作数外还有操作码,这就意味着不可能用全部32bit来表示立即数。事实上,ARM机器指令中,仅用了最低的12bit来表示立即数。那么我们自然推论立即数的范围是-2048——2047,这意味着MOV pc, #8192这样的指令是非法的。但事实情况并非如此,MOV pc,#8192是合法且能正常运行的。真实情况是,ARM机器指令可以表示的立即数范围是-2^31--2^31-1,只不过它只能表示这其中的 2^12个数字而已。ARM是这样用12bit来表示一个立即数的:将12bit划分为2部分——高4位和低8位,将低8位补0扩展为32位,然后循环右移
X位(X = 高4位表示的无符号整数*2),例如:如果32位机器码中低12bit为0x512,则其表示的立即数为0x04800000
图1 - 4 12bit立即数
这里,请大家不妨现在先思考2个问题,我们将在后续章节中予以解答:
(1) 为什么ARM要这样设计,而不是按照我们最常见的想法(即:12bit就表示-2^11 --2^11-1中的数)
(2) 如果我们需要mov r0, #10000这样的指令,应该怎么办?(常数10000不能按照如上的方法进行表示)
3. 寄存器间接寻址
寄存器间接寻址指令中的地址码给出的是一个通用寄存器的编号,所需的操作数保存在寄存器指定地址的存储单元中,即寄存器中存放的是操作数的内存地址。例如:
LDR R0, [R2]表示将R2中存放的数作为内存地址,到该内存处取出存放的数,放到寄存器R0中
图1 - 5 执行LDRR0, [R2]前的情况 图1 -6 执行LDRR0, [R2]后的情况
1.2.2最常见指令精解
了解了基本的寻址方式后,我们现在来看一看最常用的汇编指令
1. 单寄存器加载指令。主要有
加载字指令:LDR r0, [r1],将内存中的一个字(4个字节)加载到寄存器r0中
加载字节指令:LDRB r0, [r1],将内存中的一个字节加载到寄存器r0中
有符号数加载字节指令:LDRSB r0, [r1],这条指令与上一条指令的不同之处在于,由于加载的是一个字节,而不是一个字,所以需要确定寄存器r0的高24bit是什么。对于上一条指令,r0的高24bit补0,而本条指令,r0的高24bit补符号位,也就是补r0的bit7
2. 单寄存器存储指令。主要有
存储字指令:STR r0, [r1],将r0中的值存储到内存的4个字节中
存储字节指令:STRB r0, [r1],将r0的低8bit存储到内存的1个字节中
3. 分支指令,共3条:B、BL、BX
B label :跳转到标号label处,也就是说在该条b指令执行后,下一条执行的指令是标号label处的指令。
BL label :与B指令的功能相同,也实现跳转,不同之处在于,bl在跳转的同时还要将返回地址(bl指令的下一条指令的地址)保存到lr中
BX r0 :将r0的值作为地址,跳转到该地址处,并根据r0的值决定是否在ARM和thumb态之间进行切换。
特别说明:
B和BL指令,其跳转范围限制在当前指令的±32M字节地址内(ARM指令为字对齐,最低2位地址固定为0)。
4. 数据处理指令
MOV r0, r1:将r1的值赋给r0
ADD(SUB) r0, r1, r2:将r1的值加上(减去)r2的值,结果存放到r0中
AND(ORR, EOR) r0, r1, r2:将r1的值与(或、异或)r2的值,结果存放到r0中
CMP r1, r2:比较r1与r2值的大小
特别需要说明的问题:
指令CMP r1,r2,其运行细节是:执行r1 - r2的操作,如果结果为负数,则置位CPSR的N位,清零Z位;结果为0,则清零CPSR的N位,置位Z位;结果为正,则清零CPSR的N位,清零Z位。但r1 - r2的结果并不保存。CMP指令通常用于分支跳转。例如,如下的C程序
int i,j;
if (i = = j) {
i++;
} else {
j++;
}
如果使用汇编语句改写的话,就应该写为:
使用ldr指令将变量i的值放入r0
使用ldr指令将变量j的值放入r1
cmp r0, r1
addeq r0, r0, #1
使用streq指令将r0的值放入变量i中
beq label
add r1, r1, #1
使用str指令将r1的值放入变量j中
label 其它代码
……
其中addeq, streq, beq这几条指令,是add, str, b指令的条件执行版本。讲到这里就不得不讲解一下什么是条件执行了。ARM指令集的所有指令均支持条件执行,条件执行指的是,指令可以根据自己即将被执行时的情况(CPSR的条件代码标志位)决定自身是否被执行。eq表示如果CPSR的Z位为1(对于本程序,实际上就是r0的值与r1的值相等,因为cmp会根据 r0与r1的值设置Z位)的情况下,该指令要执行,否则不执行。
其它条件助记符如下:
表1 - 2 条件助记符
条件助记符
标志
含义
EQ
Z=1
相等
NE
Z=0
不相等
CS/HS
C=1
无符号数大于或等于
CC/LO
C=0
无符号数小于
MI
N=1
负数
PL
N=0
正数或零
VS
V=1
溢出
VC
V=0
没有溢出
HI
C=1,Z=0
无符号数大于
LS
C=0,Z=1
无符号数小于或等于
GE
N=V
有符号数大于或等于
LT
N!=V
有符号数小于
GT
Z=0,N=V
有符号数大于
LE
Z=1,N!=V
有符号数小于或等于
AL
任何
无条件执行 (指令默认条件)
NV
任何
从不执行(不要使用)
1.3.1汇编伪操作在汇编程序中的使用范例
掌握了基本的ARM汇编指令后,要写出简单的ARM汇编程序,还必须要掌握基本的ARM汇编伪操作(directive)。现在我们来看一个简单的汇编程序,该程序调用子程序完成了加法操作。
1 ;文件名:TEST.S
2 ;功能:实现两个寄存器相加
3 AREA Example,CODE,READONLY ;声明代码段Example
4 ENTRY ;标识程序入口
5 CODE32 ;声明32位ARM指令
6 START MOV R0,#0 ;设置参数
7 MOV R1,#10
8 BL ADD_SUB;调用子程序ADD_SUB
9 LOOP B LOOP ;跳转到LOOP
10 ADD_SUB
11 ADD R0,R0,R1 ;R0 = R0 + R1
12 MOV PC,LR ;子程序返回
13 END ;文件结束
第 6、7行将传递给子程序的参数存放在r0和r1中,第8行调用子程序。第11、12行是子程序的代码,完成了2个参数相加,并将结果放在r0后返回主程序。第6、9、10行的START、LOOP、ADD_SUB是标号,最经常用于跳转指令B和BL,由于汇编语法要求的缘故,标号必须顶格写(即:不能在行首有空格),否则编译器会报错。与之对应的是,汇编指令一定不能顶格写。
很明显分号(;)在汇编程序中是注释符号,相当于C语言的// 号。除此之外,当然大家注意到了第3、4、5、13行是我们没学习过的符号,其实它们就是本文的重点——ARM汇编伪操作。首先我先来解释这几个伪操作, 第3行定义了一个代码段。汇编伪操作AREA表示定义一个段,其段名为Example,CODE表明是代码段(而不是数据段),属性为只读(READONLY),从而表示第6——12行是程序代码(而不是程序数据)。第4行的ENTRY表示整个程序的入口点(即:程序运行的第一条指令。注1)是第6行的MOV指令。第5行的CODE32表示第6——12行的程序代码是ARM指令,而不是thumb指令。第13行的END表示源代码文件结束,其背后的含义就是:如果程序员在第13行后还写有汇编指令,编译器也根本不会理会这些代码,更不会去编译它们,当然这些代码也就不可能出现在最后的可执行文件中。哈哈,所以请务必记住,在END伪操作的后面再写代码,那是无用功,写了也白写。不要不以为然哟,根据经验,初学者总是会犯这样的错误。
特别说明:第9行的含义是要让程序在运行结束后,在第9行进行死循环,从而让整个程序定格在第9行。这一点也许你很困惑:在写应用程序时,程序结束就结束了,源代码根本不需要再去写个死循环。但你现在要弄清楚:你写应用程序时,有OS为你处理程序结束后的若干事情。可是,你现在已经得不到OS服务。如果你不自己写第9行的代码,那么当你认为程序已经运行结束(第8行执行完成)的时候,CPU不会聪明地停下来,它会继续任劳任怨地去取指第11行,继续运行,这不是你所希望的。其实这还不是最糟糕的,最糟糕的是,如果你的程序没有11-13行,那么CPU任劳任怨取出的指令其实是内存中的随机数,但CPU却会把它当作指令来执行,那么,你认为此时会出现什么情况呢?哈哈,只有天知道!
注1:ENTRY的本意并非如此,此处的含义仅是ENTRY的副作用而以。关于其本意,后续章节将予以解释。
1.3.2最常见汇编伪操作精解
当然,伪操作远不止这几条,下面我们再来介绍经常使用的若干伪操作。
(1) GBLA:定义全局算术变量(准确说,应该是全局符号),例如:GBLAtestval
(2)SETA:对全局算术符号进行赋值,例如:testval SETA 9;testval SETAtestval + 1
(3) DCD:在编译时为整数分配字存储空间,例如:DCD 0x123456ab,这条伪操作将导致编译器在最终的二进制可执行文件中分配一个字的空间,并在该空间中存放整数0x123456ab
(4)DCB:在编译时为数分配字节存储空间,例如:DCB ‘a’,这条伪操作将导致编译器在最终的二进制可执行文件中分配一个字节的空间,并在该空间中存放字符a的ASCII码
(5) IF,ELSE及ENDIF:相当于C语言的条件编译,例如:
GBLAtestval
testval SETA 9
IFtestval < 5
mov r0, #testval
ELSE
movr1, #testval
ENDIF
IF :DEF:testval
movr2, #testval
ELSE
INFO 4, "you should definetestval"
ENDIF
编译器编译该段代码的结果是:
mov r1, #9
mov r2, #9
(6) WHILE及WEND :例如
GBLAtestval
testval SETA 1
WHILE testval <= 3
testval SETA testval + 1
mov r0,#testval
WEND
编译器编译该段代码的结果是:
mov r0, #2
mov r0, #3
mov r0, #4
(7) MACRO 、MEND及MEXIT:相当于C语言的宏替换,例如:
MACRO
$label xmac$p1,$p2
; code1
$label.loop1
;code2
BGE $label.loop1
$label.loop2
;code3
BL $p1
BGT $label.loop2
; code4
ADR r0,$p2
;code5
MEND
;主程序
abc xmac subr1,de
编译器编译该段代码的结果是:
;code1
abc.loop1
;code2
BGE abc.loop1
abc.loop2
;code3
BL subr1
BGT abc.loop2
;code4
ADR r0,de
;code5
8. EQU:相当于C语言的宏定义,例如:testval EQU 4
9. EXPORT: 参见“ATPCS与混合编程”
10. IMPORT:参见“ATPCS与混合编程”
非常重要的一点是:必须深刻理解汇编伪操作是给编译器提供某些必要的信息,以帮助编译器正确完成程序的编译。当编译完成后,汇编伪操作就完成了它的历史使命,它不可能在最终的可执行程序的二进制代码中留下哪怕是一点点痕迹,当然也就不可能在程序运行时受到CPU的“青睐”。总之记住一句话,汇编伪操作是给编译器看的,而不是给CPU看的。这是汇编伪操作与汇编指令最大的区别。
1.3.3汇编伪操作列表
为了保持内容的完整,下面给出较为完整的汇编伪操作列表。如需完整的列表,请自行查阅ads自带的“OnlineBooks”相关章节。
l 符号定义(Symbol Definition)伪操作:
表1 - 3符号定义伪操作
伪操作
语法格式
作用
GBLA
GBLA Variable
声明一个全局的算术变量,并将其初始化成0
GBLL
GBLL Variable
声明一个全局的逻辑变量,并将其初始化成{FALSE}
GBLS
GBLS Variable
声明一个全局的字符串变量,并将其初始化成空串“”
LCLA
LCLA Variable
声明一个局部的算术变量,并将其初始化成0
LCLL
LCLL Variable
声明一个局部的逻辑变量,并将其初始化成{FALSE}
LCLS
LCLS Variable
声明一个局部的串变量,并将其初始化成空串“”
SETA
Variable SETA expr
给一个全局或局部算术变量赋值
SETL
Variable SETL expr
给一个全局或局部逻辑变量赋值
SETS
Variable SETS expr
给一个全局或局部字符串变量赋值
RLIST
name LIST (list of registers)
为一个通用寄存器列表定义名称
CN
name CN expr
为一个协处理器的寄存器定义名称
CP
name CP expr
为一个协处理器定义名称
DN/SN
name DN/SN expr
DN/SN为一个双精度/单精度的VFP寄存器定义名称
FN
name FN expr
为一个FPA浮点寄存器定义名称
l 数据定义(Data Definition)伪操作:
表1 - 4 数据定义伪操作
伪操作
语法格式
作用
LTORG
LTORG
声明一个数据缓冲池(也称为文字池)的开始
MAP
MAP expr {,base_register}
定义一个结构化的内存表(Storage Map)的首地址
FIELD
{label} FIELD expr
定义一个结构化内存表中的数据域
SPACE
{label} SPACE expr
分配一块连续内存单元,并用0初始化
DCB
{label} DCB expr {, expr}
分配一段字节内存单元,并用expr初始化
DCD
DCDU
{label} DCD {U} expr {, expr}…
分配一段字(对齐)的内存单元,DCD可能在分配的第1个内存单元前插入填补字节(padding),以保证分配的内存是字对齐的,DCDU不需要对齐
DCFD/
DCFDU
{label} DCFD{U} fpliteral{,fpliteral}...
为双精度的浮点数分配字对齐的内存单元
DCFS/
DCFSU
{label} DCFS{U} fpliteral{,fpliteral}...
为单精度的浮点数分配字对齐的内存单元
DCI
{label} DCI expr{,expr}...
在ARM代码中分配一段字对齐的内存单元;在Thumb代码中,分配一段半字对齐的半字内存单元
DCQ/DCQU
{label} DCQ{U} {-}literal{,{-}literal}...
分配一段以双字(8个字节)为单位的内存
DCW/ DCWU
{label} DCW expr{,expr}...
DCW用于分配一段半字对齐的半字内存单元
l 汇编控制(Assembly Control)伪操作:
表1 - 5 汇编控制伪操作
伪操作
语法格式
作用
IF, ELSE及ENDIF
IF logical expr
…
{ELSE
…}
ENDIF
能够根据条件把一段源代码包括在汇编语言程序内或者将其排除在程序之外
WHILE及WEND
WHILE logical expr
…
WEND
能够根据条件重复汇编相同的一段源代码
MACRO, MEND及MEXIT
MACRO
{$label} macroname{$param
{,$param}…}
…;宏代码
MEND
MACRO标识宏定义的开始,MEND标识宏定义的结束,MEXIT用于从宏中跳转出去,用MACRO和MEND定义的一段代码,称为宏定义体,通过宏名称来调用宏
l 信息报告(Reporting)伪操作:
表1 - 6 信息报告伪操作
伪操作
语法格式
作用
ASSERT
ASSERT logical expr
对汇编程序的第二遍扫描中,如果其中ASSERT中条件不成立,ASSERT伪操作将报告该错误信息。
INFO
INFO numberic-expr, string-expr
对汇编程序的第一遍扫描或者第二遍扫描时INFO伪操作报告诊断信息
OPT
OPT n
通过OPT伪操作可以在源程序中设置列表选项
TTL
TTL title
在列表文件的每一页的开头插入一个标题
l 其他(Miscellaneous)伪操作:
表1 - 7 其他伪操作
伪操作
语法格式
作用
CODE16
CODE16
告诉汇编编译器后面的指令序列为16位的Thumb指令
CODE32
CODE32
告诉汇编编译器后面的指令序列为32位的ARM指令
EQU
name EQU expr{, type}
为数字常量,基于寄存器的值和程序中的标号(基于PC的值)定义一个字符名称,类似于C语言中的#define宏定义
AREA
AREA sectionment {, attr}{, attr}…
定义一个代码段或者数据段
ENTRY
ENTRY
指定程序的入口点
END
END
告诉编译器已经到了源程序结尾
ALIGN
ALIGN {expr{, offset)}
通过添加补丁字节使当前位置满足一定的对齐方式
EXPORT/
GLOBAL
EXPORT symbol{[WEAK]}
声明一个符号可以被其他文件引用
IMPORT/
EXTERN
IMPORT Symbol{WEAK}
EXTERN symbol{(WEAK)}
告诉编译器当前的符号不是在本源文件中定义的,而是在其他源文件中定义的,在本源文件中可能引用该符号
GET/INCLUDE
GET filename
将一个源文件包含到当前源文件中,并将被包含的文件在其当前位置进行汇编处理
INCBIN
INCBIN filename
将一个文件包含到当前源文件中,被包含的文件不进行汇编处理
KEEP
KEEP{symbol}
告诉编译器将局部符号包含在目标文件的符号表中
NOFP
NOFP
禁止源程序中包含浮点运算指令
REQUIRE
REQUIRE lable
指定段之间的相互依赖关系
现在我们已经掌握了所有知识,可以编写简单的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异常处理”