在使用ARM汇编的过程中,经常因内存对齐的问题,引发各种奇怪的错误,
以及在使用MDK环境编写Cortex-M3的汇编语言时
ADR Rd,label
汇编成一条与PC有关的指令,并表现为如下形式:
ADR Rd,{PC}+n ;其中n的取值不是2,就是4
这经常让人产生疑惑,因为不知道{PC}到底是一种什么样的表达。
现在,通过一段测试代码,来看看内存对齐是如何引发错误的,以及通过它的反汇编,说明{PC}的性质。
代码如下:
;*******************************************************************************
;* 文件名 : base.asm
;* 作 者 : IT马夫
;* 日 期 : 2014-06-27
;* 描 述 : Cortex-M3汇编非对齐访问错误示例
;*******************************************************************************
STACK_TOP EQU 0x20002000 ;栈顶位置,SRAM区首地址为0x20000000,栈向下生长,因此可用的栈空间为8k
AREA RESET, DATA, READONLY ;数据区,随启动模式变化,默认是Flash区,第一个数据区或代码区的名称一定要为RESET,否则编译器报错
DCD STACK_TOP ;栈顶指针,存储位置:0x08000000
DCD START ;主程序入口地址,这里实际是中断向量表的第二项,复位向量的位置0x08000004
AREA CM3ex, CODE, READONLY ;代码区开始,代码区的名称为CM3ex
ENTRY ;标记执行的第一条指令,紧接着上一个位置
START MOV R0,#9 ;设置参数,指令地址:0x08000008
MOV R1,#6 ;设置参数,指令地址:0x0800000C
ADD R2,PC,#4 ;测试代码,指令地址:0x08000010
ADR R2,{PC}+2 ;测试代码,指令地址:0x08000012
ADR R2,SUM ;测试代码,指令地址:0x08000014
LDR R3,SUM ;测试代码,指令地址:0x08000016
LDR R4,=SUM ;加载SUM的入口地址,指令地址:0x08000018
BLX R4 ;调用SUM,并将下一条指令地址保存在LR中,指令地址:0x0800001A
B . ;死循环,指令地址:0x0800001C
SUM PROC ;过程开始
ADD R2, R0,R1 ;R2=R0+R1,指令地址:0x0800001E
BX LR ;过程返回,LR的值载入PC,指令地址:0x08000022
ENDP ;过程结束
END ;汇编程序结束
上述代码将会产生两个错误:
User\base.s(22): error: A1867E: Immediate 0x00000006 out of range for this operation. Permitted values are multiples of 4 from 0x00000000 to 0x000003FF
User\base.s(23): error: A1875E: Register Rn must be from R0 to R7 in this instruction
这两个错误其实就是一个错误,都是非对齐访问引起的。
首先,对于ADR和LDR与标号有关的内存访问,在不加.W后缀的情况下,都只能汇编成16位的Thumb指令。
因此,22行与23行的两条指令都是16位的。由于他们的标号都会汇编成PC与相对于PC的偏移量构成。
由于16位编码的原因,偏移量由8位表示,且根据规定,只能是0~1020之间4的倍数。
显然,22行的指令地址为0x08000014,而标号SUM的地址为0x0800001E,相差为10
(考虑到指令流水线,PC的值相差10-4=6,ADR R2,SUM将用指令ADD R2,PC,#6代替,显然6不能被4整除),
因此无法生成相对于PC的ADD指令,编译器报告错误:立即数0x00000006超出该操作的范围。允许的值是0x00000000到0x000003FF间4的倍数
23行的指令地址为0x08000016,与标号SUM的地址0x0800001E相差为8,这样看来是能被4整除的,但是怎么也报错?
其实,这就是内存对齐的问题了。执行时,取出PC,先将它字对齐,这样0x08000016就变成了0x08000014,实际上还是与SUM像差10(考虑到流水线机制,PC值相差6)
这样,LDR R3,SUM将使用LDR R3,[PC,#6]代替,该指令编码如下:
LDR Rt ,[PC,#<imm8>]
编译器从低位到高位编码指令,8位立即数<imm8>要求是0~1020间4的倍数,显然,指令中的立即数6不合法!
由于LDR指令有两种编码格式,编译器尝试第二种格式。
这种编码格式如下:
LDR Rt,[Rn,#imm5]
编译器从低位到高位编码指令,我们看到,Rt与Rn只有3位的编码,这意味着,它们只能在R0~R7之间选择。
首先,该指令的Rt选择了R3,而Rn确是PC(R15),超过了允许范围,汇编至此停止,编译器报告错误:该指令的寄存器Rn必须在R0到R7之间
解决的办法很简答,在这两条指令后加一条NOP指令(半字),使指令与标号SUM的PC值相差增加2,由6变为8,能被4整除!
正确代码如下:
;*******************************************************************************
;* 文件名 : base.asm
;* 作 者 : IT马夫
;* 日 期 : 2014-06-27
;* 描 述 : Cortex-M3解决非对齐访问错误的示例
;*******************************************************************************
STACK_TOP EQU 0x20002000 ;栈顶位置,SRAM区首地址为0x20000000,栈向下生长,因此可用的栈空间为8k
AREA RESET, DATA, READONLY ;数据区,随启动模式变化,默认是Flash区,第一个数据区或代码区的名称一定要为RESET,否则编译器报错
DCD STACK_TOP ;栈顶指针,存储位置:0x08000000
DCD START ;主程序入口地址,这里实际是中断向量表的第二项,复位向量的位置0x08000004
AREA CM3ex, CODE, READONLY ;代码区开始,代码区的名称为CM3ex
ENTRY ;标记执行的第一条指令,紧接着上一个位置
START MOV R0,#9 ;设置参数,指令地址:0x08000008
MOV R1,#6 ;设置参数,指令地址:0x0800000C
ADD R2,PC,#4 ;测试代码,指令地址:0x08000010
ADR R2,{PC}+2 ;测试代码,指令地址:0x08000012
ADR R2,SUM ;测试代码,指令地址:0x08000014
LDR R3,SUM ;测试代码,指令地址:0x08000016
LDR R4,=SUM ;加载SUM的入口地址,指令地址:0x08000018
BLX R4 ;调用SUM,并将下一条指令地址保存在LR中,指令地址:0x0800001A
NOP ;为解决对齐问题而加的空指令,指令地址:0x0800001C
B . ;死循环,指令地址:0x0800001E
SUM PROC ;过程开始
ADD R2, R0,R1 ;R2=R0+R1,指令地址:0x08000020
BX LR ;过程返回,LR的值载入PC,指令地址:0x08000024
ENDP ;过程结束
NOP ;使整段代码是字对齐的,指令地址:0x08000026
END ;汇编程序结束
现在,来关注反汇编窗口
结合ADR指令的16位编码格式:
ADR Rd ,<label> 即 ADD Rd ,PC,#<imm8>
分析红色框中的机器码:
A201:10100010 00000001 编码中imm8=1,指令中<imm8>=4*imm8=4
A200:10100010 00000000 编码中imm8=0,指令中<imm8>=4*imm8=0
A202:10100010 00000010 编码中imm8=2,指令中<imm8>=4*imm8=8
我们得知:这三条指令分别解释为:
ADD R2,PC,#4
ADD R2,PC,#0
ADD R2,PC,#8
考虑到指令流水线机制,将上述三条加法指令计算的结果都加上4,非字对齐结果按字对齐,就得到存入R2中数据的值,
也就是反汇编窗口中@符号后面的值。
仔细分析反汇编窗口中的三条指令:
ADR r2,{pc}+4
ADR r2,{pc}+2
ADR r2,{pc}+4
可以发现,
其实{PC}就是表示PC的当前值(没有采取任何对齐操作)
而后面加的数字,是有规律的
当指令 在字对齐边界上时,如在地址0x08000010和地址0x08000014处,该数字是4
而 在半字对齐边界上时,如在地址0x08000012处,该数字是2
这个数字的目的在于将PC补成字对齐的,而且已经考虑到了指令流水线对PC的影响,之后再与8位的立即数相加!直接得到结果。
我们来看彩色框中的四个部分,分别是{PC}的值,立即数<imm8>的编码值,{PC}之后的数字,和r2中的结果
分析如下:
r2 = {pc} + 4*imm8 + 4 = 0x08000010 + 4*1 + 4 =0x08000018
r2 = {pc} + 4*imm8 + 2 = 0x08000012 + 4*0 + 2 = 0x08000014
r2 = {pc} + 4*imm8 + 4 = 0x08000014 + 4*2 + 4 = 0x08000020
最后:代码窗口中第21行的指令
LDR R2,{PC}+2
不同于反汇编窗口中的表达,它是一条伪指令,表示将PC的当前值+2载入到R2中。
该伪指令后面的数字不受限制,即可以是
LDR R2,{PC}+n ;n是寻址范围内的任意值
对于反汇编窗口或汇编源文件中出现的,形如:
LDR R2,{PC}-n
的指令,参照上述是+的两种情况对应分析。