Linux内核启动流程分析之Makefile

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_ 等等函数一一分析它们。

猜你喜欢

转载自blog.csdn.net/weixin_41354745/article/details/82381790