U-boot 链接加载
做了近两年 ARM 下的驱动开发,常用的各个设备驱动基本都碰过,不过 Boot 由于任务安排的缘故(公司一直有专人在做),一直没有机会接触,从刚开始接触嵌入式的时候,就一直想弄清楚板子上电后,程序是怎么执行的,不过看了下公司 boot 源码,就很快放弃了,当时对汇编充满了畏惧,做了 1 年多的驱动后,再看汇编感觉就没那边痛苦了,最近把 boot 的资料整理下,把我觉得 boot 比较核心的部分,完整的看了一遍,现在做个记号。我把我觉得我之前比较困惑的难点整理出来,也许大家一起讨论下,也许和我一样的新手就可以少走些弯路。
BOOT 的核心就是 relocate,目前见到的典型嵌入式系统,除了处理器,至少都有ROM(norflash,nandflash)RAM(SDRAM),一般把 Bootloader 代码放在 norflash 里面,而 nandflash 因为本身硬件原因不能随机访问,一般只是用来放应用程序。在系统加电或复位后,CPU 通常由 CPU 制造商预先安排上地址取指令。arm 体系下一般都是 0x0 地址取它的第一条指令,即 PC = 0 开始。
和 boot 紧密相关的个人觉得就是一下几点:
1.remap.
remap 比较简单,和 MMU 的功能可以看做是等价的,只是一般 remap 地址估定为 0x0,网上有个帖子叫<
>专门讲了它对 remap 的理解,对 remap 的作用是这样讲的: 当 ARM 处理器上电或者 Reset 之后,处理器从 0x0 取指。因此,必须保证系统上电时,0x0 处有指令可以执行。所以,上电的时候,0x0 地址处必定是 ROM 或者 Flash(NOR)。但是,为了加快启动的速度,也方便可以更改异常向量表,加快中断响应速度,往往把异常向量表映射到更快、更宽(32bit/16bit)的 RAM 中。但是异常向量表的开始地址是由 ARM 架构决定的,必须位于 0x0 处,因此,必须把 RAM 映射到 0x0。
文中提到了 ARM 处理器 remap 的三种情况,如下
1)如果处理器有专门的寄存器可以完成 Remap。那么 Remap 是通过 Remap 寄存器的相应 bit 置 1 完成的。 如 Atmel AT91xx
2)如果处理器没有专门的寄存器,但是 memory 的 bank 控制寄存器可以用来配置 bank 的起始地址,那么只要把 RAM 的起始地址编程为 0x0,也可以完成 remap。如 samsung s3c4510 .
3)如果上面两种机制都没有,那么 Remap 就不要做了。因为处理器实现决定了 SDRAM 对应的 bank 地址是不能改变的。如Samsung S3c2410.
不过我的看法有点稍微不一样,如果上面两种机制都没有,那么 Remap 就不要做了,它给的典型例子是 Samsung S3c2410 ,2410 虽然 sdram 对应的 bank 地址不能改变,但它有 MMU 功能,MMU 可以起到 remap 的作用,常用的最典型的应该是例子 Samsung S3c44b0,它既没有 mmu,SDRAM 对应地址又没办法改变。顺便补充下除了 4510 可以改变每个 bank 的地址,还有华邦的 w90P740(arm7),呵呵,我现在用的 U 就是这款 U,可以把 bank 的地址随意的设置。
2.relocate .
relocate (地址重定位),个人觉得这个是 boot 里面最麻烦也是最核心的部分,刚开始看 boot 代码的时候,它简直是我的恶梦,不知道大家分析 boot 的源码流程是否这样,也可能我大学不是计算机的,没学过编译原理(现在也没看过)对链接和加载一无所知,有两个星期非常痛苦,就是不懂人家 boot 里面的链接脚本为什么要那样写。网上关于 uboot 的帖子很多,但对链接加载这块,始终写的不详细,不知道是不是太过于基础了,高手都不愿意讲。最后自己找资料,发现其实一切痛苦的根源都是对链接和加载不太清楚造成的,但个人感觉 boot 除了初始化以外就是搬运程序,如何搬运?为什么要那样搬运都需要对硬件板的地址分布很清楚,而这些都是链接决定的,所以非弄清楚不可!
1).我们为什么需要relocate ?经济方面,(nandflash和norflash 每兆价格相差悬殊),把boot代码放在norflash里面(为什么不放在nandflash里面,因为nandflash读需要驱动支持,norflash可以直接访问),boot通常很小,只需要占用几十k的空间,所以只需要很小的norflash芯片,这样很便宜,而应用程序通常很大,所以用价格低廉nandflash来储存。实际应用中,通过执行boot程序,把nandflash里面代码和数据搬运到内存中来执行,这样比程序直接放在norflash里执行快。另外还有运行速度方面的差别,程序在norflash里执行的速度远远小于在sdram中执行的速度,为了追求更高的速度,也需要relocate,让程序在sdram里面执行 。
2).关于加载域(VMA)和运行域(LMA),(反了,加载域是 LMA,运行域是VMA ,BY imjacob)杜春雷在它那本经典的<>一书专门有一章来讲加载域和运行域不一致的情况,但我当初接触了它的这些加载域和运行域后,看uboot的lds,uboot的lds没有设置LMA,只是设置了VMA,为此我疑惑很久。直到耐心的看了那本链接器和加载器的书才豁然明白( ),任何一个链接器和加载器的基本工作都非常简单: 将更抽象的名字与更底层的名字绑定起来,好让程序员使用更抽象的名字编写代码。链接器的就是把源文件进行符号解析,把解析出来的符号和地址的进行绑定,把全局变量、函数、标号等等这些符合和地址绑定起来。
3).boot上电后开始能够正确执行还有个很重要的原因,是要保证boot在系统加电或复位后最初执行的代码是跟地址无关的,(即在代码搬运前所执行的代码是与地址无关),地址无关即地址无关代码生成的这个映象文件可以被放在内存中的任何一个地址上运行。对于地址无关的代码,寻址是基于pc值的,在pc值上+/-一个偏移值,得到加载地址,如跳转指令B。当我们执行完代码搬运,就需要跳到和地址相关的地方去执行,ldr pc,_start_armboot,执行这条程序相对偏编译加载指令,会让PC指向_start_armboot地址上存放的地址中去,这时地址相关代码就开始运行了。因为在bin映象生成的时候(编译链接的时候),就已经把 start_armboot 这个符号,和实际地址绑定在一起,当我们执行ldr pc,_start_armboot,程序就从ROM中跳到RAM中执行了,但前提是我们进行了代码搬移,如果没有代码搬运而执行ldr pc,_start_armboot,相应的RAM中没有代码,程序就马上飞掉了,所有我们在搬运之前不能执行地址相关代码,必须执行地址无关代码。
拿u-boot-1.1.4 下的smdk2410 来做例子,和smdk2410 board密切相关的就两个文件夹\board\smdk2410 和\cpu\arm920t,里面核心文件就u-boot.lds,config.mk,start.S。
ENTRY(_start)
SECTIONS
{
. = 0x00000000;//从 0 地址起始
. = ALIGN(4);
.text :
{
cpu/arm920t/start.o (.text)
*(.text)
}
. = ALIGN(4);
.rodata : { *(.rodata) }
. = ALIGN(4);
.data : { *(.data) }
. = ALIGN(4);
.got : { *(.got) }
. = .;
__u_boot_cmd_start = .;
.u_boot_cmd : { *(.u_boot_cmd) }
__u_boot_cmd_end = .;
. = ALIGN(4);
__bss_start = .;//为搬运代码提供的符号,来标明 bss 段地址,方便 relocate
.bss : { *(.bss) }
_end = .; //定义整个 image 的结束地址
}
u-boot.lds 是链接脚本文件,我刚开始看这个链接脚本文件时,我疑惑很久,不明白 lds 中 VMA= LMA(资料上很多链接脚本包括我们公司项目里面自己写的 lds 脚本是通过 AT 命令设置过 LMA,这样看起来地址空间分配更清晰),而且整个 image 的VMA 按照 lds 为基址为 0x0,而 2410 芯片不能 remap,0x0 地址是 ROM 的区域,不是运行时 RAM 的地址,我的理解是代码段地址应该是指向该硬件板内存区域,设置 .text=TEXT_BASE 而不是 lds 中的.text=0x0,这个疑点弄的我当时很郁闷,想了很久也没想没有搞清楚 u-boot 这样链接脚本都能让 boot 跑起来,当我把编译出来的 bin 烧到 norflash 中,uboot 居然跑起来了,同时发现了一个问题,在 u-boot.map 中发现 .text 是从 config.mk 定义 TEXT_BASE =0x33f80000,而不是 lds 设置的 0x0,这又让我吃惊,没清楚是怎么会事,手上有介绍移植 uboot 的资料,但都对 uboot 链接这部分,写的不够详细,知道是 config.mk 文件搞的鬼,但把 makefile 文件看了几遍都没找不到是怎么回事(还是对 makefile 不熟啊!),最后把编译 uboot 的过程看了隐藏了个机关是
arm-linux-ld –Tu-boot-1.1.4\board\smdk2410\u-boot.lds –Ttext 0x33f80000
arm-linux-objcopy --gap-fill =0xff –O binary uboot ubtoot.bin
不知道 uboot 设计者为什么要在这里加一个–Ttext 而不是在 lds 就设置?而很多移植 uboot 的资料对 lds 文件都有所描述,但这个重要的细节似乎都漏掉了,不知道是不是因为太基础了,所以没有讲。
不过从最后生成的 bin 上看 arm-linux-objcopy --gap-fill =0xff –O binary uboot ubtoot.bin 没有对链接生成的 elf 文件进行重定位,因此它的运行地址是 config.mk 定义 TEXT_BASE 为基地址,顺序按照 lds 的顺序依次增加的,所以整个 uboot最初运行的流程是
◊ reset ◊_start ◊cpu_init_crit relocate
这个部分就是完成初始化,设 SVC32,关看门狗,关中断,设置时钟,初始化 SDRAM(为代码搬运到 SDRAM 做准备),这些都很简单
relocate:
adr r0, _start
ldr r1, _TEXT_BASE
cmp r0, r1
beq stack_setup
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2
add r2, r0, r2
copy_loop:
ldmia r0!, {r3-r10}
stmia r1!, {r3-r10}
cmp r0, r2
ble copy_loop
看了下网上的帖子,adr 指令,网上很多人被这这个指令弄郁闷,我看杜春雷的<>P143 讲,这个指令是基于 PC 或者寄存器的,读到是地址无关的,一般被编译器替换为 SUB r0, pc,#offset ,不要理解为读取符合表中_start符号的地址(0x33f80000)。offset 是在编译时就已经计算好的偏移量。接下来要用到链接时确定的符号地址了,_armboot_start(0x33f80000), _bss_start(0x33f97954)这些可以在 u-boot.map 里面的看到, size of armboot =0x33f97954-0x33f80000 ,把_start:0x0 (norflsh)开始的 (.text)、(.data)代码和数据往 SDRAM 里_TEXT_BASE确定的地址(0x33f80000)搬运。s3c2410 的 SDRAM 基地址是 0x3000_0000,由于 uboot 支持的这个 board SDRAM 是64M,(0x3000_0000---0x3400_0000),所以把 u-boot.bin 搬运到内存的高端地址。然后跳到内存中执行,提高速度。
之后就 relocate RAM)◊ ldr pc, _start_armboot ( ROM◊ clear_bss ◊ stack_setup ◊
_start_armboot: .word start_armboot ( u-boot-1.1.4\lib_arm\board.c)
stack_setup , clear_bss 设置堆栈清 bss 段,都是为进入 C 语言做初始化准备,通过对 start_armboot 链接后已经把这个函数地址已经绑定在 RAM 中,当执行完 ldr pc, label 指令,程序将从标号绑定地址开始执行,从而实现了从地址无关程序到地址相关的转变,我们做代码搬移也是为了跳转做准备,如果没有搬移,直接访问地址相关,由于 RAM 中都是随机值,一跳转就马上飞了。当进入 start_armboot C 函数,剩下的都没什么难度了。可以慢分析源码搞定。2410 没有 remap 寄存器, relocate 时候要容易些,有 remap 寄存器的芯片在 relocate 时候进行 remap 会让情况更复杂些。不过原理都差不多。在进入 board.c 后,uboot 还做了一次代码搬运如下,大概如下图,不过分两种,一种是把宿主机传送的 image 通过串口或者网
络传到内存开始执行,或者从 nandflash 里把应用搬到内存开始执行,不过原理都差不多。
正好公司内部给我们做了板级初始化培训,把硬件板初始流程注意要点整理出来,和 boot 这部分初始化对比,可以发现硬件板初始化流程都差不多。比较头痛还是链接这部分,这方面的资料感觉太少了,没人可以指点,自己看这部分资料看的很痛苦。
◊【CPU 核相关初始化】
◊【Watchdog 初始化】
◊【GPIO 初始化】
◊【系统时钟初始化】
◊【内存初始化】
◊【模式初始化】
◊【中断向量初始化】
◊【MMU 初始化】
◊【Cache 初始化】
◊【总线初始化】
◊【语言相关初始化】
【设备相关初始化】
4.elf 格式和 bin 格式
executable and linking format (ELF)重定位,可以参与程序的链接(创建一个程序)和程序的执行(运行一个程序),主要链接和执行,但介绍 elf 文件的资料很多,没时间仔细看,和实际密切的就是调试程序时候都用 elf 格式调试,因为它包含了调试所需的各种符号,固化的时候都是用的 bin 格式,是可执行映象,用 objcopy 把 elf 转换成 bin ,不过网上介绍 bin 格式的资料很少,只是知道 bin 程序,只要把 pc(程序指针)设置为 bin 映象的入口地址,就可以正确执行, objcopy 可以对 elf 转换成 bin 再进行地址重定位,不过目前还没看见过这么干过,对于 elf 和 bin 这些理解的都不系统,资料也很少,工作中,集成开发工具 IDE 又把这些设置都给屏蔽起来,有没有那个强人能写一个文档,把这些都系统的讲清楚就好了!
顺便问下,论坛上上海的多不多,大家找工作都是在网上找的?有个 MM 拉我去上海,虽然对现在工作很满意,不过 MM 比工作更重要,要我做选择,只有去上海了,不过在 51job 上投了点简历,都石沉大海,按理说 2 年也不短了,至少也会冒一个泡的,有没有上海的能够指点下,你们在上海石怎么找相关工作的?
补充一个当时找资料看见对网上一个帖子,感觉写的很精辟的,关于地址无关的解释,网页地址被改成相当路径了,就没办法地址粘贴出来,现在把原文粘贴出来.
关键词: 地址无关
术语
地址无关: 编译地址不等于运行地址.
地址相关: 编译地址等于运行地址.
常见的一些 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 的+/-运行显然是有一个地址范围的, 所以我们在上面选择代码量小的地址无关, 是很明智的.
而访问地址相关的代码, 只需要使用其它的寻址指令就行了. 但在这之前, 必须保证代码被放在正确的地址上, 所以通常都会有一个复制代码的过程, 然后就是跳转到一个标号, 地址相关代码就开始运行了.
原文地址 http://blog.sina.com.cn/s/blog_4a9fb5cf01008d8u.html
阅读(1978) | 评论(0) | 转发(0) |