ARM平台下bl和ldr指令的区别

时间:2021-09-18 01:20:14

学linux驱动,断断续续已经有个把月了,但一直停留在跟着视频模仿的阶段,不知其所然更不知其所以然。就决定,按照玩单片的方式,先去玩玩裸机的程序开发,把自己做的电路板小模块测试下(本来想在linux系统下去测试,由于不知道驱动是否移植成功,就一直没写测试程序)。这周按照韦东山老师的书,在裸机下作了点外设的测试,算是巩固了下单片机开发的手段,和一些外设的知识。由于有着比较扎实的单片机开发经验(个人这么感觉的),所以上手很快,也没有特别大的难度(都是简单外设,网卡,USB什么的都没动,感觉有点复杂)。裸机开发过程中主要是第一段引导程序编写比较关键,在写汇编时,也主要是对bl和ldr指令在函数跳转时不是很清楚。百度下,找了篇文章学习了下。

主要就是:

bl一般是地址无关的跳转,最简单理解就是跳转函数也是在4K片内RAM里的。这个不管你编译时候它存放的位置,函数间的相互的位置关系是不会变的,函数间是一个确定的相对位置关系。跳转的实现是通过PC指针加减一个相对偏移量来完成。显然,这个相对偏移量是有限制的。

ldr是地址相关的,用来跳转片外函数的,也就是这个代码运行时是在片外RAM的。在这种情况下,如果用bl跳转,PC可以用的最大偏移量仍然 无法满足跳转的需要,就需要用ldr指令实现PC指针的改变。也就是,函数间跳转使用的是绝对地址

这算是我个人简单的助记的理解。如果有朋友看到,觉得这个解释容易出错或者不合理,希望指出,让我有更好的理解。下面就贴出我参考学习的文章。

转自:http://www.cnblogs.com/shenlian/archive/2011/11/30/2269341.html


一,按lds文件连接的不同模块,不能用bl实现跳转
一个错误的例子:
1.crt0.s
@******************************************************************************
@ File:crt0.s
@ 功能:通过它转入C程序
@******************************************************************************
.extern main
.text
.global _start
_start:
ldr sp, =1024*4 @设置堆栈,注意:不能大于4k ,
@这儿堆栈可以设置为0x34000000,根据内存地址空间分配确定

bl main @调用C程序中的main函数
halt_loop:
b halt_loop
2.leds.c
@******************************************************************************
@ file leds.c
@ main函数
@******************************************************************************
int main()
{
__asm__
(
" ldrb r0, [r1], #1\n"
" strb r0, [r2], #1\n"
);
return 0;
}
3.Makefile
CFLAGS := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer -ffreestanding -c
leds : crt0.s leds.c
arm-linux-gcc $(CFLAGS) -o crt0.o crt0.s
arm-linux-gcc $(CFLAGS) -o leds.o leds.c
arm-linux-ld -Tleds.lds crt0.o leds.o -o leds_tmp.o
arm-linux-objcopy -O binary -S leds_tmp.o leds
arm-linux-objdump -D -b binary -m arm leds >ttt.s
clean:
rm -f leds
rm -f leds.o
rm -f leds_tmp.o
rm -f crt0.o
4.leds.lds 文件
SECTIONS {
firtst 0x00000000 : { crt0.o }
second 0x00000000 : AT(0x0100) { leds.o }
}
5.反汇编代码ttt.s
00000000 :
0: e3a0da01 mov sp, #40Array6 ; 0x1000
4: ebfffffd bl 0x0 //这儿不能调转到main 因为bl跳转有限制
8: eafffffe b 0x8
...
100: e24dd040 sub sp, sp, #64 ; 0x40
104: e3a00000 mov r0, #0 ; 0x0
108: e28dd040 add sp, sp, #64 ; 0x40
10c: e1a0f00e mov pc, lr
110: 43434700 cmpmi r3, #0 ; 0x0
114: 4728203a undefined
118: 202Array554e eorcs r5, rArray, lr, asr #10
11c: 2e332e33 mrccs 14, 1, r2, cr3, cr3, {1}
120: 00000032 andeq r0, r0, r2, lsr r0
通过上面的例子可以看到crt0中的bl main出错
"4: ebfffffd bl 0x0 "
bl没有成功。
6.改正方法1:
原lds文件把俩个目标文件分开排列,这里把俩个目标文件指定到一起,这样不能重定位

修改后的lds文件
SECTIONS {
firtst 0x00000000 : { crt0.o leds.o }

}
改正后的效果
0: e3a0da01 mov sp, #40Array6 ; 0x1000
4: eb000000 bl 0xc //这里bl跳转到正确的地址
8: eafffffe b 0x8
c: e24dd040 sub sp, sp, #64 ; 0x40
10: e3a00000 mov r0, #0 ; 0x0
14: e28dd040 add sp, sp, #64 ; 0x40
18: e1a0f00e mov pc, lr
1c: 43434700 cmpmi r3, #0 ; 0x0
20: 4728203a undefined
24: 202Array554e eorcs r5, rArray, lr, asr #10
28: 2e332e33 mrccs 14, 1, r2, cr3, cr3, {1}
2c: 00000032 andeq r0, r0, r2, lsr r0
二,使用ldr命令来实现长跳转(改正方法2)
1.
ldr pc, =main @调用C程序中的main函数
通过ldr 对pc赋值来实现跳转
@******************************************************************************
@ File:crt0.s
@ 功能:通过它转入C程序
@******************************************************************************
.extern main
.text
.global _start
_start:
ldr sp, =1024*4 @设置堆栈,注意:不能大于4k,nand flash中的代码在复位后会移到内部ram中,此ram只有4k
ldr pc, =main @调用C程序中的main函数
halt_loop:
b halt_loop
2.leds.lds文件
SECTIONS {
firtst 0x00000000 : { crt0.o }
second 0x30000000 : AT(0x1000) { leds.o }
}

3.反汇编结果
00000000 :
0: e3a0da01 mov sp, #40Array6 ; 0x1000
4: e5Arrayff000 ldr pc, [pc, #0] ; 0xc
8: eafffffe b 0x8
c: 30000000 andcc r0, r0, r0
...
1000: e4d10001 ldrb r0, [r1], #1



地址无关: 编译地址不等于运行地址.
地址相关: 编译地址等于运行地址.

常见的一些Boot(如, U-Boot, VIVI)和Linux Kernel代码开始的一段是位置无关的, 意思就是说运行地址与编译地址无关. 如, Kernel编译地址是0xc0008000, 而运行地址是0x30008000.
为什么?
为什么代码的编译地址和运行地址会不相等呢? 原因主要有以下几种: 1) 对于Boot, 用于存放Boot代码的存储器容量小于代码量. 如, Boot片有4K, 而代码通常有50-60K. 这样, 通常会在前4K代码里, 让Boot把自己复制到RAM, 再接着运行.这里我们需要作出一个选择, 是让前面的代码与地址相关, 还是让后面的代码与地址相关呢? 显然我们会选择前面一段代码量小的与地址无关. 2) 对于Linux Kernel, 它是运行在虚拟地址空间的, 如0xc0008000, 但在MMU打开之前, 通常这个地址是
不存在的, 也就是说在MMU打开之前, Kernel的代码必须是地址无关的.
怎么办?
对于位置无关的代码, 寻址是基于pc值的, 在pc值上+/-一个偏移值, 得到运行地址.以ARM为例, 用adr来寻址, adr的实际上是一个宏指令, 在代码编译时, 会被编译器替换成对pc的+/-运算
这里要注意, 对pc的+/-运行显然是有一个地址范围的, 所以我们在上面选择代码量小的地址无关, 是很明智的.

而访问地址相关的代码, 只需要使用其它的寻址指令就行了. 但在这之前, 必须保证代码被放在正确的地址上, 所以通常都会有一个复制代码的过程, 然后就是跳转到一个标号, 地址相关代码就开始运行