根据执行make uImage指令,去顶层目录中的Makefile去寻找对应项
当在顶层目录的Makefile中查找uImage关键字,会出现查无此字,但是在arch/arm/Makefile中可以找到uImage,但是在顶层目录中却能执行make uImage这条指令,所以可以推测出顶层目录的Makefile一定包含了arch/arm目录下的Makefile,打开顶层目录的Makefile可以发现以下这句话:
include $(srctree)/arch/$(ARCH)/Makefile
其中$(srctree)表示源码目录,$(ARCH)的值为arm
在上一章《初步了解Linux内核 (1)》中说到.config会生成include/config/auto.conf文件,该文件中包含了许多配置项,它会被子目录的Makefile使用,而对于顶层目录的Makefile它也会使用它,可以发现这么一句话:
-include include/config/auto.conf
至于为什么顶层目录会包含该文件,我在第一章是说到了Makefile会根据.config内容进行编译,而Makefile是间接的包含.config,即包含include/config/auto.conf,对于auto.conf与.config的区别只是将.config文件中的注释去掉,并根据顶层Makefile中定义的变量增加一些变量而已。
继续根据make uImage指令,打开arch/arm目录下的Makefile,关键的内容如下:
zImage Image xipImage bootpImage uImage: vmlinux
$(Q)$(MAKE) $(build)=$(boot) MACHINE=$(MACHINE) $(boot)/$@
uImage依赖于vmlinux,对于uImage组成,它是头部加上真正的内核,而vmlinux则是真正的内核。
对于vmlinux,它的目标依赖规则在顶层目录的Makefile中定义。内容如下:
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds
vmlinux-init : $(head-y) $(init-y)
head-y := arch/arm/kernel/head.o arch/arm/kernel/init_task.o
init-y := init/
init-y := $(patsubst %/, %/built-in.o, $(init-y)) = init/built-in.o
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
core-y := usr/
core-y += kernel/ mm/ fs/ ipc/ security/ crypto/ block/
core-y := $(patsubst %/, %/built-in.o, $(core-y))
= usr/built-in.o kernel/built-in.o mm/built-in.o
fs/built-in.o ipc/built-in.o security/built-in.o
crypto/built-in.o block/built-in.o
libs-y = lib/lib.a lib/built-in.o
drivers-y := drivers/built-in.o sound/built-in.o
net-y := net/built-in.o
从上面的依赖可以得知,编译vmlinux时会用到链接文件vmlinux.lds,它位于arch/arm/kernel目录。还有许多文件,例如usr/下的所有相关文件组成的built-in.o,kernel/下的所有相关文件组成的built-in.o,mm/下的所有相关文件组成的built-in.o等等,其次还会包含函数库,位于lib目录下的lib.a,还会包含drivers目录下所有相关的驱动文件组成的built-in.o,还会包含net目录下与网卡有关的所有相应文件组成的built-in.o。
接下来想知道如何将以上的所有原材料编译成内核,有两种方法,一种是分析Makefile,还有一种是直接编译内核,从编译信息中去查看。
在这里选用第二种方法,执行指令make uImage V=1,可以得出以下信息:
arm-linux-ld -EL -p --no-undefined -X -o vmlinux
-T arch/arm/kernel/vmlinux.lds
arch/arm/kernel/head.o arch/arm/kernel/init_task.o init/built-in.o --start-group usr/built-in.o arch/arm/kernel/built-in.o arch/arm/mm/built-in.o arch/arm/common/built-in.o arch/arm/mach-s3c2410/built-in.o arch/arm/mach-s3c2400/built-in.o arch/arm/mach-s3c2412/built-in.o arch/arm/mach-s3c2440/built-in.o arch/arm/mach-s3c2442/built-in.o arch/arm/mach-s3c2443/built-in.o arch/arm/nwfpe/built-in.o arch/arm/plat-s3c24xx/built-in.o kernel/built-in.o mm/built-in.o fs/built-in.o ipc/built-in.o security/built-in.o crypto/built-in.o block/built-in.o arch/arm/lib/lib.a lib/lib.a arch/arm/lib/built-in.o lib/built-in.o drivers/built-in.o sound/built-in.o net/built-in.o --end-group .tmp_kallsyms2.o
从以上信息可以知道编译时所用到的链接文件以及是如何组织所有原材料文件。
打开链接脚本文件,内容如下:
SECTIONS
{
. = (0xc0000000) + 0x00008000;
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head)
}
.init : { /* Init code and data */
*(.init.text)
_einittext = .;
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
__tagtable_begin = .;
*(.taglist.init)
__tagtable_end = .;
. = ALIGN(16);
__setup_start = .;
*(.init.setup)
__setup_end = .;
...
}
一开始指出该内核应该存放在哪里执行,然后指出先存放所有文件的.text.head段,接着存放所有文件的.init.text段等等,所以根据编译信息可以得知,程序会先执行head.S文件。所以打开head.S文件,它位于arch/arm/kernel目录下。分析该文件,顺藤摸瓜去解析Linux内核启动时会做哪些事情。文件内容大致如下:
...
ENTRY(stext)
msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE @ ensure svc mode
@ and irqs disabled
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
adr lr, __enable_mmu @ return (PIC) address
add pc, r10, #PROCINFO_INITFUNC
...
对于以下几条指令:
mrc p15, 0, r9, c0, c0 @ get processor id
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
beq __error_p @ yes, error 'p'
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
bl __create_page_tables
内核首先执行它会去读取寄存器,获取CPU的ID,然后bl __lookup_processor_type检查是否支持该板子的处理器,如果能够支持则会继续执行bl __lookup_machine_type,检查内核是否支持该单板。而该单板的值是Uboot启动内核时传入的机器ID(即Uboot中最后调用theKernel (0, bd->bi_arch_number, bd->bi_boot_params)时,所传入的bi_arch_number,它的值为MACH_TYPE_S3C2440)。对于内核来说,它能够支持哪些CPU还有能够支持哪些单板,则是内核本身就定义好的。
进入到__lookup_machine_type函数,内容如下:
3: .long .
.long __arch_info_begin
.long __arch_info_end
...
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type
teq r3, r1 @ matches loader number?
beq 2f @ found
add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc
cmp r5, r6
blo 1b
mov r5, #0 @ unknown machine
2: mov pc, lr
分析以上代码,可以知道r3寄存器保存标号3所指示的地址,r4 = “.”,r5 = __arch_info_begin,r6 = __arch_info_end,而__arch_info_begin与__arch_info_end则是在链接文件中定义的,在链接文件中有以下这句话:
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
表示了在__arch_info_begin和__arch_info_end之间存放所有文件的.arch.info.init段。这里有一个疑问,就是这段空间里面,这些.arch.info.init都是怎么放进去的?
通过在内核源目录中搜索关键字,能够在在include/asm-arm/mach目录中的arch.h找到如下的定义:
#define MACHINE_START(_type,_name) \
static const struct machine_desc __mach_desc_##_type \
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_##_type, \
.name = _name,
#define MACHINE_END \
};
根据该宏去搜索,则能找到一些定义,以下以S3C2440为例:
MACHINE_START(S3C2440, "SMDK2440")
/* Maintainer: Ben Dooks <ben@fluff.org> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
MACHINE_END
通过展开则为
static const struct machine_desc __mach_desc_S3C2440
__used __attribute__((__section__(".arch.info.init"))) =
{
.nr = MACH_TYPE_S3C2440,
.name = SMDK2440,
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.init_irq = s3c24xx_init_irq,
.map_io = smdk2440_map_io,
.init_machine = smdk2440_machine_init,
.timer = &s3c24xx_timer,
};
通过该定义,则会将该结构体的数据存放到__arch_info_begin和__arch_info_end之间,所以__lookup_machine_type函数中就可以去检测。
当内核支持该单板的话,接下来则会建立页表,因为在链接文件中,定义了. = (0xc0000000) + 0x00008000,可以得出这些代码段使用的都是虚拟地址,而内存则是从0x30000000开始的。当建立好页表之后则使能MMU。
在我们启动Uboot的过程中,已经知道Uboot将需要传给内核的一些参数,并将这些参数写入某个约定的地址,而该地址在启动内核的时候已经传给内核了。而内核怎么去获取这些参数及如何处理。跟踪__switch_data最后可以发现函数:start_kernel,它是内核启动过程中执行的第一个C函数。