Linux内核启动过程概述
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 disabledmrc p15, 0, r9, c0, c0 @ get processor idbl __lookup_processor_type @ r5=procinfo r9=cpuidmovs r10, r5 @ invalid processor (r5=0)?beq __error_p @ yes, error 'p'bl __lookup_machine_type @ r5=machinfomovs r8, r5 @ invalid machine (r5=0)?beq __error_a @ yes, error 'a'bl __create_page_tablesldr r13, __switch_data @ address to jump to after@ mmu has been enabledadr lr, __enable_mmu @ return (PIC) addressadd 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, 3bldmia r3, {r4, r5, r6}sub r3, r3, r4 @ get offset between virt&physadd r5, r5, r3 @ convert virt addresses toadd r6, r6, r3 @ physical address space 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine typeteq r3, r1 @ matches loader number?beq 2f @ foundadd r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desccmp r5, r6blo 1bmov r5, #0 @ unknown machine 2: mov pc, lr3: .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 <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
如圖所示,在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 <ben@fluff.org> */.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 + 4ldmia r3!, {r4, r5, r6, r7}cmp r4, r5 @ Copy data segment if needed 1: cmpne r5, r6ldrne fp, [r4], #4strne fp, [r5], #4bne 1bmov fp, #0 @ Clear BSS (and zero fp) 1: cmp r6, r7strcc fp, [r6],#4bcc 1bldmia r3, {r4, r5, r6, sp}str r9, [r4] @ Save processor IDstr r1, [r5] @ Save machine typebic r4, r0, #CR_A @ Clear 'A' bitstmia r6, {r0, r4} @ Save control register valuesb start_kernelmmap_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_kernelsetup_arch(&command_line);setup_command_line(command_line);unknown_bootoptionobsolete_checksetup parse_early_paramdo_early_param rest_init;kernel_initprepare_namespacemount_rootinit_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()中的應用程序。內核啟動流程講解完畢^_^
轉載于:https://www.cnblogs.com/xieqing/p/6519880.html
總結
以上是生活随笔為你收集整理的Linux内核启动过程概述的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web_find和web_reg_fin
- 下一篇: Python-基础知识-字典dict和集