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

s3c2440学习系列5(bootloader)

2017年12月21日 ⁄ 综合 ⁄ 共 4684字 ⁄ 字号 评论关闭

 

 

一: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项都不会改变,所有的进程的这部分页表都是一样的。为什么会这样?这样的要求是必须要这样做,因为我们在嵌入式的存储系统一般来说主要是flashnornand),因为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.Smips_cache_resetcache进行初始化。接着调用cache.Smips_cache_lock。这个调用的目的,起初让我不解,后来才知道。这时ddr ram并没有配置好,而如果直接调用c语言的函数必须完成栈的设置,而栈必定要在ram中。所以,只有先把一部分cache拿来当ram用。做法就是把一部分cache配置为栈的地址,锁定。这样,当读写栈的内存空间时,只会访问cache,而不会访问真的ram地址了。

这时,配置栈的地址,进行调用函数board_init_fboard.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里面用的是PICposition-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的执行代码直接从flashcopyram的相应区域。

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

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

之后,就进入board.cboard_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进行压缩,kernelload地址是:-a 8a000000kernel的入口地址是0x881f6040

 

抱歉!评论已关闭.