今天給大家帶來的是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內核,讓它變得更快更方便是博主的最終目標。博主會繼續學習,然後把知識更好的分享給大家!