1. 程式人生 > >Linux核心啟動過程概述 Linux核心啟動過程概述

Linux核心啟動過程概述 Linux核心啟動過程概述

Linux核心啟動過程概述

 

 版權宣告:本文原創,轉載需宣告作者ID和原文連結地址。

 

  Hi!大家好,我是CrazyCatJack。今天給大家帶來的是Linux核心啟動過程概述。希望能夠幫助大家更好的理解Linux核心的啟動,並且創造出自己的核心^_^

  Linux的啟動程式碼真的挺大,從彙編到C,從Makefile到LDS檔案,需要理解的東西很多。畢竟Linux核心是由很多人,花費了巨大的時間和精力寫出來的。而且直到現在,這個世界上仍然有成千上萬的程式設計師在不斷完善Linux核心的程式碼。今天我們主要講解的是Linux-2.6.22.6這個核心版本。說句實話,博主也不確定自己能夠講好今天這個題目,因為這個題目太大太難。但是博主有信心,將自己學會的內容清楚地告訴大家,希望大家也能夠有所收穫。

1.啟動檔案head.S和head-common.S 

  首先,我們必須明確“我們為什麼要啟動Linux核心”。沒錯,當然是因為我們想要使用Linux系統,要明確我們的最終目的是使用Linux上的應用程式。這些應用程式可以是純軟體的,也可以是硬體相關的。博主是做嵌入式開發的,那麼我想要的當然就是用Linux核心來更好的控制我的硬體。無論是做機器人、無人機或者其他智慧硬體這都是必然趨勢。首先我們來看核心的啟動檔案head.S。

 

複製程式碼
    .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

    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
複製程式碼

  首先看這段彙編程式碼,它主要是用來做一些核心啟動前的檢測:__lookup_processor_type 檢測核心是否支援當前CPU、__lookup_machine_type檢測是否支援當前單板,並且__create_page_tables建立頁表,__enable_mmu使能MMU。如果在一系列的自檢過程後發現不支援,則跳到__error_p或__error_a。這裡我們首先開啟__lookup_machine_type。

 

複製程式碼
    .type    __lookup_machine_type, %function
__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

3:    .long    .
    .long    __arch_info_begin
    .long    __arch_info_end
複製程式碼

  我們在arch\arm\kernel找到__lookup_machine_type被定義在head-common.S檔案中。開始分析程式碼:首先,讀出3b的地址給r3,這裡的3b就是下面的那個3:所對應的虛擬地址。然後用ldmia指令將r3存放的虛擬地址分別存入r4,r5,r6。所以現在

r4=. ; r5=__arch_info_begin ; r6=__arch_info_end

然後用r3-r4求出偏移地址,再利用這個偏移地址求出r5和r6的實際實體地址。其中__arch_info_begin和__arch_info_end定義在核心目錄arch\arm\kernel下vmlinux.lds檔案中,經過起始虛擬地址= (0xc0000000) + 0x00008000逐層疊加得到。

 

複製程式碼
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 = .;
複製程式碼

  這裡的__arch_info_begin和__arch_info_end中間存放的是段屬性為.arch.info.init的結構體。這裡我們可以直接在linux下查詢核心中包含.arch.info.init的檔案。

 

複製程式碼
Direction:include/asm-arm/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 \ };
Direction:arch/arm/mach-s3c2440
MACHINE_START(S3C2440, "SMDK2440") /* Maintainer: Ben Dooks <[email protected]> */ .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
複製程式碼

  如圖所示,在include/asm-arm/arch.h中找到了定義的結構體型別machine_desc,並且在程式碼中它的段屬性被強制定義成了.arch.info.init。這樣做的目的是在剛剛我們看到的vmlinux.lds連結指令碼檔案中,可以將具有.arch.info.init段屬性的結構體統一放在__arch_info_begin和__arch_info_end之間。非常便於處理。那麼現在我們將這個結構體展開,看看它的內容。也就是將arch/arm/mach-s3c2440中的引數傳入。展開後如下:

 

複製程式碼
#define MACHINE_START(_type,_name)            \
static const struct machine_desc __mach_desc_S3C2440    \
 __used                            \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr        = MACH_TYPE_S3C2440,        \
    .name        = "SMDK2440",
/* Maintainer: Ben Dooks <[email protected]> */
    .phys_io    = S3C2410_PA_UART,
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,                    //0x30000100

    .init_irq    = s3c24xx_init_irq,
    .map_io        = smdk2440_map_io,
    .init_machine    = smdk2440_machine_init,
    .timer        = &s3c24xx_timer,
};
複製程式碼

  現在我們看到,定義的結構體型別machine_desc,內容為.nr到.timer。我們可以看出這個結構體大概是儲存硬體資訊。nr存放機器ID,name存放單板名稱,phys_io存放輸入輸出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot傳給核心的啟動引數(TAG),init_irq存放的是中斷初始化資訊,map_io為IO的對映表,init_machine存放的是單板的初始化資訊,timer存放的是單板的定時器資訊。

 

複製程式碼
struct machine_desc {
    /*
     * Note! The first four elements are used
     * by assembler code in head-armv.S
     */
    unsigned int        nr;        /* architecture number    */
    unsigned int        phys_io;    /* start of physical io    */
    unsigned int        io_pg_offst;    /* byte offset for io 
                         * page tabe entry    */

    const char        *name;        /* architecture name    */
    unsigned long        boot_params;    /* tagged list        */

    unsigned int        video_start;    /* start of video RAM    */
    unsigned int        video_end;    /* end of video RAM    */

    unsigned int        reserve_lp0 :1;    /* never has lp0    */
    unsigned int        reserve_lp1 :1;    /* never has lp1    */
    unsigned int        reserve_lp2 :1;    /* never has lp2    */
    unsigned int        soft_reboot :1;    /* soft reboot        */
    void            (*fixup)(struct machine_desc *,
                     struct tag *, char **,
                     struct meminfo *);
    void            (*map_io)(void);/* IO mapping function    */
    void            (*init_irq)(void);
    struct sys_timer    *timer;        /* system tick timer    */
    void            (*init_machine)(void);
};
複製程式碼

  我們開啟arch.h檔案,看到對machine_desc結構體的定義確實和我們剛剛所說的一樣。再回到head-common.S檔案,這裡對mmap_switch定義:

 

複製程式碼
    .type    __mmap_switched, %function
__mmap_switched:
    adr    r3, __switch_data + 4

    ldmia    r3!, {r4, r5, r6, r7}
    cmp    r4, r5                @ Copy data segment if needed
1:    cmpne    r5, r6
    ldrne    fp, [r4], #4
    strne    fp, [r5], #4
    bne    1b

    mov    fp, #0                @ Clear BSS (and zero fp)
1:    cmp    r6, r7
    strcc    fp, [r6],#4
    bcc    1b

    ldmia    r3, {r4, r5, r6, sp}
    str    r9, [r4]            @ Save processor ID
    str    r1, [r5]            @ Save machine type
    bic    r4, r0, #CR_A            @ Clear 'A' bit
    stmia    r6, {r0, r4}            @ Save control register values
    b    start_kernel
複製程式碼

  mmap_switch做了很多工作,這裡我們看到有複製資料段,清BSS段,儲存CPU的ID,儲存機器ID,清‘A’位,儲存控制暫存器的值,然後就到了C語言段——start_kernel函式。

 

2.C語言段——start_kernel

  

複製程式碼
asmlinkage void __init start_kernel(void)
{ local_irq_disable(); early_boot_irqs_off(); early_init_irq_lock_class(); /* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); tick_init(); boot_cpu_init(); page_address_init(); printk(KERN_NOTICE); printk(linux_banner); setup_arch(&command_line); setup_command_line(command_line); printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line); parse_early_param(); parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); init_IRQ(); profile_init(); if (!irqs_disabled()) printk("start_kernel(): bug: interrupts were enabled early\n"); early_boot_irqs_on(); local_irq_enable(); console_init(); rest_init(); }
複製程式碼

  接下來進入start_kernel啟動核心的C函式。上面是start_kernel的部分程式碼。這部分程式碼的主要作用是處理uboot傳遞來的引數,設定與體系結構相關的環境,初始化控制檯,最後執行應用程式,實現功能。這裡我把start_kernel函式的幾個主要功能的子函式逐層寫出,幫助大家理解start_kernel的功能結構。

 

複製程式碼
start_kernel
    setup_arch(&command_line);
    setup_command_line(command_line);
    unknown_bootoption
        obsolete_checksetup    
    parse_early_param
        do_early_param        
    rest_init;
        kernel_init
            prepare_namespace
                mount_root
            init_post
複製程式碼

  這裡每一個退格(TAB)都代表此函式被上一個函式呼叫(例如obsolete_checksetup是unknown_bootoption呼叫的函式)。setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動引數的(處理TAG)。obsolete_checksetup從__setup_start到 __setup_end,呼叫用非early標識的函式;do_early_param從__setup_start到 __setup_end,呼叫用early標識的函式(但因為__setup_param(str, fn, fn, 0)中early賦值為0,所以不在這裡呼叫),所以我們主要用obsolete_checksetup。這在後面我們會提到。mount_root是掛載根檔案系統,因為Linux上的應用程式最終要在根檔案系統上執行。最後是init_post中執行應用程式。那麼現在就有一個問題,Linux核心是如何接收uboot傳來的根檔案系統資訊的呢?

 

bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

  上面是uboot啟動時列印的環境變數。其中我們能夠看到根檔案系統掛載到第4個分割槽:root=/dev/mtdblock3 (從0分割槽開始)。上面我們提到過,setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動引數的(處理TAG)。但這個處理只是簡單的複製貼上而已,這兩個函式將TAG儲存,但並未進行真正的處理。那麼真正告訴核心在哪裡掛載的函式是什麼呢?我們通過檢視prepare_namespace可以看到一個saved_root_name。查詢saved_root_name,發現在Do_mounts.c檔案中有對它的呼叫:

 

複製程式碼
static int __init root_dev_setup(char *line)
{
    strlcpy(saved_root_name, line, sizeof(saved_root_name));
    return 1;
}

__setup("root=", root_dev_setup);   //傳入一個字串,一個函式
複製程式碼

  根據我們之前的經驗,我們可以猜測這個__setup巨集,也是定義了一個結構體。通過查詢__setup我們找到了它的巨集定義:

 

複製程式碼
Dir:init.h
#define __setup(str, fn)                    \
    __setup_param(str, fn, fn, 0)


#define __setup_param(str, unique_id, fn, early)            \
    static char __setup_str_##unique_id[] __initdata = str;    \
    static struct obs_kernel_param __setup_##unique_id    \
        __attribute_used__                \
        __attribute__((__section__(".init.setup")))    \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_##unique_id, fn, early }
複製程式碼

  在init.h檔案裡,定義__setup等於__setup_param。那麼在__setup_param的巨集定義裡,我們可以知道:它先定義了一個字串,然後定義了一個結構體型別obs_kernel_param __setup。這個結構體的段屬性為.init.setup,內容為一個字串,一個函式,還有early。具備這個屬性的結構體被連結指令碼檔案放到一起,從__setup_start到 __setup_end搜尋呼叫。在vmlinux.lds中
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;

  但是在Flash裡沒有分割槽,只能和uboot一樣,將分割槽在程式碼裡寫死。一般在啟動Linux的時候,Linux會自動打印出分割槽的資訊。這裡我的分割槽是這樣的:

 

Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

  我們搜尋這個分割槽名 grep "\"bootloader\"" * -nR。在arch/arm/plat-s3c24xx中找到分割槽程式碼:

 

複製程式碼
static struct mtd_partition smdk_default_nand_part[] = {
    [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset    = 0,
    },
    [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
    },
    [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
    },
    [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
    }
};
複製程式碼

  就是這樣,在處理完uboot傳遞的引數,進行CPU和單板的校驗,掛載根檔案系統等一系列操作後,最終核心執行init_post()中的應用程式。核心啟動流程講解完畢^_^

 

題外話:最近博主在自學Linux kernel和Linux device driver,感覺有難度。但是還是很有意義的,因為能夠看到前輩的程式碼,心裡真的很高興。我就希望自己也能夠修改Linux原始碼,寫出適合自己硬體的Linux系統。不僅如此,我還希望能夠將自己的程式碼開源,分享給更多的人。完善Linux核心,讓它變得更快更方便是博主的最終目標。博主會繼續學習,然後把知識更好的分享給大家!

 版權宣告:本文原創,轉載需宣告作者ID和原文連結地址。

 

  Hi!大家好,我是CrazyCatJack。今天給大家帶來的是Linux核心啟動過程概述。希望能夠幫助大家更好的理解Linux核心的啟動,並且創造出自己的核心^_^

  Linux的啟動程式碼真的挺大,從彙編到C,從Makefile到LDS檔案,需要理解的東西很多。畢竟Linux核心是由很多人,花費了巨大的時間和精力寫出來的。而且直到現在,這個世界上仍然有成千上萬的程式設計師在不斷完善Linux核心的程式碼。今天我們主要講解的是Linux-2.6.22.6這個核心版本。說句實話,博主也不確定自己能夠講好今天這個題目,因為這個題目太大太難。但是博主有信心,將自己學會的內容清楚地告訴大家,希望大家也能夠有所收穫。

1.啟動檔案head.S和head-common.S 

  首先,我們必須明確“我們為什麼要啟動Linux核心”。沒錯,當然是因為我們想要使用Linux系統,要明確我們的最終目的是使用Linux上的應用程式。這些應用程式可以是純軟體的,也可以是硬體相關的。博主是做嵌入式開發的,那麼我想要的當然就是用Linux核心來更好的控制我的硬體。無論是做機器人、無人機或者其他智慧硬體這都是必然趨勢。首先我們來看核心的啟動檔案head.S。

 

複製程式碼
    .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

    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
複製程式碼

  首先看這段彙編程式碼,它主要是用來做一些核心啟動前的檢測:__lookup_processor_type 檢測核心是否支援當前CPU、__lookup_machine_type檢測是否支援當前單板,並且__create_page_tables建立頁表,__enable_mmu使能MMU。如果在一系列的自檢過程後發現不支援,則跳到__error_p或__error_a。這裡我們首先開啟__lookup_machine_type。

 

複製程式碼
    .type    __lookup_machine_type, %function
__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

3:    .long    .
    .long    __arch_info_begin
    .long    __arch_info_end
複製程式碼

  我們在arch\arm\kernel找到__lookup_machine_type被定義在head-common.S檔案中。開始分析程式碼:首先,讀出3b的地址給r3,這裡的3b就是下面的那個3:所對應的虛擬地址。然後用ldmia指令將r3存放的虛擬地址分別存入r4,r5,r6。所以現在

r4=. ; r5=__arch_info_begin ; r6=__arch_info_end

然後用r3-r4求出偏移地址,再利用這個偏移地址求出r5和r6的實際實體地址。其中__arch_info_begin和__arch_info_end定義在核心目錄arch\arm\kernel下vmlinux.lds檔案中,經過起始虛擬地址= (0xc0000000) + 0x00008000逐層疊加得到。

 

複製程式碼
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 = .;
複製程式碼

  這裡的__arch_info_begin和__arch_info_end中間存放的是段屬性為.arch.info.init的結構體。這裡我們可以直接在linux下查詢核心中包含.arch.info.init的檔案。

 

複製程式碼
Direction:include/asm-arm/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 \ };
Direction:arch/arm/mach-s3c2440
MACHINE_START(S3C2440, "SMDK2440") /* Maintainer: Ben Dooks <[email protected]> */ .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
複製程式碼

  如圖所示,在include/asm-arm/arch.h中找到了定義的結構體型別machine_desc,並且在程式碼中它的段屬性被強制定義成了.arch.info.init。這樣做的目的是在剛剛我們看到的vmlinux.lds連結指令碼檔案中,可以將具有.arch.info.init段屬性的結構體統一放在__arch_info_begin和__arch_info_end之間。非常便於處理。那麼現在我們將這個結構體展開,看看它的內容。也就是將arch/arm/mach-s3c2440中的引數傳入。展開後如下:

 

複製程式碼
#define MACHINE_START(_type,_name)            \
static const struct machine_desc __mach_desc_S3C2440    \
 __used                            \
 __attribute__((__section__(".arch.info.init"))) = {    \
    .nr        = MACH_TYPE_S3C2440,        \
    .name        = "SMDK2440",
/* Maintainer: Ben Dooks <[email protected]> */
    .phys_io    = S3C2410_PA_UART,
    .io_pg_offst    = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
    .boot_params    = S3C2410_SDRAM_PA + 0x100,                    //0x30000100

    .init_irq    = s3c24xx_init_irq,
    .map_io        = smdk2440_map_io,
    .init_machine    = smdk2440_machine_init,
    .timer        = &s3c24xx_timer,
};
複製程式碼

  現在我們看到,定義的結構體型別machine_desc,內容為.nr到.timer。我們可以看出這個結構體大概是儲存硬體資訊。nr存放機器ID,name存放單板名稱,phys_io存放輸入輸出口,io_pg_offst存放IO的偏移地址,boot_params存放uboot傳給核心的啟動引數(TAG),init_irq存放的是中斷初始化資訊,map_io為IO的對映表,init_machine存放的是單板的初始化資訊,timer存放的是單板的定時器資訊。

 

複製程式碼
struct machine_desc {
    /*
     * Note! The first four elements are used
     * by assembler code in head-armv.S
     */
    unsigned int        nr;        /* architecture number    */
    unsigned int        phys_io;    /* start of physical io    */
    unsigned int        io_pg_offst;    /* byte offset for io 
                         * page tabe entry    */

    const char        *name;        /* architecture name    */
    unsigned long        boot_params;    /* tagged list        */

    unsigned int        video_start;    /* start of video RAM    */
    unsigned int        video_end;    /* end of video RAM    */

    unsigned int        reserve_lp0 :1;    /* never has lp0    */
    unsigned int        reserve_lp1 :1;    /* never has lp1    */
    unsigned int        reserve_lp2 :1;    /* never has lp2    */
    unsigned int        soft_reboot :1;    /* soft reboot        */
    void            (*fixup)(struct machine_desc *,
                     struct tag *, char **,
                     struct meminfo *);
    void            (*map_io)(void);/* IO mapping function    */
    void            (*init_irq)(void);
    struct sys_timer    *timer;        /* system tick timer    */
    void            (*init_machine)(void);
};
複製程式碼

  我們開啟arch.h檔案,看到對machine_desc結構體的定義確實和我們剛剛所說的一樣。再回到head-common.S檔案,這裡對mmap_switch定義:

 

複製程式碼
    .type    __mmap_switched, %function
__mmap_switched:
    adr    r3, __switch_data + 4

    ldmia    r3!, {r4, r5, r6, r7}
    cmp    r4, r5                @ Copy data segment if needed
1:    cmpne    r5, r6
    ldrne    fp, [r4], #4
    strne    fp, [r5], #4
    bne    1b

    mov    fp, #0                @ Clear BSS (and zero fp)
1:    cmp    r6, r7
    strcc    fp, [r6],#4
    bcc    1b

    ldmia    r3, {r4, r5, r6, sp}
    str    r9, [r4]            @ Save processor ID
    str    r1, [r5]            @ Save machine type
    bic    r4, r0, #CR_A            @ Clear 'A' bit
    stmia    r6, {r0, r4}            @ Save control register values
    b    start_kernel
複製程式碼

  mmap_switch做了很多工作,這裡我們看到有複製資料段,清BSS段,儲存CPU的ID,儲存機器ID,清‘A’位,儲存控制暫存器的值,然後就到了C語言段——start_kernel函式。

 

2.C語言段——start_kernel

  

複製程式碼
asmlinkage void __init start_kernel(void)
{ local_irq_disable(); early_boot_irqs_off(); early_init_irq_lock_class(); /* * Interrupts are still disabled. Do necessary setups, then * enable them */ lock_kernel(); tick_init(); boot_cpu_init(); page_address_init(); printk(KERN_NOTICE); printk(linux_banner); setup_arch(&command_line); setup_command_line(command_line); printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line); parse_early_param(); parse_args("Booting kernel", static_command_line, __start___param, __stop___param - __start___param, &unknown_bootoption); init_IRQ(); profile_init(); if (!irqs_disabled()) printk("start_kernel(): bug: interrupts were enabled early\n"); early_boot_irqs_on(); local_irq_enable(); console_init(); rest_init(); }
複製程式碼

  接下來進入start_kernel啟動核心的C函式。上面是start_kernel的部分程式碼。這部分程式碼的主要作用是處理uboot傳遞來的引數,設定與體系結構相關的環境,初始化控制檯,最後執行應用程式,實現功能。這裡我把start_kernel函式的幾個主要功能的子函式逐層寫出,幫助大家理解start_kernel的功能結構。

 

複製程式碼
start_kernel
    setup_arch(&command_line);
    setup_command_line(command_line);
    unknown_bootoption
        obsolete_checksetup    
    parse_early_param
        do_early_param        
    rest_init;
        kernel_init
            prepare_namespace
                mount_root
            init_post
複製程式碼

  這裡每一個退格(TAB)都代表此函式被上一個函式呼叫(例如obsolete_checksetup是unknown_bootoption呼叫的函式)。setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動引數的(處理TAG)。obsolete_checksetup從__setup_start到 __setup_end,呼叫用非early標識的函式;do_early_param從__setup_start到 __setup_end,呼叫用early標識的函式(但因為__setup_param(str, fn, fn, 0)中early賦值為0,所以不在這裡呼叫),所以我們主要用obsolete_checksetup。這在後面我們會提到。mount_root是掛載根檔案系統,因為Linux上的應用程式最終要在根檔案系統上執行。最後是init_post中執行應用程式。那麼現在就有一個問題,Linux核心是如何接收uboot傳來的根檔案系統資訊的呢?

 

bootcmd=nand read.jffs2 0x30007FC0 kernel; bootm 0x30007FC0
bootargs=noinitrd root=/dev/mtdblock3 init=/linuxrc console=ttySAC0

  上面是uboot啟動時列印的環境變數。其中我們能夠看到根檔案系統掛載到第4個分割槽:root=/dev/mtdblock3 (從0分割槽開始)。上面我們提到過,setup_arch(&command_line)和setup_command_line(command_line)就是用來處理uboot傳遞進來的啟動引數的(處理TAG)。但這個處理只是簡單的複製貼上而已,這兩個函式將TAG儲存,但並未進行真正的處理。那麼真正告訴核心在哪裡掛載的函式是什麼呢?我們通過檢視prepare_namespace可以看到一個saved_root_name。查詢saved_root_name,發現在Do_mounts.c檔案中有對它的呼叫:

 

複製程式碼
static int __init root_dev_setup(char *line)
{
    strlcpy(saved_root_name, line, sizeof(saved_root_name));
    return 1;
}

__setup("root=", root_dev_setup);   //傳入一個字串,一個函式
複製程式碼

  根據我們之前的經驗,我們可以猜測這個__setup巨集,也是定義了一個結構體。通過查詢__setup我們找到了它的巨集定義:

 

複製程式碼
Dir:init.h
#define __setup(str, fn)                    \
    __setup_param(str, fn, fn, 0)


#define __setup_param(str, unique_id, fn, early)            \
    static char __setup_str_##unique_id[] __initdata = str;    \
    static struct obs_kernel_param __setup_##unique_id    \
        __attribute_used__                \
        __attribute__((__section__(".init.setup")))    \
        __attribute__((aligned((sizeof(long)))))    \
        = { __setup_str_##unique_id, fn, early }
複製程式碼

  在init.h檔案裡,定義__setup等於__setup_param。那麼在__setup_param的巨集定義裡,我們可以知道:它先定義了一個字串,然後定義了一個結構體型別obs_kernel_param __setup。這個結構體的段屬性為.init.setup,內容為一個字串,一個函式,還有early。具備這個屬性的結構體被連結指令碼檔案放到一起,從__setup_start到 __setup_end搜尋呼叫。在vmlinux.lds中
  __setup_start = .;
   *(.init.setup)
  __setup_end = .;

  但是在Flash裡沒有分割槽,只能和uboot一樣,將分割槽在程式碼裡寫死。一般在啟動Linux的時候,Linux會自動打印出分割槽的資訊。這裡我的分割槽是這樣的:

 

Creating 4 MTD partitions on "NAND 256MiB 3,3V 8-bit":
0x00000000-0x00040000 : "bootloader"
0x00040000-0x00060000 : "params"
0x00060000-0x00260000 : "kernel"
0x00260000-0x10000000 : "root"

  我們搜尋這個分割槽名 grep "\"bootloader\"" * -nR。在arch/arm/plat-s3c24xx中找到分割槽程式碼:

 

複製程式碼
static struct mtd_partition smdk_default_nand_part[] = {
    [0] = {
        .name   = "bootloader",
        .size   = 0x00040000,
        .offset    = 0,
    },
    [1] = {
        .name   = "params",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00020000,
    },
    [2] = {
        .name   = "kernel",
        .offset = MTDPART_OFS_APPEND,
        .size   = 0x00200000,
    },
    [3] = {
        .name   = "root",
        .offset = MTDPART_OFS_APPEND,
        .size   = MTDPART_SIZ_FULL,
    }
};
複製程式碼

  就是這樣,在處理完uboot傳遞的引數,進行CPU和單板的校驗,掛載根檔案系統等一系列操作後,最終核心執行init_post()中的應用程式。核心啟動流程講解完畢^_^

 

題外話:最近博主在自學Linux kernel和Linux device driver,感覺有難度。但是還是很有意義的,因為能夠看到前輩的程式碼,心裡真的很高興。我就希望自己也能夠修改Linux原始碼,寫出適合自己硬體的Linux系統。不僅如此,我還希望能夠將自己的程式碼開源,分享給更多的人。完善Linux核心,讓它變得更快更方便是博主的最終目標。博主會繼續學習,然後把知識更好的分享給大家!