在bootloader的帮助下,内核被载入到内存中,内核映像被加载到内存并获得控制权之后,内核启动流程开始。通常,内核映像以压缩形式存储,并不是一个可以执行的内核。因此内核的首要工作是自解压内核映像。
内核编译生成vmlinux后,通常会对其进行压缩,得到zImage(小内核,小于512KB)或者bzImage(大内核,大于512KB),在它们的头部嵌有解压缩程序。真正的内核执行映像其实是在编译时生arch/${ARCH}/boot/文件夹中的bin文件。
以arm为例
Linxu镜像的格式:
(1)Image:直接生成并未压缩的内核,一般用于PC机。
(2)zImage:Image的压缩版,采用gzip进行压缩,比Image体积小,但启动时需要进行自解压,嵌入式系统中一般采用此方法。
(3)uImage:是u-boot专用的一种内核镜像格式,它是在zImage的基础上又添加了一个长度为0x40的标签头,在u-boot启动时会去掉此头信息,仍按zImage启动,头信息主要用于区分不同格式的内核镜像。
(4)xipImage:片上执行的未压缩内核
(5)bootpImage:将内核与根文件系统制作在一起的镜像。
内核的压缩和解压缩代码都在文件夹arch/arm/boot/compressed下,编译完毕后将产生head.o, misc.o , piggy.gzip.o, vmlinux, decompress.o这几个文件。
(1)head.o是内核的头部文件,负责初始设置。
(2)misc.o将主要负责内核的解压工作。
(3)piggy.gzip.o是一个压缩的内核(kernel/vmlinux)
(4)vmlinux是没有(zImage是压缩过后内核)压缩过的内核。它是由piggy.gzip、head.o、misc.o组成的。
(5)decompress.o是为支持很多其它的压缩格式而新引入的。
压缩过的kernel入口第一个源代码位置在arch/arm/boot/compressed/head.S,它将调用函数decompress_kernel(),这个函数在arch/arm/boot/compressed/misc.c中。decompress_kernel又调用arch_decomp_setup进行设置,decompress_kernel又调用gunzip()将内核放于指定的位置。
而gunzip位于kernel/lib/inflate.c, inflate.c是从gzip源程序中分离出来的,
启动流程分析:
1、通过linux/arch/arm/boot/compressed目录下的Makefile寻找到下面代码段:
$(obj)/vmlinux: $(obj)/vmlinux.lds $(obj)/$(HEAD) $(obj)/piggy.o \
$(addprefix $(obj)/, $(OBJS)) $(lib1funcs) $(ashldi3) \
$(bswapsdi2) $(efi-obj-y) FORCE
@$(check_for_multiple_zreladdr)
$(call if_changed,ld)
@$(check_for_bad_syms)
2、从这里我们可以得知链接脚本vmlinux.lds,所以分析vmlinux.lds,根据其中ENTRY(stext)得到内核入口函数为stext,
面stext在linux/arch/arm/kernel/head.S中
3、head.S中内核引导阶段
ENTRY(stext)
。
。
。
bl __lookup_processor_type @ r5=procinfo r9=cpuid //处理器是否支持
movs r10, r5 @ invalid processor (r5=0)?
THUMB( it eq ) @ force fixup-able long branch encoding
beq __error_p @ yes, error 'p' //不支持则打印错误信息
。
。
。
bl __create_page_tables //创建页表
/*
* The following calls CPU specific code in a position independent
* manner. See arch/arm/mm/proc-*.S for details. r10 = base of
* xxx_proc_info structure selected by __lookup_processor_type
* above. On return, the CPU will be ready for the MMU to be
* turned on, and r0 will hold the CPU control register value.
*/
ldr r13, =__mmap_switched @ address to jump to after //保存MMU使能后跳转地址
@ mmu has been enabled
adr lr, BSYM(1f) @ return (PIC) address
mov r8, r4 @ set TTBR1 to swapper_pg_dir
ARM( add pc, r10, #PROCINFO_INITFUNC )
THUMB( add r12, r10, #PROCINFO_INITFUNC )
THUMB( mov pc, r12 )
1: b __enable_mmu
4、查找标签__mmap_switched所在位置:linux/arch/arm/kernel/head-common.S
__mmap_switched:
/*
* The following fragment of code is executed with the MMU on in MMU mode,
* and uses absolute addresses; this is not position independent.
*
* r0 = cp#15 control register
* r1 = machine ID
* r2 = atags/dtb pointer
* r9 = processor ID
*/
//保存设备信息、设备树及启动参数存储地址
。
。
。
b start_kernel
从start_kernel函数开始,内核进入C语言部分,完成内核大部分初始化工作。
vmlinux.lds.S
vmlinux.lds.S是如何组织内核的每个函数存放在内核镜像文件的位置,我们知道在编译内核生成内核文件的时候,其实这个过程分两步,一个是编译,另一个是链接,vmlinux.lds.S要做的就是告诉编译器如何链接编译好的各个内核.o文件。
小知识:链接器中的entry
链接器 按以下优先顺序设入口点,找到即停止
1. -e 命令行选项
2. 脚本中的entry(symbol)命令
3. 如定义了start的值,取其值为入口点
4. 如果存在.text section,使用.text section的第一字节的位置值
5. 使用值0
所以
OUTPUT_ARCH(arm)
ENTRY(stext)
表明我们指定的stext作为程序的入口点。
1、.=PAGE_OFFSET+TEXT_OFFSET
arch/arm/include/asm中的memory.h文件定义了:
#define PAGE_OFFSET UL(CONFIG_PAGE_OFFSET)
CONFIG_PAGE_OFFSET是在内核配置里面配置的,PAGE_OFFSET代表的是内核image的起始虚拟地址,在arch/arm/Makefile中定义:
textofs-y := 0x00008000
TEXT_OFFSET := $(textofs-y)
TEXT_OFFSET代表的是内核的image存放在内存中地址,注意这个地址为相对内存的起始地址的偏移量,是个相对的偏移量不是实际的存放内存物理地址。而且这个偏移量必须为0xXXXX8000,XXXX为任意值。
2、简单例子
SECTIONS
{
. = 0×10000; /**把定位器符号置为0x10000(若不指定,则该符号的初始值为0)*/
.text : { *(.text) } /* 将所有输入文件的.text section合并成一个.text section,该section的地址由定位器符号的值指定*/
. = 0×8000000; /* 把定位器符号置为0x8000000*/
.data : { *(.data) } /* 将所有输入文件的.data section合并成一个.data section,该section的地址被置为0x8000000*/
.bss : { *(.bss) } /* 将所有输入文件的.bss section合并成一个.bss section,该section的地址被置为0x8000000+.data section的大小*/
/*连接器每读完一个section描述后,将定位器符号的值增加该section的大小,这里没有考虑对齐约束*/
}
.是一个特殊的符号,它是定位器,一个位置指针,指向程序地址空间内的某位置,该符号只能在SECTIONS命令内使用。
3、简单命令
INCLUDE filename: 包含其他名为filename的链接脚本,可以嵌套使用,最大深度为10
INPUT(files):将括号内的文件做为链接过程的输入文件。ld首先在当前目录下寻找该文件,如果没找到,则在由-L指定的搜索路径下搜索。
GROUP(files):指定需要重复搜索符号定义的多个输入文件。file必须是库文件,且file文件作为一组被ld重复扫描,直到不再有新的未定义的引用出现。
OUTPUT(filename):定义输出文件的名字。同ld的-o选项,不过-o选项的优先级更高。
SEARCH_DIR(path):定义搜索路径。同ld的-L选项,不过由-L指定的路径要比它定义的优先被搜索。
STARTUP(filename):指定filename为第一个输入文件
OUTPUT_FORMAT(DEFAULT,BIG,LITTLE):定义三种输出文件的格式,若有命令行选项-EB,则使用第2个BFD格式,若有命令行选项-EL,则使用第3鼐BFD格式,否则默认选第一个BFD格式。
TARGET(BFDNAME):设置输入文件的BFD格式。
ASSERT(EXP,MESSAGE):如果EXP不为真,终止连接过程
EXTERN(SYMBOL SYMBOL...):在输出文件中增加未定义的符号,如同连接器选项-u
FORCE_COMMON_ALLOCATION:为common symbol(通用符号)分配空间,即使用了-r连接选项也为其分配
NOCROSSREFS(SECTION SECTION...):检查列出的输出secion,如果发现他们之间有相互引用,则报错。对于某些系统,特别是内存较紧张的嵌入式系统,某些section是不能同时存在内存中的,所以他们之间不能相互引用
OUtPUT_ARCH(arch):设置输出文件的machine architecture
.4、SECTIOINS命令
SECTIONS命令告诉ld如何把输入文件的sections映射到输出文件的各个section:如何将输入section合并为输出section,如何把输出section放入程序地址空间(VMA)和进程地址空间(LMA)。
(1)输出section描述
SECTION-NAME [ADDRESS] [(TYPE)] : [AT(LMA)]
{
OUTPUT-SECTION-COMMAND
OUTPUT-SECTION-COMMAND
…
} [>REGION] [AT>LMA_REGION] [:PHDR HDR ...] [=FILLEXP]
SECTION-NAME左右的空白、圆括号、冒号是必须的,ADDRESS是一个表达式,它的值用于设置VMA,如果没有该选项且有REGION选项,那么连接器将根据REGION设置VMA,哪果也没有REGION选项,那么连接器将根据定位符‘.’的值设置该section的VMA,将定位符号的值调整到满足输出section对齐要求后的值。
TYPE设置输出section的类型,如果没有指定TYPE类型,那么连接器根据输出section引用的输入section的类型设置该输出section类型。
默认情况下,LMA等于VMA,但可以通过AT(LMA)项画指定LMA。