嵌入式Linux启动分为两个部分,系统引导与Linux启动。系统引导将完成Linux装入内存前,初始化CPU和相关IO设备,并将Linux调入内存的工作。系统引导主要由BootLoader实现。在BootLoader将Linux内核调入内存之后,将权力交给LinuxKernel,进入Linux的启动部分。以下详细分析启动的过程与使用的文件。
一、系统引导与BootLoader
BootLoader因嵌入式系统的不同与PC机有很大不同,这里将以Hyper250(Inter Xscale GDPXA250)的启动为例来分析。由于没有BIOS驱动主板,EnbeddedOS必须由bootloader驱动所有的硬件,并完成硬件的初始化工作。
所有的初始化文件在hyper250/Bootloader目录下。
首先分析开机运行的分件:
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/start_xscale.S
文件包含两个库文件:
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/include/config.h
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/include/start_xscale.h
文件config.h主要完成系统各硬件的宏定义与设定,xscale.h主要完成对系统芯片的及系统操作的设定。
以下分析config.h文件:
(1)存储总线设备的宏定义:定义Flash的大小、字长等信息,定义SRAM的基址、大小和块大小。
(2)动态内存设定:定义DRAM的大小、基址。
(3)软件包信息:包名称、版本号。
(4)设定BOOT LOADER的位置:在DRAM和SRAM的最大值、DRAM装入位置、栈的基址。
(5)设定kernel的位置:在DRAM和SRAM的基址、KERNEL的最大值、KERNEL中块的数量。
(6)设定文件系统的位置:根目录在DRAM和SRAM的基址、文件系统的最大值、文件系统中块的数量。
(7)设定LOADER程序:LOADER程序的静态内存基址、LOADER程序的最大值、块的数量。
(8)网络设定
以下分析start_xcalse.h文件:
(1)定义内存基址(A0000000)
(2)定义中断基址(40D00000)和中断保护栈的偏移量
(3)定义时钟管理基址(41300000)和寄存器偏移及其初始值
(4)定义GPIO接口寄存器基址(40E00000)及各寄存器的偏移
(5)定义GPIO接口各寄存器的初始值
(6)定义内存控制寄存器基址(48000000)和各寄存器的偏移
(7)定义内存控制寄存器的初始值
(8)定义电源管理寄存器的参数
(9)定义FFUART寄存器的基址(40100000)和各寄存器的偏移
(10)定义FFUART各寄存器的初始值
以下分析start_xcalse.S文件:
(1)设定中断基址(40D00000),完成中断保护栈的初始化
(2)初始化GPIO接口
(3)初始化内存SDRAM
(4)将Bootloader从Flash拷贝到SDRAM中
(5)装入Linux内核镜像,将内核从Flash(000C 0000)装入SDRAM(A0008000)中.
(6)设定保护栈
(7)调用main.c的主函数c_main()
以上start_xcalse.S通过APCS的编程标准书写的汇编文件初始化了系统相关的硬件,并且完成了BootLoader的装入内存和Linux内核的装入,最后将权力转交给main.c。
以下将分析main.c文件:
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/main.c
以及两个库文件
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/include/main.h
hyper250/Bootloader/X-Hyper250R1.1-Boot/src/include/scc.h
#2
二、Linux启动过程分析
1.Makefile分析:
在分析arch/arm/boot/compressed目录下的文件的时候,对于Makefile的分析是很重要的,因为内核将在这个目录相产生。这里主要工作是对内核的压缩和解压工作。本目录在编译完成后将产生vmlinux、head.o、misc.o、head-xscale.o、piggy.o这几个文件。其中vmlinux是没有压缩过的内核。head.o是内核的头部文件,负责初始设置。misc.o将主要负责内核的解压工作,它在head.o之后。head-xscale.o文件主要针对Xscale的初始化,将在链接时与head.o合并。piggy.o是一个中间文件,其实是一个压缩的内核,只不过没有和初始化文件及解压文件链接而已。
2.Decompress分析:
在BootLoader完成系统的引导以后并将Linux内核调入内存之后,调用bootLinux(),这个函数将跳转到kernel的起始位置。如果kernel没有压缩,就可以启动了。如果kernel压缩过,则要进行解压,在压缩过的kernel头部有解压程序。压缩过得kernel入口第一个文件源码位置在arch/arm/boot/compressed/head.S。它将调用函数decompress_kernel(),这个函数在文件arch/arm/boot/compressed/misc.c中,decompress_kernel()又调用proc_decomp_setup(),arch_decomp_setup()进行设置,然后使用在打印出信息“Uncompressing Linux...”后,调用gunzip()。将内核放于指定的位置。
启动首先运行的文件有:
arch/arm/boot/compressed/head.S
arch/arm/boot/compressed/head-xscale.S
arch/arm/boot/compressed/misc.c
这些文件主要用于解压内核和以及启动内核映象。一旦内核启动,则这些文件所占内存空间将被释放。而且,一旦系统通过reset重起,当BootLoader将压缩过的内核放入内存中,首先执行的必然是这些代码。
以下分析head.S文件:
(1)对于各种Arm CPU的DEBUG输出设定,通过定义宏来统一操作。
(2)设置kernel开始和结束地址,保存architecture ID。
(3)如果在ARM2以上的CPU中,用的是普通用户模式,则升到超级用户模式,然后关中断。
(4)分析LC0结构delta offset,判断是否需要重载内核地址(r0存入偏移量,判断r0是否为零)。
这里是否需要重载内核地址,我以为主要分析arch/arm/boot/Makefile、arch/arm/boot/compressed/Makefile和arch/arm/boot/compressed/vmlinux.lds.in三个文件,主要看vmlinux.lds.in链接文件的主要段的位置,LOAD_ADDR(_load_addr)=0xA0008000,而对于TEXT_START(_text、_start)的位置只设为0,BSS_START(__bss_start)=ALIGN(4)。对于这样的结果依赖于,对内核解压的运行方式,也就是说,内核解压前是在内存(RAM)中还是在FLASH上,因为这里,我们的BOOTLOADER将压缩内核(zImage)移到了RAM的0xA0008000位置,我们的压缩内核是在内存(RAM)从0xA0008000地址开始顺序排列,因此我们的r0获得的偏移量是载入地址(0xA0008000)。接下来的工作是要把内核镜像的相对地址转化为内存的物理地址,即重载内核地址。
(5)需要重载内核地址,将r0的偏移量加到BSS region和GOT table中。
(6)清空bss堆栈空间r2-r3。
(7)建立C程序运行需要的缓存,并赋于64K的栈空间。
(8)这时r2是缓存的结束地址,r4是kernel的最后执行地址,r5是kernel境象文件的开始地址。检查是否地址有冲突。
将r5等于r2,使decompress后的kernel地址就在64K的栈之后。
(9)调用文件misc.c的函数decompress_kernel(),解压内核于缓存结束的地方(r2地址之后)。此时各寄存器值有如下变化:
r0为解压后kernel的大小
r4为kernel执行时的地址
r5为解压后kernel的起始地址
r6为CPU类型值(processor ID)
r7为系统类型值(architecture ID)
(10)将reloc_start代码拷贝之kernel之后(r5+r0之后),首先清除缓存,而后执行reloc_start。
(11)reloc_start将r5开始的kernel重载于r4地址处。
(12)清除cache内容,关闭cache,将r7中architecture ID赋于r1,执行r4开始的kernel代码。
关于head-xscale.S文件,它定义了xcale处理器的64k的cache缓存的实现代码和关闭MMU及缓存的代码,这些代码将在链接过程中与head.S的合并。
关于misc.c文件,它引入了以下几个文件:
include/linux/kernel.h
include/asm-arm/arch-pxa/uncompress.h
include/asm-arm/proc-armv/uncompress.h
include/asm-arm/uaccess.h
lib/inflate.c
以下分析misc.c文件的decompress_kernel()函数:
(1)首先传入参数:解压后内核地址,缓存开始地址,缓存结束地址,arch id。这些参数通过寄存器r0(r5),r1,r2,r3(r7)传入。
(2)接着执行proc_decomp_setup(),它在include/asm-arm/proc-armv/uncompress.h文件中。主要刷新并起用i cache,锁住交换缓存,这是一段嵌入的arm汇编代码。
(3)接着执行arch_decomp_setup(),它在include/asm-arm/arch-pxa/uncompress.h文件中,是一个空函数,用于扩展。
(4)然后执行makecrc(),它在lib/inflate.c中,主要将产生CRC-32 table,进行循环冗余校验。
(5)调用gunzip()解压kernel,它也在lib/inflate.c中。
(6)返回head.S,解压后kernel的长度传给r0,解压后的内核地址预先在r5中定义了。