现在的位置: 首页 > 综合 > 正文

bootloader分析

2017年12月22日 ⁄ 综合 ⁄ 共 5295字 ⁄ 字号 评论关闭




一:
bootloader
的设计要求

关于嵌入式系统
bootloader
的功能,主要是整个系统的启动加载,为操作系统内核准备好环境,引导
kernel
的运行。

一般在开发的过程中,
bootloader
有两种操作模式,启动加载和下载,前一种是将操作系统从
flash
中加载到
sdram
中运行。后者是通过某种通信方式将操作系统从开发主机
download
到目标机的
ram
中,然后可以通过
bootloadr
提供的命令选择是否将
load
下来的操作系统写到
flash
中。

按照上面的需求,一般来说
bootloader
具备下面几个要求:

1
:能够引导
kernel
的启动,即能够建立操作系统的运行环境,以及执行到进入操作系统的入口,通常就是我们所说的
start-kernel
中,在
wrt160nv2
中,会进入到
kernel_entry
,在该函数中会建立一个内核进程堆栈,可以认为它是
init
进程的父进程的内核堆栈,它的
id
号为
0
。然后该函数会调用
init-arch
,接着到了
start-kernel

2
:为了支持调试,以及上面所描述的两种操作模式,会在
bootloader
中一般来说,会使用
uart
的驱动(打印消息到串口),网卡驱动(支持
tftp
功能),
flash
驱动(将
tftp
下载的
code
写到
flash
中)等。除了这个以外,还有一些其他的功能,例如,在
bootloader
里面和将要起来的操作系统进行一些参数的传梯,例如,网卡地址,文件系统的位置信息等。

 

二:启动过程分析

      

内核除了可以动态加栽的
module
以外,所有的数据是常驻内存中的,在
kernle
起来以后我们也可以看到,页表的后
256
项都不会改变,所有的进程的这部分页表都是一样的。为什么会这样?这样的要求是必须要这样做,因为我们在嵌入式的存储系统一般来说主要是
flash

nor

nand
),因为
nand
不能片上执行,假设你的内核部分在
nand
上,那么关于这部份
code
的使用,你需要首先读取
nand
上面数据到内存中再来运行,导致速度特别慢。同样的道理,虽然
nor
可以片上执行,但是它的执行速度相对于
sdram
来说是特别慢的。那照速度来说,为什么不把所有的数据,内核和文件系统中的内容等等全部都读到
sdram
中,这样也不行,以为
sdram
的容量是有限的,虽然大于
flash
的容量,但是因为
flash
中的数据一般来说都是压缩的,并且在程序运行期间,堆栈,等也需要占用很大一部分中间,还有,一些数据,例如
bss
中的数据,占用内存空间,但是不占用
flash
的空间。而
kernel
的每一部分,使用非常频繁,所以按照效率与速度考虑,将
kernel
全部导入到
sdram
中。驱动程序,可以有所选择,可以选择是否加载,比如
wireless
驱动,你根本不使用或者偶尔使用,那么
wireless
驱动可以选择性的加载,但是这样
wireless
驱动的数据内容一般都放在文件系统空间。但是如果是网卡驱动,你经常要使用,那么还是直接放在
kernel
中,否则,你就象使用
windows
时候,就算你自己知道驱动的路径,但是它还是在使用设备的时候要说找不到设备驱动。

从上面的描述:我们可以看出,我们在
bootloader
中需要也是必须要将
kernel
读入到
sdram
中,最后执行
kernel
的时候,它的数据还是非压缩的。


uboot
中,加电以后,在
u-boot.lds
中可以入口是
_start
,该函数正式拉开了
bootloader
的序幕。在
u-boot.map
中我们可以看到它的运行地址就是
0x00000000bfc00000
。在
nor
型号的
flash
中,可以片上执行。

 

u-boot
的启动过程比较简单,大致做下面的工作:

1 cpu
初始化

2
时钟,串口,内存(
ddr ram
)初始化

3
内存划分,分配栈,数据,配置参数,以及
u-boot
代码在内存中的位置。

4

u-boot
代码做
relocate

5
初始化
malloc,flash,pci
以及外设(比如,网口)

6
进入命令行或者直接启动
Linux kernel

 

程序从
start.S

_start
开始执行。首先,初始化中断向量,寄存器清零,大致包括
32
个通用寄存器
reg0-reg31
和协处理器的一些寄存器:
CP0_WATCHLO

   
 CP0_WATCHHI


   
 CP0_CAUSE


CP0_COUNT

CP0_COMPARE
等等。

之后,配置寄存器
CP0_STATUS
,设置所使用的协处理器,中断以及
cpu
运行级别(核心级)。


配置
gp
寄存器,把
GOT
段的地址赋给
gp
寄存器。(
gp
寄存器的用处会在后面
relocate code
的部分详细解释),主要目的是工作频率配置,比如
cpu
的主频,总线(
AHB
),
DDR
工作频率等。然后,调用
cache.S

mips_cache_reset

cache
进行初始化。接着调用
cache.S

mips_cache_lock
。这个调用的目的,起初让我不解,后来才知道。这时
ddr ram
并没有配置好,而如果直接调用
c
语言的函数必须完成栈的设置,而栈必定要在
ram
中。所以,只有先把一部分
cache
拿来当
ram
用。做法就是把一部分
cache
配置为栈的地址,锁定。这样,当读写栈的内存空间时,只会访问
cache
,而不会访问真的
ram
地址了。

这时,配置栈的地址,进行调用函数
board_init_f

board.c

进入函数
board_init_f
后,首先做一系列初始化:

timer_init   

时钟初始化

       
        env_init   

环境变量初始化(取得环境变量存放的地址)

       
        init_baudrate   

串口速率

       
        serial_init   
   

串口初始化

       
        console_init_f   

配置控制台

       
        display_banner   

显示
u-boot
启动信息,版本号等

       
        checkboard   
   

执行
board
相关的操作。

       
        init_func_ram   

初始化内存,配置
ddr controller

这一系列工作完成后,串口和内存都已经可以用了。然后,就要把内存进行划分,

在内存的最后一部分,留出
u-boot
代码大小的空间,准备把
u-boot
代码从
flash
搬移到这里然后,是堆的空间,
malloc
的内存就来自于这里。紧接着放两个全局数据结构
bd_info
global_data

和环境变量
boot_params
。最后,是栈的空间。

准备进行
relocate
code

relocate code
的意思是这样的。通常
u-boot
的执行代码肯定是在
flash
上(当调试的时候也可以放在
ram
上)。当启动起来以后,要把它从
flash
上搬移到
ram
里运行。这个工作就叫做
relocate code

但是,问题在于,
flash
上的地址和
ram
上的地址是不同的。当我们把代码从
flash
上搬移到
ram
上以后,当执行函数跳转时,代码里的函数地址还是
flash
上的地址,所以一跳就跳回去了。

这怎么办呢
?


u-boot
里面用的是
PIC

position-independent code
)的方式解决这个问题。

简单介绍一下其原理。当你用
PIC
方式时,在用
gcc
编译时需加上
-fpic
的选项。编译器会为你的可执行代码建立一个
GOT(global
offset table)

的段。一个地址在
GOT
表中有一项,里面存放地址的信息,而在使用这个地址时,只要根据这个地址的编号(也可以叫做偏移量
offset
)找到表中相应的项目,就可以取得那个地址了。

而如果位置发生变化,只要对
GOT
表中的地址进行修改就可以了。

我们可以通过反汇编,看一个简单的函数调用例子:

lw    t9,1088(gp)

jalr    t9

这里,
gp
存放的就是
GOT
表的起始地址,而
1088
就是要调用函数的
offset
,也就是说
GOT
表的那个位置存放着它的地址。
lw   
t9,1088(gp)

把函数地址放入
t9

然后调用就可以了。知道了
PIC
的原理,解释
u-boot
relocate code

的方法就简单了。

简单的说就把
u-boot
的执行代码直接从
flash

copy

ram
的相应区域。

然后,把
GOT
表中的地址都加上一个偏移量,这个偏移量就是
flash
里的地址与
ram
里的地址的差。

还有其他一些工作比如:设置新的栈指针,从
flash
代码里跳转到
ram
代码里
等等。

之后,就进入
board.c

board_init_r
函数,在这个函数里初始化
malloc,flash,pci
以及外设(比如,网口),最后进入命令行或者直接启动
Linux
kernel

这样,
u-boot
的启动工作就完成了

三:代码分析

加载
kernel
的函数:
do_bootm

启动的时候,读取
kernel
的前
64
个字节:

typedef struct
image_header {


uint32_t  
ih_magic;      
/* Image Header Magic Number   
*/


uint32_t  
ih_hcrc; 
/* Image Header CRC Checksum 
*/


uint32_t  
ih_time;  
/* Image Creation Timestamp      
*/


uint32_t  
ih_size;   
/* Image Data Size
      
*/


uint32_t
ih_load; 
/* Data  
Load 
Address         
*/


uint32_t
ih_ep;           
/* Entry Point Address


       
*/


uint32_t  
ih_dcrc; 
/* Image Data CRC Checksum    
*/


uint8_t          
ih_os;            
/* Operating System            
*/


uint8_t          
ih_arch;  
/* CPU architecture             
*/


uint8_t          
ih_type;  
/* Image Type              
*/


uint8_t          
ih_comp;
/* Compression Type           
*/


uint8_t          
ih_name[IH_NMLEN];  
/* Image Name            
*/

}
image_header_t;

这个头是怎么来的呢?通过
mkimage
过来的:

cd $(IMAGEDIR) ;
$(CUR_DIR)/mkimage -A mips -O linux -T kernel -C $(COMP) -a 8a000000 -e $(shell readelf -h
$(ROOTDIR)/$(LINUXDIR)/vmlinux | grep "Entry" | awk '{print $$4}') -n
"Linux Kernel Image" 
-d
$(USER)_tmp.trx $(USER)_uImage

addpattern -i $(IMAGEDIR)/$(USER)_uImage -o $(IMAGEDIR)/code.bin -g
-s;

根据
code
中的上下文,该
(USER)_uImage
是通过
lzma
进行压缩,
kernel

load
地址是:
-a 8a000000

kernel
的入口地址是
0x881f6040

关于地址的说明,可以看其他同事写的文档《关于
MIPS
地址的一些问题
.doc
》。

在函数
do_bootm
中,





kernel
解压缩在
8a000000
地址处,解压缩函数中的
data
参数已经去掉了上面的
64
字节的头。那程序什么时候开始进入到
sdram
中运行呢?在
start.s
中,函数
relocate_code

U-boot


relocate

到内存的最高端。拷贝完代码之后,注意没有拷贝
kernel

,就在
in-ram

中执行了,在解压缩的时候,把
kernel

解压缩到
sdram

 

如果我们的
code

是未经过压缩,根据
code

里面的情况,他会比较
kernel


load

地址是否和
addr

地址一致,例如:

如果使用
mkimage
-a addr -e addr


那么
tftp


下载
kernel

就一定不能下载
addr




,否则,
kernelrun

不起来。


因为
u-boot

并不搬运
kernel

代码,


也就是没有把
header

去掉。


所以


只有入口是
addr+0x40

才是
kernel

的入口。


当然也不能下到
< addr + 2M

的地方,


否则搬运的时候会有一些覆盖,


导致搬运后的
kernel

不完整,
bootm

的时候,
u-boot

就会
RESET

的。


 

 

关于命令的说明:所有的命令都放在
__u_boot_cmd_start

节中,这连接的确定了地址,但是
uboot

经过搬运之后,要重新设置起地址。
Uboot

对于自己扩充需要的命令比较方便。

 

 

 

注:

(上述的说明中,有可能自己的理解有误)

 

 

抱歉!评论已关闭.