新聞中心

        EEPW首頁 > 嵌入式系統 > 設計應用 > linux內核啟動過程——基于S3C2410

        linux內核啟動過程——基于S3C2410

        作者: 時間:2016-11-09 來源:網絡 收藏
        本文以流行的Samsung公司的S3C2410,openmoko平臺和linux-2.6.24為例,介紹如何在ZIX嵌入式開發環境下探索linux內核啟動過程

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

        本文引用地址:http://www.104case.com/article/201611/317999.htm

        在u-boot的do_bootm_linux函數里,實現了處理器架構相關的linux內核加載代碼,特別是tags傳遞。

        該函數中,在lib_arm/bootm.c的76行調用了getenv將bootargs環境變量保存在commandline

        char*commandline =getenv("bootargs");

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

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

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

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

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

        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

        這也標志著u-boot將系統完全的交給了OS,bootloader生命終止。之后代碼在133行會讀取cpsr并判斷是否處理器處于supervisor模式——從u-boot進入kernel,系統已經處于SVC32模式;而利用angel進入則處于user模式,還需要額外兩條指令。之后是再次確認中斷關閉,并完成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地址處將分段信息導入r0-r6、ip、sp等寄存器,并檢查代碼是否運行在與鏈接時相同的目標地址,以決定是否進行處理。由于現在很少有人不使用loader和tags,將zImage燒寫到rom直接從0x0位置執行,所以這個處理是必須的(但是zImage的頭現在也保留了不用loader也可啟動的能力)。arm架構下自解壓頭一般是鏈接在0x0地址而被加載到0x30008000運行,所以要修正這個變化。涉及到

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

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

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

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

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

        后面在211行進行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,并為后面解壓縮設置64KB的臨時malloc空間

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

        接下來238行進行檢查,確定內核解壓縮后的Image目標地址是否會覆蓋到zImage頭,如果是則準備將zImage頭轉移到解壓出來的內核后面

        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

        真實情況——在大多數的應用中,內核編譯都會把壓縮的zImage和非壓縮的Image鏈接到同樣的地址,s3c2410平臺下即是0x30008000。這樣做的好處是,人們不用關心內核是Image還是zImage,放到這個位置執行就OK,所以在解壓縮后zImage頭必須為真正的內核讓路。

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

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

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

        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,因為代碼地址變化了不能讓cache再命中被內核覆蓋的老地址。然后跳轉到新的地址繼續執行

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

        注意——zImage在解壓后的搬移和跳轉會給gdb調試內核帶來麻煩。因為用來調試的符號表是在編譯是生成的,并不知道以后會被搬移到何處去,只有在內核解壓縮完成之后,根據計算出來的參數“告訴”調試器這個變化。以撰寫本文時使用的zImage為例,內核自解壓頭重定向后,reloc_start地址由0x30008360變為0x30533e60。故我們要把vmlinux的符號表也相應的從0x30008000后移到0x30533b00開始,這樣gdb就可以正確的對應源代碼和機器指令。

        隨著頭部代碼移動到新的位置,不會再和內核的目標地址沖突,可以開始內核自身的搬移了。此時r0寄存器存放的是內核長度(嚴格的說是長度外加128Byte的棧),r4存放的是內核的目的地址0x30008000,r5是目前內核存放地址,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行清除并關閉cache,清零r0,將machine ID存入r1,atags指針存入r2,再跳入0x30008000執行真正的內核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自解壓過程結束。

        從zImage頭跳轉進來,此時的狀態

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

        內核代碼入口在linux-2.6.24-moko-linuxbj/arch/arm/kernel/head.S文件的83行。首先進入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行進一步查詢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行,該函數首將標號3的實際地址加載到r3,然后將編譯時生成的__proc_info_begin虛擬地址載入到r5,__proc_info_end虛擬地址載入到r6,標號3的虛擬地址載入到r7。由于adr偽指令和標號3的使用,以及__proc_info_begin等符號在linux-2.6.24-moko-linuxbj/arch/arm/kernel/vmlinux.lds而不是代碼中被定義,此處代碼不是非常直觀,想弄清楚代碼緣由的讀者請耐心閱讀這兩個文件和adr偽指令的說明。

        r3和r7分別存儲的是同一位置標號3的物理地址(由于沒有啟用mmu,所以當前肯定是物理地址)和虛擬地址,所以兒者相減即得到虛擬地址和物理地址之間的offset。利用此offset,將r5和r6中保存的虛擬地址轉變為物理地址

        __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中讀出內核編譯時寫入的processor ID和之前從cpsr中讀到的processor ID對比,查看代碼和CPU硬件是否匹配(想在arm920t上運行為cortex-a8編譯的內核?不讓!)。如果編譯了多種處理器支持,如versatile板,則會循環每種type依次檢驗,如果硬件讀出的ID在內核中找不到匹配,則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完全一樣,請參考前段

        __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合法性,然后創建初始頁表

        bl __vet_atags bl __create_page_tables

        創建頁表的代碼在218行,首先將內核起始地址-0x4000到內核起始地址之間的16K存儲器清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位,得到內核第一個1MB空間的段地址存入r6,在s3c2410平臺該值是0x300。接著根據此值存入映射標識

        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

        完成頁表設置后回到102行,為打開虛擬地址映射作準備。設置sp指針,函數返回地址lr指向__enable_mmu,并跳轉到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和頁表,跳轉到__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,跳轉到head-common.S的__mmap_switched,至此代碼進入虛擬地址的世界

        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行開始清除內核bss段,processor ID保存在r9,machine ID報存在r1,atags地址保存在r2,并將控制寄存器保存到r7定義的內存地址。接下來跳入linux-2.6.24-moko-linuxbj/init/main.c的507行,start_kernel函數。這里只粘貼部分代碼

        __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行,是硬件無關的C初始化代碼

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

        s3c2410平臺linux-2.6.24內核早期的匯編初始化到這里就結束了

        調試技巧:

        利用gdb調試內核Image啟動流程是一種很好的分析手段,要使用好這種手段有一個問題需要解決——內核地址映射問題

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

        調試器無法自動接受這樣的地址轉變,需要使用上文介紹的訣竅,手工“告訴”調試器該怎么做。

        對內核編譯產生的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

        可以看到內核符號表的.text鏈接虛擬地址是0xc0027000,所以在mmu處于關閉的階段中,應該將內核符號表在調試器里加載到0x30027000地址。使得head.S入口.text.head正好是0x30008000,與實際的內存一致。



        評論


        技術專區

        關閉
        主站蜘蛛池模板: 札达县| 六枝特区| 奉新县| 简阳市| 保康县| 饶阳县| 贵定县| 盐山县| 赣州市| 当阳市| 贞丰县| 竹北市| 深州市| 娱乐| 新巴尔虎左旗| 禄丰县| 黔江区| 佛学| 赤城县| 板桥市| 哈密市| 福海县| 白水县| 金溪县| 卓资县| 襄城县| 宁城县| 安康市| 明水县| 女性| 宜章县| 玉树县| 无锡市| 永仁县| 固安县| 台前县| 沙湾县| 锦州市| 广饶县| 滕州市| 九龙坡区|