ARM指令学习笔记
参考---------- <百度> "arm百度百科","NDS百度百科" 百度>
本文引用地址://m.amcfsurvey.com/article/201611/316807.htm接下来就是对arm指令的学习。
因为有过前面8086指令的学习,并且也写过像高精度计算这样的汇编程序,看arm指
心里面老在比较这两套指令。
arm7TDMI(-S)指令系统有两套指令集,分别是32位的Arm指令集和16位的thumb指令集。简单点说:arm支持arm内核的所有特点,具有高效、快速的特点;而thumb指令集灵活、小巧。二者可以互相调用,Thumb指令集可以看做是Arm的压缩形式的子集,是针对代码的密度问题而提出的,Thumb指令都有对应的Arm指令,但却不是一个完整的系统,例如:Thumb指令集没有协处理器指令,信号量指令以及访问CPSR或SPSR的指令,没有乘加指令及64位乘法指令等,且指令的第二操作数受到限制;除了跳转指令B有条件执行功能外,其它指令基本为无条件执行.,等等。不一一叙述。而Arm指令集出了具有很多Thumb没有的功能外,它最大的特点就是:高效。
Arm的寄存器是37个,包括
31generalregisters(Rxx)
6status registers(xPSR)
对这37个寄存器的详细描述我们可以从nocash.emubase.de这个网站上得到。
学习arm指令,最先接触的是寻址指令。
Arm寻址指令可分为九类:
Arm指令 80x86中有吗?
1.寄存器寻址;
2.立即寻址;
3.寄存器移位寻址; 无
4.寄存器间接寻址;
5..基址寻址;
6.多寄存器寻址; 无
7.堆栈寻址; 无
8.块拷贝寻址; 无
9.相对寻址。
可以看到arm寻址指令里面有个很大的特色是它的寄存器移位寻址,即第二个操作数在与第一个操作数结合之前可以选择进行移位操作,例如:MOVR0,R2,LSL#3。而在80x86指令中这得要三步走:一次赋值(否则影响寻址变量的值)、一次移位、一次寻址。不仅带来了视觉上的不便,而且给书写带来了麻烦(更容易出错)。另外,Arm指令还可以进行多寄存器寻址,无疑简化了操作(具体点讲,就是少写很多"LD""ST")。还可以看到我们在arm指令中只需加个"!"就可以决定中间值是否保留。更加方便的是我们可以自由选择变址前后指针的变化,例如块拷贝寻址中就有四种:STMIA,STMIB,STMDA,STMDB.连递减还是递增,先变址还是先复制,Arm都给你预先设计好了,不能不说它周全,这些都是80x86里没有的,极大地方便了程序员的程序设计。
顺带说一下其另一个便捷之处,加载/装填数据时,可以在命令后加H/B来表示对半字/字节的数据操作,默认情况下是字。并且ARM可以实现一组寄存器和一块连续内存之间传送数据,如LDMIA和STMIA指令。
看完寻址,接下来才是Arm指令的重点,
Arm指令的基本格式为:
————————arm的32伪指令二进制格式
其中<>内的项是必须的,而{}内的项是可选的。
Opcode:指令助记符
Cond:执行条件
S:是否影响cpsr寄存器的值
Rd:目标寄存器
Rn:第一个操作数的寄存器
Oprand2:第二个操作数
ARM指令可以分为6大类
(1) 跳转指令 B、BL、BLX、BX
(2)数据处理指令 数据传送算术逻辑运算比较
(3)程序状态寄存器传输指令
(4)load/store指令
(5)协处理指令
(6)异常中断指令
可以看到
MOVE rcra
CMP rarb
JG NEXT
MOVE rcrb
NEXT
..............
而在arm中可以很方便地写成
MOVE rcra
CMP rarb
MOVECC rcrb
灵活运用第二个操作数还能大大提高代码的效率,例如我们要将r1寄存器中的数乘以9,可以在arm中很方便地写成
ADD R1,R1,R1,LSL#3
换成80x86,那就是一堆步骤了
MOVE R2,R1
SHL R2,3
ADD R1,R2
就是说arm的指令更人性化,加上后来对arm伪指令的学习,我个人觉得arm指令集是汇编中的高级语言了。
在ARM中乘法操作可以用于任意两个寄存器结果可以保存到任意寄存器,而80x86则需要先将被乘数保存于AL/AX中再做乘法。并且高位和地位还要分开保存至AX/AL和DX/AH中,麻烦!
ARM的中断指令有:
(1)复位异常
(2)未定义指令异常
(3)软件中断异常
(4)预取指中断异常
(5)数据中止异常
(6)中断请求异常
(7)快速中断(FIQ)请求异常
种类繁多学起来很头疼,不像80x86从头到尾就是INT中断那样清爽。中断这块本人不甚明白,还需继续努力学习,
接下来的学习就到了伪指令。首先伪指令不是ARM指令集中的指令,只是为了编程方便编译器定义了伪指令,使用时可以像其他ARM指令一样使用,但在编译时这些指令会被等效的ARM指令代替。虽说有些伪指令只是一些极其简单的替换,但却极大地方便我们编程。可以说它是以它是一种特殊的助记符。
对于伪指令的学习也只是概念性的,没法深刻,很多指令虽然知道意思但完全不知道哪儿用的着。有待今后的时间吧。
总的来说,ARM指令有一下几种
1.符号定义伪指令
全局变量声明:GBLA、GBLL和GBLS。
局部变量声明:LCLA、LCLL和LCLS。
变量赋值:SETA、SETL和SETS。
为一个通用寄存器列表定义名称:RLIST。
为一个协处理器的寄存器定义名称:CN。
为一个协处理定义名称:CP。
为一个VFP寄存器定义名称:DN和SN。
为一个FPA浮点寄存器定义名称:FN。
2.数据定义伪指令
声明一个文字池:LTORG。
定义一个结构化的内存表的首地址:MAP。
定义结构化内存表中的一个数据域:FIELD。
分配一块内存空间,并用0初始化:SPACE。
分配一段字节的内存单元,并用指定的数据初始化:DCB。
分配一段字的内存单元,并用指令的数据初始化:DCD和DCDU。
分配一段字的内存单元,将每个单元的内容初始化为该单元相对于静态基址寄存器的偏移量:DCDO。
分配一段双字的内存单元,并用双精度的浮点数据初始化:DCFD和DCFDU。
分配一段字的内存单元,并用单精度的浮点数据初始化:DCFS和DCFSU。
分配一段字的内存单元,并用单精度的浮点数据初始化,指定内存单元存放的是代码,而不是数据:DCI。
分配一段双字的内存单元,并用64位整数数据初始化:DCQ和DCQU。
分配一段半字的内存单元,并用指定的数据初始化:DCW和DCWU。
断言错误:ASSERT。这个指令比较神奇,可以在程序首写一些诸如ASSERTtop<>temp的断言错误指令,在汇编编译器对汇编程序的第二遍扫描中,如果其中
ASSERT条件不成立,ASSERT伪指令将报告该错误信息,从而减少错误。有点像C++中try和catch。
3.汇编控制伪指令/宏伪指令
汇编控制伪指令用于条件汇编、宏定义、重复汇编控制等。该类伪指令如下:
条件汇编控制:IF、ELSE和ENDIF
宏定义:MACRO和MEND
重复汇编:WHILE及WEND
这些就是它有点像高级语言的地方,可以用伪指令实现某些高级语句。其实高级语言不就是一个个汇编指令的打包吗?大同小异。
值得提及的是MACRO和MEND,这个东西感觉就是C中的#define,很强大。
其伪指令格式:
MACRO
{$label}macroname{$parameter}{$parameter}…
其中:$label宏指令被展开时,label可被替换成相应的符号,通常为一个标号在一个 符号前使用$表示被汇编时将使用相应的值替代$后的符号。
macroname所定义的宏的名称。
$parameter宏指令的参数。当宏指令被展开时将被替换成相应的值,类似于函数 中的形式参数。
可以实现参数的传递!!这种伪指令让人看上去就有想去尝试使用的冲动,以后一定会有机会的!
这里简单假想下使用:
C语言中:#definebigger(a,b)(a>b)
可以写成(可能会有错,仅仅尝试一下而已):
MACRO
$labelbigger$a,$b
$label
;GL1为一个定义的全局变量
CMP $a,$b
MOVEGT GL1,1
MOVELE GL1,1
MEND
调用testbiggera,b
然后在GL1中得到大小结果
当然我们可以直接比较,这里只是为了演示一下。
4.杂项伪指令
杂项伪指令在汇编编程设计较为常用,如段定义伪指令,入口点设置伪指令,包含 文件伪指令,标号导出或引入声明等,该类伪指令如下:
边界对齐:ALIGN。
段定义:AREA。
指令集定义:CODE16和CODE32。
汇编结束:END。
程序入口:ENTRY。
常量定义:EQU。
声明一个符号可以被其它文件引用:EXPORT和GLORBAL。
声明一个外部符号:IMPORT和EXTERN。
包含文件:GET和INCLUDE。
包含不被汇编的文件:INCBIN。
保留符号表中的局部符号:KEEP。
禁止浮点指令:NOFP。
指示两段之间的依赖关系:REQUIRE。
堆栈8字节对准:PEQUIRE8和PRESERVE8。
给特定的寄存器命名:RN。
标记局部标号使用范围的界限:ROUT。
最后一块是C与汇编混合编程。
(1)c中内嵌汇编。曾今看过个故事:一个物理学家写一个模拟天体运行的程序,分别从算法和指令两方面经行优化,用了一个月的时间将一个原本要几年才能出结果的程序缩短到十几分钟。这其中有个很重要的一步,就是将某些重用性很大的高级语言程序块用汇编语言直接书写。大大缩短了程序运行的极限时间。曾今也好奇过,高级语言里面怎么去内嵌汇编?
_asm
{
指令[;指令]
...
[指令]
}
内嵌汇编程序对寄存器、常量、标号等有很多限制,就不多说了。
(2)汇编中内嵌c语言程序
(3)C与汇编互相调用
在学习ARM指令的过程中,遇到过很多问题,第一次碰到往往非常不解,还有些个该注意的地方,当然了,问题和需呀注意的地方远不止这些。这些只是个人觉得一些比较典型的,写在这里与诸位分享:
1.#immed_8r常数表达式时“该常数必须对应8位位图,即常数是由一个8位的常数循环移位偶数位得到的。”
其意思是这样:#immed_8r在芯片处理时表示一个32位数,但是它是由一个8位数(比如:01011010,即0x5A)通过循环移位偶数位得到(10000000000000000000000000010110,就是0x5A通过循环右移2位(偶数位)的到的)。
而10100000000000000000000000010110,就不符合这样的规定,编译时一定出错。因为你可能通过将10110101循环右移位得到它,但是不可能通过循环移位偶数位得到。
10110000000000000000000000010110,也不符合这样的规定,很明显:101101011有9位。
2.什么叫带符号扩展.
当从16位向32位赋值时,若选择无符号扩展,那高位补零。选择有符号扩展,那32中的16位按照16位最高位补齐。
例如1101010110101010------->11111111111111111101010110101010
0101010110101010------->000000000000000101010110101010
关于为什么这样补,可以参照补码定义,这里介绍一种简单的补码计算方法:
N位绝对值为k的数的补码为:2^n-k.比那个取反加一得来得清爽一点。
3.我们说有四种类型的堆栈寻址方式,LDMFA,STMFA,LDMEA,STMEA。
注意F表示full,E表示empty,A表示after,B表示before。
我们假设:在C语言中stack[]为堆栈数组,top为堆的顶指针。为方便理解。我用c语言描述了一下。
堆栈是一种数据结构,按先进后出(FirstInLastOut,FILO)的方式工作,使用一个称作堆栈指针的专用寄存器指示当前的操作位置,堆栈指针总是指向栈顶。
当堆栈指针指向最后压入堆栈的数据时,称为满堆栈(FullStack),而当堆栈指针指向下一个将要放入数据的空位置时,称为空堆栈(EmptyStack)。
同时,根据堆栈的生成方式,又可以分为递增堆栈(AscendingStack)和递减堆栈(DecendingStack),当堆栈由低地址向高地址生成时,称为递增堆栈,当堆栈由高地址向低地址生成时,称为递减堆栈。这样就有四种类型的堆栈工作方式,ARM微处理器支持这四种类型的堆栈工作方式,即:
◎Fulldescending满递减堆栈
堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。
ARM-Thumb过程调用标准和ARM、ThumbC/C++编译器总是使用Fulldescending类型堆栈。
C语言表示:stack[--top]=value
◎Fullascending满递增堆栈
堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向堆栈最后一个元素(最后一个元素是最后压入的数据)。
C语言表示:stack[top--]=value
◎Emptydescending空递减堆栈
堆栈首部是低地址,堆栈向高地址增长。栈指针总是指向下一个将要放入数据的空位置。
C语言表示:stack[++top]=value
◎Emptyascending空递增堆栈
堆栈首部是高地址,堆栈向低地址增长。栈指针总是指向下一个将要放入数据的空位置。
操作堆栈的汇编指令
C语言表示:stack[top++]=value
4.算术位移/逻辑位移/循环位移
算术位移,逻辑位移逻辑右移最高位补0,最低位进入CF,相当于每移一位除以2,一般对于无符号数使用 如:133/8=16余5 MOVAL,10000101B MOVCL,03H SHRAL,CL AL=10H=16 算术右移最高位(即符号位)保持不变,而不是补0最低位进入CF.相当于每移一位除2,一般对于有符号数使用8/8 MOVAL,10000000B MOVCL,03H SARAL,CL AL=0F0H=-16
----------分别对应逻辑左移、逻辑右移、算术右移、循环右移
5.关于ARM的B,BL跳转指令
假设跳转指令处的地址是A,跳转目标处的地址是B.
B,BL指令保存的是偏移地址,这个地址的计算方法是:
1.B-(A+8).A+8是因为ARM的流水线使得指令执行到A处时,PC实际的值是A+8.2.第一步得到的值是4的倍数,因为ARM的指令是4对齐的,即最低两位为00.于是将这个值右移两位.
3.得到最终偏移
执行时:
1.取出偏移
2.左移两位
3.加入PC,这时PC的值刚好为目标处的地址值,即目标地址指令进入取指,流水线前两级被清空
但是为什么是减去8呢?这因为ARM7是三级流水线。
那什么三级流水线是什么?
PC代表程序计数器,流水线使用三个阶段,因此指令分为三个阶段执行:1.取指(从存储器装载一条指令);2.译码(识别将要被执行的指令);3.执行(处理指令并将结果写回寄存器)。即执行时取指已经提前两个字了,即8个字节。
6.什么是软中断?
软中断是利用硬件中断的概念,用软件方式进行模拟,实现宏观上的异步执行效果。很多情况下,软中断和"信号"有些类似,同时,软中断又是和硬中断相对应的,"硬中断是外部设备对CPU的中断","软中断通常是硬中断服务程序对内核的中断","信号则是由内核(或其他进程)对某个进程的中断"(《Linux内核源代码情景分析》第三章)。 软中断的一种典型应用就是所谓的"下半部"(bottomhalf),它的得名来自于将硬件中断处理分离成"上半部"和"下半部"两个阶段的机制:上半部在屏蔽中断的上下文中运行,用于完成关键性的处理动作;而下半部则相对来说并不是非常紧急的,通常还是比较耗时的,因此由系统自行安排运行时机,不在中断服务上下文中执行。bottomhalf的应用也是激励内核发展出目前的软中断机制的原因。 软中断是linux系统原“底半处理”的升级,在原有的基础上发展的新的处理方式,以适应多cpu、多线程的软中断处理。 一般来说,软中断是由内核机制的触发事件引起的(例如进程运行超时),但是不可忽视有大量的软中断也是由于和硬件有关的中断引起的,例如当打印机端口产生一个硬件中断时,会通知和硬件相关的硬中断,硬中断就会产生一个软中断并送到操作系统内核里,这样内核就会根据这个软中断唤醒睡眠在打印机任务队列中的处理进程。 在网络编程中,软中断用来引发协议层代码的执行
7.关于程序状态的切换
程序不能通过修改直接修改CPSR中的T控制位直接将程序状态切换到Thumb状态,必须通过BX等指令完成程序状态的切换
8.对于LDMIA指令,Rn的最终值是加载的值,而不是增加后的地址
评论