1 关于嵌入式系统的启动 (SHARE FOR ALL)
嵌入式 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 是 ( 没有-- lw:zImage是压缩过的内核) 压缩过的内核。 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 中定义了。
#3
3.kernel 进入文件分析:
随后系统将调入文件: arch/arm/kernel/head_armv.S 或 arch/arm/kernel/head_armo.S 。对于 arm 的 kernel 而言,有两套 .S 文件: _armv.S 和 _armo.S. 选择 _armv.S 还是 _armo.S 依赖于处理器。 ARM 的 version 1, version 2, 都只支持 26 位的地址空间。 version 3 开始支持 32 位的地址空间,同时还向后兼容 26 位的地址空间。 version 4 开始不再向后兼容 26 位的地址空间。这里由于 Hyper250 使用的是 version7 ,故只涉及文件 head_armv.S 。
head_armv.S 是内核的入口点,在内核被解压到预定位置后,它将运行, 这里简要说明其主要工作:
(1) 首先,关中断并进入保护模式,这里将建立虚拟地址到物理地址的映射。 ( 见第二章内存分析 )
(2) 调用 lookup_processor_type ,查询 CUP 和其 ID 是否在 .proc.info 表中,如果存在,则令 r10 指向此结构,在 CPU 的内核入口文件中。如果不是则提示 error : p 并挂起。关于 r10 指向的结构,他所属的内核入口文件,以 Hyper250 为例: arch/arm/mm/proc-xcale.S 。
这里要要注意的是,此处操作的对象是由 vmlinux-armv.lds.in 链接文件定位的段 .proc.info 中,这个段定义在 proc-xcale.S 文件末尾,这里要注意,上面并没有使系统进入保护模式,所以在这里对 .proc.info 寻址的时候,为了得到相对地址,做了一个相对寻址的变换。这里好象只用了这个结构的前 3 位:处理器类型值 (value) ,处理器值掩码 (mask) , MMU 标志值 (mmuflags) 。这 3 个值在分别放在寄存器 r5(0x69052100) 、 r6(0xfffff7f0) 、 r8(0x00000c0e) 中, r5 和 r6 只是用于和获得的处理器的 ID 相比较,而 r8 则有两个可能的值,分别表示 MMU 的状态:如果 MMU 开启,即 CACHE_WRITE_THROUGH ,则 r8=0x00000c0a ,否则 r8=0x00000c0e 。这里 r8 的值将会保持到初始页表时使用。
r10 此时指向段 .proc.info 的开始地址。
(3) 寄存器 r1 中的系统类型值 (unique architecture number) ,这个系统类型值的定义,并且由 bootloader 传入。在文件 arch/arm/tools/mach-types 中:
machine_is_xxx CONFIG_xxxx MACH_TYPE_xxx number
xhyper250R1 ARCH_PXA_XHYPER250R1 PXA_XHYPER250R1 200
(4) 调用 lookup_architecture_type ,将以 r1 的值检查 .arch.info 表,这是个 struct machine_desc 由文件 arch/arm/mach-pxa/xhyper250R1.c 中的 MACHINE_START() 创建。假如没有此结构则提示 error : a 并挂起。
这里要注意的是,段 .arch.info 的定位在 vmlinux-armv.lds.in 文件中紧接 .proc.info ,这个段定义在 include/asm-arm/mach/arch.h 文件中,使用了宏定义 MACHINE_START() 。文件首先定义了一个结构体 machine_desc ,段 .arch.info 主体部分使用了宏定义 MACHINE_START() 其中嵌入这个结构体。
通常来讲 MACHINE_START() 的实现应该在文件 arch/arm/kernel/arch.c 中,而这里 hyper250 的源码中, MACHINE_START() 宏定义在 arch/arm/mach-pxa/xhyper250R1.c 中完成了定义,下面详细分析这个结构:
(A)MACHINE_START
MACHINE_START(_type,_name) 这宏开始处嵌入一个静态结构 machine_desc ,并且立即声明段 .arch.info 。
_type 是 MACH_TYPE(PXA_XHYPER250R1) ,用以赋值给 machine_desc 中的 nr ,这就是系统类型值 number(200) 。
_name 是描述系统类型的字符串,用以赋值给 machine_desc 中的 name 为 char* 。
以下几个宏定义均在包含在 machine_desc 的赋值中,也在段 .arch.info 中。
(B)MAINTAINER
MAINTAINER(n) ,这个 n 并没有赋值给 machine_desc 结构, n 是 "Hybus Co,. ltd." 字符串,公司名字罢了。
(C)BOOT_MEM
BOOT_MEM(_pram,_pio,_vio) ,这里面很关键,又 3 个变量:
_pram ,传值给 phys_ram :物理内存的开始地址,程序中赋值为: 0xa0000000 。
_pio ,传值给 phys_io :物理 io 的开始地址,程序中赋值为: 0x40000000 。
_vio ,传值给 io_pg_offst : io 页表的偏移,程序中赋值为: _vio=0xfc000000 ,不过要进行转换: ((_vio)>>18)&0xfffc=0x3f00
(D)BOOT_PARAMS
BOOT_PARAMS(_params) 这个宏定义了启动参数页表的偏移: param_offset ,程序中赋值为: 0xa0000100 。
(E)FIXUP( 接下来三个宏定义分别是三个函数指针:这些函数都在 machine_desc 结构中定义并且在 xhyper250R1.c 中实现。 )
FIXUP(fixup_xhyper250R1) 宏指向 fixup_xhyper250R1 函数,这个函数有 4 个参数:
fixup_xhyper250R1(struct machine_desc *desc, struct param_struct *params, char **cmdline, struct meminfo *mi)
struct machine_desc :这个结构体前面已经提过了。
param_struct :这个结构体定义在 include/asm/setup.h 中,这是一个向 kernel 传递参数的结构体。
char **cmdline :好像用于定义输出窗口行数。
struct meminfo :这个结构体定义在 include/asm/setup.h 中,这是一个对物理内存区间描述的结构体,它将整个地址空间分为 8 个区间,通常一个区必须是连续的地址并且是同一类型的设备,而用于特殊目的的地址将划分为一个独立的区。首先定义 nr_banks: 块号,然后是结构体 bank[NR_BANKS] , NR_BANKS 为 8 。结构体 bank[NR_BANKS] 中有: start 、 size 、 node 。
下面分析这个函数 fixup_xhyper250R1 的工作,
首先,调用宏 SET_BANK 并赋值为 SET_BANK(0, 0xa0000000, 64*1024*1024) ,这个宏定义在 arch/arm/mach-pxa/generic.h 文件中。 SET_BANK 主要完成设置结构体 meminfo 中 bank[_nr] 的 start 、 size 和 node 。以上为例,则完成了 bank[0] 区间中的 start=0xa0000000 , size=64*1024*1024=64M , node=(__start) - PHYS_OFFSET) >> 27=0
接着,使 mi 的 nr_banks=1 ,好象设定了这个结构只有一个区。要注意的是 meminfo 将在 page_init() 中用于初始化页面。
(F)MAPIO
MAPIO(xhyper250R1_map_io) 宏指向 xhyper250R1_map_io 函数,这个函数没有参数,主要用于 io 地址从虚拟地址到物理地址的映射关系。
这个函数调用了 pxa_map_io() 和 iotable_init(xhyper250R1_io_desc) :
pxa_map_io() 函数定义在 arch/arm/mach-pxa/generic.h 文件中,实现在 arch/arm/mach-pxa/generic.c 中,主要调用了 iotable_init() 函数来进行 io 地址的区间映象。 iotable_init(struct map_desc *) 函数中,参数 map_desc 结构体定义在文件 include/asm-arm/map.h 中,主要有: virtual 、 physical 、 length 和一些标志位: domain 、 read 、 write 、 cache 、 buffer 等。 iotable_init() 函数在文件 arch/arm/mm/mm-armv.c 中,循环调用 create_mapping() 来处理 map_desc 的映射关系。 create_mapping 函数主要工作就是将 io 的虚拟地址到物理地址的映射关系按照 PAGE_SIZE(4K) 的页来进行映射,同时还有段的映射关系。(关于内存的映射将在第 2 章中详细分析)
(G)INITIRQ
INITIRQ(xhyper250R1_init_irq) 指向了 xhyper250R1_init_irq 函数,这个函数将主要完成中断的初始化,这里主要调用了函数 pxa_init_irq() ,这个函数实现在 arch/arm/mach-pxa/irq.c 中。接着调用了 set_GPIO_IRQ_edge() 函数,这个函数也在 irq.c 中。(关于中断的分析将在以后进行)
我们以上通过分析宏 MACHINE_START 而分析了结构体 machine_desc 的一个实例的赋值,我们这里其实只用这个结构体很少一部分信息,主要有三个参数内存物理内存的开始地址、物理 io 的开始地址、 io 页表的偏移,分别存于寄存器 r5(phys_ram=0xa0000000) 、 r6(phys_io=0x40000000) 、 r7(io_pg_offst=0x3f00) 中,并返回。
(5) 初始化页表,映射了 4M 的 RAM ,以使内核运行。
这里值得注意的是: r5 此时为物理内存开始地址 (0xa0000000) ,程序利用宏定义 pgtbl ,将 r4 成为页表首地址 0xC0004000 。然后清空内核目录 swapper_pg_dir 开始的 16K 空间。
(6) 设置 lr 为返回地址 __ret ,以使下面的程序得以跳转返回。
(7) 使 pc=[r10+12] ,也就是跳转到 _xscale_proc_init 结构中的 b __xscale_setup 位置,这个结构在 arch/arm/mm/proc-xcale.S 中。我们来看看这个结构:
__pxa250_proc_info: <--r10 指向这个地址
.long 0x69052100
.long 0xfffff7f0
#if CACHE_WRITE_THROUGH
.long 0x00000c0a
#else
.long 0x00000c0e <-- 这个参数传入了 r8 中
#endif
b __xscale_setup <--[r10+12]
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP|HWCAP_HALF|HWCAP_THUMB|HWCAP_FAST_MULT|HWCAP_EDSP
.long cpu_pxa250_info
.long xscale_processor_functions
.size __pxa250_proc_info, . - __pxa250_proc_info
__xscale_setup 相关的程序多是对协处理器 cp15 的操作,之中用到了宏 F_BIT|I_BIT|SVC_MODE ,
相关的宏定义在文件 include/asm-arm/proc-armv/ptrace.h 中。
#define SVC_MODE 0x13
#define T_BIT 0x20
#define F_BIT 0x40
#define I_BIT 0x80
(8) 通过 proc-xcale.S 中 __xscale_setup 设置 MMU ,并通过 __ret 返回 head_armv.S 。
(9) 在 __ret 返回处设置 lr 通过 __switch_data 返回到 __mmap_switched 。
(10) 打开 MMU ,将 pipeline 清空,以使所有的内存得以正确的访问。并返回到 __mmap_switched 。
(11)__mmap_switched 通过 __switch_data 获得数据,并设置了 stack pointer 。
(12) 清空 BSS ,并保存 CPU 类型值 (processor ID) 以及系统类型 (machine type) 等。
(13) 跳转到 start_kernel 。
from:http://blog.csdn.net/zxl1217/archive/2008/11/20/3339602.aspx