前面一段时间一直在移植U-Boot,Linux内核和构建根文件系统,其中有些地方还不是很明白,现在回过头来,理解一下U-boot的启动流程,以及 u-Boot是如何加载引导内核启动的。这里的分析也都是以U-Boot-2009.08版本为基础的,可能会和以前的版本有所不同。在这里也不打算一句 句分析U-Boot的源码,只是想把U-Boot一步一步怎么最终能够加载Linux内核的过程,分析一下。
首先,我们应该理解Bootloader是什么?它有什么作用?其实它就是系统上电后运行的和小段程序。
1 BootLoader的概念
在系统上电后,需要一段程序来进行初始化:关闭WATCHDOG,改变系统时钟,初始化存储控制器,将更多的代码复制到内存中。并将操作系统内核复制到内 存中运行,这就段程序代码就叫做Bootloader。没有一个Bootloader完全支持所有CPU,所以我们要想使用Bootloaser一般情况 下要自己进行修改,我们可以增强Bootloader的功能,让它具有网络功能,可以通过NFS远程下载Linux内核和根文件系统,可以烧写Linux 内核和根文件系统到NandFlash中,而这些功能对于最终的用户来说是没有什么意义的,它们看到的只是Bootloader引导Linux内核启动这 一个功能,而其余的功能只对开发人员很有用处。也就是说在开发期间这些功能是必不可少的。 (1)启动加载模式:这种模式也称为“自主”模式。也就是Bootloader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入,这种模式是在嵌入式产品发布里的通用模式。 (2) 下载模式:在这种模式下,目标机上的Bootloader将通过串口连接或网络连接等通信手段从主机下载文件,例如:下载内核映像和根文件系统映像等。从 主机下载的文件 通常首先被Bootloader保存到目标机的RAM中,然后再被Bootloader写到目标上的Flash类的固态存储设备中,Bootloader 的这种模式是在在开发时使用的工作于这种模式的Bootloader通常都 会向它的终端用户提供一个简单的命令行接口。 在嵌入式Linux系统中从软件的角度通常可以分为4个层次: (1)引导加载程序,包括固化在固件中的boot代码(可选)和Bootloader两大部分。 有些CPU在运行Bootloader之前运行一段固化的程序 ,比如x86结构的CPU就是先运行BIOS中的固件,然后才运行硬盘的第一个分区中的BootLoader。在大多数的嵌入式系统中并没有固件,Bootloader是上电后第一个执行的程序。 (2)Linux内核 嵌入式定制的内核以及启动参数,启动参数可以是Bootloader传递给内核的,也可以是内核默认的。 (3)文件系统 包括根文件系统和建立于Flash内存设备之上的文件系统。里面包括了Linux系统能够运行所必要的应用程序和库文件等。比如可以给用户提供操作Linux的控制shell程序。 (4)用户应用程序 特定于用户的应用程序,它们也存储在文件系统中,有时在用户应用程序和内核层之间可以还会包括一个嵌入式图形用户界面。 2. Bootloader启动的两个阶段 从固态存储设备上启动的Bootloader大多都是两阶段的启动过程,第一阶段使用汇编来实现。它完成一些依赖于CPU体系结构的初始化,并调用第二阶段的代码,第二阶段则通常使用C语言来实现,这样可以实现更复杂的功能,而且代码会有更好的可读性和可移植性。 (1) Bootloader第一阶段的功能 1)硬件设备初始化 2)为加载Bootloader的第二阶段准备RAM空间。 3)复制Bootloader的第二阶段代码到RAM空间中。 4)设置好栈 5)跳转到第二阶段代码的C入口点。 在第一阶段进行的硬件初始化一般包括:关闭WATCHDOG,关中断,设置 CPU的速度和时钟频率RAM初始化等。这些不都是必需的。 (2)Bootloader第二阶段的功能 1)初始化本阶段要使用的硬件设备 2)检测系统内存映射 3)将内核映像和根文件系映象从Flash望到RAM空间中 4)为内核设置启动参数 5)调用内核 将内核存放在适当的位置后,直接跳到它的入口点即可调用内核,调用内核之前,下列条件要满足 (1)CPU寄存器的设置 R0=0. R1=机器类型ID;对于ARM结构的CPU,其机器类型ID在linux/arch/arm/tools/mach-types R2=启动参数标记列表在RAM中起始基地址 (2)CPU工作模式 必须禁止中断(IRQs和FIQs) CPU必须为SVC模式 (3)Cach和MMU的设置 MMU必须关闭 指令Cach可以打开也可以关闭 数据Cach必须关闭
U-Boot启动流程(Linux内核)的分析(二) (2010-05-06 21:54)
这 一篇主要就是U-Boot的config.mk进行了分析。如果要使用开发板board/<board_name>,就先执行 “make<board_name>_config”命令进行配置,然后执行”make all“,就可以生成 如下3个文
件。 U-boot.bin:二进制可执行文件,它就是可以直接烧入ROM,NORFlash的文件 u-Boot:ELF格式的可执行文件, U-Boot.srec:Motorla S-Record格式的可执行文件 对于S3C2410的开发板,执行”make smdk2410_config“."make all"后生成的U-Boot.bin可以烧入NOR Flash中运行,启动后可以看到串口输出一些信息后进行控制界面。
1。 U-boot的配置过程
在顶层Makefile中可以看到如下代码:
. . . . . . . . . . . MKCONFIG : = $( SRCTREE) / mkconfig . . . . . . . . smdk2410_config : unconfig @$( MKCONFIG) $( @ : _config= ) arm arm920t smdk2410 samsung s3c24x0
这是在根目录下的MAKEFILE文件中的两个语句,其中的MKCONFIG就是根目录下的mkconfi文件。$(@:_config=)的结 果就是将”smdk2410_config“中的_config去掉,结果为“smdk2410”.所以“make smdk2410_config”实际上就是执行如下命令:
. / mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0
mkconfig的作用,在mkconfig文件开头第6行给出了它的用法
# Parameters: Target Architecture CPU Board [ VENDOR] [ SOC]
对于S3C2410 S3C2440,它们被称为Soc(systme on chip),上面除CPU外,还集成了包括UART,USB控制器,NANDFlash控制器等设备,称为片上外 设。 下面看一下makeconfig的作用。 (1)确定开发板名称BOARD_NAME,相关代码如下:
APPEND= no # Default : Create new config file BOARD_NAME= "" # Name to print in make output while [ $# - gt 0 ] ; do case "$1" in - - ) shift ; break ; ; - a) shift ; APPEND= yes ; ; - n) shift ; BOARD_NAME= "${1%%_config}" ; shift ; ; * ) break ; ; esac done
对于./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0命令,其中没有 "--","-a","-n"等符号,所以上面几行不会执行。
[ "${BOARD_NAME}" ] | | BOARD_NAME= "$1"
执行完上面的这句后,BOADR_NAME的值等于第1个参数,即"s3ck2410" (2)创建到平台开发板相关折头文件的链接
if [ "$SRCTREE" ! = "$OBJTREE" ] ; then //判断源代码目录和目标文件目录是否是一样 mkdir - p ${ OBJTREE} / include mkdir - p ${ OBJTREE} / include2 cd ${ OBJTREE} / include2 rm - f asm ln - s ${ SRCTREE} / include / asm - $ 2 asm LNPREFIX= "../../include2/asm/" cd . . / include rm - rf asm - $ 2 rm - f asm mkdir asm - $ 2 ln - s asm - $ 2 asm else cd . / include rm - f asm ln - s asm - $ 2 asm fi
直接在源代码目录下编译时,条件不满足,将执行else分支的代码,在else分支中,进入include目录,删除asm文件,然后再次建立 asm文件,并令它链接向asm-$2目录,即asm-arm。
rm - f asm - $ 2/ arch //删除asm-$2/arch目录,即asm-arm/arch if [ - z "$6" - o "$6" = "NULL" ] ; then //$6="s3c24x0"不为空,也不为NULL,执行else分支 ln - s ${ LNPREFIX} arch- $ 3 asm - $ 2/ arch //LNPREFIX 为空,这个命令实际上等同于"ln - s arch-s3c24x0 asm-arm/arch" else ln - s ${ LNPREFIX} arch- $ 6 asm - $ 2/ arch fi if [ "$2" = "arm" ] ; then //重新建立/asm-arm/proc文件,并让它链接向proc-armv目录 rm - f asm - $ 2/ proc ln - s ${ LNPREFIX} proc- armv asm - $ 2/ proc fi
(3)创建顶层MAKEFILE包含的文件include/config.mk
# # Create include file for Make # echo "ARCH = $2" > config. mk echo "CPU = $3" > > config. mk echo "BOARD = $4" > > config. mk [ "$5" ] & & [ "$5" ! = "NULL" ] & & echo "VENDOR = $5" > > config. mk [ "$6" ] & & [ "$6" ! = "NULL" ] & & echo "SOC = $6" > > config. mk
对于./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0命令,上面几行创建的config.mk文件的内容如下 :
ARCH = arm CPU = arm920t BOARD = smdk2410 SOC = s3c24x0
(4)创建开发板相关的头文件include/config.h
# # Create board specific header file # if [ "$APPEND" = "yes" ] # Append to existing config file then echo > > config. h else > config. h # Create new config file fi echo "/* Automatically generated - do not edit */" > > config. h echo "#include <configs/$1.h>" > > config. h echo "#include <asm/config.h>" > > config. h exit 0
APPEND维持原值"NO",所以config.h被重新建立,也就是执行echo "#include <configs/$1.h>" >>config.h #include <configs/smdk2410.h> 总之,当你执行make smdk2410_config ,实际的作用就是执行./mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0,它将产生如下的
几种作用 (1) 开发板的名称 BOARD_NAME等于 $1 (2)创建到平台,开发板相关的头文件的链接,如下所示 ln -s asm-$2 asm ln -s arch-$6 asm-S2/arch ln - s proc-armv asmn-$2/proc 如果$2不是arm的话,此行没有 (3)创建顶层Makefile包含的incldue /config.mk,如下所示 ARCH = $2 CPU = $3 BOARD = $4 VENDOR = $ $5 为空,或者NULL的话,些行没有 SOC = $6 (4) 创建开发板相关的头文件include/config.h,如下 所示 #include <config.h/$1.h> 从上面执行完命令后的结果,可以看出来,如果要在board目录下新建一个开发板<board_name>的目录,则在 include/configs 目录下也要建立一个文件<board_name>.h,里面存放的就是开发板<board_name>的配置信息。 3.U-Boot的编译,连接过程
# load ARCH, BOARD, and CPU configuration include $( obj) include / config. mk export ARCH CPU BOARD VENDOR SOC # set default to nothing for native builds ifeq ( $ ( HOSTARCH) , $ ( ARCH) ) CROSS_COMPILE ? = endif # load other configuration include $( TOPDIR) / config. mk
这是根目录下的Makefile中与ARM相关的代码。 第 一行中包含的config.mk文件,就是在第一开始配置过程中制作出来的include/conifg.mk文件,我们在一开始配置U-boot时执行 过mkconfig。mini2440 时生成的文件,其中定义了ARCH,CPU,BOARD,SOC等。4个变量的值为arm,arm920t,smdk2410,s3c24x0.我们在执 行mkconfig。mini2440时,其实执行的是如下的命令:
. / mkconfig smdk2410 arm arm920t smdk2410 NULL s3c24x0
最后一句话include $(TOPDIR)/config.mk 包含顶层目录的config.mk文件。它根据上面4个变量的值确定了编译器。编译选项等。在顶层的config.mk中可以看到:
fdef VENDOR BOARDDIR = $( VENDOR) / $ ( BOARD) else BOARDDIR = $( BOARD) endif ifdef BOARD sinclude $( TOPDIR) / board/ $ ( BOARDDIR) / config. mk # include board specific rules endif LDSCRIPT : = $( TOPDIR) / board/ $ ( BOARDDIR) / u- boot. lds LDFLAGS + = - Bstatic - T $( obj) u- boot. lds $( PLATFORM_LDFLAGS) ifneq ( $ ( TEXT_BASE) , ) LDFLAGS + = - Ttext $( TEXT_BASE) endif
在u-boot-2009.08\board\samsung\smdk2410\config.mk中定义了“TEXT_BASE = 0x33F80000”.所以最终结果是:BOARDDIR为smdk2410;LDFLAGS中有“-T \cpu\arm920t\u-boot.lds -Ttext 0x33f80000”.其中的-Ttext $(TEXT_BASE),这句指明了代码段的起始地址。为什么是0x33F8 0000呢?这是将NAND中Uboot拷贝到RAM中的起始地址,所以在代码拷贝到RAM之前不能使用绝对地址来寻址数据,只能用相对地址,在以下将用 虚拟地址来指Uboot在RAM中的地址,也就是0x33F80000 继续分析MAKEFIle文件:
OBJS = cpu/ $ ( CPU) / start. o LIBS = lib_generic/ libgeneric. a LIBS + = lib_generic/ lzma/ liblzma. a LIBS + = lib_generic/ lzo/ liblzo. a LIBS + = $( shell if [ - f board/ $ ( VENDOR) / common/ Makefile ] ; then echo \ "board/$(VENDOR)/common/lib$(VENDOR).a" ; fi) LIBS + = cpu/ $ ( CPU) / lib$( CPU) . a
从上面的第一行我们可以看到OBJS的第一个值为"cpu/$(CPU)/start.o",即"cpu/arm920t/start.o"。下面的几行 指定了LIBS变量,也就是平台,开发板相关的各个目录,通用目录下相应的库。OBJS LIBS所代表的.o,.a文件构成了U-Boot,它们通过下面相应的源文件编译得到。
$( OBJS) : depend $( MAKE) - C cpu/ $ ( CPU) $( if $( REMOTE_BUILD) , $ @ , $ ( notdir $@) ) $( LIBS) : depend $( SUBDIRS) $( MAKE) - C $( dir $( subst $( obj) , , $ @ ) )
对于OBJS中的每个成员,都将进入cpu/$(CPU)目录编译它们,现在的OBJS为cpu/arm920t/start.o。它由cpu /arm920t/start.S编译得到。对于LIBS中的每个成员,都将进入相应的子目录执行"make命令"。当所有的OBJS,LIBS所表示 的.o .a文件都生成后,就剩最后的连接了,这对应MAKEFILE中的下面几行:
$( obj) u- boot. srec: $( obj) u- boot $( OBJCOPY) - O srec $< $@ $( obj) u- boot. bin: $( obj) u- boot $( OBJCOPY) ${ OBJCFLAGS} - O binary $< $@ $( obj) u- boot. ldr: $( obj) u- boot $( obj) tools/ envcrc - - binary > $( obj) env- ldr. o $( LDR) - T $( CONFIG_BFIN_CPU) - c $@ $< $( LDR_FLAGS) $( obj) u- boot. ldr. hex : $( obj) u- boot. ldr $( OBJCOPY) ${ OBJCFLAGS} - O ihex $< $@ - I binary $( obj) u- boot. ldr. srec: $( obj) u- boot. ldr $( OBJCOPY) ${ OBJCFLAGS} - O srec $< $@ - I binary $( obj) u- boot. img: $( obj) u- boot. bin . / tools/ mkimage - A $( ARCH) - T firmware - C none \ - a $( TEXT_BASE) - e 0 \ - n $( shell sed - n - e 's/.*U_BOOT_VERSION//p' $( VERSION_FILE) | \ sed - e 's/"[ ]*$$/ for $(BOARD) board"/' ) \ - d $< $@ . . . . . . . . . . . . . . . . GEN_UBOOT = \ UNDEF_SYM= ` $ ( OBJDUMP) - x $( LIBBOARD) $( LIBS) | \ sed - n - e 's/.*\($(SYM_PREFIX)__u_boot_cmd_.*\)/-u\1/p' | sort | uniq`; \ cd $( LNDIR) & & $( LD) $( LDFLAGS) $$UNDEF_SYM $( __OBJS) \ - - start- group $( __LIBS) - - end- group $( PLATFORM_LIBS) \ - Map u- boot. map - o u- boot $( obj) u- boot: depend $( SUBDIRS) $( OBJS) $( LIBBOARD) $( LIBS) $( LDSCRIPT) $( obj) u- boot. lds $( GEN_UBOOT)
先使用$(obj)u-boot:规则连接得到ELF格式的U-Boot,最后转换为二进制格式u-boot.bin.S-Record格式u- Boot.srec.其中LDFLAGS确定了连接方式,也就是-T \cpu\arm920t\u-boot.lds -Ttext 0x33f80000指定了程序的布局地址,\cpu\arm920t\U-Boot.lds文件如下:
UTPUT_FORMAT( "elf32-littlearm" , "elf32-littlearm" , "elf32-littlearm" ) OUTPUT_ARCH( arm) ENTRY( _start) SECTIONS { . = 0x00000000; . = ALIGN( 4) ; . text : { cpu/ arm920t/ start. o ( . text) * ( . text) } . = ALIGN( 4) ; . rodata : { * ( SORT_BY_ALIGNMENT( SORT_BY_NAME( . 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 ( NOLOAD) : { * ( . bss) . = ALIGN( 4) ; } _end = . ; }
从cpu/arm920t/start.o (.text) 被放在程序的最前面,所以U-Boot的入口点在cpu/arm920t/start.s中, 总结一下U-Boot的编译流程: (1)首先编译cpu/$(CPU)/start.s,对于不同的CPU,还可能编译cpu/$(CPU)下面的其他文件。 (2)然后,对于平台开发板相关的每个目录,每个通用目录都使用它们各自的MAKEFILE生成相应和库。 (3)将1,2步骤生成的.o.a文件按照$(BOARDDIR)/config.mk 文件中指定的代码段起始地址。$(obj)u-boot.lds 连接脚本进行连接。 (4)第3步得到的是ELF格式的U-Boot,后面MAKEFILE还会将它转换为二进制格式 S-Record格式。
U-boot启动流程(Linux内核)的分析(三) (2010-05-06 22:17)
U-boot属于两阶段的Bootloader,第一阶段的文件为cpu/arm920t/start.S 和board\samsung\smdk2410/lowlevel_init.S,前者是平台相关的,后者是开发板相关的。
1.U-Boot第一阶段代码分析 (1)硬件设备初始化 依次完成如下设置:将CPU的工作模式设为管理模式(SVC),关闭WATCHDOG,设置FCLK,HCLK,PCLK的比例,关闭MMU,CACHE。代码在cpu/arm920t/start.S中, (2)为加载Bootloader的第二阶段代码准备RAM空间。 所谓准备RAM空间,就是初始化内存芯片,使它可用,对于S3C24x0,通过在Start.S中调用lowlevel_init函数来设置存储控制器,使得外接
SDRAM可用,lowlevel_init.S,文件是与开发板相关的,这表示如果外接的设备不一样,可以修改lowlevel_init.S文件中的相关的宏。
_TEXT_BASE: . word TEXT_BASE //这里是获得代码段的起始地址,我的是0x33F80000(在board/xxx/config.mk中 //可到找到“TEXT_BASE=0x33F80000” . globl lowlevel_init //这里相当于定义一个全局的lowlevel_init以方便调用 lowlevel_init: /* memory control configuration */ /* make r0 relative the current location so that it */ /* reads SMRDATA out of FLASH rather than memory ! */ ldr r0, = SMRDATA //SMDATA表示这 13个寄存器的值存放的开始地址,值为0x33F8xxxx,处于内 //存中,这一句的作用是把其值加载到r0中 ldr r1, _TEXT_BASE // 把代码的起始地址(0x33F80000)加载到r1中 sub r0, r0, r1 //r0减去r1其结果存入r0,也即SMDATA中的起始地址0x33F8xxxx减去 //0x33F80000,其结果就是13个寄存器的值在NOR Flash存放的开始地址 ldr r1, = BWSCON /* Bus Width Status Controller */ //存储控制器的基地址 add r2, r0, # 13* 4 //在计算出来的存放地址加上#13*4,然后其结果保存在r2中 //13 个寄存器,每个寄存器占4个字节 0: ldr r3, [ r0] , # 4 //内存中r0的值加载到r3中,然后r0加4,即下一个寄存器的 str r3, [ r1] , # 4 //读出寄存器的值保存到r1中,然后r1也偏移4 cmp r2, r0 //比较r0与r2的值,如果不等继续返回0:执行,也即13个寄存器的值 // 是否读完 bne 0b /* everything is fine now */ mov pc, lr //程序跳转,返回到cpu_init_crit中 . ltorg /* the literal pools origin */ SMRDATA: . . . . . . . . . . . . . . . . . . .
(3)复制Bootloader的第二阶段代码到RAM空间中 这里将整个U-Boot代码都复制到SDRAM中,这在cpu/arm920t/start.s中实现
relocate: /* 将U-Boot复制到RAM中 */ adr r0, _start /* r0:当前代码的开始地址 */ ldr r1, _TEXT_BASE /* r1:代码段的连接地址*/ cmp r0, r1 /* 测试现在是在FLash中,还在是RAM中,如果要从NandFlash启动的话,这里要根据需要修改 */ beq stack_setup /*如果已经在RAM中,则不需要复制*/ ldr r2, _armboot_start /*_armboot_start在前面定义,是第一条指令的运行地址*/ ldr r3, _bss_start /*在连接脚本U-Boot.lds中定义,是代码段的结束地址*/ sub r2, r3, r2 /* r2 <- 代码段长度 */ add r2, r0, r2 /* r2 <-代码段的结束地址 */ copy_loop: ldmia { r3- r10} /* 从地址[r0]处获得数据 */ stmia { r3- r10} /* 复制到地址[r1]处 */ cmp r0, r2 /* 判断是否复制完毕 */ ble copy_loop /*没有复制完,则继续*/ # endif /* CONFIG_SKIP_RELOCATE_UBOOT */
上面这段程序,在使用NANDFlash启动时,需要修改。 (4)设置好栈
/*栈的设置灵活性很大,只要让sp寄存器指向一段没有使用的内存即可*/
stack_setup: ldr r0, _TEXT_BASE /* _TEXT_BASE 为代码段的开始地址,值为0x33F80000 */ sub r0, r0, # CONFIG_SYS_MALLOC_LEN /* 代码段下面,留出一段内存以实现malloc */ sub r0, r0, # CONFIG_SYS_GBL_DATA_SIZE /* 再留出一段内存,存一些全局参数 */ # ifdef CONFIG_USE_IRQ sub r0, r0, # ( CONFIG_STACKSIZE_IRQ+ CONFIG_STACKSIZE_FIQ) # endif sub sp, r0, # 12 /* 最后,留出12字节的内存给abort异常 */ clear_bss: ldr r0, _bss_start /* 下面的都是栈 */ ldr r1, _bss_end /* stop here */ mov r2, # 0x00000000
(5)跳转到第二阶段代码的C入口点 在跳转之前,还要清除BSS段(初始值0,无初始值的全局变量,静态变量放在BSS段。
clear_bss: ldr r0, _bss_start /* BSS段的开始地址,它的值在连接脚本中U-Boot.lds中确定 */ ldr r1, _bss_end /* BSS段的结束地址,它的值在连接脚本u-Boot.lds中确定 */ mov r2, # 0x00000000 /* clear */ clbss_l: str r2, [ r0] /* 向BSS段中写入0值 */ add r0, r0, # 4 cmp r0, r1 ble clbss_l
现在,C函数的运行环境已经完全准备好,通过如下命令直接跳转,这之后在内存中执行,原先在NorFlash中,它将调用lib_arm/boadr.c中的star_armboot函数,这是第二阶段的入口。
ldr pc, _start_armboot _start_armboot: . word start_armboot
2 U-Boot第二阶段代码分析
U-boot在启动内核之前可以让用户决定是否进入下载模式,即进入U-Boot的控制界面。 第二阶段从lib_arm/borad.c中的start_armboot函数开始,程序的流程如下 :
(1)初始化本阶段要使用到的硬件设备
最主要的是设置系统时钟,初始化串口,只要这两个设置好了,就可以从串口看到打印信息。 board_init 函数设置MPLL,改变系统时钟,它是开发板相关函数。board\samsung\smdk2410/smdk2410.c中实现。串口的初始化函数主 要是serial_init,它设置UART控制器,是CPU相关的函数,在cpu/arm920t/s3c24x0/serial.c中实现。
(2)检测系统内存映射
对于特定的开发板,其内存的分布是明确的,所以可以直接设置,board\samsung\smdk2410\smdk2410.c中的dram_init函数指定了本开发板的内存起始地址为0x30000000,大小为0x40000000。
int dram_init ( void ) { gd- > bd- > bi_dram[ 0] . start = PHYS_SDRAM_1; gd- > bd- > bi_dram[ 0] . size = PHYS_SDRAM_1_SIZE; return 0; }
(3) 为内核设置启动参数 在start_armboot()函数的最后,调用main_loop()函数,进行一个无限循环,该函数在common/main.c文件中定义。
u-boot-2009.08\lib_arm\bootm.c文件中,定义了引导Linux内核的 do_bootm_linux()函数,U_Boot也是通过标记列表向内核传递参数的,一般而言,设置这以下两个标记就可以了,在配置文件 include/configs/smdk2410.h中,增加如下两个配置项即可:
# define CONFIG_SETUP_MEMORY_TAGS 1 # define CONFIG_CMDLINE_TAG 1
对于ARM架构的CPU来说,都是通过u-boot-2009.08\lib_arm\bootm.c中的do_bootm_linux函数来启动内核 的,这个函数中,设置标记列表,最后通过“theKernel (0, machid, bd->bi_boot_params);”调用内核,其中,这里第1、2、3个参数就分别存储在r0、r1、r2中。theKernel指向内核 存放的地址,(对于ARM架构的CPU,通常是0x30008000),bd->bi_boot_params就是在board_init函数设置 的机器类型ID,而bd->bi_boot_params就是标记列表的地址
U-boot启动流程(Linux内核)的分析(四) (2010-05-06 22:29)
在上一篇中分析到u-Boot启动Linux内核的函数do_bootm_linux,这一篇则着重分析,U-boot是如果一步一步启动内核的。
我们可以看到在,start_armboot()函数的最后,在一个无限循环中调用了函数main_loop(),该函数在common/main.c文件中被定义,我们可以看到下面的一段代码:
# if defined( CONFIG_BOOTDELAY) & & ( CONFIG_BOOTDELAY > = 0) s = getenv ( "bootdelay" ) ; //得到环境变量中bootdelay bootdelay = s ? ( int ) simple_strtol( s, NULL , 10) : CONFIG_BOOTDELAY; debug ( "### main_loop entered: bootdelay=%d\n\n" , bootdelay) ;
如果定义了CONFIG_BOOTDELAY,则在没有CONFIG_BOOTDELAY秒中,串口上没有输入,则会进行自动的引导Linux内核。也就是执行bootcmd命令。
# ifdef CONFIG_BOOTCOUNT_LIMIT //启动次数的限制功能,如果到达一定次数,将不能启动u-boot. if ( bootlimit & & ( bootcount > bootlimit) ) {//检查是否超出启动次数限制 printf ( "Warning: Bootlimit (%u) exceeded. Using altbootcmd.\n" , ( unsigned ) bootlimit) ; s = getenv ( "altbootcmd" ) ; //启动延时 } else # endif /* CONFIG_BOOTCOUNT_LIMIT */ s = getenv ( "bootcmd" ) ;// 获得启动参数 debug ( "### main_loop: bootcmd=\"%s\"\n" , s ? s : "<UNDEFINED>" ) ; // 这里如果bootdelay大于0,并且中间没有被中断的话,执行命令行参数 if ( bootdelay > = 0 & & s & & ! abortboot ( bootdelay) ) { # ifdef CONFIG_AUTOBOOT_KEYED int prev = disable_ctrlc( 1) ; /* disable Control C checking */ # endif # ifndef CONFIG_SYS_HUSH_PARSER run_command ( s, 0) ; //运行启动的命令行,例如 可以使用tftp命令 # else parse_string_outer( s, FLAG_PARSE_SEMICOLON | FLAG_EXIT_FROM_LOOP) ; # endif
到这里我们就可以看到是怎么调用设置的命令行参数的,在这里还要使用到bootm命令,先来看看bootm命令的实现,在common/cmd_bootm.c
# define CONFIG_BOOTM_LINUX 1 # define CONFIG_BOOTM_NETBSD 1 # define CONFIG_BOOTM_RTEMS 1 # ifdef CONFIG_BOOTM_LINUX extern boot_os_fn do_bootm_linux; # endif # ifdef CONFIG_BOOTM_NETBSD static boot_os_fn do_bootm_netbsd; # endif
可 以看出如果定义了CONFIG_BOOTM_LINUX这个宏的话,就会使用外部文件定义的do_bootm_linux函数,在arm体系结构中,就是 在lib_arm/bootm.c文件中,可以从lib_arm/bootm.c文件中的59行看到do_bootm_linux()的定义。其中第64 行声明了这样一个函数指针theKernel
void ( * theKernel) ( int zero, int arch, uint params) ;
看看它的名字和参数的命名我们也可以猜到这个其实就是内核的入口函数的指针了。几个参数的命名也说明了下文提到的ARM Linux内核启动要求的第一条,因为根据ACPS(ARM/Thumb Procedure Call Standard)的规定,这三个参数就是依次使用r0,r1和r2来传递的。接下来第73行就是给这个函数指针赋值:
theKernel = ( void ( * ) ( int , int , uint) ) images- > ep;
可 以看到theKernel被赋值为images->ep,这个image指使用tools/mkimage工具程序制作uImage时加在 linux.bin.gz前面的一个头部,而ep结构体成员保存的就是使用mkimage时指定的-e参数的值,即内核的入口点(Entry Point)。知道了images->ep的意义之后,给theKernel赋这个值也就是理所当然的了。 image是bootm_headers结构体的指针,可以在inlcude/image.h文件中看到这个结构体的定义如下:
typedef struct bootm_headers { ............................
int fit_noffset_fdt; /* FDT blob subimage node offset */ # endif # ifndef USE_HOSTCC image_info_t os; /* os image info */ ulong ep; /* entry point of OS */ ulong rd_start, rd_end; /* ramdisk start/end */ ...............
}
最后是对内核入口函数的调用,发生在第128行:
theKernel ( 0, machid, bd- > bi_boot_params) ;
调用的时候对参数进行赋值,r0=0,r1=bd->bi_arch_number,r2=bd-> bi_boot_params,一个都不少。至此U-Boot的使命完成,开始进入ARM Linux的世界。 要知道哪个地址是启动内核,哪个地址启动文件系统,要分析common/cmd_bootm.c中的函数 do_bootm,因为引导kernel就是bootm这条命令的工作,do_bootm是命令bootm的执行函数现在我们来分析一下 common/cmd_bootm.c中的函数do_bootm,这是bootm命令的处理函数.do_bootm()函数中的很多功能都是分成了函数的 形式,而在以前的版本中没有这么有结构层次,这里我们也只是分析对引导Linux内核有作用的部分,因为这是一个在common文件夹下的文件,也就意味 着,在引导别的操作系统时也会用到这个函数,而不单单是Linux操作系统.
int do_bootm ( cmd_tbl_t * cmdtp, int flag, int argc, char * argv[ ] ) { ulong iflag; ulong load_end = 0; int ret; boot_os_fn * boot_fn; # ifndef CONFIG_RELOC_FIXUP_WORKS static int relocated = 0; /* relocate boot function table */ if ( ! relocated) { int i; for ( i = 0; i < ARRAY_SIZE( boot_os) ; i+ + ) if ( boot_os[ i] ! = NULL ) boot_os[ i] + = gd- > reloc_off; relocated = 1; } # endif /* determine if we have a sub command */ if ( argc > 1) { char * endp; simple_strtoul( argv[ 1] , & endp, 16) ; if ( ( * endp ! = 0) & & ( * endp ! = ':' ) & & ( * endp ! = '#' ) ) return do_bootm_subcommand( cmdtp, flag, argc, argv) ; } if ( bootm_start( cmdtp, flag, argc, argv) ) //提取mkimage生成的文件头部,放到bootm_headers_t结构体中 return 1; iflag = disable_interrupts( ) ; # if defined( CONFIG_CMD_USB) usb_stop( ) ; # endif # ifdef CONFIG_AMIGAONEG3SE /* * We've possible left the caches enabled during * bios emulation, so turn them off again */ icache_disable( ) ; dcache_disable( ) ; # endif ret = bootm_load_os ( images. os, & load_end, 1) ; //加载操作系统的关键部分 确定使用的地址 if ( ret < 0) { //出错处理 if ( ret = = BOOTM_ERR_RESET) do_reset ( cmdtp, flag, argc, argv) ; if ( ret = = BOOTM_ERR_OVERLAP) { if ( images. legacy_hdr_valid) { if ( image_get_type ( & images. legacy_hdr_os_copy) = = IH_TYPE_MULTI) puts ( "WARNING: legacy format multi component " "image overwritten\n" ) ; } else { puts ( "ERROR: new format image overwritten - " "must RESET the board to recover\n" ) ; show_boot_progress ( - 113) ; do_reset ( cmdtp, flag, argc, argv) ; } } if ( ret = = BOOTM_ERR_UNIMPLEMENTED) { if ( iflag) enable_interrupts( ) ; show_boot_progress ( - 7) ; return 1; } } lmb_reserve( & images. lmb, images. os. load, ( load_end - images. os. load) ) ; if ( images. os. type = = IH_TYPE_STANDALONE) {//独立的应用程序 if ( iflag) enable_interrupts( ) ; /* This may return when 'autostart' is 'no' */ bootm_start_standalone( iflag, argc, argv) ; return 0; } show_boot_progress ( 8) ; # ifdef CONFIG_SILENT_CONSOLE //这里处理Linux操作系统 if ( images. os. os = = IH_OS_LINUX) fixup_silent_linux( ) ; //该函数中处理bootarg参数 # endif boot_fn = boot_os[ images. os. os] ; if ( boot_fn = = NULL ) { if ( iflag) enable_interrupts( ) ; printf ( "ERROR: booting os '%s' (%d) is not supported\n" , genimg_get_os_name( images. os. os) , images. os. os) ; show_boot_progress ( - 8) ; return 1; } arch_preboot_os( ) ; /*下面的函数,继续引导内核的镜像,复制image header 到全局变量header; 检查header的魔数,检查数,header和image中的这两个。确定image的体系结构和类型(KERNEL or MULTI),关闭中断,加载image到header中的加载地址*/ boot_fn( 0, argc, argv, & images) ; //调用do_bootm_linux()函数 show_boot_progress ( - 9) ; # ifdef DEBUG puts ( "\n## Control returned to monitor - resetting...\n" ) ; # endif do_reset ( cmdtp, flag, argc, argv) ; return 1; }
下面我们看一下bootm_load_os()函数
static int bootm_load_os( image_info_t os, ulong * load_end, int boot_progress) { uint8_t comp = os. comp; ulong load = os. load; ulong blob_start = os. start; ulong blob_end = os. end; ulong image_start = os. image_start; ulong image_len = os. image_len; uint unc_len = CONFIG_SYS_BOOTM_LEN; const char * type_name = genimg_get_type_name ( os. type) ; switch ( comp) { //判断image的压缩类型 case IH_COMP_NONE: if ( load = = blob_start) { printf ( " XIP %s ... " , type_name) ; } else { printf ( " Loading %s ... " , type_name) ; //如果在Image head中加载的地址和bootm命令参数2指定的地址相同,则不需要复制,直接执行 if ( load ! = image_start) { memmove_wd ( ( void * ) load, ( void * ) image_start, image_len, CHUNKSZ) ; } } * load_end = load + image_len; puts ( "OK\n" ) ; break ; case IH_COMP_GZIP: printf ( " Uncompressing %s ... " , type_name) ; if ( gunzip ( ( void * ) load, unc_len, ( uchar * ) image_start, & image_len) ! = 0) { puts ( "GUNZIP: uncompress, out-of-mem or overwrite error " "- must RESET board to recover\n" ) ; if ( boot_progress) show_boot_progress ( - 6) ; return BOOTM_ERR_RESET; } * load_end = load + image_len; break ; # ifdef CONFIG_BZIP2 case IH_COMP_BZIP2: //判断是什么类型的压缩类型 printf ( " Uncompressing %s ... " , type_name) ; int i = BZ2_bzBuffToBuffDecompress ( ( char * ) load, & unc_len, ( char * ) image_start, image_len, CONFIG_SYS_MALLOC_LEN < ( 4096 * 1024) , 0) ; if ( i ! = BZ_OK) { printf ( "BUNZIP2: uncompress or overwrite error %d " "- must RESET board to recover\n" , i) ; if ( boot_progress) show_boot_progress ( - 6) ; return BOOTM_ERR_RESET; } * load_end = load + unc_len; break ; # endif /* CONFIG_BZIP2 */ # ifdef CONFIG_LZMA case IH_COMP_LZMA: printf ( " Uncompressing %s ... " , type_name) ;
.................... return 0; }
如果image header中指示的加载地址和bootm命令中参数2指定的地址不相同,则表示要从image header中指示的加载地址处把image data copy到bootm命令中参数2指定的地址处,然后再执行。
bootm命令是用来引导经过u-boot的工具mkimage打包后的kernel image的。
mkimage的用法 uboot源代码的tools/目录下有mkimage工具,这个工具可以用来制作不压缩或者压缩的多种可启动映象文件。 mkimage在制作映象文件的时候,是在原来的可执行映象文件的前面加上一个0x40字节的头,记录参数所指定的信息,这样uboot才能识别这个映象 是针对哪个CPU体系结构的,哪个OS的,哪种类型,加载内存中的哪个位置, 入口点在内存的那个位置以及映象名是什么?到这里整个U-Boot是如何启动Linux内核的,基本上也就清楚了,特别是如何向Linux内核传送的参 数。
PS:下面是“ARM Linux Kernel Boot Requirements”,这篇文章中介绍的,引导Linux内核启动的必须要满足的几个条件:
* CPU register settings //这里也就是我们的theKernel中的作用 o r0 = 0. o r1 = machine type number. o r2 = physical address of tagged list in system RAM. * CPU mode o All forms of interrupts must be disabled ( IRQs and FIQs. ) o The CPU must be in SVC mode. ( A special exception exists for Angel. ) * Caches, MMUs o The MMU must be off. o Instruction cache may be on or off. o Data cache must be off and must not contain any stale data. * Devices o DMA to/ from devices should be quiesced. * The boot loader is expected to call the kernel image by jumping directly to the first instruction of the kernel image.
结合 http://blog.chinaunix.net/uid-28458801-id-3492939.html