Linux内核源码中有许多的Makefile文件,这些文件又要包含一些其他文件(比如配置信息、通用的规则等),这些文件构成了Linux的Makefile体系。该体系可分为五类:
1.顶层Makefile;
2. .config;
3. arch/$(ARCH)/Makefile(对应体系结构的Makefile);
4. scripts/Makefile.*(Makefile共用的通用规则、脚本等);
5. kbuild Makefiles(就是各级子目录下的Makefile)
这五类文件使得Makefile具有三个作用:
1.决定编译哪些文件;
2.怎么编译这些文件;
3.怎么连接这些文件,它们的顺序如何。
子目录下的Makefile比较简单:
obj-y += xxx.o
obj-m += xxx.o
在编译.config时候会自动生成的auto.conf与autoconf.h,这两个文件会被包含在顶层Makefile文件中,具体作用可参考该文章:https://blog.csdn.net/weixin_41354745/article/details/82356158
在执行make uImage的时候发现,这个目标是在arch/arm/Makefile中,该Makefile被包含进顶层Makefile中。
在顶层Makefile中发现:
include $(srctree)/arch/$(ARCH)/Makefile
在arch/arm/Makefile中可以看到uImage依赖于vmlinux:
zImage Image xipImage bootpImage uImage: vmlinux
uImage实际上是一个头部+真正的内核,而真正的内核就是vmlinux。
那么vmlinux依赖于哪些文件?在顶层Makefile中进行搜索得到:
vmlinux: $(vmlinux-lds) $(vmlinux-init) $(vmlinux-main) $(kallsyms.o) FORCE
vmlinux-init := $(head-y) $(init-y)
vmlinux-main := $(core-y) $(libs-y) $(drivers-y) $(net-y)
可以看到顶层Makefile将内核的十三个目录(不包括arch、include、Document、scripts)分为五类,head-y是以文件名出现的,而非目录。对于没有MMU的处理器,MMUEXT的值为-nommu,有MMU的处理器,MMUEXT的值为空。
vmlinux-init := $(head-y) $(init-y)
head-y := arch/arm/kernel/head$(MMUEXT).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))
最终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-all := $(vmlinux-init) $(vmlinux-main)
vmlinux-lds := arch/$(ARCH)/kernel/vmlinux.lds
因此,vmlinux的原材料就是以上这几部分。编译内核时,将依次进入init-y、core-y、libs-y、driver-y和net-y所列出的目录中执行它们的Makefile,每个子目录都会生成一个built-in.o(libs-y所列目录下,有可能生成lib.a文件),最后head-y所表示的文件和这些built-in.o、lib.a一起被连接成内核映象文件vmlinux。
那么这几部分是怎么按照什么顺序编译执行的?我们编译一下内核,查看输出信息:
rm vmlinux //先删除原来编译出来的vmlinux
make uImage
查看串口输出内容:
arm-linux-ld -EL -p --no-undefined -X -o vmlinux //输出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
通过查看编译过程得知:
第一个文件是:arch/arm/kernel/head.s;
链接脚本是:arch/arm/kernel/vmlinux.lds。
所有文件链接的顺序就是按照编译过程出现的顺序执行,而每个文件中的各个段的存放位置由vmlinux.lds链接脚本决定。
查看vmlinux.lds:
SECTIONS
{
. = (0xc0000000) + 0x00008000; //一开始指定了内核的存放地址,且是一个虚拟地址
.text.head : {
_stext = .;
_sinittext = .;
*(.text.head) //一开始存放所有文件的这一个段
}
.init : { /* Init code and data */
*(.init.text) //接下来放所有文件的init段
_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 = .;
__early_begin = .;
*(.early_param.init)
__early_end = .;
__initcall_start = .;
......
分析一下内核的第一个文件:arch/arm/kernel/head.s
/*
* Kernel startup entry point.
*/
.section ".text.head", "ax"
.type stext, %function
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 @建立一级页表将虚拟地址与实际内存地址对应
在内核启动的时候,函数__lookup_processor_type会去读这些寄存器p15, 0, r9, c0, c0,从r9寄存器获取处理器id,判断这个内核能否支持该款处理器,如果能支持,r5寄存器返回一个用于描述处理器的结构体的地址,继续向下执行,否则,r5的值为0,跳转至__error_a函数中。
当能够支持该款处理器后,进入__lookup_machine_type函数判断能否支持该单板(一个编译好的内核能够支持的单板是固定的),如果能支持,r5寄存器返回一个用来描述这个开发板结构的地址,否则r5的值为0。
如何判断能否支持一个单板呢?
uboot启动内核时会传入机器id。
/* r1 = machine architecture number
* Returns:
* r3, r4, r6 corrupted
* r5 = mach_info pointer in physical address space
*/
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b @ r3 = 3b的物理地址(此时MMU还没启动)
ldmia r3, {r4, r5, r6} @ r4 = "." 表示3b的虚拟地址, r5 = __arch_info_begin, r6 = __arch_info_end
/* __arch_info_begin = .;
* *(.arch.info.init)
* __arch_info_end = .;
* __arch_info_begin与__arch_info_end在内核代码中找不到,它们存在于链接脚本中。
*/
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
__arch_info_begin与__arch_info_end在内核代码中找不到,那是谁被定义成了*(.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 \
};
搜索MACHINE_START,在arch/arm/mach-smdk2410.c中:
MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch
* to SMDK2410 */
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
MACHINE_END
结合分析:
MACHINE_START(SMDK2410, "SMDK2410")
#define MACHINE_START(_type,_name)
得到:
static const struct machine_desc __mach_desc_SMDK2410\
__used \
__attribute__((__section__(".arch.info.init"))) = { \
.nr = MACH_TYPE_SMDK2410, \
.name = SMDK2410,
/* Maintainer: Jonas Dietsche */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = smdk2410_map_io,
.init_irq = s3c24xx_init_irq,
.init_machine = smdk2410_init,
.timer = &s3c24xx_timer,
};
相当于定义一个machine_desc结构体。
所以连接文件arch/arm/kernel/vmlinux.lds中这部分代码:
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
说明了内核映象中定义有若干个proc_info_list结构,表明它支持的CPU,这些结构都被定义在“.proc.info.init”段,起始地址为__proc_info_begin,结束地址为__proc_info_end。
内核中对每种支持的开发板都会定义一个machine_desc结构,所有的machine_desc结构都处于“.arch.info.init”段,在连接内核时,它们被组织在一起,开始地址为__arch_info_begin,结束地址为__arch_info_end。
继续向下执行完后,总结内核第一个文件所执行的是处理uboot传入的参数(机器id,启动参数),详细功能分为:
1.判断是否支持这个cpu;
2.判断是否支持这个单板;
3.建立页表;
4.使能MMU;
5.跳到start_kernel(这是内核的第一个C函数,处理uboot传入的启动参数)。
在start_kernel函数中:
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
......
/* 会用这两个函数解析uboot传入的启动参数 */
setup_arch(&command_line);
setup_command_line(command_line);
......
/* 调用该函数 */
rest_init();
}
而在rest_init()函数中:
static void noinline __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
/* 创建内核的线程函数,调用kernel_init */
kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
......
}
在kernel_init函数中:
static int __init kernel_init(void * unused)
{
......
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
......
}
会调用prepare_namespace()函数:
/*
* Prepare the namespace - decide what/where to mount, load ramdisks, etc.
*/
void __init prepare_namespace(void)
{
......
/* 挂接根文件系统 */
mount_root();
......
}
假设已经挂接根文件系统后,执行init_post():
/*
* Ok, we have completed the initial bootup, and
* we're essentially up and running. Get rid of the
* initmem segments and start the user-mode stuff..
*/
init_post();
在init_post()函数中:
static int noinline init_post(void)
{
/* 打开/dev/console */
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console.\n");
......
/* 执行应用程序 */
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
......
}
总结得到内核启动流程如下:
start_kernel
setup_arch /* 解析uboot传入的启动参数 */
setup_command_line /* 解析uboot传入的启动参数 */
parse_early_param
do_early_param
从__setup_start到__setup_end,调用early函数
unknown_bootoption
obsolete_checksetup
从__setup_start到__setup_end,调用非early函数
rest_init
kernel_init
prepare_namespace
mount_root /* 挂接根文件系统 */
init_post
/* 执行应用程序 */
由于内核的最终目的是要执行应用程序,而应用程序在文件系统中,所以需要利用mount_root函数挂接根文件系统,但是根文件系统要挂接到哪个分区呢?
在start_kernel函数中,mount_root之前都是一些处理参数的函数,而mount_root函数会把文件系统挂接到哪个分区是由命令行参数指定的,那么uboo传入的命令行参数“bootargs=noinitrd root=/dev/mtdlock3 ……”一开始的时候会被保存在某个字符串中。内核会调用启动流程中的parse_ 等等函数一一分析它们。