ARM linux的啟動部分源代碼簡略分析
以友善之臂的mini2440開發闆為平台,以較新的核心linux-2.6.32.7版本為例,僅作說明之用。
當核心映像被加載到RAM之後,Bootloader的控制權被釋放。核心映像并不是可直接運作的目标代碼,而是一個壓縮過的zImage(小核心)。但是,也并非是zImage映像中的一切均被壓縮了,映像中包含未被壓縮的部分,這部分中包含解壓縮程式,解壓縮程式會解壓縮映像中被壓縮的部分。zImage使用gzip壓縮的,它不僅僅是一個壓縮檔案,而且在這個檔案的開頭部分内嵌有gzip解壓縮代碼。當zImage被調用時它從arch/arm/boot/compressed/head.S的start彙編例程開始執行。這個例程進行一些基本的硬體設定,并調用arch/arm/boot/compressed/misc.c中的decompress_kernel()解壓縮核心。
arch/arm/kernel/head.S檔案是核心真正的啟動入口點,一般是由解壓縮核心的程式來調用的。首先先看下對于運作這個檔案的要求:
MMU = off; D-cache = off; I-cache = 無所謂,開也可以,關也可以; r0 = 0;r1 = 機器号;r2 = atags 指針。
這段代碼是位置無關的,是以,如果以位址0xC0008000來連結核心,那麼就可以直接用__pa(0xc0008000)位址來調用這裡的代碼。
其實,在這個(Linux核心中總共有多達幾十個的以head.S命名的檔案)head.S檔案中的一項重要工作就是設定核心的臨時頁表,不然mmu開起來也玩不轉,但是核心怎麼知道如何映射記憶體呢?linux的核心将映射到虛位址0xCxxx xxxx處,但他怎麼知道在4GB的位址空間中有哪一片ram是可用的,進而可以映射過去呢?
因為不同的系統有不通的記憶體映像,是以,LINUX約定,要調用核心代碼,一定要滿足上面的調用要求,以為最初的核心代碼提供一些最重要的關于機器的資訊。核心代碼開始的時候,R1存放的是系統目标平台的代号,對于一些常見的,标準的平台,核心已經提供了支援,隻要在編譯的時候選中就行了,例如對X86平台,核心是從實體位址1M開始映射的。
好了好了,看下面的代碼。
ENTRY(stext)是這個檔案的入口點。最初的幾行是這樣的:
setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, r9
@ ensure svc mode
@ and irqs disabled
// 設定為SVC模式,關閉中斷和快速中斷
// 此處設定系統的工作狀态為SVC,arm有7種狀态每種狀态
// 都有自己的堆棧,SVC為管理模式,具有完全的權限,可以執行任意指令
// 通路任意位址的記憶體
// setmode是一個宏,其定義為:
// .macro setmode, mode, reg
// msr cpsr_c, #\mode
// .endm
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'
這幾行是查詢處理器的類型的,我們知道arm系列有很多型号,arm7、arm9、arm11、Cortex核等等類型,這麼多型号要如何區分呢?其實,在arm的15号協處理器(其實ARM暫時也就這麼一個協處理器)中有一個隻讀寄存器,存放與處理器相關資訊。
__lookup_processor_type是arch/arm/kernel/head-common.S檔案中定義的一個例程,這個head-common.S用include指令被包含在head.S檔案中。其定義為:
__lookup_processor_type:
adr r3, 3f
ldmia r3, {r5 - r7}
add r3, r3, #8
sub r3, r3, r7 @ get offset between virt&phys
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3 @ physical address space
1: ldmia r5, {r3, r4} @ value, mask
and r4, r4, r9 @ mask wanted bits
teq r3, r4
beq 2f
add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list)
cmp r5, r6
blo 1b
mov r5, #0 @ unknown processor
2: mov pc, lr
ENDPROC(__lookup_processor_type)
這個例程接受處理器ID(儲存在寄存器r9中)為參數,查找連結器建立的支援的處理器表。此時此刻還不能使用__proc_info表的絕對位址,因為這時候MMU還沒有開啟,是以此時運作的程式沒有在正确的位址空間中。是以不得不計算偏移量。若沒有找到processor ID對應的處理器,則在r5寄存器中傳回傳回0,否則傳回一個proc_info_list結構體的指針(在實體位址空間)。proc_info_list結構體在<asm/procinfo.h>檔案中定義:
struct proc_info_list {
unsigned int cpu_val;
unsigned int cpu_mask;
unsigned long __cpu_mm_mmu_flags; /* used by head.S */
unsigned long __cpu_io_mmu_flags; /* used by head.S */
unsigned long __cpu_flush; /* used by head.S */
const char *arch_name;
const char *elf_name;
unsigned int elf_hwcap;
const char *cpu_name;
struct processor *proc;
struct cpu_tlb_fns *tlb;
struct cpu_user_fns *user;
struct cpu_cache_fns *cache;
};
第一項是CPU id,将與協處理器中讀出的id作比較,其餘的字段也都是與處理器相關的資訊,到下面初始化的過程中自然會用到。
另外,這個例程加載符位址的代碼也是挺值得我輩學習的:
加載一個符号的位址,這個符号在加載語句前面(下面)定義,forward嘛,這個符号為3,離這條語句最近的那個。在那個符号為3的位置我們看到這樣的代碼:
.align 2
3: .long __proc_info_begin
.long __proc_info_end
4: .long .
.long __arch_info_begin
.long __arch_info_end
搜尋這兩個符号的值,在檔案arch/arm/kernel/vmlinux.lds.S中:
__proc_info_begin = .;
*(.proc.info.init)
__proc_info_end = .;
這兩個符号分别是一種初始化的段的結束開始位址和結束位址。為了了解由struct proc_info_list結構體組成的段的實際構成,我們還是得要了解一下在系統中到底都有哪些變量是聲明了要被放到這個段的。用關鍵字.proc.info.init來搜,全部都是arch/arm/mm/proc-*.S檔案,這些都是特定于處理器的彙編語言檔案,對于我們的mini2440, 自然是要看proc-arm920.S檔案的,在其中可以看到這些内容:
.section ".proc.info.init", #alloc, #execinstr
.type __arm920_proc_info,#object
__arm920_proc_info:
.long 0x41009200
.long 0xff00fff0
.long PMD_TYPE_SECT | \
PMD_SECT_BUFFERABLE | \
PMD_SECT_CACHEABLE | \
PMD_BIT4 | \
PMD_SECT_AP_WRITE | \
PMD_SECT_AP_READ
b __arm920_setup
.long cpu_arch_name
.long cpu_elf_name
.long HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB
.long cpu_arm920_name
.long arm920_processor_functions
.long v4wbi_tlb_fns
.long v4wb_user_fns
#ifndef CONFIG_CPU_DCACHE_WRITETHROUGH
.long arm920_cache_fns
#else
.long v4wt_cache_fns
#endif
.size __arm920_proc_info, . - __arm920_proc_info
看到這兒我們再回國頭去看__lookup_processor_type的代碼:
ldmia r3, {r5 - r7}
sub r3, r3, r7
盡管符号3處隻有兩個有效值,但它加載了三個數,而第三個數,我們看到是這樣定義的:
.long .
__lookup_processor_type中,給r3加上8,也就是讓r3指向“.”的位址,然後用r3減r7來擷取虛拟位址與實體位址的差,這樣看來,“.”就應該是虛拟空間(編譯位址)裡那個資料的位址。
之後的代碼獲得__proc_info_begin和__arch_info_end這兩個符号在實體空間中的位址:
add r5, r5, r3 @ convert virt addresses to
add r6, r6, r3
然後便是在那個段中逐個的檢查struct proc_info_list結構體,以找到與我們的CPU相比對的:
__lookup_processor_type例程會傳回在檔案arch/arm/mm/proc-arm920.S中定義的一個儲存有與我們的處理器相關的資訊的struct proc_info_list結構體的位址。
接下來我們繼續看stext的代碼:
bl __lookup_machine_type @ r5=machinfo
movs r8, r5 @ invalid machine (r5=0)?
beq __error_a @ yes, error 'a'
在獲得了處理器資訊之後,則調用__lookup_machine_type來查找機器資訊。這個例程同樣也在arch/arm/kernel/head-common.S檔案中定義。這個例程的定義如下:
__lookup_machine_type:
adr r3, 4b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ get offset between virt&phys
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
mov r5, #0 @ unknown machine
ENDPROC(__lookup_machine_type)
處理的過程和上面的__lookup_processor_type還是挺相似的。這個例程接收r1中傳進來的機器号作為參數,然後,在一個由struct machine_desc結構體組成的段中查找和我們的機器号比對的struct machine_desc結構體,這個結構體在arch/arm/include/asm/mach/arch.h檔案中定義,用于儲存機器的資訊:
struct machine_desc {
/*
* Note! The first four elements are used
* by assembler code in head.S, head-common.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);
同樣這個例程也用到了同上面很相似的方式來獲得符号的位址:
b代表back,即向後,這個符号為4,緊接着我們前面看到的那個為3的标号:
在檔案arch/arm/kernel/vmlinux.lds.S中我們可以看到段的定義:
__arch_info_begin = .;
*(.arch.info.init)
__arch_info_end = .;
這兩個符号也是分别表示某種初始化的段的開始位址和結束位址。為了找到段的填充内容,還是得要了解一下到底都有哪些struct machine_desc結構體類型變量聲明了要被放到這個段的。用關鍵字.arch.info.init 來搜尋所有的核心源檔案。在arch/arm/include/asm/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 \
定義機器結構體,也就是.arch.info.init段中的内容,都是要通過兩個宏MACHINE_START和MACHINE_END來完成的啊,MACHINE_START宏定義一個truct machine_desc結構體,并初始化它的機器号字段和機器名字段,可以在arch/arm/tools/mach-types檔案中看到各種平台的機器号的定義。那接着我們來搜MACHINE_START吧,這是一個用于定義機器結構體的宏,是以可以看到這個符号好像都是在arch/arm/mach-*/mach-*.c這樣的檔案中出現的,我們感興趣的應該是arch/arm/mach-s3c2440/ mach-mini2440.c檔案中的這個符号:
MACHINE_START(MINI2440, "MINI2440")
/* Maintainer: Michel Pollet <[email protected]> */
.phys_io = S3C2410_PA_UART,
.io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc,
.boot_params = S3C2410_SDRAM_PA + 0x100,
.map_io = mini2440_map_io,
.init_machine = mini2440_init,
.init_irq = s3c24xx_init_irq,
.timer = &s3c24xx_timer,
MACHINE_END
OK, __lookup_machine_type這個例程的我們也搞明白了。回憶一下,啟動代碼現在已經完成的工作,R10寄存器中為指向proc_info_list結構體的指針(實體位址空間),這個結構體包含有關于我們的處理器的一些重要資訊。R8寄存器中為指向一個與我們的平台相比對的machine_desc結構體的指針,這個結構體中儲存有一些關于我們的平台的重要資訊。
回來接着看arch/arm/kernel/head.S檔案中的stext:
bl __vet_atags
這個例程同樣同樣也是在arch/arm/kernel/head-common.S檔案中定義:
__vet_atags:
tst r2, #0x3 @ aligned?
bne 1f
ldr r5, [r2, #0] @ is first tag ATAG_CORE?
cmp r5, #ATAG_CORE_SIZE
cmpne r5, #ATAG_CORE_SIZE_EMPTY
ldr r5, [r2, #4]
ldr r6, =ATAG_CORE
mov pc, lr @ atag pointer is ok
1: mov r2, #0
mov pc, lr
ENDPROC(__vet_atags)
這個例程接收機器資訊(R8寄存器)為參數,并檢測r2中傳入的ATAGS 指針的合法性。核心使用tag來作為bootloader傳遞核心參數的方式。系統要求r2中傳進來的ATAGS指針式4位元組對齊的,同時要求ATAGS清單的第一個tag是一個ATAG_CORE類型的。
此時R10寄存器中儲存有指向CPU資訊結構體的指針,R8寄存器中儲存有指向機器結構體的指針,R2寄存器中儲存有指向tag表的指針,R9中還儲存有CPU ID資訊。
回到arch/arm/kernel/head.S檔案中的stext,之後就要進入初始化過程中比較關鍵的一步了,開始設定mmu,但首先要填充一個臨時的核心頁表,映射4m的記憶體,這在初始化過程中是足夠了:
bl __create_page_tables
這個例程設定初始頁表,這裡隻設定最起碼的數量,隻要能使核心運作即可,r8 = machinfo,r9 = cpuid,r10 = procinfo,在r4寄存器中傳回實體頁表位址。
__create_page_tables例程在檔案arch/arm/kernel/head.S中定義:
__create_page_tables:
pgtbl r4 @ page table address
// pgtbl是一個宏,本檔案的前面部分有定義:
// .macro pgtbl, rd
// ldr \rd, =(KERNEL_RAM_PADDR - 0x4000)
// KERNEL_RAM_PADDR在本檔案的前面有定義,為(PHYS_OFFSET + TEXT_OFFSET)
// PHYS_OFFSET在arch/arm/mach-s3c2410/include/mach/memory.h定義,
// 為UL(0x30000000)
// 而TEXT_OFFSET在arch/arm/Makefile中定義,為核心鏡像在記憶體中到記憶體
// 開始位置的偏移(位元組),為$(textofs-y)
// textofs-y也在檔案arch/arm/Makefile中定義,
// 為textofs-y := 0x00008000
// r4 = 30004000為臨時頁表的起始位址
// 首先即是初始化16K的頁表,高12位虛拟位址為頁表索引,是以為
// 4K*4 = 16K,大頁表,每一個頁表項,映射1MB虛拟位址。
// 這個地方還來了個循環展開,以優化性能。
mov r0, r4
mov r3, #0
add r6, r0, #0x4000
1: str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
// PROCINFO_MM_MMUFLAGS在arch/arm/kernel/asm-offsets.c檔案中定義,
// 為DEFINE(PROCINFO_MM_MMUFLAGS,
// offsetof(struct proc_info_list, __cpu_mm_mmu_flags));
// R10寄存器儲存的指針指向是我們前面找到的proc_info_list結構嘛。
// 為核心的第一個MB建立一緻的映射,以為打開MMU做準備,這個映射将會被
// paging_init()移除,這裡使用程式計數器來獲得相應的段的基位址。
// 這個地方是直接映射。
mov r6, pc
mov r6, r6, lsr #20 @ start of kernel section
orr r3, r7, r6, lsl #20 @ flags + kernel base
str r3, [r4, r6, lsl #2] @ identity mapping
// 接下來為核心的直接映射區設定頁表。KERNEL_START在檔案的前面定義,
// 為KERNEL_RAM_VADDR,即核心的虛拟位址。
// 而KERNEL_RAM_VADDR在檔案的前面定義,則為(PAGE_OFFSET + TEXT_OFFSET)
// 映射完整的核心代碼段,初始化資料段。
// PAGE_OFFSET為核心鏡像開始的虛拟位址,在
// arch/arm/include/asm/memory.h中定義。在配置核心時標明具體值,預設
// 為0xC0000000。
// 因為最高12位的值是頁表中的偏移位址,而第三高的四位必然為0,
// 每個頁表項為4位元組,右移20位之後,還得再左移兩位回來,是以,這裡隻// 是左移18位。
// R3寄存器在經過了上面的操作之後,實際上是變成了指向核心鏡像代碼段
// 的指針(實體位址),在這個地方,再一次為核心鏡像的第一個MB做了映射。
// R6随後指向了核心鏡像的尾部。R0為頁表項指針。
// 這裡以1MB為機關來映射核心鏡像。
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4
add r6, r4, r6, lsr #18 //得到頁表的結束實體位址
1: cmp r0, r6
add r3, r3, #1 << 20
strls r3, [r0], #4
bls 1b
// 為了使用啟動參數,将實體記憶體的第一MB映射到核心虛拟位址空間的
// 第一個MB,r4存放的是頁表的位址。這裡的PAGE_OFFSET的虛拟位址
// 比上面的KERNEL_START要小0x8000
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
// 上面的這個步驟顯得似乎有些多餘。
// 總結一下,這個建立臨時頁表的過程:
// 1、為核心鏡像的第一個MB建立直接映射
// 2、為核心鏡像完整的建立從虛拟位址到實體位址的映射
// 3、為實體記憶體的第一個MB建立到核心的虛拟位址空間的第一個MB的映射。
// OK,核心的臨時頁表建立完畢。整個初始化臨時頁表的過程都沒有修改R8,
// R9和R10。
ENDPROC(__create_page_tables)
回到stext:
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
這個地方實際上是在r13中儲存了另一個例程的位址。後面的分析中,遇到執行到這個例程的情況時會有詳細說明。
接着看stext:
adr lr, BSYM(__enable_mmu) @ return (PIC) address
BSYM()是一個宏,在檔案arch/arm/include/asm/unified.h中定義,為:
#define BSYM(sym) sym
也就是說這個語句也僅僅是把__enable_mmu例程的位址加載進lr寄存器中。為了友善之後調用的函數傳回時,直接執行__enable_mmu例程。
接着看stext下一句:
ARM( add pc, r10, #PROCINFO_INITFUNC )
ARM()也是一個宏,同樣在檔案arch/arm/include/asm/unified.h中定義,當配置核心為生成ARM鏡像,則為:#define ARM(x...) x
是以這一條語句也就是在調用一個例程。R10中儲存的是procinfo結構的位址。PROCINFO_INITFUNC符号在arch/arm/kernel/asm-offsets.c檔案中定義,為:
DEFINE(PROCINFO_INITFUNC, offsetof(struct proc_info_list, __cpu_flush));
也就是調用結構體proc_info_list的__cpu_flush成員函數。回去檢視arch/arm/mm/proc-arm920.S檔案中struct proc_info_list結構體的變量的定義,可以看到這個成員為:
也就是說,在設定好核心臨時頁表之後調用了例程__arm920_setup,這個例程同樣在arch/arm/mm/proc-arm920.S中:
__arm920_setup:
mov r0, #0
mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4
#ifdef CONFIG_MMU
mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4
adr r5, arm920_crval
ldmia r5, {r5, r6}
mrc p15, 0, r0, c1, c0 @ get control register v4
bic r0, r0, r5
orr r0, r0, r6
這一段首先使i,d caches内容無效,然後清除write buffer,接着使TLB内容無效。接下來加載變量arm920_crval的位址,我們看到arm920_crval變量的内容為:
rm920_crval:
crval clear=0x00003f3f, mmuset=0x00003135, ucset=0x00001130
crval為一個宏,在arch/arm/mm/proc-macros.S中定義:
.macro crval, clear, mmuset, ucset
.word \clear
.word \mmuset
.word \ucset
.endm
其實也就是定義兩個變量而已。之後,在r0中,得到了我們想要往協處理器相應寄存器中寫入的内容。
之後的 __arm920_setup傳回,mov pc, lr,即是調用例程__enable_mmu,這個例程在檔案arch/arm/kernel/head.S中:
__enable_mmu:
#ifdef CONFIG_ALIGNMENT_TRAP
orr r0, r0, #CR_A
bic r0, r0, #CR_A
#ifdef CONFIG_CPU_DCACHE_DISABLE
bic r0, r0, #CR_C
#ifdef CONFIG_CPU_BPREDICT_DISABLE
bic r0, r0, #CR_Z
#ifdef CONFIG_CPU_ICACHE_DISABLE
bic r0, r0, #CR_I
mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
domain_val(DOMAIN_IO, DOMAIN_CLIENT))
mcr p15, 0, r5, c3, c0, 0 @ load domain access register
mcr p15, 0, r4, c2, c0, 0 @ load page table pointer
b __turn_mmu_on
在這兒設定了頁目錄位址(r4寄存器中儲存),然後設定domain的保護,在前面建立頁表的例程中,注意到,頁表項的控制資訊,是從struct proc_info_list結構體的某字段中取的,其頁目錄項的 domain都是0,domain寄存器中的domain 0對應的是0b11,表示通路模式為manager,不受限制。在這裡同時也完成r0的某些位的進一步設定。
然後,__enable_mmu例程又調用了__turn_mmu_on,在同一個檔案中定義:
__turn_mmu_on:
mov r0, r0
mcr p15, 0, r0, c1, c0, 0 @ write control reg
mrc p15, 0, r3, c0, c0, 0 @ read id reg
mov r3, r3
mov r3, r13
mov pc, r3
ENDPROC(__turn_mmu_on)
接下來寫控制寄存器:
mcr p15, 0, r0, c1, c0 ,0
一切設定就此生效,到此算是完成了打開d,icache和mmu的工作。
注意:arm的d cache必須和mmu一起打開,而i cache可以單獨打開。其實,cache和mmu的關系實在是緊密,每一個頁表項都有标志标示是否是cacheable的,可以說本來就是設計一起使用的
前面有提到過,r13中存放的其實是另外一個例程的位址,其值是變量__switch_data的第一個字段,即一個函數指針的值,__switch_data變量是在arch/arm/kernel/head-common.S中定義的:
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long _data @ r5
.long __bss_start @ r6
.long _end @ r7
.long processor_id @ r4
.long __machine_arch_type @ r5
.long __atags_pointer @ r6
.long cr_alignment @ r7
.long init_thread_union + THREAD_START_SP @ sp
前面的ldr r13 __switch_data,實際上也就是加載符号__mmap_switched的位址,實際上__mmap_switched是一個arch/arm/kernel/head-common.S中定義的例程。接着來看這個例程的定義,在arch/arm/kernel/head-common.S檔案中:
__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
mov fp, #0 @ Clear BSS (and zero fp)
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}
str r9, [r4] @ Save processor ID
str r1, [r5] @ Save machine type
str r2, [r6] @ Save atags pointer
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ Save control register values
b start_kernel
ENDPROC(__mmap_switched)
這個例程完成如下工作:
1、使r3指向__switch_data變量的第二個字段(從1開始計數)。
2、執行了一條加載指令,也就是在r4, r5, r6, r7寄存器中分别加載4個符号__data_loc,_data, __bss_start ,_end的位址,這四個符号都是在連結腳本arch/arm/kernel/vmlinux.lds.S中出現的,辨別了鏡像各個段的位址,我們應該不難猜出他們所代表的段。
3、如果需要的話則複制資料段(資料段和BSS段是緊鄰的)。
4、初始化BSS段,全部清零,BSS是未初始化的全局變量區域。
5、又看到一條加載指令,同樣在一組寄存器中加載借個符号的位址,r4中為processor_id,r5中為__machine_arch_type, r6中為__atags_pointer, r7中為cr_alignment ,sp中為init_thread_union + THREAD_START_SP。
6、接着我們看到下面的幾條語句,則是用前面擷取的資訊來初始化那些全局變量r9,機器号被儲存到processor_id處;r1寄存器的值,機器号,被儲存到變量__machine_arch_type中,其他的也一樣。
7、重新設定堆棧指針,指向init_task的堆棧。init_task是系統的第一個任務,init_task的堆棧在task structure的後8K,我們後面會看到。
8、最後就要跳到C代碼的 start_kernel。
到此為止,彙編部分的初始化代碼就結束了
O,My God.初始化代碼的彙編部分終于結束。進而進入了與體系結構無關的Linux核心部分。start_kernel()會調用一系列初始化函數來設定中斷,執行進一步的記憶體配置。
現在讓我們來回憶一下目前的系統狀态:
臨時頁表已經建立,在0X30004000處,映射了映像檔案大小空間,虛位址0XC000000被映射到0X30000000。CACHE,MMU 都已經打開。堆棧用的是任務init_task的堆棧。
如果以為到了c代碼可以松一口氣的話,就大錯特措了,linux的c也不比彙編好懂多少,相反倒掩蓋了彙編的一些和機器相關的部分,有時候更難懂。其實作 為編寫作業系統的c代碼,隻不過是彙編的另一種寫法,和機器代碼的聯系是很緊密的。另外,這些start_kernel()中調用的C函數,每一個都具有舉足輕重的地位,它們中的許多都肩負着初始化核心中的某個子系統的重要使命,而Linux核心中每一個子系統都錯綜複雜,牽涉到各種軟體、硬體的複雜算法,是以了解起來倒真的是挺困難的。
start_kernel函數在 init/main.c中定義:
528 asmlinkage void __init start_kernel(void)
529 {
530 char * command_line;
531 extern struct kernel_param __start___param[], __stop___param[];
532
533 smp_setup_processor_id();
534
535 /*
536 * Need to run as early as possible, to initialize the
537 * lockdep hash:
538 */
539 lockdep_init();
540 debug_objects_early_init();
541
542 /*
543 * Set up the the initial canary ASAP:
544 */
545 boot_init_stack_canary();
546
547 cgroup_init_early();
548
549 local_irq_disable();
550 early_boot_irqs_off();
551 early_init_irq_lock_class();
552
553 /*
554 * Interrupts are still disabled. Do necessary setups, then
555 * enable them
556 */
557 lock_kernel();
558 tick_init();
559 boot_cpu_init();
560 page_address_init();
561 printk(KERN_NOTICE "%s", linux_banner);
562 setup_arch(&command_line);
563 mm_init_owner(&init_mm, &init_task);
564 setup_command_line(command_line);
565 setup_nr_cpu_ids();
566 setup_per_cpu_areas();
567 smp_prepare_boot_cpu(); /* arch-specific boot-cpu hooks */
568
569 build_all_zonelists();
570 page_alloc_init();
571
572 printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
573 parse_early_param();
574 parse_args("Booting kernel", static_command_line,
575 __start___param, __stop___param - __start___param,
576 &unknown_bootoption);
577 /*
578 * These use large bootmem allocations and must precede
579 * kmem_cache_init()
580 */
581 pidhash_init();
582 vfs_caches_init_early();
583 sort_main_extable();
584 trap_init();
585 mm_init();
586 /*
587 * Set up the scheduler prior starting any interrupts (such as the
588 * timer interrupt). Full topology setup happens at smp_init()
589 * time - but meanwhile we still have a functioning scheduler.
590 */
591 sched_init();
592 /*
593 * Disable preemption - early bootup scheduling is extremely
594 * fragile until we cpu_idle() for the first time.
595 */
596 preempt_disable();
597 if (!irqs_disabled()) {
598 printk(KERN_WARNING "start_kernel(): bug: interrupts were "
599 "enabled *very* early, fixing it\n");
600 local_irq_disable();
601 }
602 rcu_init();
603 radix_tree_init();
604 /* init some links before init_ISA_irqs() */
605 early_irq_init();
606 init_IRQ();
607 prio_tree_init();
608 init_timers();
609 hrtimers_init();
610 softirq_init();
611 timekeeping_init();
612 time_init();
613 profile_init();
614 if (!irqs_disabled())
615 printk(KERN_CRIT "start_kernel(): bug: interrupts were "
616 "enabled early\n");
617 early_boot_irqs_on();
618 local_irq_enable();
619
620 /* Interrupts are enabled now so all GFP allocations are safe. */
621 gfp_allowed_mask = __GFP_BITS_MASK;
622
623 kmem_cache_init_late();
624
625 /*
626 * HACK ALERT! This is early. We're enabling the console before
627 * we've done PCI setups etc, and console_init() must be aware of
628 * this. But we do want output early, in case something goes wrong.
629 */
630 console_init();
631 if (panic_later)
632 panic(panic_later, panic_param);
633
634 lockdep_info();
635
636 /*
637 * Need to run this when irqs are enabled, because it wants
638 * to self-test [hard/soft]-irqs on/off lock inversion bugs
639 * too:
640 */
641 locking_selftest();
642
643 #ifdef CONFIG_BLK_DEV_INITRD
644 if (initrd_start && !initrd_below_start_ok &&
645 page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
646 printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
647 "disabling it.\n",
648 page_to_pfn(virt_to_page((void *)initrd_start)),
649 min_low_pfn);
650 initrd_start = 0;
651 }
652 #endif
653 page_cgroup_init();
654 enable_debug_pagealloc();
655 kmemtrace_init();
656 kmemleak_init();
657 debug_objects_mem_init();
658 idr_init_cache();
659 setup_per_cpu_pageset();
660 numa_policy_init();
661 if (late_time_init)
662 late_time_init();
663 sched_clock_init();
664 calibrate_delay();
665 pidmap_init();
666 anon_vma_init();
667 #ifdef CONFIG_X86
668 if (efi_enabled)
669 efi_enter_virtual_mode();
670 #endif
671 thread_info_cache_init();
672 cred_init();
673 fork_init(totalram_pages);
674 proc_caches_init();
675 buffer_init();
676 key_init();
677 security_init();
678 vfs_caches_init(totalram_pages);
679 signals_init();
680 /* rootfs populating might need page-writeback */
681 page_writeback_init();
682 #ifdef CONFIG_PROC_FS
683 proc_root_init();
684 #endif
685 cgroup_init();
686 cpuset_init();
687 taskstats_init_early();
688 delayacct_init();
689
690 check_bugs();
691
692 acpi_early_init(); /* before LAPIC and SMP init */
693 sfi_init_late();
694
695 ftrace_init();
696
697 /* Do the rest non-__init'ed, we're now alive */
698 rest_init();
699 }
接着我們來近距離的觀察一下start_kernel函數中調用的這些重量級的函數。
首先來看setup_arch(&command_line)函數,這個函數(對于我們的mini2440平台來說)在arch/arm/kernel/setup.c中定義:
664 void __init setup_arch(char **cmdline_p)
665 {
666 struct tag *tags = (struct tag *)&init_tags;
667 struct machine_desc *mdesc;
668 char *from = default_command_line;
669
670 unwind_init();
671
672 setup_processor();
673 mdesc = setup_machine(machine_arch_type);
674 machine_name = mdesc->name;
675
676 if (mdesc->soft_reboot)
677 reboot_setup("s");
678
679 if (__atags_pointer)
680 tags = phys_to_virt(__atags_pointer);
681 else if (mdesc->boot_params)
682 tags = phys_to_virt(mdesc->boot_params);
683
684 /*
685 * If we have the old style parameters, convert them to
686 * a tag list.
687 */
688 if (tags->hdr.tag != ATAG_CORE)
689 convert_to_tag_list(tags);
690 if (tags->hdr.tag != ATAG_CORE)
691 tags = (struct tag *)&init_tags;
692
693 if (mdesc->fixup)
694 mdesc->fixup(mdesc, tags, &from, &meminfo);
695
696 if (tags->hdr.tag == ATAG_CORE) {
697 if (meminfo.nr_banks != 0)
698 squash_mem_tags(tags);
699 save_atags(tags);
700 parse_tags(tags);
701 }
702
703 init_mm.start_code = (unsigned long) _text;
704 init_mm.end_code = (unsigned long) _etext;
705 init_mm.end_data = (unsigned long) _edata;
706 init_mm.brk = (unsigned long) _end;
707
708 /* parse_early_param needs a boot_command_line */
709 strlcpy(boot_command_line, from, COMMAND_LINE_SIZE);
710
711 /* populate cmd_line too for later use, preserving boot_command_line */
712 strlcpy(cmd_line, boot_command_line, COMMAND_LINE_SIZE);
713 *cmdline_p = cmd_line;
714
715 parse_early_param();
716
717 paging_init(mdesc);
718 request_standard_resources(&meminfo, mdesc);
719
720 #ifdef CONFIG_SMP
721 smp_init_cpus();
722 #endif
723
724 cpu_init();
725 tcm_init();
726
727 /*
728 * Set up various architecture-specific pointers
729 */
730 init_arch_irq = mdesc->init_irq;
731 system_timer = mdesc->timer;
732 init_machine = mdesc->init_machine;
733
734 #ifdef CONFIG_VT
735 #if defined(CONFIG_VGA_CONSOLE)
736 conswitchp = &vga_con;
737 #elif defined(CONFIG_DUMMY_CONSOLE)
738 conswitchp = &dummy_con;
739 #endif
740 #endif
741 early_trap_init();
742 }
來看一些我們比較感興趣的地方:
1、666行,struct tag指針類型的局部變量指向了預設的tag清單init_tags,該靜态變量在setup_arch()定義同檔案的前面有如下定義:
636 /*
637 * This holds our defaults.
638 */
639 static struct init_tags {
640 struct tag_header hdr1;
641 struct tag_core core;
642 struct tag_header hdr2;
643 struct tag_mem32 mem;
644 struct tag_header hdr3;
645 } init_tags __initdata = {
646 { tag_size(tag_core), ATAG_CORE },
647 { 1, PAGE_SIZE, 0xff },
648 { tag_size(tag_mem32), ATAG_MEM },
649 { MEM_SIZE, PHYS_OFFSET },
650 { 0, ATAG_NONE }
651 };
第679行檢察__atags_pointer指針的有效性,這個指針是在前面,跳轉到start_kernel函數的彙編例程最後設定的幾個變量之一,用的是R2寄存器的值。如果bootloader通過R2傳遞了tag清單的話,自然是要使用bootloader穿的進來的tag清單的。
2、第688行的字元指針類型的局部變量from指向了default_command_line靜态變量,這個變量同樣在前面有定義:
124 static char default_command_line[COMMAND_LINE_SIZE] __initdata = CONFIG_CMDLINE;
傳遞給核心的指令行參數,是可以在核心配置的時候設定的。
3、第673行以machine_arch_type為參數調用了setup_machine()函數,而這個函數的定義為:
369 static struct machine_desc * __init setup_machine(unsigned int nr)
370 {
371 struct machine_desc *list;
372
373 /*
374 * locate machine in the list of supported machines.
375 */
376 list = lookup_machine_type(nr);
377 if (!list) {
378 printk("Machine configuration botched (nr %d), "
379 " unable to continue.\n", nr);
380 while (1);
381 }
382
383 printk("Machine: %s\n", list->name);
384
385 return list;
386 }
在arch/arm/kernel/head-common.S檔案中,我們看到了一個對于__lookup_machine_type例程的封裝的可被C語言程式調用的彙編語言編寫的函數lookup_machine_type(),接收機器号,查表,然後傳回比對的struct machine_desc結構體的指針。在這裡,對于我們的mini2440,傳回的自然是arch/arm/mach-s3c2440/ mach-mini2440.c檔案中定義的結構體了:
然後,machine_desc結構體的name成員的值被賦給全局變量machine_name。
第681行,若bootloader沒有傳遞tag清單給核心,則檢測machine_desc結構體的boot_params字段,看看特定的平台是否傳遞了标記清單。
第730、731、732行分别将machine_desc結構體的init_irq、timer和init_machine成員值賦給了三個全局變量init_arch_irq、system_timer和init_machine,即是設定特定體系結構的指針。初始化的後面階段自然會用到。
start_kernel()函數調用同檔案下的rest_init(void)函數,rest_init(void)函數調用 kernel_thread()函數以啟動第一個核心線程,該線程執行kernel_init()函數,而原執行序列會調用cpu_idle(),等待排程。
作為核心線程的kernel_init()函數繼續完成一些設定,并在最後調用同檔案下的init_post()函數,而該函數挂在根檔案系統,打開/dev/console裝置,重定向stdin、stdout和stderr到控制台。之後,它搜尋檔案系統中的init程式(也可以由“init=”指令行參數指定init程式),并使用run_init_process()函數執行init程式。(事實上,run_init_process()函數又調用了kernel_execve()來實際執行程式)。搜尋init程式的順序為/sbin/init、/etc/init、/bin/init、和/bin/sh。在嵌入式系統中,多數情況下,可以給核心傳入一個簡單的shell腳本來啟動必需的嵌入式應用程式。
至此,漫長的Linux核心引導和啟動過程就結束了,而kernel_init()對應的由rest_init(void)函數建立的第一個線程也進入使用者模式。
參考文獻:
arm 嵌入式LINUX啟動過程:
http://blog.ednchina.com/yujiebaomei/4153/message.aspx
http://www.cnblogs.com/bluepointcq/articles/490954.html
Linux裝置驅動開發詳解,宋寶華
【新浪微網誌】 張昺華--sky
【twitter】 @sky2030_
【facebook】 張昺華 zhangbinghua
本文版權歸作者和部落格園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接配接,否則保留追究法律責任的權利.