前面我们已经使用汇编语言来编写了一些小的代码段用来简单地验证我们学习到的一些知识,这篇笔记详细地探讨一下用得非常频繁的LDR伪指令。
我们在之前的汇编测试代码里大量用到了LDR伪指令用来跳转到指定的函数(标号)处去执行,所以直观上LDR给我们的感觉就是一条跳转指令。其实在指令集里面,本身是有一条LDR指令的,它用来从内存里面加载数据到通用寄存器中。但是这里我们要讨论的并不是这条指令集里面的LDR指令,而是汇编语言里面的LDR伪指令,其详细的介绍可以参考《ARM Cortex-A(armV7)编程手册V4.0》的 A.1.46 LDR (pseudo-instruction)部分。
这里我们只讨论目前用得比较多的 LDR PC, =LABEL 的情况,LABEL是代码段里的一个标号,这条伪指令表示执行流跳转到LABEL的地方去执行,因为它的目的寄存器使用的是PC。
LDR伪指令经过汇编后,会生成MOV,MVN,或是一条相对于PC的LDR指令。我们以《三、CP15协处理器》里面的测试代码为例子来看一看,其中启动部分的代码如下:
.global _start /* 全局标号 */
/*
* 描述: _start函数,首先是中断向量表的创建
* 参考文档:ARM Cortex-A(armV7)编程手册V4.0.pdf P42,3 ARM Processor Modes and Registers(ARM处理器模型和寄存器)
* ARM Cortex-A(armV7)编程手册V4.0.pdf P165 11.1.1 Exception priorities(异常)
*/
_start:
ldr pc, =Reset_Handler /* 复位中断 */
ldr pc, =Undefined_Handler /* 未定义中断 */
ldr pc, =SVC_Handler /* SVC(Supervisor)中断 */
ldr pc, =PrefAbort_Handler /* 预取终止中断 */
ldr pc, =DataAbort_Handler /* 数据终止中断 */
ldr pc, =NotUsed_Handler /* 未使用中断 */
ldr pc, =IRQ_Handler /* IRQ中断 */
ldr pc, =FIQ_Handler /* FIQ(快速中断)未定义中断 */
/* 复位中断 */
Reset_Handler:
cpsid i /* 关闭全局中断 */
/* 关闭I,DCache和MMU
* 采取读-改-写的方式。
*/
mrc p15, 0, r0, c1, c0, 0 /* 读取CP15的C1寄存器到R0中 */
bic r0, r0, #(0x1 << 12) /* 清除C1寄存器的bit12位(I位),关闭I Cache */
bic r0, r0, #(0x1 << 2) /* 清除C1寄存器的bit2(C位),关闭D Cache */
bic r0, r0, #0x2 /* 清除C1寄存器的bit1(A位),关闭对齐 */
bic r0, r0, #(0x1 << 11) /* 清除C1寄存器的bit11(Z位),关闭分支预测 */
bic r0, r0, #0x1 /* 清除C1寄存器的bit0(M位),关闭MMU */
mcr p15, 0, r0, c1, c0, 0 /* 将r0寄存器中的值写入到CP15的C1寄存器中 */
/* 设置各个模式下的栈指针,
* 注意:IMX6UL的堆栈是向下增长的!
* 堆栈指针地址一定要是4字节地址对齐的!!!
* DDR范围:0X80000000~0X9FFFFFFF
*/
/* 进入IRQ模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x12 /* r0或上0x13,表示使用IRQ模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80600000 /* 设置IRQ模式下的栈首地址为0X80600000,大小为2MB */
/* 进入SYS模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x1f /* r0或上0x13,表示使用SYS模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0x80400000 /* 设置SYS模式下的栈首地址为0X80400000,大小为2MB */
/* 进入SVC模式 */
mrs r0, cpsr
bic r0, r0, #0x1f /* 将r0寄存器中的低5位清零,也就是cpsr的M0~M4 */
orr r0, r0, #0x13 /* r0或上0x13,表示使用SVC模式 */
msr cpsr, r0 /* 将r0 的数据写入到cpsr_c中 */
ldr sp, =0X80200000 /* 设置SVC模式下的栈首地址为0X80200000,大小为2MB */
cpsie i /* 打开全局中断 */
b main /* 跳转到main函数 */
/* 未定义中断 */
Undefined_Handler:
ldr r0, =Undefined_Handler
bx r0
/* SVC中断 */
SVC_Handler:
ldr r0, =SVC_Handler
bx r0
/* 预取终止中断 */
PrefAbort_Handler:
ldr r0, =PrefAbort_Handler
bx r0
/* 数据终止中断 */
DataAbort_Handler:
ldr r0, =DataAbort_Handler
bx r0
/* 未使用的中断 */
NotUsed_Handler:
ldr r0, =NotUsed_Handler
bx r0
/* IRQ中断!重点!!!!! */
IRQ_Handler:
push {lr} /* 保存lr地址 */
push {r0-r3, r12} /* 保存r0-r3,r12寄存器 */
mrs r0, spsr /* 读取spsr寄存器 */
push {r0} /* 保存spsr寄存器 */
mrc p15, 4, r1, c15, c0, 0 /* 从CP15的C0寄存器内的值到R1寄存器中
* 参考文档ARM Cortex-A(armV7)编程手册V4.0.pdf P49
* Cortex-A7 Technical ReferenceManua.pdf P68 P138
*/
add r1, r1, #0X2000 /* GIC基地址加0X2000,也就是GIC的CPU接口端基地址 */
ldr r0, [r1, #0XC] /* GIC的CPU接口端基地址加0X0C就是GICC_IAR寄存器,
* GICC_IAR寄存器保存这当前发生中断的中断号,我们要根据
* 这个中断号来绝对调用哪个中断服务函数
*/
push {r0, r1} /* 保存r0,r1 */
cps #0x13 /* 进入SVC模式,允许其他中断再次进去 */
push {lr} /* 保存SVC模式的lr寄存器 */
ldr r2, =system_irqhandler /* 加载C语言中断处理函数到r2寄存器中*/
blx r2 /* 运行C语言中断处理函数,带有一个参数,保存在R0寄存器中 */
pop {lr} /* 执行完C语言中断服务函数,lr出栈 */
cps #0x12 /* 进入IRQ模式 */
pop {r0, r1}
str r0, [r1, #0X10] /* 中断执行完成,写EOIR */
pop {r0}
msr spsr_cxsf, r0 /* 恢复spsr */
pop {r0-r3, r12} /* r0-r3,r12出栈 */
pop {lr} /* lr出栈 */
subs pc, lr, #4 /* 将lr-4赋给pc */
/* FIQ中断 */
FIQ_Handler:
ldr r0, =FIQ_Handler
bx r0
我们来看一下第一条指令 < ldr pc, =Reset_Handler > 生成的指令是什么样子的。我们在编译代码的时候,顺便进行了反汇编,如下是启动文件部分的反汇编指令片段:
print_c0.elf: file format elf32-littlearm
Disassembly of section .text:
87800000 <_start>:
87800000: e59ff100 ldr pc, [pc, #256] ; 87800108 <FIQ_Handler+0x8>
87800004: e59ff100 ldr pc, [pc, #256] ; 8780010c <FIQ_Handler+0xc>
87800008: e59ff100 ldr pc, [pc, #256] ; 87800110 <FIQ_Handler+0x10>
8780000c: e59ff100 ldr pc, [pc, #256] ; 87800114 <FIQ_Handler+0x14>
87800010: e59ff100 ldr pc, [pc, #256] ; 87800118 <FIQ_Handler+0x18>
87800014: e59ff100 ldr pc, [pc, #256] ; 8780011c <FIQ_Handler+0x1c>
87800018: e59ff100 ldr pc, [pc, #256] ; 87800120 <FIQ_Handler+0x20>
8780001c: e59ff100 ldr pc, [pc, #256] ; 87800124 <FIQ_Handler+0x24>
87800020 <Reset_Handler>:
87800020: f10c0080 cpsid i
87800024: ee110f10 mrc 15, 0, r0, cr1, cr0, {0}
87800028: e3c00a01 bic r0, r0, #4096 ; 0x1000
8780002c: e3c00004 bic r0, r0, #4
87800030: e3c00002 bic r0, r0, #2
87800034: e3c00b02 bic r0, r0, #2048 ; 0x800
87800038: e3c00001 bic r0, r0, #1
8780003c: ee010f10 mcr 15, 0, r0, cr1, cr0, {0}
87800040: e10f0000 mrs r0, CPSR
87800044: e3c0001f bic r0, r0, #31
87800048: e3800012 orr r0, r0, #18
8780004c: e129f000 msr CPSR_fc, r0
87800050: e59fd0d0 ldr sp, [pc, #208] ; 87800128 <FIQ_Handler+0x28>
87800054: e10f0000 mrs r0, CPSR
87800058: e3c0001f bic r0, r0, #31
8780005c: e380001f orr r0, r0, #31
87800060: e129f000 msr CPSR_fc, r0
87800064: e59fd0c0 ldr sp, [pc, #192] ; 8780012c <FIQ_Handler+0x2c>
87800068: e10f0000 mrs r0, CPSR
8780006c: e3c0001f bic r0, r0, #31
87800070: e3800013 orr r0, r0, #19
87800074: e129f000 msr CPSR_fc, r0
87800078: e59fd0b0 ldr sp, [pc, #176] ; 87800130 <FIQ_Handler+0x30>
8780007c: f1080080 cpsie i
87800080: ea000037 b 87800164 <main>
87800084 <Undefined_Handler>:
87800084: e59f0080 ldr r0, [pc, #128] ; 8780010c <FIQ_Handler+0xc>
87800088: e12fff10 bx r0
8780008c <SVC_Handler>:
8780008c: e59f007c ldr r0, [pc, #124] ; 87800110 <FIQ_Handler+0x10>
87800090: e12fff10 bx r0
87800094 <PrefAbort_Handler>:
87800094: e59f0078 ldr r0, [pc, #120] ; 87800114 <FIQ_Handler+0x14>
87800098: e12fff10 bx r0
8780009c <DataAbort_Handler>:
8780009c: e59f0074 ldr r0, [pc, #116] ; 87800118 <FIQ_Handler+0x18>
878000a0: e12fff10 bx r0
878000a4 <NotUsed_Handler>:
878000a4: e59f0070 ldr r0, [pc, #112] ; 8780011c <FIQ_Handler+0x1c>
878000a8: e12fff10 bx r0
878000ac <IRQ_Handler>:
878000ac: e52de004 push {lr} ; (str lr, [sp, #-4]!)
878000b0: e92d100f push {r0, r1, r2, r3, ip}
878000b4: e14f0000 mrs r0, SPSR
878000b8: e52d0004 push {r0} ; (str r0, [sp, #-4]!)
878000bc: ee9f1f10 mrc 15, 4, r1, cr15, cr0, {0}
878000c0: e2811a02 add r1, r1, #8192 ; 0x2000
878000c4: e591000c ldr r0, [r1, #12]
878000c8: e92d0003 push {r0, r1}
878000cc: f1020013 cps #19
878000d0: e52de004 push {lr} ; (str lr, [sp, #-4]!)
878000d4: e59f2058 ldr r2, [pc, #88] ; 87800134 <FIQ_Handler+0x34>
878000d8: e12fff32 blx r2
878000dc: e49de004 pop {lr} ; (ldr lr, [sp], #4)
878000e0: f1020012 cps #18
878000e4: e8bd0003 pop {r0, r1}
878000e8: e5810010 str r0, [r1, #16]
878000ec: e49d0004 pop {r0} ; (ldr r0, [sp], #4)
878000f0: e16ff000 msr SPSR_fsxc, r0
878000f4: e8bd100f pop {r0, r1, r2, r3, ip}
878000f8: e49de004 pop {lr} ; (ldr lr, [sp], #4)
878000fc: e25ef004 subs pc, lr, #4
87800100 <FIQ_Handler>:
87800100: e59f001c ldr r0, [pc, #28] ; 87800124 <FIQ_Handler+0x24>
87800104: e12fff10 bx r0
87800108: 87800020 strhi r0, [r0, r0, lsr #32]
8780010c: 87800084 strhi r0, [r0, r4, lsl #1]
87800110: 8780008c strhi r0, [r0, ip, lsl #1]
87800114: 87800094 ; <UNDEFINED> instruction: 0x87800094
87800118: 8780009c ; <UNDEFINED> instruction: 0x8780009c
8780011c: 878000a4 strhi r0, [r0, r4, lsr #1]
87800120: 878000ac strhi r0, [r0, ip, lsr #1]
87800124: 87800100 strhi r0, [r0, r0, lsl #2]
87800128: 80600000 rsbhi r0, r0, r0
8780012c: 80400000 subhi r0, r0, r0
87800130: 80200000 eorhi r0, r0, r0
87800134: 8780223d ; <UNDEFINED> instruction: 0x8780223d
87800138: 00001e41 andeq r1, r0, r1, asr #28
8780013c: 61656100 cmnvs r5, r0, lsl #2
87800140: 01006962 tsteq r0, r2, ror #18
87800144: 00000014 andeq r0, r0, r4, lsl r0
87800148: 412d3705 teqmi sp, r5, lsl #14
8780014c: 070a0600 streq r0, [sl, -r0, lsl #12]
87800150: 09010841 stmdbeq r1, {r0, r6, fp}
87800154: 00040a02 andeq r0, r4, r2, lsl #20
可以看到,< ldr pc, =Reset_Handler > 这条伪指令最终被汇编成了 < ldr pc, [pc, #256] > 这样一条指令,也就是生成了一条相对于PC的LDR指令,表示从相对于当前PC的256字节偏移处取一个字(4字节)的数据,放到PC里面。我们知道这条指令实际上是想跳转到 Reset_Handler 处去执行代码,所以加载到的值必然应该是 Reset_Handler 这个标号的值(从反汇编文件中可以知道这个值是0x87800020)。那么这样我们可以推断,当前PC偏移处256字节的位置,必然存放的是0x87800020这个数据。而在执行 < ldr pc, [pc, #256] > 这条指令的时候,按照我们之前讲过的流水线的知识,当前PC的值应该是 0x87800000 + 8 = 0x87800008,再加上256字节的偏移,就等于 0x87800108,所以可以推断0x87800108 ~ 0x8780010B这4个字节的内存里面,存放的就是 Reset_Handler 标签的值0x87800020;而且我们看到所有的中断向量生成的指令都是类似的,故可以判断0x87800108起始的一片内存其实就是用来存放中断函数地址的,由于这片地址被用来存放常量数据,故又叫做字面量池(literal pool)。
接下来我们设计一段测试代码来验证一下我们的推断,我们只需要从0x87800108和0x8780010C这两个地址读取一个字的数据出来,看是不是Reset_Handler和Undefined_Handler的值就可以了,测试代码如下:
.data
name_reset:
.asciz "Reset_Handler"
name_undefined:
.asciz "Undefined_Handler"
.text
main:
bl int_init
bl imx6u_clkinit
bl delay_init
bl clk_enable
bl led_init
bl uart_init
ldr r0, =name_reset
#读取0x87800108地址处的一个字
ldr r2, =0x87800108
ldr r1, [r2]
#打印读取到的值
bl print_register
ldr r0, =name_undefined
#读取0x8780010C地址处的一个字
ldr r2, =0x8780010C
ldr r1, [r2]
#打印读取到的值
bl print_register
bl loop_task
下图是编译运行后,串口打印出的结果:
我们打印出了0x87800108地址和0x8780010C地址处的字面量值,而且可以看到跟反汇编文件里面的Reset_Handler标号和Undefined_Handler标号是一致的。这样,我们就探知了LDR伪指令加载立即数的工作原理,这些立即数被当作字面量被集中放在代码镜像的literal pool区域里面, 编译器在编译LDR伪指令的时候,会计算对应立即数在literal pool里面与当前PC值的偏移,生成一条相对于PC值的LDR指令。