一:
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
对于自己扩充需要的命令比较方便。
注:
(上述的说明中,有可能自己的理解有误)