新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計(jì)應(yīng)用 > linux內(nèi)核啟動(dòng)過程——基于S3C2410

linux內(nèi)核啟動(dòng)過程——基于S3C2410

作者: 時(shí)間:2016-11-09 來源:網(wǎng)絡(luò) 收藏
本文以流行的Samsung公司的S3C2410,openmoko平臺(tái)和linux-2.6.24為例,介紹如何在ZIX嵌入式開發(fā)環(huán)境下探索linux內(nèi)核啟動(dòng)過程。

Linux內(nèi)核啟動(dòng)一般由外部的bootloader引導(dǎo),也可以在內(nèi)核頭部嵌入一個(gè)loader,實(shí)際的應(yīng)用中這兩種方式都會(huì)經(jīng)常遇到。所以要了 解內(nèi)核啟動(dòng)最開始的過程,必須對(duì)bootloader如何引導(dǎo)內(nèi)核有所熟悉。下面我們從u-boot加載linux內(nèi)核的代碼開始分析(關(guān)于u-boot 自身的啟動(dòng)流程,請(qǐng)參考u-boot 啟動(dòng)過程 —— 基于S3C2410)。

本文引用地址:http://butianyuan.cn/article/201611/317999.htm

在u-boot的do_bootm_linux函數(shù)里,實(shí)現(xiàn)了處理器架構(gòu)相關(guān)的linux內(nèi)核加載代碼,特別是tags傳遞。

該函數(shù)中,在lib_arm/bootm.c的76行調(diào)用了getenv將bootargs環(huán)境變量保存在commandline

char*commandline =getenv("bootargs");

然后解析uImage文件頭,并且按照頭中的定義分解和加載uImage。所以這部分代碼的運(yùn)行取決于uImage文件是如何生成的,本文不做過多敘述,可參考另文了解u-boot使用。接下來進(jìn)行tags設(shè)置工作,分別調(diào)用了

  • setup_start_tag()
  • setup_memory_tag()
  • setup_commandline_tag()
  • setup_initrd_tag()
  • setup_end_tag()

然后對(duì)TLB、cache等進(jìn)行ivalid操作,這是通過在lib_arm/bootm.c的156行調(diào)用cleanup_before_linux()實(shí)現(xiàn),然后即可跳入從uImage中分解出來的內(nèi)核Image或zImage入口

cleanup_before_linux (); theKernel (0, machid, bd->bi_boot_params); /* does not return */return;

在s3c2410平臺(tái)上,該入口theKernel一般是物理地址0x30008000。如果我們使用zImage自解壓內(nèi)核映像,對(duì)應(yīng)的代碼正是 自解壓頭,位置在內(nèi)核源碼linux-2.6.24-moko-linuxbj的arch/arm/boot/compressed/start.S第 114行的start符號(hào)

start: .type start,#function .rept 8 mov r0, r0 .endr b 1f .word 0x016f2818 @ Magic numbers to help the loader .word start @ absolute load/run zImage address .word _edata @ zImage end address 1: mov r7, r1 @ save architecture ID mov r8, r2 @ save atags pointer

這也標(biāo)志著u-boot將系統(tǒng)完全的交給了OS,bootloader生命終止。之后代碼在133行會(huì)讀取cpsr并判斷是否處理器處于supervisor模式——從u-boot進(jìn)入kernel,系統(tǒng)已經(jīng)處于SVC32模式;而利用angel進(jìn)入則處于user模式,還需要額外兩條指令。之后是再次確認(rèn)中斷關(guān)閉,并完成cpsr寫入

mrs r2, cpsr @ get current mode tst r2, #3 @ not user? bne not_angel mov r0, #0x17 @ angel_SWIreason_EnterSVC swi 0x123456 @ angel_SWI_ARM not_angel: mrs r2, cpsr @ turn off interrupts to orr r2, r2, #0xc0 @ prevent angel from running msr cpsr_c, r2

然后在LC0地址處將分段信息導(dǎo)入r0-r6、ip、sp等寄存器,并檢查代碼是否運(yùn)行在與鏈接時(shí)相同的目標(biāo)地址,以決定是否進(jìn)行處理。由于現(xiàn)在很少有人不使用loader和tags,將zImage燒寫到rom直接從0x0位置執(zhí)行,所以這個(gè)處理是必須的(但是zImage的頭現(xiàn)在也保留了不用loader也可啟動(dòng)的能力)。arm架構(gòu)下自解壓頭一般是鏈接在0x0地址而被加載到0x30008000運(yùn)行,所以要修正這個(gè)變化。涉及到

  • r5寄存器存放的zImage基地址
  • r6和r12(即ip寄存器)存放的got(global offset table)
  • r2和r3存放的bss段起止地址
  • sp棧指針地址

很簡單,這些寄存器統(tǒng)統(tǒng)被加上一個(gè)你也能猜到的偏移地址 0x30008000。該地址是s3c2410相關(guān)的,其他的ARM處理器可以參考下表

  • PXA2xx是0xa0008000
  • IXP2x00和IXP4xx是0x00008000
  • Freescale i.MX31/37是0x80008000
  • TI davinci DM64xx是0x80008000
  • TI omap系列是0x80008000
  • AT91RM/SAM92xx系列是0x20008000
  • Cirrus EP93xx是0x00008000

這些操作發(fā)生在代碼172行開始的地方,下面只粘貼一部分

add r5, r5, r0 add r6, r6, r0 add ip, ip, r0

后面在211行進(jìn)行bss段的清零工作

not_relocated: mov r0, #0 1: str r0, [r2], #4 @ clear bss str r0, [r2], #4 str r0, [r2], #4 str r0, [r2], #4 cmp r2, r3 blo 1b

然后224行,打開cache,并為后面解壓縮設(shè)置64KB的臨時(shí)malloc空間

bl cache_on mov r1, sp @ malloc space above stack add r2, sp, #0x10000 @ 64k max

接下來238行進(jìn)行檢查,確定內(nèi)核解壓縮后的Image目標(biāo)地址是否會(huì)覆蓋到zImage頭,如果是則準(zhǔn)備將zImage頭轉(zhuǎn)移到解壓出來的內(nèi)核后面

cmp r4, r2 bhs wont_overwrite sub r3, sp, r5 @ > compressed kernel size add r0, r4, r3, lsl #2 @ allow for 4x expansion cmp r0, r5 bls wont_overwrite mov r5, r2 @ decompress after malloc space mov r0, r5 mov r3, r7 bl decompress_kernel

真實(shí)情況——在大多數(shù)的應(yīng)用中,內(nèi)核編譯都會(huì)把壓縮的zImage和非壓縮的Image鏈接到同樣的地址,s3c2410平臺(tái)下即是0x30008000。這樣做的好處是,人們不用關(guān)心內(nèi)核是Image還是zImage,放到這個(gè)位置執(zhí)行就OK,所以在解壓縮后zImage頭必須為真正的內(nèi)核讓路。

在250行解壓完畢,內(nèi)核長度返回值存放在r0寄存器里。在內(nèi)核末尾空出128字節(jié)的棧空間用,并且使其長度128字節(jié)對(duì)齊。

add r0, r0, #127 + 128 @ alignment + stack bic r0, r0, #127 @ align the kernel length

算出搬移代碼的參數(shù):計(jì)算內(nèi)核末尾地址并存放于r1寄存器,需要搬移代碼原來地址放在r2,需要搬移的長度放在r3。然后執(zhí)行搬移,并設(shè)置好sp指針指向新的棧(原來的棧也會(huì)被內(nèi)核覆蓋掉)

add r1, r5, r0 @ end of decompressed kernel adr r2, reloc_start ldr r3, LC1 add r3, r2, r3 1: ldmia r2!, {r9 - r14} @ copy relocation code stmia r1!, {r9 - r14} ldmia r2!, {r9 - r14} stmia r1!, {r9 - r14} cmp r2, r3 blo 1b add sp, r1, #128 @ relocate the stack

搬移完成后刷新cache,因?yàn)榇a地址變化了不能讓cache再命中被內(nèi)核覆蓋的老地址。然后跳轉(zhuǎn)到新的地址繼續(xù)執(zhí)行

bl cache_clean_flush add pc, r5, r0 @ call relocation code

注意——zImage在解壓后的搬移和跳轉(zhuǎn)會(huì)給gdb調(diào)試內(nèi)核帶來麻煩。因?yàn)橛脕碚{(diào)試的符號(hào)表是在編譯是生成的,并不知道以后會(huì)被搬移到何處去,只有在內(nèi)核解壓縮完成之后,根據(jù)計(jì)算出來的參數(shù)“告訴”調(diào)試器這個(gè)變化。以撰寫本文時(shí)使用的zImage為例,內(nèi)核自解壓頭重定向后,reloc_start地址由0x30008360變?yōu)?x30533e60。故我們要把vmlinux的符號(hào)表也相應(yīng)的從0x30008000后移到0x30533b00開始,這樣gdb就可以正確的對(duì)應(yīng)源代碼和機(jī)器指令。

隨著頭部代碼移動(dòng)到新的位置,不會(huì)再和內(nèi)核的目標(biāo)地址沖突,可以開始內(nèi)核自身的搬移了。此時(shí)r0寄存器存放的是內(nèi)核長度(嚴(yán)格的說是長度外加128Byte的棧),r4存放的是內(nèi)核的目的地址0x30008000,r5是目前內(nèi)核存放地址,r6是CPU ID,r7是machine ID,r8是atags地址。代碼從501行開始

reloc_start: add r9, r5, r0 sub r9, r9, #128 @ do not copy the stack debug_reloc_start mov r1, r4 1: .rept 4 ldmia r5!, {r0, r2, r3, r10 - r14} @ relocate kernel stmia r1!, {r0, r2, r3, r10 - r14} .endr cmp r5, r9 blo 1b add sp, r1, #128 @ relocate the stack

接下來在516行清除并關(guān)閉cache,清零r0,將machine ID存入r1,atags指針存入r2,再跳入0x30008000執(zhí)行真正的內(nèi)核Image

call_kernel: bl cache_clean_flush bl cache_off mov r0, #0 @ must be zero mov r1, r7 @ restore architecture number mov r2, r8 @ restore atags pointer mov pc, r4 @ call kernel

zImage自解壓過程結(jié)束。

從zImage頭跳轉(zhuǎn)進(jìn)來,此時(shí)的狀態(tài)

  • MMU為off
  • D-cache為off
  • I-cache為dont care,on或off沒有關(guān)系
  • r0為0
  • r1為machine ID
  • r2為atags指針

內(nèi)核代碼入口在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head.S文件的83行。首先進(jìn)入SVC32模式,并查詢CPU ID,檢查合法性

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

接著在87行進(jìn)一步查詢machine ID并檢查合法性

bl __lookup_machine_type @ r5=machinfo movs r8, r5 @ invalid machine (r5=0)? beq __error_a @ yes, error a

其中__lookup_processor_type在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head-common.S文件的149行,該函數(shù)首將標(biāo)號(hào)3的實(shí)際地址加載到r3,然后將編譯時(shí)生成的__proc_info_begin虛擬地址載入到r5,__proc_info_end虛擬地址載入到r6,標(biāo)號(hào)3的虛擬地址載入到r7。由于adr偽指令和標(biāo)號(hào)3的使用,以及__proc_info_begin等符號(hào)在linux-2.6.24-moko-linuxbj/arch/arm/kernel/vmlinux.lds而不是代碼中被定義,此處代碼不是非常直觀,想弄清楚代碼緣由的讀者請(qǐng)耐心閱讀這兩個(gè)文件和adr偽指令的說明。

r3和r7分別存儲(chǔ)的是同一位置標(biāo)號(hào)3的物理地址(由于沒有啟用mmu,所以當(dāng)前肯定是物理地址)和虛擬地址,所以兒者相減即得到虛擬地址和物理地址之間的offset。利用此offset,將r5和r6中保存的虛擬地址轉(zhuǎn)變?yōu)槲锢淼刂?/p>__lookup_processor_type: adr r3, 3f ldmda r3, {r5 - r7} sub r3, r3, r7 @ get offset between virt&phys add r5, r5, r3 @ convert virt addresses to add r6, r6, r3 @ physical address space

然后從proc_info中讀出內(nèi)核編譯時(shí)寫入的processor ID和之前從cpsr中讀到的processor ID對(duì)比,查看代碼和CPU硬件是否匹配(想在arm920t上運(yùn)行為cortex-a8編譯的內(nèi)核?不讓?。?。如果編譯了多種處理器支持,如versatile板,則會(huì)循環(huán)每種type依次檢驗(yàn),如果硬件讀出的ID在內(nèi)核中找不到匹配,則r5置0返回

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

__lookup_machine_type在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head-common.S文件的197行,編碼方法與檢查processor ID完全一樣,請(qǐng)參考前段

__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

代碼回到head.S第92行,檢查atags合法性,然后創(chuàng)建初始頁表

bl __vet_atags bl __create_page_tables

創(chuàng)建頁表的代碼在218行,首先將內(nèi)核起始地址-0x4000到內(nèi)核起始地址之間的16K存儲(chǔ)器清0

__create_page_tables: pgtbl r4 @ page table address /* * Clear the 16K level 1 swapper page table */ mov r0, r4 mov r3, #0 add r6, r0, #0x4000 1: str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 str r3, [r0], #4 teq r0, r6 bne 1b

然后在234行將proc_info中的mmu_flags加載到r7

ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

在242行將PC指針右移20位,得到內(nèi)核第一個(gè)1MB空間的段地址存入r6,在s3c2410平臺(tái)該值是0x300。接著根據(jù)此值存入映射標(biāo)識(shí)

mov r6, pc, lsr #20 @ start of kernel section orr r3, r7, r6, lsl #20 @ flags + kernel base str r3, [r4, r6, lsl #2] @ identity mapping

完成頁表設(shè)置后回到102行,為打開虛擬地址映射作準(zhǔn)備。設(shè)置sp指針,函數(shù)返回地址lr指向__enable_mmu,并跳轉(zhuǎn)到linux-2.6.24-moko-linuxbj/arch/arm/mm/proc-arm920.S的386行,清除I-cache、D-cache、write buffer和TLB

__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 #endif

然后返回head.S的158行,加載domain和頁表,跳轉(zhuǎn)到__turn_mmu_on

__enable_mmu: #ifdef CONFIG_ALIGNMENT_TRAP orr r0, r0, #CR_A #else bic r0, r0, #CR_A #endif #ifdef CONFIG_CPU_DCACHE_DISABLE bic r0, r0, #CR_C #endif #ifdef CONFIG_CPU_BPREDICT_DISABLE bic r0, r0, #CR_Z #endif #ifdef CONFIG_CPU_ICACHE_DISABLE bic r0, r0, #CR_I #endif 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

在194行把mmu使能位寫入mmu,激活虛擬地址。然后將原來保存在sp中的地址載入pc,跳轉(zhuǎn)到head-common.S的__mmap_switched,至此代碼進(jìn)入虛擬地址的世界

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, r3 mov pc, r13

在head-common.S的37行開始清除內(nèi)核bss段,processor ID保存在r9,machine ID報(bào)存在r1,atags地址保存在r2,并將控制寄存器保存到r7定義的內(nèi)存地址。接下來跳入linux-2.6.24-moko-linuxbj/init/main.c的507行,start_kernel函數(shù)。這里只粘貼部分代碼

__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

在main.c第507行,是硬件無關(guān)的C初始化代碼

asmlinkage void __init start_kernel(void) { char * command_line; extern struct kernel_param __start___param[], __stop___param[]; smp_setup_processor_id();

s3c2410平臺(tái)linux-2.6.24內(nèi)核早期的匯編初始化到這里就結(jié)束了

調(diào)試技巧:

利用gdb調(diào)試內(nèi)核Image啟動(dòng)流程是一種很好的分析手段,要使用好這種手段有一個(gè)問題需要解決——內(nèi)核地址映射問題

  1. 代碼執(zhí)行的早期是處于mmu關(guān)閉的狀態(tài)下,軟件直接使用硬件相關(guān)的物理地址(s3c2410的ram從0x30000000開始);
  2. 后來啟用mmu并建立映射之后,軟件使用虛擬地址。在3G用戶空間配置下(大多數(shù)32位嵌入式系統(tǒng)都是此配置),內(nèi)核使用的PAGE_OFFSET為0xc0000000,與硬件無關(guān),核心虛擬地址變?yōu)閺?xc0000000開始;

調(diào)試器無法自動(dòng)接受這樣的地址轉(zhuǎn)變,需要使用上文介紹的訣竅,手工“告訴”調(diào)試器該怎么做。

對(duì)內(nèi)核編譯產(chǎn)生的vmlinux文件使用objdump工具

$ /usr/local/linuxbj/eabi-glibc/arm/bin/arm-linuxbj-linux-gnueabi-objdump -t vmlinux|more vmlinux: file format elf32-littlearm SYMBOL TABLE: c0008000 l d .text.head 00000000 .text.head c0008240 l d .init 00000000 .init c0027000 l d .text 00000000 .text c03377ec l d .notes 00000000 .notes c0338000 l d __ksymtab 00000000 __ksymtab c033ca40 l d __ksymtab_gpl 00000000 __ksymtab_gpl c033e2d0 l d __ksymtab_gpl_future 00000000 __ksymtab_gpl_future c033e2e8 l d __ksymtab_strings 00000000 __ksymtab_strings c034c9ec l d __param 00000000 __param c034e000 l d .data 00000000 .data c0373e20 l d .bss 00000000 .bss

可以看到內(nèi)核符號(hào)表的.text鏈接虛擬地址是0xc0027000,所以在mmu處于關(guān)閉的階段中,應(yīng)該將內(nèi)核符號(hào)表在調(diào)試器里加載到0x30027000地址。使得head.S入口.text.head正好是0x30008000,與實(shí)際的內(nèi)存一致。



評(píng)論


技術(shù)專區(qū)

關(guān)閉