做了近两年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 ,网上有个帖子叫<<ARM remap与重定位摘抄>>专门讲了它对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),杜春雷在它那本经典的<<arm体系结构与编程>>一书专门有一章来讲加载域和运行域不一致的情况,但我当初接触了它的这些加载域和运行域后,看uboot的lds ,uboot的lds没有设置LMA,只是设置了VMA,为此我疑惑很久.直到耐心的看了那本链接器和加载器的书才豁然明白(http://bbs.chinaunix.net/viewthread.php?tid=817770 ),任何一个链接器和加载器的基本工作都非常简单: 将更抽象的名字与更底层的名字绑定起来,好让程序员使用更抽象的名字编写代码,链接器的就是把源文件进行符号解析,把解析出来的符号和地址的进行绑定,把全局变量,函数,标号等等这些符合和地址绑定起来.
3.boot上电后开始能够正确执行还有个很重要的原因,是要保证boot在系统加电或复位后最初执行的代码是跟地址无关的,(即在代码搬运前所执行的代码是与地址无关),地址无关即地址无关代码生成的这个映象文件可以被放在内存中的任何一个地址上运行。对于地址无关的代码, 寻址是基于pc值的, 在pc值上+/-一个偏移值, 得到运行地址,如跳转指令B.当我们执行完代码搬运,就需要跳到和地址相关的地方去执行,即我们的RAM中,一般是跳转到一个标号, 这时地址相关代码就开始运行了ldr 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 ubootubtoot.bin没有对链接生成的elf文件进行重定位,因此它的运行地址是config.mk 定义TEXT_BASE为基地址,顺序按照lds的顺序依次增加的,所以整个uboot最初运行的流程是
_start  reset  cpu_init_crit  relocate
这个部分就是完成初始化,设SVC32,关看门狗,关中断,设置时钟,初始化SDRAM(为代码搬运到SDRAM做准备),这些都很简单
relocate: /* relocate U-Boot to RAM */
adr r0, _start /* r0 <- current position of code */
ldr r1, _TEXT_BASE /* test if we run from flash or RAM */
cmp r0, r1 /* don't reloc during debug */
beq stack_setup
ldr r2, _armboot_start
ldr r3, _bss_start
sub r2, r3, r2 /* r2 <- size of armboot */
add r2, r0, r2 /* r2 <- source end address */
copy_loop:
ldmia r0!, {r3-r10} /* copy from source address [r0] */
stmia r1!, {r3-r10} /* copy to target address [r1] */
cmp r0, r2 /* until source end addreee [r2] */
ble copy_loop
看了下网上的帖子,adr指令,网上很多人被这这个指令弄郁闷,我看杜春雷的<<arm体系结构与编程>>P143讲, 这个指令是基于PC或者寄存器的,读到是地址无关的,一般被编译器替换为SUB r0, pc,#offset ,不要理解为读取符合表中_start符号的地址(0x33f80000).在我们上电开始执行时,pc从0开始,所以现在r0值为0 +offset,不等于_TEXT_BASE(0x33f80000).接下来要用到链接时确定的符号地址了,_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  stack_setup  clear_bss  ldr pc, _start_armboot ( ROMRAM)
_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还做了一次代码搬运如下,大概如下图,不过分两种,一种是把pc机传的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的+/-运行显然是有一个地址范围的, 所以我们在上面选择代码量小的地址无关, 是很明智的.
而访问地址相关的代码, 只需要使用其它的寻址指令就行了. 但在这之前, 必须保证代码被放在正确的地址上, 所以通常都会有一个复制代码的过程, 然后就是跳转到一个标号, 地址相关代码就开始运行了.