學習目標: 瞭解arch/arm/kernel/head.S作為內核啟動的第一個文件所實現的功能! 前面通過對內核Makefile的分析,可以知道arch/arm/kernel/head.S是內核啟動的第一個文件。另外,U-boot調用內核時,r1寄存器中存儲“機器類型ID”,內核會使用它。 打開a ...
學習目標:
瞭解arch/arm/kernel/head.S作為內核啟動的第一個文件所實現的功能!
前面通過對內核Makefile的分析,可以知道arch/arm/kernel/head.S是內核啟動的第一個文件。另外,U-boot調用內核時,r1寄存器中存儲“機器類型ID”,內核會使用它。
打開arch/arm/kernel/head.S文件,可以看到stext函數是內核入口函數,函數內容如下:
76 .section ".text.head", "ax" /* 定義一個.text.head段,段的屬性a是允許段,x是可執行 */ 77 .type stext, %function /* 定義u-boot進入內核的入口函數 */ 78 ENTRY(stext) /* 入口地址stext函數 */ 79 msr cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE /* 關中斷,設置CPU工作在管理模式 */
80
81 mrc p15, 0, r9, c0, c0 /* 獲取CPU的ID */
82 bl __lookup_processor_type /* 調用函數,輸入參數r9=cpuid,返回值r5=procinfo */
83 movs r10, r5 /* 不支持當前CPU,則返回值r5=0 */ 84 beq __error_p /* 如果r5=0,則列印錯誤 */ 85 bl __lookup_machine_type /* 調用函數,返回值r5=machinfo */
86 movs r8, r5 /* 不支持當前開發板,返回值r5=machinfo */ 87 beq __error_a /* 如果r5=0,則列印錯誤 */ 88 bl __create_page_tables /* 創建頁表 */
先來大概介紹入口函數stext每條語句實現的功能,然後再詳細分析__lookup_processor_type和__lookup_machine_type這兩個函數:
第79行通過設置CPSR寄存器來確保處理器進入管理模式,並且禁止中斷。
第81行讀取協處理器CP15的寄存器C0獲得CPU ID。
第82行調用__lookup_processor_type函數,檢測內核是否支持當前CPU。如果支持,r5寄存器返回一個用來描述處理器結構的地址,否則r5的值為0。
第85行調用__lookup_machine_type函數,確定內核是否支持當前開發板。如果支持,r5寄存器返回一個用來描述這個開發板的結構的地址,否則r5的值為0。
第88行調用__create_page_table函數,其中的__create_page_table函數用來創建以及頁表以建立虛擬地址到物理地址的映射關係,它用到__lookup_processer_type函數返回的proc_info_list結構。
如果__lookup_processor_type、__lookup_machine_type這兩個函數中有一個返回值為0,則內核不能啟動,如果配置內核時使能了CONFIG_DEBUG_LL,還會列印錯誤提示信息。
在介紹__lookup_processor_type和__lookup_machine_type這兩個函數之前,還要先插講一些內容。我們在前面簡要的說過__lookup_processor_type和__lookup_machine_type這兩個函數分別是用來檢測內核是否支持當前架構的處理器、是否支持當前開發板,如果內核想實現檢測功能,那麼內核中一定會存放自己所支持的處理器架構信息以及所支持的開發板信息,下麵先來找到內核中這些信息是如何被定義的。
內核中,定義了若幹個pro_info_list結構,表示它所支持的CPU。對於ARM架構的CPU,這些結構體的源碼在arch/arm/mm/目錄下,例如proc-arm920.S中的如下代碼,它表示arm920架構CPU的pro_info_list結構。
448 .section ".proc.info.init", #alloc, #execinstr 449 450 .type __arm920_proc_info,#object 451 __arm920_proc_info: 452 .long 0x41009200 /* cpu val */ 453 .long 0xff00fff0 /* cpu mask */
內核所支持每個處理器架構都有自己的pro_info_list結構體用來保存自己CPU信息,這些不同處理器使用的pro_info_list結構體都被強制定義在“.proc_info_init”段中。在連接內核時,這些結構體被組織在一起,起始地址為__proc_info_begin,結束地址為__proc_info_end。可以從連接腳本arch/arm/kernel/vmlinux.lds中看出來。
35 __proc_info_begin = .; #.proc_info_init段起始地址(連接程式是動態確定) 36 *(.proc.info.init) 37 __proc_info_end = .; #.proc_info_init結束地址(連接程式時動態確定)
再來看內核中支持開發板信息內容,是如何被定義和存放的。內核中對於每種所支持的開發板都會使用巨集MACHINE_START、MACHINE_END來定義一個machine_desc結構,這個結構定義了開發板相關的一些屬性和函數,比如機器ID、起始I/O物理地址、Bootloader傳入參數的地址、中斷初始化函數等等。例如SMDK2410開發板,在arch/arm/mach-sc32410/mach-smdk2410.c中定義。
198 MACHINE_START(SMDK2410, "SMDK2410") /* @TODO: request a new identifier and switch 199 * to SMDK2410 */ 200 /* Maintainer: Jonas Dietsche */ 201 .phys_io = S3C2410_PA_UART, 202 .io_pg_offst = (((u32)S3C24XX_VA_UART) >> 18) & 0xfffc, 203 .boot_params = S3C2410_SDRAM_PA + 0x100, 204 .map_io = smdk2410_map_io, 205 .init_irq = s3c24xx_init_irq, 206 .init_machine = smdk2410_init, 207 .timer = &s3c24xx_timer, 208 MACHINE_END
在內核的include/asm/-arm/mach/arch.h文件中找到第198、202行的巨集MACHINE_START、MACHINE_END定義,這兩個巨集定義如下:
50 #define MACHINE_START(_type,_name) \ 51 static const struct machine_desc __mach_desc_##_type \ 52 __used \ 53 __attribute__((__section__(".arch.info.init"))) = { \ 54 .nr = MACH_TYPE_##_type, \ 55 .name = _name, 56 57 #define MACHINE_END \ 58 };
按照上述巨集定義,將198行~208行代碼展開如下所示:
static const struct machine_desc __mach_desc_SMDK2410 \ __used \ __attribute__((__section__(".arch.info.init"))) = { \ .nr = MACH_TYPE_SMDK2410, \ .name = "SMDK2410", .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, };
將198~208行的代碼展開,可以看出這段內容定義了一個靜態常量的machine_desc類型結構體__mach_desc_SMDK2410。其中的MACH_TYPE_SMDK2410在arch/arm/tools/mach-types中定義,最後這個文件會被轉換成一個頭文件include/asm/arm/mach-type.h被其它文件包含。machine_des結構體在include/asm-arm/mach/arch.h文件中定義。__attribute__((__section__(".arch.info.init")))語句表示將所有的machine_desc結構都存放在“.arch.info.init”段中,在連接內核時,它們被組織到一起,開始地址為__arch_info_begin,結束地址為__arch_info_end。可以從連接腳本arch/arm/kernel/vmlinux.lds中看出來。
38 __arch_info_begin = .; #.arch.info.init段起始地址(連接時動態確定) 39 *(.arch.info.init) 40 __arch_info_end = .; #.arch.info.init段結束地址(連接時動態確定)
有了上面的插講內容作為鋪墊,下麵對 __lookup_processor_type和__lookup_machine_type這兩個函數如何去實現各自功能理解就會更加方便了。先來看 __lookup_processor_type函數,在arch/arm/kernel/head-common.S文件中定義如下:
145 .type __lookup_processor_type, %function 146 __lookup_processor_type: 147 adr r3, 3f 148 ldmda r3, {r5 - r7} 149 sub r3, r3, r7 @ get offset between virt&phys 150 add r5, r5, r3 @ convert virt addresses to 151 add r6, r6, r3 @ physical address space 152 1: ldmia r5, {r3, r4} @ value, mask 153 and r4, r4, r9 @ mask wanted bits 154 teq r3, r4 155 beq 2f 156 add r5, r5, #PROC_INFO_SZ @ sizeof(proc_info_list) 157 cmp r5, r6 158 blo 1b 159 mov r5, #0 @ unknown processor 160 2: mov pc, lr
176 .long __proc_info_begin 177 .long __proc_info_end 178 3: .long . 179 .long __arch_info_begin 180 .long __arch_info_end
在調用__enable_mmu函數之前使用的都是物理地址,而內核卻以虛擬地址鏈接的。所以在訪問pro_info_list結構之前,先將它的虛擬地址轉換為物理地址,上面代碼的147行~151行就是實現上述的轉換。
第147行先獲得178行物理地址。adr指令基於pc寄存器計算地址,此時MMU功能關閉,PC寄存器中使用的還是物理地址,所以執行“adr r3,3f”後,r3記憶體放的是178行代碼的物理地址, 指令中的3f的f是forward的意思,意思是跳到程式的後面(往下)。
第148行用來獲取第176行~178行定義的數據:__proc_info_begin、__pro_info_end和"."。前兩個變數是在連接內核時確定,它們是虛擬地址,在前面插講中我們對這__proc_info_begin、__pro_info_end已經做出了詳細介紹,"."表示當前的代碼在編譯鏈接後的虛擬地址。ldmda r3, {r5-r7}指令,從源地址[r3]讀取4個位元組數據放到寄存器中,每讀一次r3-4,而指令執行後r3內容不變,數據存放到寄存器規則是低地址對於低寄存器編號,高地址對於高寄存器編號。
第149行計算物理地址和虛擬地址的差值,第150~151根據這個差值計算__pro_info_begin、__pro_info_end的物理地址。
下麵的代碼一次讀取存放在“.proc_info_init”段中每個cpu架構的pro_info_list結構體前面兩個成員,判斷cpu_val是否等於r9&cpu_mask,r9是讀取head.S中獲取的CPU ID.如果比較相等,則表示內核支持當前CPU,直接返回這個結構地址。如果“.proc_info_init”段所有pro_info_list結構都不支持這個CPU,則返回0。
第160行是子函數調用返回語句。
接著再來分析__lookup_machine_type這個函數,同樣的這個函數也在在arch/arm/kernel/head-common.S文件中定義,其代碼如下所示:
193 .type __lookup_machine_type, %function 194 __lookup_machine_type: 195 adr r3, 3b @address of 3b, Physical address 196 ldmia r3, {r4, r5, r6} @r4="." virtual address of 3b,r5=__arch_info_begin ,r6=__arch_info_end 197 sub r3, r3, r4 @ get offset between virt&phys 198 add r5, r5, r3 @ convert virt addresses to 199 add r6, r6, r3 @ physical address space 200 1: ldr r3, [r5, #MACHINFO_TYPE] @ get machine type 201 teq r3, r1 @ matches loader number? 202 beq 2f @ found 203 add r5, r5, #SIZEOF_MACHINE_DESC @ next machine_desc 204 cmp r5, r6 205 blo 1b 206 mov r5, #0 @ unknown machine 207 2: mov pc, lr
第195行先獲得178行物理地址。adr指令基於pc寄存器計算地址,此時MMU功能關閉,PC寄存器中使用的還是物理地址,所以執行“adr r3,3f”後,r3記憶體放的是178行代碼的物理地址, 指令中的3b的b是backward的意思,意思是跳到程式的前面(往上)。
第196行用來獲取第178行~180行定義的數據:__arch_info_begin、__arch_info_end和"."。前兩個變數都是在連接內核時確定,它們是虛擬地址,在前面插講中我們對這__arch_info_begin、__arch_info_end已經做出了詳細介紹,"."表示當前的代碼在編譯鏈接後的虛擬地址。ldmia r3, {r5-r7}指令,從源地址[r3]讀取4個位元組數據放到寄存器中,每讀一次r3+4,而指令執行後r3內容不變,數據存放到寄存器規則是低地址對於低寄存器編號,高地址對於高寄存器編號。
第197行計算物理地址和虛擬地址的差值,第198~199行根據這個差值計算__arch_info_begin、__arch_info_end的物理地址。
下麵的代碼讀取存放在“.arch_info_init”段中每個machine_des結構體機器類型ID,判斷uboot傳入機器ID(通過r1寄存器傳入)是否等於內核中存放的不同machine_des結構體的機器類型ID。如果比較相等,則表示內核支持當前開發板,直接返回這個結構地址。如果“.arch_info_init”段所有存放machine_des結構都不支持這個開發板,則返回0。
第207行是子函數調用返回語句。
繼續分析arch/arm/kernel/head.S代碼
97 ldr r13, __switch_data @ address to jump to after 98 @ mmu has been enabled 99 adr lr, __enable_mmu @ return (PIC) address 100 add pc, r10, #PROCINFO_INITFUNC
第97行把__switch_data函數地址存放到r13寄存器中,存放的地址為虛擬地址。
第99行把__enable_mmu函數地址存放到lr(r4)寄存器中,存放地址為物理地址。
第100行#PROCINFO_INITFUNC為16,程式計數器pc值=r10+16,由上面分析可以知道r10內容為內核中描述當前處理器結構的地址,也就是arch/arm/mm/pro-arm920.S文件中__arm920_proc_info入口地址,pc=r10+16,即跳轉下麵代碼第502處執行__arm920_setup函數。
451 __arm920_proc_info: 452 .long 0x41009200 453 .long 0xff00fff0 454 .long PMD_TYPE_SECT | \ 455 PMD_SECT_BUFFERABLE | \ 456 PMD_SECT_CACHEABLE | \ 457 PMD_BIT4 | \ 458 PMD_SECT_AP_WRITE | \ 459 PMD_SECT_AP_READ 460 .long PMD_TYPE_SECT | \ 461 PMD_BIT4 | \ 462 PMD_SECT_AP_WRITE | \ 463 PMD_SECT_AP_READ 464 b __arm920_setup 465 .long cpu_arch_name ................... 476 #endif 477 .size __arm920_proc_info, . - __arm920_proc_info
__arm920_setup函數內容如下:
385 .type __arm920_setup, #function 386 __arm920_setup: 387 mov r0, #0 388 mcr p15, 0, r0, c7, c7 @ invalidate I,D caches on v4 389 mcr p15, 0, r0, c7, c10, 4 @ drain write buffer on v4 390 #ifdef CONFIG_MMU 391 mcr p15, 0, r0, c8, c7 @ invalidate I,D TLBs on v4 392 #endif 393 adr r5, arm920_crval 394 ldmia r5, {r5, r6} 395 mrc p15, 0, r0, c1, c0 @ get control register v4 396 bic r0, r0, r5 397 orr r0, r0, r6 398 mov pc, lr 399 .size __arm920_setup, . - __arm920_setup
__arm920_setup函數功能是禁止ICache、DCache、數據Cache、指令Cache,最後將lr寄存器內容傳送給PC程式計數器,由上面知道lr寄存器存放為__enable_mmu函數地址,此時程式將跳轉到__enable_mmu函數處執行。__enable_mmu函數在arch/arm/kernel/head.S文件中,內容如下:
152 .type __enable_mmu, %function 153 __enable_mmu: 154 #ifdef CONFIG_ALIGNMENT_TRAP 155 orr r0, r0, #CR_A 156 #else 157 bic r0, r0, #CR_A 158 #endif 159 #ifdef CONFIG_CPU_DCACHE_DISABLE 160 bic r0, r0, #CR_C 161 #endif 162 #ifdef CONFIG_CPU_BPREDICT_DISABLE 163 bic r0, r0, #CR_Z 164 #endif 165 #ifdef CONFIG_CPU_ICACHE_DISABLE 166 bic r0, r0, #CR_I 167 #endif 168 mov r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \ 169 domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \ 170 domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \ 171 domain_val(DOMAIN_IO, DOMAIN_CLIENT)) 172 mcr p15, 0, r5, c3, c0, 0 @ load domain access register 173 mcr p15, 0, r4, c2, c0, 0 @ load page table pointer 174 b __turn_mmu_on
__turn_mmu_on代碼如下:
187 .align 5 188 .type __turn_mmu_on, %function 189 __turn_mmu_on: 190 mov r0, r0 191 mcr p15, 0, r0, c1, c0, 0 @ write control reg 192 mrc p15, 0, r3, c0, c0, 0 @ read id reg 193 mov r3, r3 194 mov r3, r3 195 mov pc, r13
__enable_mmu函數用來使能MMU,最後將r13寄存器內容賦給PC寄存器,由上面分析可知r13寄存器內容為__switch_data函數地址(此處地址為虛擬地址,因為已經開啟了MMU功能),最終程式跳轉到arm/arch/kernel/head-common.S文件__switch_data入口地址。__switch_data地址存放內容如下:
14 .type __switch_data, %object 15 __switch_data: 16 .long __mmap_switched 17 .long __data_loc @ r4 18 .long __data_start @ r5 19 .long __bss_start @ r6 20 .long _end @ r7 21 .long processor_id @ r4 22 .long __machine_arch_type @ r5 23 .long cr_alignment @ r6 24 .long init_thread_union + THREAD_START_SP @ sp
__switch_data地址處存放內容為__mmap_switched地址,PC跳轉到__mmp_switched處執行,__mmp_swirched函數內容如下:
34 .type __mmap_switched, %function 35 __mmap_switched: 36 adr r3, __switch_data + 4 37 38 ldmia r3!, {r4, r5, r6, r7} 39 cmp r4, r5 @ Copy data segment if needed 40 1: cmpne r5, r6 41 ldrne fp, [r4], #4 42 strne fp, [r5], #4 43 bne 1b 44 45 mov fp, #0 @ Clear BSS (and zero fp) 46 1: cmp r6, r7 47 strcc fp, [r6],#4 48 bcc 1b 49 50 ldmia r3, {r4, r5, r6, sp} 51 str r9, [r4] @ Save processor ID 52 str r1, [r5] @ Save machine type 53 bic r4, r0, #CR_A @ Clear 'A' bit 54 stmia r6, {r0, r4} @ Save control register values 55 b start_kernel
第36~43行實現複製數據段。
第45~48行實現清除BSS段。
第50行設置棧指針。
第51行保存CPU ID。
第52行存機器類型ID。
第55行跳轉到start_kernel函數執行。
總結:引導階段代碼做了以下內容
1、首先檢查內核是否支持當前架構處理器,然後檢測是否支持當前開發板,若支持執行後續操作。
2、設置頁表、使能MMU
3、執行調用內核執行第一個C函數start_kernel之前的常規工作,包括複製數據段、清除BSS段等