我们在学习ARM的时候,一般都不用看汇编启动代码,直接使用芯片厂商提供的汇编启动代码,但是要想深入了解ARM内部原理,就必须掌握一定的汇编知识。
我们在前面总结了处理器架构与指令集,那么汇编和处理器架构、指令集有什么关系呢?先看下图:
从上图可以看出,不同的处理器架构、不同指令集合对应不同汇编指令。可以说,一种指令集就对应一种汇编指令,汇编是开发者与计算机交互的接口,
总结一下,汇编语言是指令集构架的机器码一对一的人类可以理解的翻译,是用人类看得懂的语言来描述指令集。否则指令集的机器码都是一堆二进制数字,人类读起来非常麻烦,但汇编是用类似人类语言的方式描述指令集,从而控制不同的处理器按照人们的想法去工作。
对于CPU来说,它只能识别二进制码,那怎么能识别高级语言呢?于是人们开发了编译器,依照如下顺序,将高级语言翻译成二进制码:
可以说,汇编语言就是高级语言和二进制机器码的桥梁。
关于处理器架构、指令集、寄存器的介绍请看笔者以前的文章。
本文针对ARM架构的芯片讲解其相关的指令集。
1 工具
工欲善其事,必先利其器,在学习ARM汇编之前,我们先准备好学习软件。这里推荐使用的是VisUAL。VisUAL是一款ARM汇编模拟器,支持Windows、Mac OS X和Linux系统。
【注】下载VisUAL需要VPN,如果没有VPN请自行参看后文提示获取。
VisUAL模拟的ARM板子如下图所示:
它没有模拟外设,仅仅模拟了CPU、ROM、RAM。红色区域是ROM,不能读不能写,只能运行其中的程序。ROM区域本来可以读的,这是VisUAL的局限。RAM区域可读可写。
VisUAL 支持一小部分 ARM UAL 指令。这些主要是算术、逻辑、加载/存储和分支指令。下面给出了指令语法的简短摘要。有关详细信息和示例,请移步VisUAL关官网。
1.1 VisUAL设置
VisUAL设置基本不需要什么设置,界面也很简单,常用的有设置背景、编码字体与颜色。设置方式如下:
值得注意的是,设置字体大小需要点击回车才能生效。
1.2 VisUAL使用
首选在编辑区写好你要模拟的汇编代码,点击运行按钮就可进行调试。
值得一提的是,VisUAL可以回退运行,在运行完一条指令后,还可以回退到上一条指令,非常的实用。
当代码运行错误,会提示错误信息,点击[Reset],根据提示修改错误的代码。
根据提示修改代码后,点击[Execute],运行结果如下:
运行成功后,不仅可以看到寄存器的情况,还可以快速查内存情况。以上指令的含义后文会详细讲解。
当然啦,VisUAL还提供了内存分析工具,使用功能方法如下。
好了,关于的VisUAL工具的介绍就这些了,后面会结合ARM的具体汇编指令进一步使用VisUAL工具。
2 ARM汇编编程简介
首先,我们先看一个简单的汇编程序:
area ff,code,readonly ;声明代码段
code32 ;声明为32位ARM指令
entry ;声明程序入口
start
;b指令
;1.b 跳转范围+_ 32M b + 标号
;b start
;b stop
;2.bl 子函数调用
;会把预取指令的地址保存在lr(r14)
;3.bx 子函数返回
mov r0,#9
mov r1,#15
mov r5,#9
bl func
;int func(int a,int b)
stop
b stop
func
mov r5,#1
loop
cmp r0,r1
beq stop1
subgt r0,r0,r1
sublt r1,r1,r0
b loop
stop1
bx lr
end
可以看出,ARM汇编程序用“;”号进行注释。
当然啦,看不懂上面的示例不要紧,后面会在详细介绍。。
2.1汇编语言程序格式
一个完整的ARM汇编由两部分组成:声明,实际代码段两部分组成。
1、声明
在一个程序之前先要进行声明:
A.声明代码段:
用AREA指令定义一个段,说明所定义段的相关属性。(说明段的名字,段的属性)
B.声明ARM指令:
用CODE32或CODE16来声明程序为32位ARM指令或是16位Thumb指令。
C.声明程序入口:
用ENTRY指令标识程序的入口点。
注:这3个声明缺一不可。在程序完成后要用END 指令声明程序结束。每一个汇编程序段都必须有一条END指令,指示代码段的结束。
2、段
A.在ARM汇编语言程序中,以程序段为单位组织代码。段是相对独立的指令或数据序列,具有特定的名称。
B.段的分类
代码段:代码段的内容为执行代码
数据段:数据段存放代码运行时需要用到的数据。
注:一个汇编程序至少有一个代码段。如果程序较长时,可以分割为多个代码段和数据段。多个段在程序编译连接时最终形成一个可执行的映像文件。
C.段具有以下的属性
2.2汇编语言的语句格式
[LABEL] OPERATION [OPERAND] [;COMMENT]
标号域 操作助记符域 操作数域 注释域
1.标号域(LABLE)
A.标号域用来表示指令的地址、变量、过程名、数据的地址和常量。
B.标号是可以自己起名的标识符,语句标号可以是大小写字母混合,通常以字母开头,由字母、数字、下划线等组成。
C.语句标号不能与寄存器名、指令助记符、伪指令(操作)助记符、变量名同名。
D.语句标号必须在一行的开头书写,不能留空格。
2.操作助记符域(OPERATION)
A.操作助记符域可以为指令、伪操作、宏指令或伪指令的助记符。
B.ARM汇编器对大小写敏感,在汇编语言程序设计中,每一条指令的助记符可以全部用大写、或全部用小写,但不允许在一条指令中大、小写混用。
C.所有的指令都不能在行的开头书写,必须在指令的前面有空格,然后再书写指令。
D.指令助记符和后面的操作数或操作寄存器之间必须有空格,不可以在这之间使用逗号。
3.操作数域(OPERAND)
操作数域表示操作的对象,操作数可以是常量、变量、标号、寄存器名或表达式,不同对象之间必须用逗号“,”分开。
2.3 ARM指令集格式
基本格式是操作码,后跟可选条件码,可选S (set flags),如下所示
Operation{cond}{S} Rd, Rn, Operand2
1.其中<>中的项是必须的,{}中的项是可选的。
2.opcode 表示指令助记符。
- cond:表示执行条件。
- S:表示是否影响CPSR寄存器的值。
- Rd:表示目标寄存器。
- Rn:表示第一个操作数的寄存器。
- operand2:表示第2个操作数。
3.“operand2”具有如下的形式:
A.#immed_8r:常数表达式
eg:
MOV R0,#1
ADD R0,R1,#0X0F
B.Rm:寄存器形式。
即在寄存器方式下,操作数即为寄存器的数值。
eg:
MOV PC,R0
ADD R1,R1,R2
C.Rm,shift:寄存器移位方式。
将寄存器的移位结果作为操作数,当Rm值保持不变。
- ASR #n:表示算术右移n位。
- LSR #n:表示逻辑右移n位。
- ROR #n:表示循环右移n位。
- RRX #n:带扩展的循环右移n位。
- LSL #n:逻辑左移n位。
4.使用条件码“cond”可以实现高效的逻辑操作,提高代码的效率。
- 所有的ARM指令都可以条件执行。
- Thumb指令只有B(跳转)指令具有条件执行功能。
注:如果执行中不表明条件码,默认为无条件(AL)执行。
2.4汇编程序中常用的符号
在汇编语言程序设计中,经常使用各种符号表示变量、常量和地址
A.符号由大小写字母、数字以及下划线组成。
B.符号区分大小写,同名的大、小写符号会被编译器认为是两个不同的符号。
C.符号在其作用范围内必须唯一,即在其作用范围内不可有同名的符号。
D.自定义的符号名不能与系统的保留字相同。
【注】符号名不应与指令或伪指令同名。
1.程序中的变量
- ARM汇编程序所支持的变量有数字变量,逻辑变量和字符串变量
- 在ARM汇编程序设计中,可使用GBLA,GBLL,GBLS伪定义声明全局变量,使用LCLA,LCLL,LCLS声明局部变量,并可使用SETA,SETL和SETS对其经行初始化。
2.程序中的常量
ARM汇编程序所支持常量有数字常量,逻辑常量和字符串常量。
3.程序中的变量代换
程序中的变量可通过代换操作取的一个常量。代换操作符为”$”。使用示例:
LCLS S1
LCLS S2 ;定义局部字符串变量S1和S2
S1 SETS “Test!”
S2 SETS “This is a $ S1”;S2的值为“This is a Test
3 ARM汇编指令详解
ARM公司针对ARM架构的芯片发布了三种指令集:
1.ARM指令集:每条指令是是 32位的,每条指令能承载更多的信息,因此使用最少的指令完成功能,所以在相同频率下运行速度也是最快的,但也因为每条指令是32位的而占用了最多的程序空间。
2.Thumb指令集:每条指令是 16位的,每条指令所能承载的信息少,因此它需要使用更多的指令才能完成功能,因此运行速度慢,但它也占用了最少的程序空间。
3.Thumb-2指令集:支持16位指令、32位指令混合编程, 当一个操作可以使用一条32位指令完成时就使用32位的指令, 加快运行速度, 而当一次操作只需要一条16位指令完成时就使用16位的指令,节约存储空间。
目前ARM架构主流是基于ARMv7架构的Cortex系列产品,由A、R、M三个系列组成,具体分类延续了一直以来ARM面向具体应用设计CPU的思路。其中A是应用处理器(Application Processor),基于虚拟存储的操作系统和应用程序而设计,支持ARM、Thumb、Thumb-2指令集,R是实时控制处理(Real Time Control)系列,支持支持ARM、Thumb、Thumb-2指令集,M微控制器(Micro Controller)系列,对价格相对敏感,仅支持Thumb 和Thumb-2指令集。
ARM公司针对不同指令集,ARM公司推出了: Unified Assembly Language UAL,统一汇编语言,你不需要去区分这些指令集。在程序前面用CODE32/CODE16/THUMB表示指令集ARM /Thumb/ Thumb2。
在具体讲解指令之前,我们首先要清楚两个重要的概念:立即数和寻址方式。
3.1立即数
立即数通常是指在立即寻址方式指令中给出的数,直接包含在指令中,可以是8位、16位或32位,该数值紧跟在操作码之后。
先看一条指令:
MOV R0, #VAL 其含义是把VAL这个值存入R0寄存器。
在ARM指令中,VAL的值必须是以下情况:
- 一个常数,可以通过将一个8位的值旋转任意偶数位来产生。
- 形式为0x00XY00XY的常数。
- 形式为0xXY00XY00的常数。
- 形式为0xXYXYXYXY的常数。
其中XY为十六进制数,取值范围为0x00 ~ 0xFF。
综上,VAL必须是立即数。
那么,立即数有何特别之处呢?在之前,需要了解数据编码的知识,对于开发者而言,开发者使用的是汇编指令,而汇编指令不是计算机能够理解的,计算机所能理解的只是 0 1 这两种数字。所以,汇编指令还要经过译码器译码成机器指令才能被计算机所理解。而汇编指令作为开发者和计算机交流的接口,那么常常会有一些限制。
对于ARM指令,汇编指令的对应的机器指令格式大致如下:
从上图可以看到,在一条指令中,只有后12位用于存放立即数,其中这 12 位又分为两部分:前7~0位是数值部分(immediate);后11~8 位是前7~0位要进行移位操作的移位数(rotate)。
其中 immediate有 8 位,也就是我们的立即数部分占 8 位,因此有如下结论:
- 如果一个立即数小于 0xFF(255)那么直接用前 7~0 位表示即可,此时不用移位,11~8 位的 immediate_rotate等于 0。
- 如果前八位 immediate的数值大于 255,那么就看这个数是否能有immediate中的某个数移位 2*immediate_rotate位形成的。如果能,那么就是合法立即数;否则非法。
因此,立即数的判定方法如下:
一个 32 位数用 12 位编码表示,符合以下规则才是合法立即数。
立即数 = immediate循环右移 (2 * immediate_rotate)
如果存在一个 immediate_rotate能够让该立即数由 immediate循环右移 2*immediate_rotate位(偶数位)表示,那么这个立即数就是合法的。
举两个例子吧。
Eg1: 0x234
0x234 用二进制表示:
0000 | 0000 | 0000 | 0000 | 0000 | 0010 | 0011 | 0100
其中的 1000 | 1101 部分循环右移 30 位可以和这个二进制数相同。因此immediate_rotate的值是 15,而 immediate的值是 1000 | 1101 即 8D。
Eg2: 0x132
0x132 用二进制表示:
0000 | 0000 | 0000 | 0000 | 0000 | 0001 | 0011 | 0010
没有任何部分循环右移偶数次可以成为该数本身,因此它不是一个合法的立即数。
以上方式是从立即数的本质入手去判断一个数是不是立即数,对于很多朋友不太友好,下面笔者告诉你个更加容易点。
立即数的标准判断的步骤:
Step1.把数据转换成二进制形式,从低位到高位写成4位1组的形式,最高位一组不够四位的,在最高位前面补0。
Step2.数1的个数,如果大于8个肯定不是立即数,如果小于等于8进行下面步骤。
Step3.如果数据中间有连续的大于等于24个0,循环左移4的倍数,使高位全为0。
Step4.找到最高位的1,去掉前面最大偶数个0。
Step5.找到最低位的1,去掉后面最大偶数个0。
Step6.数剩下的位数,如果小于等于8位,那么这个数就是立即数,反之就不是立即数。
典型例子:
(1)0x4FF (2)0x122 (3)0x234 (4)0xF000000F
Eg1: 0x4FF
Step1: 0100 1111 1111
Step2:其中1的个数是9个,大于8个,判定不是立即数
Eg 2: 0x122
Step1: 0001 0010 0010
Step2: 其中1的个数4个,小于8,继续
Step3: 其中没有连续大于等于24个0,继续
Step4: xx01 0010 0010 (最高位前面有3个0,最大偶数2,去掉2个0)
Step5: xx10 0011 0010 (最低位后面只有1个0,最大偶数0)
Step6: 剩下10 0011 0010 共10位,大于8,判定0x122不是立即数
Eg 3: 0x234
Step1: 0010 0011 010
Step2:其中1的个数4个,小于8,继续
Step3:其中没有连续大于等于24个0,继续
Step4: xx10 0011 0100
Step5: xx10 0011 01xx
Step6:剩下10 0011 01 共8位,等于8,判定0x234是立即数
Eg 4: 0xF000000F
Step1: 1111 0000 0000 0000 0000 0000 0000 1111
Step2:其中1的个数8个,没有大于8,继续
Step3:其中有连续24个0,循环左移4位,使高位全为0
0000 0000 0000 0000 0000 0000 0000 1111 1111
Step4: xxxx xxxx xxxx xxxx xxxx xxxx xxxx 1111 1111
Step5: xxxx xxxx xxxx xxxx xxxx xxxx xxxx 1111 1111
Step6:剩下1111 1111共8位,等于8,判定0xF000000F是立即数
好了,总结下,立即数就是一个特殊的数,它必须满足一定的规则,就是通过循环移位(偶数位),去掉前后偶数个0得到一个小于等于8位的数。如果还是不理解的,请多看几遍,仔细琢磨吧。
3.2 ARM寻址方式
接下里谈谈ARM的寻址方式。所谓寻址方式,是指处理器根据指令中给出的地址信息来寻找物理地址的方式,目前ARM指令系统支持以下几种寻址方式。
3.2.1立即寻址
也称为立即数寻址,这种寻址方式指令中就已经给出了操作数。也就是在执行指令的过程中,处理器取得指令的同时也取得了操作数,因此称为立即数寻址。例如:
MOV R0 ,#0X20000 ;将0X20000写入寄存器R0中
ADD R0, R0, #0x3F ;将R0中的值加上立即数0X37,再写入R0中
在上面两条指令中,源操作数就是立即数,要求以“#”开始,对于十六进制的立即数。
3.2.2寄存器寻址
操作数的值在寄存器中,指令中的地址码字段指出的是寄存器编号,指令执行时直接取出寄存器值来操作。寄存器寻址是各类微处理器常用的寻址方式,也是效率较高的寻址方式。寄存器寻址指令举例如下:
MOV R1,R2 ;将R2的值存入R1
SUB R0,R1,R2 ;将R1的值减去R2的值,结果保存到R0
3.2.3寄存器移位寻址
寄存器移位寻址是ARM指令集特有的寻址方式。当第2个操作数是寄存器移位方式时,第2个寄存器操作数在与第1个操作数结合之前,选择进行移位操作。寄存器移位寻址指令举例如下:
MOV R0,R2,LSL #3 ;R2的值左移3位,结果放入R0, ;即是R0=R2×8 ANDS R1,R1,R2,LSL R3 ;R2的值左移R3位,然后和R1相 ;“与”操作,结果放入R1 移位操作:
LSL移位操作:
LSR移位操作:
ASR移位操作:
ROR移位操作:
RRX移位操作:
3.2.4寄存器间接寻址
寄存器间接寻址指令中的地址码给出的是一个通用寄存器的编号,所需的操作数保存在寄存器指定地址的存储单元中,即寄存器为操作数的地址指针。寄存器间接寻址指令举例如下:
LDR R1,[R2] ;将R2指向的存储单元的数据读出 ;保存在R1中
SWP R1,R1,[R2] ;将寄存器R1的值和R2指定的存储 ;单元的内容交换
3.2.5基址寻址(变址寻址)
基址寻址就是将基址寄存器的内容与指令中给出的偏移量相加/减,形成操作数的有效地址。基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。寄存器间接寻址是偏移量为0的基址加偏移寻址。
基址寻址指令举例如下:
LDR R2,[R3,#0x0C] ;读取R3+0x0C地址上的存储单元的内容,放入R2
LDR R0,[R1] ,#4 ;R0=[R1],R1=R1+4
LDR R0,[R1,R2]! ;R0=[R1+R2]
3.2.6多寄存器寻址
多寄存器寻一次可传送几个寄存器值,允许一条指令传送16个寄存器的任何子集或所有寄存器。多寄存器寻址指令举例如下:
LDMIA R1!,{R2-R7,R12} ;将R1指向的单元中的数据读出到R2~R7、R12中(R1自动加4) STMIA R0!,{R2-R7,R12} ;将寄存器R2~R7、R12的值保存到R0指向的存储, 单元中(R0自动加4)
3.2.7堆栈寻址
堆栈是一个按特定顺序进行存取的存储区,操作顺序为“后进先出” 。堆栈寻址是隐含的,它使用一个专门的寄存器(堆栈指针)指向一块存储区域(堆栈),指针所指向的存储单元即是堆栈的栈顶。存储器堆栈可分为两种:
向上生长:向高地址方向生长,称为递增堆栈向下生长:向低地址方向生长,称为递减堆栈
堆栈指针指向最后压入的堆栈的有效数据项,称为满堆栈。(压栈时先修改sp,后数据压栈;出栈时先数据出栈,后修改sp)
堆栈指针指向下一个待压入数据的空位置,称为空堆栈。(压栈时先数据压栈,后修改sp;出栈时先修改sp,后数据出栈)
四种类型的堆栈方式:
满递增:堆栈向上增长,堆栈指针指向内含有效数据项的最高地址。指令如LDMFA、STMFA等;
空递增:堆栈向上增长,堆栈指针指向堆栈上的第一个空位置。指令如LDMEA、STMEA等;
满递减:堆栈向下增长,堆栈指针指向内含有效数据项的最低地址。指令如LDMFD、STMFD等;
空递减:堆栈向下增长,堆栈指针向堆栈下的第一个空位置。指令如LDMED、STMED等。
3.2.8相对寻址
相对寻址是基址寻址的一种变通。由程序计数器PC提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到的地址即为操作数的有效地址。相对寻址指令举例如下:
BL SUBR1 ;调用到SUBR1子程序 BEQ LOOP ;条件跳转到LOOP标号处 ...LOOP MOV R6,#1 ...SUBR1 ...
3.2.9块拷贝寻址
用于将一块数据从存储器的某一位置拷贝到另一位置。
STMIA R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之后增加,增长方向为向上增长。STMIB R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之前增加,增长方向为向上增长。STMDA R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之后增加,增长方向为向下增长。STMDB R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之前增加,增长方向为向下增长。
3.3数据处理指令
ARM内核只能在寄存器上执行数据处理,不能直接在内存上执行。所有ARM数据传送或算术逻辑运算指令均可选择使用S后缀,以使指令影响CPSR中的标志。数据处理指令(在大多数情况下)使用一个目标寄存器和两个源操作数。
数据处理指令大致可分为3类:
- 数据传送指令;数据传送指令用于在寄存器和存储器之间进行数据的双向传输;
- 算术逻辑运算指令;算术逻辑运算指令完成常用的算术与逻辑的运算,该类指令不但将运算结果保存在目的寄存器中,同时更新CPSR中的相应条件标志位;
- 比较指令。比较指令不保存运算结果,只更新CPSR中相应的条件标志位。
下表总结了数据处理过程中的常用汇编指令,给出了它们的助记操作码、操作数和它们的功能的简要描述。
1.ADC指令(带进位相加)
ADC指令的格式为:
ADC{条件}{S} 目的寄存器,操作数1,操作数2
ADC指令用于把两个操作数相加,再加上CPSR中的C条件标志位的值,并将结果存放到目的寄存器中。它使用一个进位标志位,这样就可以做比32位大的数 的加法,注意不要忘记设置S后缀来更改进位标志。操作数1应是一个寄存器,操作数2可以是一 个寄存器,被移位的寄存器,或一个立即数。
以下指令序列完成两个128位数的加法,第一个数由高到低存放在寄存器R7~R4,第二个数由高到低存放在寄存器R11~R8,运算结果由高到低存放在寄 存器R3~R0。
指令示例:
ADDS R0, R4, R8 ; 加低端的字ADCS R1, R5, R9 ;加第二个字,带进位ADCS R2, R6, R10 ;第三个字,带进位ADC R3, R7, R11 ;加第四个字,带进位
2.ADD指令(相加)
ADD指令的格式为:
ADD{条件}{S} 目的寄存器,操作数1,操作数2
ADD指令用于把两个操作数相加,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。
指令示例:
ADD R0, R1, R2 ; R0 = R1 + R2ADD R0, R1, #256 ; R0 = R1 + 256ADD R0, R2, R3, LSL#1 ; R0 = R2 + (R3 << 1)
我们在学习ARM的时候,一般都不用看汇编启动代码,直接使用芯片厂商提供的汇编启动代码,但是要想深入了解ARM内部原理,就必须掌握一定的汇编知识。
我们在前面总结了处理器架构与指令集,那么汇编和处理器架构、指令集有什么关系呢?先看下图:
从上图可以看出,不同的处理器架构、不同指令集合对应不同汇编指令。可以说,一种指令集就对应一种汇编指令,汇编是开发者与计算机交互的接口,
总结一下,汇编语言是指令集构架的机器码一对一的人类可以理解的翻译,是用人类看得懂的语言来描述指令集。否则指令集的机器码都是一堆二进制数字,人类读起来非常麻烦,但汇编是用类似人类语言的方式描述指令集,从而控制不同的处理器按照人们的想法去工作。
对于CPU来说,它只能识别二进制码,那怎么能识别高级语言呢?于是人们开发了编译器,依照如下顺序,将高级语言翻译成二进制码:
可以说,汇编语言就是高级语言和二进制机器码的桥梁。
关于处理器架构、指令集、寄存器的介绍请看笔者以前的文章。
本文针对ARM架构的芯片讲解其相关的指令集。
1 工具
工欲善其事,必先利其器,在学习ARM汇编之前,我们先准备好学习软件。这里推荐使用的是VisUAL。VisUAL是一款ARM汇编模拟器,支持Windows、Mac OS X和Linux系统。
【注】下载VisUAL需要VPN,如果没有VPN请自行参看后文提示获取。
VisUAL模拟的ARM板子如下图所示:
它没有模拟外设,仅仅模拟了CPU、ROM、RAM。红色区域是ROM,不能读不能写,只能运行其中的程序。ROM区域本来可以读的,这是VisUAL的局限。RAM区域可读可写。
VisUAL 支持一小部分 ARM UAL 指令。这些主要是算术、逻辑、加载/存储和分支指令。下面给出了指令语法的简短摘要。有关详细信息和示例,请移步VisUAL关官网。
1.1 VisUAL设置
VisUAL设置基本不需要什么设置,界面也很简单,常用的有设置背景、编码字体与颜色。设置方式如下:
值得注意的是,设置字体大小需要点击回车才能生效。
1.2 VisUAL使用
首选在编辑区写好你要模拟的汇编代码,点击运行按钮就可进行调试。
值得一提的是,VisUAL可以回退运行,在运行完一条指令后,还可以回退到上一条指令,非常的实用。
当代码运行错误,会提示错误信息,点击[Reset],根据提示修改错误的代码。
根据提示修改代码后,点击[Execute],运行结果如下:
运行成功后,不仅可以看到寄存器的情况,还可以快速查内存情况。以上指令的含义后文会详细讲解。
当然啦,VisUAL还提供了内存分析工具,使用功能方法如下。
好了,关于的VisUAL工具的介绍就这些了,后面会结合ARM的具体汇编指令进一步使用VisUAL工具。
2 ARM汇编编程简介
首先,我们先看一个简单的汇编程序:
area ff,code,readonly ;声明代码段
code32 ;声明为32位ARM指令
entry ;声明程序入口
start
;b指令
;1.b 跳转范围+_ 32M b + 标号
;b start
;b stop
;2.bl 子函数调用
;会把预取指令的地址保存在lr(r14)
;3.bx 子函数返回
mov r0,#9
mov r1,#15
mov r5,#9
bl func
;int func(int a,int b)
stop
b stop
func
mov r5,#1
loop
cmp r0,r1
beq stop1
subgt r0,r0,r1
sublt r1,r1,r0
b loop
stop1
bx lr
end
可以看出,ARM汇编程序用“;”号进行注释。
当然啦,看不懂上面的示例不要紧,后面会在详细介绍。。
2.1汇编语言程序格式
一个完整的ARM汇编由两部分组成:声明,实际代码段两部分组成。
1、声明
在一个程序之前先要进行声明:
A.声明代码段:
用AREA指令定义一个段,说明所定义段的相关属性。(说明段的名字,段的属性)
B.声明ARM指令:
用CODE32或CODE16来声明程序为32位ARM指令或是16位Thumb指令。
C.声明程序入口:
用ENTRY指令标识程序的入口点。
注:这3个声明缺一不可。在程序完成后要用END 指令声明程序结束。每一个汇编程序段都必须有一条END指令,指示代码段的结束。
2、段
A.在ARM汇编语言程序中,以程序段为单位组织代码。段是相对独立的指令或数据序列,具有特定的名称。
B.段的分类
代码段:代码段的内容为执行代码
数据段:数据段存放代码运行时需要用到的数据。
注:一个汇编程序至少有一个代码段。如果程序较长时,可以分割为多个代码段和数据段。多个段在程序编译连接时最终形成一个可执行的映像文件。
C.段具有以下的属性
2.2汇编语言的语句格式
[LABEL] OPERATION [OPERAND] [;COMMENT]
标号域 操作助记符域 操作数域 注释域
1.标号域(LABLE)
A.标号域用来表示指令的地址、变量、过程名、数据的地址和常量。
B.标号是可以自己起名的标识符,语句标号可以是大小写字母混合,通常以字母开头,由字母、数字、下划线等组成。
C.语句标号不能与寄存器名、指令助记符、伪指令(操作)助记符、变量名同名。
D.语句标号必须在一行的开头书写,不能留空格。
2.操作助记符域(OPERATION)
A.操作助记符域可以为指令、伪操作、宏指令或伪指令的助记符。
B.ARM汇编器对大小写敏感,在汇编语言程序设计中,每一条指令的助记符可以全部用大写、或全部用小写,但不允许在一条指令中大、小写混用。
C.所有的指令都不能在行的开头书写,必须在指令的前面有空格,然后再书写指令。
D.指令助记符和后面的操作数或操作寄存器之间必须有空格,不可以在这之间使用逗号。
3.操作数域(OPERAND)
操作数域表示操作的对象,操作数可以是常量、变量、标号、寄存器名或表达式,不同对象之间必须用逗号“,”分开。
2.3 ARM指令集格式
基本格式是操作码,后跟可选条件码,可选S (set flags),如下所示
Operation{cond}{S} Rd, Rn, Operand2
1.其中<>中的项是必须的,{}中的项是可选的。
2.opcode 表示指令助记符。
- cond:表示执行条件。
- S:表示是否影响CPSR寄存器的值。
- Rd:表示目标寄存器。
- Rn:表示第一个操作数的寄存器。
- operand2:表示第2个操作数。
3.“operand2”具有如下的形式:
A.#immed_8r:常数表达式
eg:
MOV R0,#1
ADD R0,R1,#0X0F
B.Rm:寄存器形式。
即在寄存器方式下,操作数即为寄存器的数值。
eg:
MOV PC,R0
ADD R1,R1,R2
C.Rm,shift:寄存器移位方式。
将寄存器的移位结果作为操作数,当Rm值保持不变。
- ASR #n:表示算术右移n位。
- LSR #n:表示逻辑右移n位。
- ROR #n:表示循环右移n位。
- RRX #n:带扩展的循环右移n位。
- LSL #n:逻辑左移n位。
4.使用条件码“cond”可以实现高效的逻辑操作,提高代码的效率。
- 所有的ARM指令都可以条件执行。
- Thumb指令只有B(跳转)指令具有条件执行功能。
注:如果执行中不表明条件码,默认为无条件(AL)执行。
2.4汇编程序中常用的符号
在汇编语言程序设计中,经常使用各种符号表示变量、常量和地址
A.符号由大小写字母、数字以及下划线组成。
B.符号区分大小写,同名的大、小写符号会被编译器认为是两个不同的符号。
C.符号在其作用范围内必须唯一,即在其作用范围内不可有同名的符号。
D.自定义的符号名不能与系统的保留字相同。
【注】符号名不应与指令或伪指令同名。
1.程序中的变量
- ARM汇编程序所支持的变量有数字变量,逻辑变量和字符串变量
- 在ARM汇编程序设计中,可使用GBLA,GBLL,GBLS伪定义声明全局变量,使用LCLA,LCLL,LCLS声明局部变量,并可使用SETA,SETL和SETS对其经行初始化。
2.程序中的常量
ARM汇编程序所支持常量有数字常量,逻辑常量和字符串常量。
3.程序中的变量代换
程序中的变量可通过代换操作取的一个常量。代换操作符为”$”。使用示例:
LCLS S1
LCLS S2 ;定义局部字符串变量S1和S2
S1 SETS “Test!”
S2 SETS “This is a $ S1”;S2的值为“This is a Test
3 ARM汇编指令详解
ARM公司针对ARM架构的芯片发布了三种指令集:
1.ARM指令集:每条指令是是 32位的,每条指令能承载更多的信息,因此使用最少的指令完成功能,所以在相同频率下运行速度也是最快的,但也因为每条指令是32位的而占用了最多的程序空间。
2.Thumb指令集:每条指令是 16位的,每条指令所能承载的信息少,因此它需要使用更多的指令才能完成功能,因此运行速度慢,但它也占用了最少的程序空间。
3.Thumb-2指令集:支持16位指令、32位指令混合编程, 当一个操作可以使用一条32位指令完成时就使用32位的指令, 加快运行速度, 而当一次操作只需要一条16位指令完成时就使用16位的指令,节约存储空间。
目前ARM架构主流是基于ARMv7架构的Cortex系列产品,由A、R、M三个系列组成,具体分类延续了一直以来ARM面向具体应用设计CPU的思路。其中A是应用处理器(Application Processor),基于虚拟存储的操作系统和应用程序而设计,支持ARM、Thumb、Thumb-2指令集,R是实时控制处理(Real Time Control)系列,支持支持ARM、Thumb、Thumb-2指令集,M微控制器(Micro Controller)系列,对价格相对敏感,仅支持Thumb 和Thumb-2指令集。
ARM公司针对不同指令集,ARM公司推出了: Unified Assembly Language UAL,统一汇编语言,你不需要去区分这些指令集。在程序前面用CODE32/CODE16/THUMB表示指令集ARM /Thumb/ Thumb2。
在具体讲解指令之前,我们首先要清楚两个重要的概念:立即数和寻址方式。
3.1立即数
立即数通常是指在立即寻址方式指令中给出的数,直接包含在指令中,可以是8位、16位或32位,该数值紧跟在操作码之后。
先看一条指令:
MOV R0, #VAL 其含义是把VAL这个值存入R0寄存器。
在ARM指令中,VAL的值必须是以下情况:
- 一个常数,可以通过将一个8位的值旋转任意偶数位来产生。
- 形式为0x00XY00XY的常数。
- 形式为0xXY00XY00的常数。
- 形式为0xXYXYXYXY的常数。
其中XY为十六进制数,取值范围为0x00 ~ 0xFF。
综上,VAL必须是立即数。
那么,立即数有何特别之处呢?在之前,需要了解数据编码的知识,对于开发者而言,开发者使用的是汇编指令,而汇编指令不是计算机能够理解的,计算机所能理解的只是 0 1 这两种数字。所以,汇编指令还要经过译码器译码成机器指令才能被计算机所理解。而汇编指令作为开发者和计算机交流的接口,那么常常会有一些限制。
对于ARM指令,汇编指令的对应的机器指令格式大致如下:
从上图可以看到,在一条指令中,只有后12位用于存放立即数,其中这 12 位又分为两部分:前7~0位是数值部分(immediate);后11~8 位是前7~0位要进行移位操作的移位数(rotate)。
其中 immediate有 8 位,也就是我们的立即数部分占 8 位,因此有如下结论:
- 如果一个立即数小于 0xFF(255)那么直接用前 7~0 位表示即可,此时不用移位,11~8 位的 immediate_rotate等于 0。
- 如果前八位 immediate的数值大于 255,那么就看这个数是否能有immediate中的某个数移位 2*immediate_rotate位形成的。如果能,那么就是合法立即数;否则非法。
因此,立即数的判定方法如下:
一个 32 位数用 12 位编码表示,符合以下规则才是合法立即数。
立即数 = immediate循环右移 (2 * immediate_rotate)
如果存在一个 immediate_rotate能够让该立即数由 immediate循环右移 2*immediate_rotate位(偶数位)表示,那么这个立即数就是合法的。
举两个例子吧。
Eg1: 0x234
0x234 用二进制表示:
0000 | 0000 | 0000 | 0000 | 0000 | 0010 | 0011 | 0100
其中的 1000 | 1101 部分循环右移 30 位可以和这个二进制数相同。因此immediate_rotate的值是 15,而 immediate的值是 1000 | 1101 即 8D。
Eg2: 0x132
0x132 用二进制表示:
0000 | 0000 | 0000 | 0000 | 0000 | 0001 | 0011 | 0010
没有任何部分循环右移偶数次可以成为该数本身,因此它不是一个合法的立即数。
以上方式是从立即数的本质入手去判断一个数是不是立即数,对于很多朋友不太友好,下面笔者告诉你个更加容易点。
立即数的标准判断的步骤:
Step1.把数据转换成二进制形式,从低位到高位写成4位1组的形式,最高位一组不够四位的,在最高位前面补0。
Step2.数1的个数,如果大于8个肯定不是立即数,如果小于等于8进行下面步骤。
Step3.如果数据中间有连续的大于等于24个0,循环左移4的倍数,使高位全为0。
Step4.找到最高位的1,去掉前面最大偶数个0。
Step5.找到最低位的1,去掉后面最大偶数个0。
Step6.数剩下的位数,如果小于等于8位,那么这个数就是立即数,反之就不是立即数。
典型例子:
(1)0x4FF (2)0x122 (3)0x234 (4)0xF000000F
Eg1: 0x4FF
Step1: 0100 1111 1111
Step2:其中1的个数是9个,大于8个,判定不是立即数
Eg 2: 0x122
Step1: 0001 0010 0010
Step2: 其中1的个数4个,小于8,继续
Step3: 其中没有连续大于等于24个0,继续
Step4: xx01 0010 0010 (最高位前面有3个0,最大偶数2,去掉2个0)
Step5: xx10 0011 0010 (最低位后面只有1个0,最大偶数0)
Step6: 剩下10 0011 0010 共10位,大于8,判定0x122不是立即数
Eg 3: 0x234
Step1: 0010 0011 010
Step2:其中1的个数4个,小于8,继续
Step3:其中没有连续大于等于24个0,继续
Step4: xx10 0011 0100
Step5: xx10 0011 01xx
Step6:剩下10 0011 01 共8位,等于8,判定0x234是立即数
Eg 4: 0xF000000F
Step1: 1111 0000 0000 0000 0000 0000 0000 1111
Step2:其中1的个数8个,没有大于8,继续
Step3:其中有连续24个0,循环左移4位,使高位全为0
0000 0000 0000 0000 0000 0000 0000 1111 1111
Step4: xxxx xxxx xxxx xxxx xxxx xxxx xxxx 1111 1111
Step5: xxxx xxxx xxxx xxxx xxxx xxxx xxxx 1111 1111
Step6:剩下1111 1111共8位,等于8,判定0xF000000F是立即数
好了,总结下,立即数就是一个特殊的数,它必须满足一定的规则,就是通过循环移位(偶数位),去掉前后偶数个0得到一个小于等于8位的数。如果还是不理解的,请多看几遍,仔细琢磨吧。
3.2 ARM寻址方式
接下里谈谈ARM的寻址方式。所谓寻址方式,是指处理器根据指令中给出的地址信息来寻找物理地址的方式,目前ARM指令系统支持以下几种寻址方式。
3.2.1立即寻址
也称为立即数寻址,这种寻址方式指令中就已经给出了操作数。也就是在执行指令的过程中,处理器取得指令的同时也取得了操作数,因此称为立即数寻址。例如:
MOV R0 ,#0X20000 ;将0X20000写入寄存器R0中
ADD R0, R0, #0x3F ;将R0中的值加上立即数0X37,再写入R0中
在上面两条指令中,源操作数就是立即数,要求以“#”开始,对于十六进制的立即数。
3.2.2寄存器寻址
操作数的值在寄存器中,指令中的地址码字段指出的是寄存器编号,指令执行时直接取出寄存器值来操作。寄存器寻址是各类微处理器常用的寻址方式,也是效率较高的寻址方式。寄存器寻址指令举例如下:
MOV R1,R2 ;将R2的值存入R1
SUB R0,R1,R2 ;将R1的值减去R2的值,结果保存到R0
3.2.3寄存器移位寻址
寄存器移位寻址是ARM指令集特有的寻址方式。当第2个操作数是寄存器移位方式时,第2个寄存器操作数在与第1个操作数结合之前,选择进行移位操作。寄存器移位寻址指令举例如下:
MOV R0,R2,LSL #3 ;R2的值左移3位,结果放入R0, ;即是R0=R2×8 ANDS R1,R1,R2,LSL R3 ;R2的值左移R3位,然后和R1相 ;“与”操作,结果放入R1 移位操作:
LSL移位操作:
LSR移位操作:
ASR移位操作:
ROR移位操作:
RRX移位操作:
3.2.4寄存器间接寻址
寄存器间接寻址指令中的地址码给出的是一个通用寄存器的编号,所需的操作数保存在寄存器指定地址的存储单元中,即寄存器为操作数的地址指针。寄存器间接寻址指令举例如下:
LDR R1,[R2] ;将R2指向的存储单元的数据读出 ;保存在R1中
SWP R1,R1,[R2] ;将寄存器R1的值和R2指定的存储 ;单元的内容交换
3.2.5基址寻址(变址寻址)
基址寻址就是将基址寄存器的内容与指令中给出的偏移量相加/减,形成操作数的有效地址。基址寻址用于访问基址附近的存储单元,常用于查表、数组操作、功能部件寄存器访问等。寄存器间接寻址是偏移量为0的基址加偏移寻址。
基址寻址指令举例如下:
LDR R2,[R3,#0x0C] ;读取R3+0x0C地址上的存储单元的内容,放入R2
LDR R0,[R1] ,#4 ;R0=[R1],R1=R1+4
LDR R0,[R1,R2]! ;R0=[R1+R2]
3.2.6多寄存器寻址
多寄存器寻一次可传送几个寄存器值,允许一条指令传送16个寄存器的任何子集或所有寄存器。多寄存器寻址指令举例如下:
LDMIA R1!,{R2-R7,R12} ;将R1指向的单元中的数据读出到R2~R7、R12中(R1自动加4) STMIA R0!,{R2-R7,R12} ;将寄存器R2~R7、R12的值保存到R0指向的存储, 单元中(R0自动加4)
3.2.7堆栈寻址
堆栈是一个按特定顺序进行存取的存储区,操作顺序为“后进先出” 。堆栈寻址是隐含的,它使用一个专门的寄存器(堆栈指针)指向一块存储区域(堆栈),指针所指向的存储单元即是堆栈的栈顶。存储器堆栈可分为两种:
向上生长:向高地址方向生长,称为递增堆栈向下生长:向低地址方向生长,称为递减堆栈
堆栈指针指向最后压入的堆栈的有效数据项,称为满堆栈。(压栈时先修改sp,后数据压栈;出栈时先数据出栈,后修改sp)
堆栈指针指向下一个待压入数据的空位置,称为空堆栈。(压栈时先数据压栈,后修改sp;出栈时先修改sp,后数据出栈)
四种类型的堆栈方式:
满递增:堆栈向上增长,堆栈指针指向内含有效数据项的最高地址。指令如LDMFA、STMFA等;
空递增:堆栈向上增长,堆栈指针指向堆栈上的第一个空位置。指令如LDMEA、STMEA等;
满递减:堆栈向下增长,堆栈指针指向内含有效数据项的最低地址。指令如LDMFD、STMFD等;
空递减:堆栈向下增长,堆栈指针向堆栈下的第一个空位置。指令如LDMED、STMED等。
3.2.8相对寻址
相对寻址是基址寻址的一种变通。由程序计数器PC提供基准地址,指令中的地址码字段作为偏移量,两者相加后得到的地址即为操作数的有效地址。相对寻址指令举例如下:
BL SUBR1 ;调用到SUBR1子程序 BEQ LOOP ;条件跳转到LOOP标号处 ...LOOP MOV R6,#1 ...SUBR1 ...
3.2.9块拷贝寻址
用于将一块数据从存储器的某一位置拷贝到另一位置。
STMIA R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之后增加,增长方向为向上增长。STMIB R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之前增加,增长方向为向上增长。STMDA R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之后增加,增长方向为向下增长。STMDB R0!,{R1-R7} ;将R1~R7的数据保存到存储器中,存储器指针在保存第一个值之前增加,增长方向为向下增长。
3.3数据处理指令
ARM内核只能在寄存器上执行数据处理,不能直接在内存上执行。所有ARM数据传送或算术逻辑运算指令均可选择使用S后缀,以使指令影响CPSR中的标志。数据处理指令(在大多数情况下)使用一个目标寄存器和两个源操作数。
数据处理指令大致可分为3类:
- 数据传送指令;数据传送指令用于在寄存器和存储器之间进行数据的双向传输;
- 算术逻辑运算指令;算术逻辑运算指令完成常用的算术与逻辑的运算,该类指令不但将运算结果保存在目的寄存器中,同时更新CPSR中的相应条件标志位;
- 比较指令。比较指令不保存运算结果,只更新CPSR中相应的条件标志位。
下表总结了数据处理过程中的常用汇编指令,给出了它们的助记操作码、操作数和它们的功能的简要描述。
1.ADC指令(带进位相加)
ADC指令的格式为:
ADC{条件}{S} 目的寄存器,操作数1,操作数2
ADC指令用于把两个操作数相加,再加上CPSR中的C条件标志位的值,并将结果存放到目的寄存器中。它使用一个进位标志位,这样就可以做比32位大的数 的加法,注意不要忘记设置S后缀来更改进位标志。操作数1应是一个寄存器,操作数2可以是一 个寄存器,被移位的寄存器,或一个立即数。
以下指令序列完成两个128位数的加法,第一个数由高到低存放在寄存器R7~R4,第二个数由高到低存放在寄存器R11~R8,运算结果由高到低存放在寄 存器R3~R0。
指令示例:
ADDS R0, R4, R8 ; 加低端的字ADCS R1, R5, R9 ;加第二个字,带进位ADCS R2, R6, R10 ;第三个字,带进位ADC R3, R7, R11 ;加第四个字,带进位
2.ADD指令(相加)
ADD指令的格式为:
ADD{条件}{S} 目的寄存器,操作数1,操作数2
ADD指令用于把两个操作数相加,并将结果存放到目的寄存器中。操作数1应是一个寄存器,操作数2可以是一个寄存器,被移位的寄存器,或一个立即数。
指令示例:
ADD R0, R1, R2 ; R0 = R1 + R2ADD R0, R1, #256 ; R0 = R1 + 256ADD R0, R2, R3, LSL#1 ; R0 = R2 + (R3 << 1)
举报