Arm入门第六讲 伪指令与Load/Store架构
ARM 汇编器支持ARM伪指令,这些伪指令在汇编阶段被翻译成ARM或者Thumb(or Thumb-2)指令(或者指令序列)
ARM伪指令不是ARM指令集中的指令,只是为了编程方便,编译器定义了伪指令。 使用的时候可以像其它的ARM指令一样使用,但是在编译的时候这些伪指令将被等效的ARM指令代替。
ARM伪指令分为如下:
- ADR 伪指令,装载程序相关,或寄存器相关地址到寄存器
- MOV32装载32位常数或地址到寄存器(ARMV6T2体系以上支持)
- LDR装载32位常数到地址寄存器(所有ARM版本均支持)
一丶伪指令
ADR伪指令
ADR伪指令会被汇编器编译成一条指令。 汇编器通常使用 ADD或者SUB 指令来实现地址装载功能。 意思就是ADR便于编写,但是底层会用别的指令来代替。 在inter x86指令集下也有伪指令的概念。 在这里如果不能用一条指令来实现ADR伪指令的功能,那么汇编器将报告错误。
ARD{cond}{.W} register,lable
cond: 可选的指令执行条件
.W : 也是可选项,指定指令的宽度
register: 目的寄存器
label: 地址,或者基于PC或者具有寄存器的表达式。
例子:
LDR R0,_START ;从内存地址_start的地址把值读入,设 R0 = 0XE1A00000
ADR R0,_START; 将_START的地址给R0,他是与位置无关的。
也就是基于地址的偏移值。 也就是当前的PC值加上_START的偏移量。
ARM中的汇编指令与ARM中的伪指令都有LDR 但是注意,汇编中的LDR与伪指令中的LDR是不一样的。
ADRL 中等范围地址读取伪指令
ADRL伪指令将基于PC的相对偏移的地址或者基于寄存器对偏移的地址读取到寄存器中,当地址是字节对齐的时候,取值范围位-64kb~64kb之间,当地址值是 字对齐的时候他的取值范围是 -256-~256kb之间。 当地址是16字节对齐 的时候,那么取值范围更大。在32BIT的Thumb-2指令中,地址取值范围为 -1MB~1MB
其实他与ADR指令一样,唯一不同就是读取的地址范围比ADR广。在编译阶段中,ADRL将产生两条指令,其中一条为多余指令。如果汇编器不能再两条指令内完成操作也会报告错误。
注意:
ADRL只能是在ARM汇编或者Thumb-2汇编中,Thumb汇编器不支持ADRL伪指令。
使用ADRL指令的时候那么被装载的地址必须和伪指令ADRL是一个段中
ADRL{cond} register,label
cond: 可选的指令执行条件
register: 目标寄存器
label: 地址,或者基于PC或者具有寄存器的表达式。
MOV32 伪指令
MOV32伪指令是装载一个32位常数或者地址 到寄存器中。与ADR ADRL 不同,他装载的地址是与地址位置相关的。
一般在汇编层面,汇编器会把它翻译成 MOV 或者 MOVT指令,这样任何32bit常亮都可以被装载到寄存器。
MOV32{COND} register,expr
cond 可选的指令执行条件
reg: 目标寄存器
expr: 表达式的形式,有以下几种。
symbol程序中定义的标号地址。
constant任意32bit常数
symbol+constant地址标号+32bit常数。
LDR 伪指令
LDR伪指令用于装载一个32bit常数和一个地址到寄存器。
LDR{COND}{.W} register, =[expr | label-expr]
cond 可选的指令执行条件
.W 可选用来指定指令宽度(Thumb-2指令支持)
reg: 目标寄存器
expr: 32位常亮表达式,汇编器会根据expr的取值情况,对LDR伪指令做如下处理。
1.expr表示的地址值 没有超过MOV 或者MOVN伪指令的地址取值范围,那么LDR在底层会用MOV和MVN来替换LDR
2.如果超出了MOV MVN 那么汇编器会将数据防暑数据缓存池(理解为内存)同时用一条基于PC的LDR伪指令来读取这个常数。
label-expr方式:
1.一个程序相关或声明位外部的表达式,那么汇编器会将label-expr表达式的值方式数据缓存池(内存)使用一条程序相关的LDR伪指令将该值取出放入寄存器。
2.一个程序相关或声明为外部表达式,汇编器会将LABEL_EXPR表达式的值放入数据缓存池,然后使用一条程序相关的LDR伪指令将该值取出并放入寄存器。 当lable-expr被声明位外部的表达式的时候,汇编器将在目标文件中插入链接重定位伪操作,并且由连接器在连接的时候生成改地址。
使用说明:
? LDR加载的地址是绝对地址,也就是与PC相关的地址。 当要装载的数据不符合MOV MVN指令的装载的时候,它会先放到数据缓存池(内存中),此时LDR伪指令处的PC值到数据缓存池中目标数据所在的地址的偏移量有一定的限制。 ARM或者32bit的Thumb-2指令中这个范围是-4kb~4kb。 Thumb或者16bit的Thumb-2指令中这个范围是0~1kb.
例子:
区别正常的LDR汇编指令与伪指令的LDR只需要看后面的=是否有
LDR R3,=0xff0 ;这句话是将0xFF0给R3。 它符合mov指令
;真正的汇编则会用 MOV R3,#0xff0 替代上条指令
LDR R2,=place; place是标号地址,这里是将标号地址读入R2中
汇编层面的指令则会变成如下:
LDR R2,[pc,offset_to_litpool]
...
litpool DCD place
二丶Load/Store架构
不是每个Arm指令都支持直接的内存处理,必须通过专门的指令把数据先从内存(Load)加载到寄存器,然后在处理数据,处理后在放到内存中(Store)
处理指令包括如下
- 单个寄存器数据传入 LDR/STR
- 块数据传输: LDM/STM
- 单个数据交换: SWP
单个寄存器数据读取指令
LDR 类型数据加载指令
LDR 字数据加载指令
LDR{条件} 目的寄存器,<存储器地址>
LDR指令是从存储器地址读取一个32位的双字数据传送到目的寄存器中。
这个指令通常用于将内存中的数据读取到通用寄存器,然后处理数据。如果目的寄存器是PC(R15 == EIP)寄存器,那么内存中读取的地址就会当做目标地址,从而实现程序的跳转。
例子:
LDR RO,[R1] ; 读取内存中R1的32位数据到R0中
LDR RO,[R1,R2]; 读内存数据R1+R2的32位数据到给R0
LDR R0,[R1,#8] ; 将R1+8的地址中的32位数据到R0。
重点 后面带有!:
LDR R0,[R1 + R2]!; R1+R2的地址中的32位数据给R0,并且R1 + R2的值赋值给R1. 意思就是不光给R0数据 R1还把自身给修改了。
LDR R0,[R1,#8]!; R0 = R1+8的地址中的值。 R1 = R1 + 8
LDR R0,[R1],R2; 将存储器R1的值读取出来给R0。 然后R1 = R1 + R2
LDR R0,[R1,R2,LSL#2]!; 将 R1 + (R2 * 4)的地址里面的值给R0,并且 R1 = R1 + R2*4
LDR R0,[R1],R2,LSL#2; 将存储区R1的值给R0,R1 = R1 + R2 * 4
LDRB 字节数据加载指令
LDRB指令是用于将存储器中一个8位的字节数据传送到目的寄存器中。同时将寄存器的高24位清零。 这个指令通常是读取8位的数据到通用寄存器,然后处理数据。 如果目的寄存器是PC寄存器,那么可以实现程序流程的跳转。
LDR{条件}B 目的寄存器,<存储器地址>
LDRB R0,[R1]; 将存储器地址R1的字节数据读入R0,并且将R0的高24位清零。
LDRB R0,[R1,#8]; R1+8的字节数据给R0,R0&= 0x000000FF;
LDRH 字数据加载指令
H就是一个16位数据。C语言中是一个 short 或者Unsigned short
这个指令是读取16位数据给通用寄存器。其效果同上。唯一不同的就是读取到寄存器中的时候高16位清零。
指令条件如下:
LDR{条件}H 目的寄存器,<地址>
LDRH R0,[R1]
单个寄存器数据设置指令
STR 双字数据存放指令
STR与LDR与之对应,并且相反。 是用于将一个32位的数据传送到存储器中。
STR{条件} 源寄存器,<存储器地址>
STR R0,[R1],#8; R0的数据设置到R1地址中,并且R1 = R1 + 8
STR R0,[R1,#8]; R0的数据存放到R1+8的地址中。
STRB 字节数据存储指令
与LDRB与之对应,是将一个字节(8位)数据设置到目的地址中
STR{条件}B 源寄存器,<存储器地址>
STRB R0,[R1]; 寄存器R0中的字节写入到R1中
STRB R0,[R1,#8]; 寄存器R0中的数据写入到 R1+8中
STRH 字数据存储指令
与LDRH与之对应,是将寄存器的低16位数据传送到存储器中。
STR{条件}H 源寄存器,<存储器地址>
STRH R0,[R1]; 寄存器R0中的字写入到R1中
STRH R0,[R1,#8]; 寄存器R0中的数据写入到 R1+8中
其它LDR/STR变形指令
LDR或者STR指令,后面都可以跟H或者B 代表2个字节或者一个字节。
而我们后面也可以跟 SH SB 代表有符号的2个字节 和有符号的一个字节。
如下:
LDRSB R0,[R1]
STRSB R1,[R0]
LDRSH R1,[RO]
STRSH RO,[R1]
块传输指令
LDM/STM简介
? 上面所讲的LDR STR指令,最高可操作的是4个字节。 但是ARM也支持一块指令的传输。 那就是 LDM,STM块传输指令。LDM STM的操作数1后面会跟着! 代表的意思就是我们是块操作。
LDM/STM块传输
看例子:
LDM R0!,{R3-R9} ; 含义是R0保存的是基地址,然后R3-R9用于保存R0中的数据 R3-R9 = 7个寄存器 每个寄存器4个字节。那么就是保存4*7 = 28个字节。 意思就是从R0读取28个字节给R3-R9保存
STMIA R1!,{R3-R9}; 从R3-R9寄存器中读取的数据写入到R1的基地址中
注意: 这里的寄存器的操作顺序与LDR是不同的。 是将R0给R3-R9 而LDR 是从操作数2读取数据给操作数1的
其中LDM后面可以跟
- IA (Increase After) 基址寄存器在执行指令后增加,意思就是执行完指令,然后自身地址自增,然后再从自增的地方继续执行指令。高级语言中可以理解为 i++
- IB: increase Before 基址寄存器在执行指令前增加,仅(arm),意思就是在执行此指令的时候,先自增然后在执行指令。 ++i
- DA: (Decrease After),基址寄存器在执行指令后减少(仅ARM) 同IA相反的操作。 高级语言中可以理解为 i--
- DB: (dEcrease Before) 基址寄存器在执行指令前减少,可以理解为--i
例子: 使用LDM STM实现数据拷贝
设R12 = 要拷贝的数据的起始地址(Src 等价于x86 ESI) Src_start
设R13 = 目的的存储地址
设R14 = 要拷贝数据的终止地址 src_end
LOOP:
LDMIA R12!,{R0-R11}; R0-R11存储了R12的数据
STMIA R13!,{R0-R11}; R0-R11数据放到R13中
CMP R12,R14;
BNE LOOP
1.首先使用LDMIA 从R12寄存器中读取数据,并且交给R0-R11保存
2.使用STMIA 把R0-R11的数据存储到R13指向的内存中
3.比较是否拷贝完毕
4.如果起始地址!= 结束地址 那么就是没拷贝完毕,继续拷贝。否则继续执行。
LDM/STM栈操作
在计算中有一种数据结构叫做栈。在x86平台下栈的应用很广,比如存储返回地址,存储局部变量,保存寄存器环境等等。总的来说栈也是内存,既然是内存那么肯定需要用Load/store架构来进行操作的。 在ARM中栈的作用起到备份的作用,因为大部分都是使用寄存器来进行操作了。LDM STM就可以操作栈,与上面讲的快传输指令一样,后面可以跟着IA IB DA DB. 那么如果操作栈就会有以下几种附加值。
-
FD 满递减栈,低地址生长,栈指针指向最后一个入栈的有效数据项,指针先减,然后再存数据。 这个和x86下一样,意思就是先开辟栈,然后在移动栈指针。
如下图:
-
ED 空递减栈,低地址生长,栈指针(sp)是指向下一个入栈的空间,他是先存数据,然后指针在递减。在x86下就是先移动栈指针,然后在进行数据保存。sp 永远指向下一栈。
例子:FD
STMFD SP!,{R0-R7,LR}; 这个指令是将RO-R7,LR的数据交给SP保存
等价于以下指令:
STMDB SP!,{R0-R7,LR};
或者
PUSH {R0-R7,LR}
LDMFD SP!,{RO-R7,LR}; 将SP的数据保存到R0-R7,LR
等价于:
LDMDB SP!,{R0-R7,LR};
或者:
pop{R0-R7,LR}