本期和大家主要分享的ARM汇编指令集中的内存操作类指令,涉及到内存操作,不得不说CPSR,SP,LR,PC是非常重要的几个寄存器,所以接下来就来具体观察一下其内存窗口的具体变化!
相对跳转: b fun 基于当前pc前后32M范围寻找标号,跳转到标号处执行代码
bl fun 基于当前pc前后32M范围寻找标号,跳转到标号处执行代码,保存返回地址到LR
修改pc的值(两种修改pc的值)
mov pc, #0x100
ldr pc, =fun ;将fun标号的链接地址给pc
绝对跳转第一种给pc赋值的缺点是:当pc赋值处的汇编代码之前需要加入代码时,那么pc处的指令就不是原来的指令了,发生了变化;但是绝对跳转的第二种方式与相对跳转其实都是把将要执行指令的地址装载给PC,进而实现指令的跳转;
接下来是非常重要的一部分,内存操作指令能够实现将某一个值写到指定地址的位置处;这样就能够实现函数的跳转(比如用堆栈记录函数的入口地址等);
内存操作指令:实现寄存器与内存之间的数据交互;
先从字面上来理解一下:
LDR LD=LoaD(加载) R=Register(寄存器) ;
STR ST=STore(存储)R=Register(寄存器)
LDR:加载指定位置32位数据到目标寄存器;(从固定地址处进行取值)
STR:保存指定位置32位数据到目标位置;(给固定位置进行赋值)
这里给出不同指令下进行数据操作的位数:
ldrb/strb 8位操作
ldrh/strh 16位操作
ldr/str 32位操作
ldrd/strd 64位操作
注意:ldr r0, fun === 基于当前pc前后4k,寻找标号,将fun标号地址处的内容给r0
伪指令和指令一样都是会生成机器码的指令,但伪指令与指令的区别是:每条汇编指令都有对应的唯一一条机器码,而伪指令可以有很多一条机器码,也可以理解为伪指令是有很多一条汇编指令组合而成。
ldr伪指令可以在立即数前加上=,以表示把一个地址写到某寄存器中
ldr伪指令和mov是比较相似的。只不过mov指令限制了立即数的长度为8位,也就是不能超过512。ldr伪指令没有这个限制。
如果使用ldr伪指令时,后面跟的立即数没有超过8位,在实际汇编的时候该ldr伪指令是被转换为mov指令的。
伪操作:告诉编译器怎么去编译指令,而它本身不生成机器码;
mov r0, #0x40000010
mov r1, #3
str r1, [r0]
ldr r2, [r0]
以下代码实现的功能是将0x40000010这个地址赋给寄存器,紧接着将3这个立即数给寄存器r1,接下来str指令将立即数3写入到地址为0x40000010的地址位置处;ldr指令实现的操作是将0x40000010处存放的数取出来存入寄存器r2中,接下来看一下寄存器和内存观察窗口;
ldr r0, [r1, #4] === r0=*(r1+4) r1=r1
str r0, [r1, #4] === *(r1+4)=r0 r1=r1
ldr r0, [r1, r2] === r0=*(r1+r2) r1=r1
str r0, [r1, r2] === *(r1+r2)=r0 r1=r1
ldr r0, [r1], #4 === r0=*(r1) r1=r1+4
str r0, [r1], #4 === *(r1)=r0 r1=r1+4
ldr r0, [r1], r2 === r0=*(r1) r1=r1+r2
str r0, [r1], r2 === *(r1)=r0 r1=r1+r2
ldr r0, [r1, #4]! === r0=*(r1+4) r1=r1+4
str r0, [r1, #4]! === *(r1+4)=r0 r1=r1+4
ldr r0, [r1, r2]! === r0=*(r1+4) r1=r1+r2
str r0, [r1, r2]! === *(r1+4)=r0 r1=r1+r2
注意:这里!表示地址一定会发生变化;
快拷贝:完成多个寄存器和连续内存空间数据交互,小编号寄存器对应低地址空间, 大编号寄存器对应高地址空间
ldm:(load much)多数据加载,将地址上的值加载到寄存器上
stm:(store much)多数据存储,将寄存器的值存到地址上
(1)IA:(Increase After) 每次传送后地址加4,其中的寄存器从左到右执行,例如:STMIA R0,{R1,LR} 先存R1,再存LR;
(2)IB:(Increase Before)每次传送前地址加4,同上;
(3)DA:(Decrease After)每次传送后地址减4,其中的寄存器从右到左执行,例如:STMDA R0,{R1,LR} 先存LR,再存R1;
(4)DB:(Decrease Before)每次传送前地址减4,同上;
(5)FD: 满递减堆栈 (每次传送前地址减4);
(6)FA: 满递增堆栈 (每次传送后地址减4);
(7)ED: 空递减堆栈 (每次传送前地址加4);
(8)EA: 空递增堆栈 (每次传送后地址加4);
此处借鉴文章:多数据操作指令
ldm r0, {r1,r2,r3} === r1=*r0 r2=*(r0+4) r3=*(r0+8) r0=r0(这里如果不是按照r1,r2,r3进行顺序排布,那么CPU也会自动进行从小到大进行排列)
stm r0, {r1-r3} === *r0=r1 *(r0+4)=r2 *(r0+8)=r3 r0=r0
ldmia/stmia 先内存操作,后地址+4
ldmda/stmda 先内存操作,后地址-4
ldmib/stmib 先地址+4,后内存操作
ldmdb/stmdb 先地址-4,后内存操作
默认 的栈是满减栈;
满减栈 sp(stack pointer)指向的内存空间不能直接使用,栈的生长方向向下;
满增栈 sp指向的内存空间不能直接使用,栈的生长方向向上;
空减栈 sp指向的内存空间可以直接使用,栈的生长方向向下 ;
空增栈 sp指向的内存空间可以直接使用,栈的生长方向向上;
ldmfd、stmfd 满减
ldmfa、stmfa 满增
ldmed、stmed 空减
ldmea、stmea 空增
入栈: stmfd sp!, {r0-r12, lr} sp为栈指针,把r0-r12, lr中的数据依次入栈;*(sp-4)=r0,*(sp-4*2)=r1,*(sp-4*3)=r2,........,*(sp-4*15)=lr;
出栈: ldmfd sp!, {r0-r12, lr} r0-r12, lr中的数据依次出栈;lr=[sp-1*4],r13=[sp-2*4],r12=[sp-3*4],......,r0=[sp-15*4]
出栈: ldmfd sp!, {r0-r12, lr}^ ^表示模式恢复(因为没对pc赋值,所以^的表示将数据恢复到User模式的[r0-lr]寄存器组中)
特殊寄存器 cpsr 的读写访问只能使用 msr 和 mrs 指令
msr:将普通寄存器中的数据写到特殊寄存器中;
mrs:将特殊寄存器中的数据写到普通寄存器中;
msr cpsr, r0 === cpsr=r0
msr cpsr_c, r0 === keil中使用
mrs r0, cpsr === r0=cpsr
mrs 指令: 对状态寄存器CPSR和SPSR进行读操作。通过读CPSR可以获得当前处理器的工作状态。读SPSR寄存器可以获得进入异常前的处理器状态(因为只有异常模式下有SPSR寄存器)。
msr指令: 对状态寄存器CPSR和SPSR进行写操作。与MRS配合使用,可以实现对CPSR或SPSR寄存器的读、修改、写操作,可以切换处理器模式、或者允许/禁止IRQ/FIQ中断等。
MRS 指令允许将 CPSR 或 SPSR_的内容移动到一个通用寄存器中去。
MSR 指令允许将一个通用寄存器中的内容移动到 CPSR 或 SPSR_寄存器中去。
以下顺序执行了一个模式的改变:
MRS R0, CPSR ;复制 CPSR 到 R0
BIC R0, R0, #0x1F ;清零模式位
ORR R0, R0, #new_mode ;选择新的模式
MSR CPSR, R0 ;写回到修改了的 CPSR
swi #12 软件中断指令,数值存放在机器码的低24位,可以利用数值进行区分
svc #12 目前将swi更新为svc指令
该指令可以用来触发软件中断;
协处理器操作:辅助主处理器进行电路管理的处理器;(比如计算一些浮点型的数据)
一般处理器有p0—p15的某一些,arm9 只有p15 协处理器;c0-c15 寄存器 32位;
MRC p2, 5, R3, C5, C6, ;请求协处理器 2 用 c5 和 c6 执行操作数 5,将结果(有符号 32 位字)传输回 R3
MRCEQ p3, 9, R3, c5, c6 2 ;条件请求协处理器 3 用 c5 和 c6 执行操作数 9(类型 2),将结果传输回 R3
在 ARM 状态,所有的指令都可以按照 CPSR 状态码和指令条件字段的状态来有条件地执行。此字段(位[31:28])确定了在什么情况下哪一个指令被执行。如果 C,N,Z 和 V 标志位的状态符合字段的条件码,将执行指令,否则忽略不执行。
有 16 种可能的条件,每种表示为在指令助记符后附加两个字符后缀。例如,一个分支(汇编语言中的 B)跳
转指令变成 BEQ 为“如果相等则分支跳转”,这意味着只有 Z 标志位被置位了才会执行分支跳转。
本期和大家主要分享的是ldr、str、ldm、stm、msr、mrs、swi、svc、mrc等ARM指令的具体介绍,对这些基础概念掌握后,接下来就能够读懂基本的2440A的启动代码了,也有助于自己去独立的书写启动代码,依次更好的实现自己想要实现的功能,更好的理解正式启动代码设计流程的巧妙之处,对CPU处理事务的流程以及处理细节会有更深的认识;