1. 程式人生 > >ARM Linux啟動流程-彙編第一階段

ARM Linux啟動流程-彙編第一階段

本文整理了ARM Linxu啟動流程的第一階段——核心自解壓,核心版本為3.12.35。我以手上的樹莓派b(ARM11)為平臺示例來分析uboot跳轉到Linux核心執行後做了哪些初始化動作,以及如何轉入真正的核心開始執行。

核心版本:Linux-3.12.35

分析檔案:linux/arch/arm/boot/compressed/head.S

單板:樹莓派b

在核心啟動前,bootloader(我使用的是uboot)做如下準備工作:

  1. CPU暫存器:R0 = 0、R1 = 機器碼(linux/arch/tools/mach-types)、R2 = tags在RAM中的實體地址
  2. CPU和MMU:SVC模式,禁止中斷,MMU關閉,資料Cache關閉。

首先給出核心自解壓部分的總流程如下:
這裡寫圖片描述
核心自解壓程式的入口:參見arch/arm/boot/compressed/vmlinux.lds(由arch/arm/boot/compressed/vmlinux.lds.in生成):

    SECTIONS  
    {  
    ......  
        *(.data)  
      }  

      . = 0;  
      _text = .;  

      .text : {  
        _start = .;  
        *(.start)  
        *(.text)  
        *(.text
.*) *(.fixup) *(.gnu.warning) *(.glue_7t) *(.glue_7) } ...... }

程式的入口點在linux/arch/arm/boot/compressed/head.S中,下面來進行詳細分析:

    start:  
            .type   start,#function  
            .rept   7  
            mov r0, r0  
            .endr  
       ARM(     mov
r0, r0 ) ARM( b 1f ) THUMB( adr r12, BSYM(1f) ) THUMB( bx r12 )

使用.type標號來指明start的符號型別是函式型別,然後重複執行.rept到.endr之間的指令7次,這裡一共執行了7次mov r0, r0指令,共佔用了4*7 = 28個位元組,這是用來存放ARM的異常向量表的。向前跳轉到標號為1處執行:

    1:  
            mrs r9, cpsr  
    #ifdef CONFIG_ARM_VIRT_EXT  
            bl  __hyp_stub_install  @ get into SVC mode, reversibly  
    #endif  
            mov r7, r1          @ save architecture ID  
            mov r8, r2          @ save atags pointer  

這裡將CPU的工作模式儲存到r9暫存器中,將uboot通過r1傳入的機器碼儲存到r7暫存器中,將啟動引數tags的地址儲存到r8暫存器中。

 /* 
     * Booting from Angel - need to enter SVC mode and disable 
     * FIQs/IRQs (numeric definitions from angel arm.h source). 
     * We only do this if we were in user mode on entry. 
     */  
    mrs r2, cpsr        @ get current mode  
    tst r2, #3          @ not user?  
    bne not_angel  
    mov r0, #0x17       @ angel_SWIreason_EnterSVC  
ARM(        swi 0x123456    )   @ angel_SWI_ARM  
THUMB(      svc 0xab        )   @ angel_SWI_THUMB 

這裡將CPU的工作模式儲存到r2暫存器中,然後判斷是否是SVC模式,如果是USER模式就會通過swi指令產生軟中斷異常的方式來自動進入SVC模式。由於我這裡在uboot中已經將CPU的模式設定為SVC模式了,所以就直接跳到not_angel符號處執行。

    not_angel:  
            safe_svcmode_maskall r0  
            msr spsr_cxsf, r9       @ Save the CPU boot mode in  
                            @ SPSR  

safe_svcmode_maskall是一個巨集,定義在arch/arm/include/asm/assembler.h中:

    /* 
     * Helper macro to enter SVC mode cleanly and mask interrupts. reg is 
     * a scratch register for the macro to overwrite. 
     * 
     * This macro is intended for forcing the CPU into SVC mode at boot time. 
     * you cannot return to the original mode. 
     */  
    .macro safe_svcmode_maskall reg:req  
    #if __LINUX_ARM_ARCH__ >= 6  
        mrs \reg , cpsr  
        eor \reg, \reg, #HYP_MODE  
        tst \reg, #MODE_MASK  
        bic \reg , \reg , #MODE_MASK  
        orr \reg , \reg , #PSR_I_BIT | PSR_F_BIT | SVC_MODE  
    THUMB(  orr \reg , \reg , #PSR_T_BIT    )  
        bne 1f  
        orr \reg, \reg, #PSR_A_BIT  
        adr lr, BSYM(2f)  
        msr spsr_cxsf, \reg  
        __MSR_ELR_HYP(14)  
        __ERET  
    1:  msr cpsr_c, \reg  
    2:  
    #else  
    /* 
     * workaround for possibly broken pre-v6 hardware 
     * (akita, Sharp Zaurus C-1000, PXA270-based) 
     */  
        setmode PSR_F_BIT | PSR_I_BIT | SVC_MODE, \reg  
    #endif  
    .endm  

這裡的註釋已經說明了,這裡是強制將CPU的工作模式切換到SVC模式,並且關閉IRQ和FIQ中斷。然後將r9中儲存的原始CPU配置儲存到SPSR中。

    #ifdef CONFIG_AUTO_ZRELADDR  
            @ determine final kernel image address  
            mov r4, pc  
            and r4, r4, #0xf8000000  
            add r4, r4, #TEXT_OFFSET  
    #else  
            ldr r4, =zreladdr  
    #endif  

核心配置項AUTO_ZRELDDR表示自動計算核心解壓地址(Auto calculation of the decompressed kernelimage address),這裡沒有選擇這個配置項,所以儲存到r4中的核心解壓地址就是zreladdr,這個引數在linux/arch/arm/boot/compressed/Makefile中:

    #ifdef CONFIG_AUTO_ZRELADDR  
            @ determine final kernel image address  
            mov r4, pc  
            and r4, r4, #0xf8000000  
            add r4, r4, #TEXT_OFFSET  
    #else  
            ldr r4, =zreladdr  
    #endif  

核心配置項AUTO_ZRELDDR表示自動計算核心解壓地址(Auto calculation of the decompressed kernelimage address),這裡沒有選擇這個配置項,所以儲存到r4中的核心解壓地址就是zreladdr,這個引數在linux/arch/arm/boot/compressed/Makefile中:

ifneq ($(CONFIG_AUTO_ZRELADDR),y)  
LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)  
endif 

核心配置項AUTO_ZRELDDR表示自動計算核心解壓地址(Auto calculation of the decompressed kernelimage address),這裡沒有選擇這個配置項,所以儲存到r4中的核心解壓地址就是zreladdr,這個引數在linux/arch/arm/boot/compressed/Makefile中:

    ifneq ($(CONFIG_AUTO_ZRELADDR),y)  
    LDFLAGS_vmlinux += --defsym zreladdr=$(ZRELADDR)  
    endif  

而ZRELADDR定義在arch/arm/boot/Makefile中:

    ifneq ($(MACHINE),)  
    include $(srctree)/$(MACHINE)/Makefile.boot  
    endif  

    # Note: the following conditions must always be true:  
    #   ZRELADDR == virt_to_phys(PAGE_OFFSET + TEXT_OFFSET)  
    #   PARAMS_PHYS must be within 4MB of ZRELADDR  
    #   INITRD_PHYS must be in RAM  
    ZRELADDR    := $(zreladdr-y)  
    PARAMS_PHYS := $(params_phys-y)  
    INITRD_PHYS := $(initrd_phys-y)  

既然看到了核心解壓地址zreladdr,也順便來看一下params_phys和initrd_phys的值,他們最終由arch/arm/mach-$(SOC)/Makefile.boot決定,我這裡使用的soc是bcm2807(bcm2835),他的Makefile.boot內容如下:
zreladdr-y := 0x00008000
params_phys-y := 0x00000100
initrd_phys-y :=0x00800000
這裡的params_phys-y和initrd_phys-y是核心引數的實體地址和initrd檔案系統的實體地址。其實除了zreladdr外這些地址uboot都會傳入的。

    /* 
     * Set up a page table only if it won't overwrite ourself. 
     * That means r4 < pc && r4 - 16k page directory > &_end. 
     * Given that r4 > &_end is most unfrequent, we add a rough 
     * additional 1MB of room for a possible appended DTB. 
     */  
    mov r0, pc  
    cmp r0, r4  
    ldrcc   r0, LC0+32  
    addcc   r0, r0, pc  
    cmpcc   r4, r0  
    orrcc   r4, r4, #1      @ remember we skipped cache_on  
    blcs    cache_on  

這裡將比較當前PC地址和核心解壓地址,只有在不會自覆蓋的情況下才會建立一個頁表,如果當前執行地址PC < 解壓地址r4,則讀取LC0+32地址處的內容載入到r0中,否則跳轉到cache_on處執行快取初始化和MMU初始化。LC0是地址表,定義在525行:

            .align  2  
            .type   LC0, #object  
    LC0:        .word   LC0         @ r1  
            .word   __bss_start     @ r2  
            .word   _end            @ r3  
            .word   _edata          @ r6  
            .word   input_data_end - 4  @ r10 (inflated size location)  
            .word   _got_start      @ r11  
            .word   _got_end        @ ip  
            .word   .L_user_stack_end   @ sp  
            .word   _end - restart + 16384 + 1024*1024  
            .size   LC0, . - LC0  

LC0+32地址處的內容為:_end -restart + 16384 + 1024*1024,所指的就是程式長度+16k的頁表長+1M的DTB空間。繼續比較解壓地址r4(0x00008000)和當前執行程式的(結束地址+16384 + 1024*1024),如果小於則不進行快取初始化並置位r4最低位進行標識。

這裡稍稍有點繞,分情況總結一下:

(1) PC >= r4:直接進行快取初始化

(2) PC < r4 && _end + 16384+ 1024*1024 > r4:不進行快取初始化

(3) PC < r4 && _end + 16384+ 1024*1024 <= r4:執行快取初始化

這裡先暫時不分析cache_on(已補充在文中最後分析),繼續沿主線往下分析:

    restart:    adr r0, LC0  
            ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}  
            ldr sp, [r0, #28]  

通過前面LC0地址表的內容可見,這裡r0中的內容就是編譯時決定的LC0的實際執行地址(特別注意不是連結地址),然後呼叫ldmia命令依次將LC0地址表處定義的各個地址載入到r1、r2、r3、r6、r10、r11、r12和SP暫存器中去。執行之後各個暫存器中儲存內容的意義如下:

(1) r0:LC0標籤處的執行地址

(2) r1:LC0標籤處的連結地址

(3) r2:__bss_start處的連結地址

(4) r3:_ednd處的連結地址(即程式結束位置)

(5) r6:_edata處的連結地址(即資料段結束位置)

(6) r10:壓縮後核心資料大小位置

(7) r11:GOT表的啟示連結地址

(8) r12:GOT表的結束連結地址

(9) sp:棧空間結束地址
195~196行這段程式碼通過反彙編來看著部分程式碼會更加清晰(反彙編arch/arm/boot /compressed/vmlinux):

    000000c0 <restart>:  
          c0:   e28f0e13    add r0, pc, #304    ; 0x130  
          c4:   e8901c4e    ldm r0, {r1, r2, r3, r6, sl, fp, ip}  
          c8:   e590d01c    ldr sp, [r0, #28]  

由於我的環境中實際的執行在實體地址為0x00008000處,所以r0 = pc + 0x130 = 0x00008000 + 0xc0 + 0x8 + 0x130 = 0x00008000 +0x1F8,而0x000081F8實體地址處的內容就是LC0:

    000001f8 <LC0>:  
         1f8:   000001f8    strdeq  r0, [r0], -r8  
         1fc:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
         200:   006f2a98    mlseq   pc, r8, sl, r2  ; <UNPREDICTABLE>  
         204:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
         208:   006f2a45    rsbeq   r2, pc, r5, asr #20  
         20c:   006f2a58    rsbeq   r2, pc, r8, asr sl  ; <UNPREDICTABLE>  
         210:   006f2a7c    rsbeq   r2, pc, ip, ror sl  ; <UNPREDICTABLE>  
         214:   006f3a98    mlseq   pc, r8, sl, r3  ; <UNPREDICTABLE>  
         218:   007f69d8    ldrsbteq    r6, [pc], #-152  
         21c:   e320f000    nop {0}  

在獲取了LC0的連結地址和執行地址後,就可以通過計算這兩者之間的差值來判斷當前執行的地址是否就是編譯時的連結地址。

    /* 
     * We might be running at a different address.  We need 
     * to fix up various pointers. 
     */  
    sub r0, r0, r1      @ calculate the delta offset  
    add r6, r6, r0      @ _edata  
    add r10, r10, r0        @ inflated kernel size location  

將執行地址和連結地址的偏移儲存到r0暫存器中,然後更新r6和r10中的地址,將其轉換為實際的執行地址。

    /* 
     * The kernel build system appends the size of the 
     * decompressed kernel at the end of the compressed data 
     * in little-endian form. 
     */  
    ldrb    r9, [r10, #0]  
    ldrb    lr, [r10, #1]  
    orr r9, r9, lr, lsl #8  
    ldrb    lr, [r10, #2]  
    ldrb    r10, [r10, #3]  
    orr r9, r9, lr, lsl #16  
    orr r9, r9, r10, lsl #24  

註釋中說明了,核心編譯系統在壓縮核心時會在末尾處以小端模式附上未壓縮的核心大小,這部分程式碼的作用就是將該值計算出來並儲存到r9暫存器中去。

    #ifndef CONFIG_ZBOOT_ROM  
            /* malloc space is above the relocated stack (64k max) */  
            add sp, sp, r0  
            add r10, sp, #0x10000  
    #else  
            /* 
             * With ZBOOT_ROM the bss/stack is non relocatable, 
             * but someone could still run this code from RAM, 
             * in which case our reference is _edata. 
             */  
            mov r10, r6  
    #endif  

這裡將映象的結束地址儲存到r10中去,我這裡並沒有定義ZBOOT_ROM(如果定義了ZBOOT_ROM則bss和stack是非可重定位的),這裡將r10設定為sp結束地址上64kb處(這64kB空間是用來作為堆空間的)。
接下來核心如果配置為支援裝置樹(DTB)會做一些特別的工作,我這裡沒有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳過。

    /* 
     * Check to see if we will overwrite ourselves. 
     *   r4  = final kernel address (possibly with LSB set) 
     *   r9  = size of decompressed image 
     *   r10 = end of this image, including  bss/stack/malloc space if non XIP 
     * We basically want: 
     *   r4 - 16k page directory >= r10 -> OK 
     *   r4 + image length <= address of wont_overwrite -> OK 
     * Note: the possible LSB in r4 is harmless here. 
     */  
            add r10, r10, #16384  
            cmp r4, r10  
            bhs wont_overwrite  
            add r10, r4, r9  
            adr r9, wont_overwrite  
            cmp r10, r9  
            bls wont_overwrite  

這裡r4、r9和r10中的內容見註釋。這部分程式碼用來分析當前程式碼是否會和最後的解壓部分重疊,如果有重疊則需要執行程式碼搬移。首先比較核心解壓地址r4-16Kb(這裡是0x00004000,包括16KB的核心頁表存放位置)和r10,如果r4 – 16kB >= r10,則無需搬移,否則繼續計算解壓後的核心末尾地址是否在當前執行地址之前,如果是則同樣無需搬移,不然的話就需要進行搬移了。

總結一下可能的3種情況:

(1) 核心起始地址– 16kB >= 當前映象結束地址:無需搬移

(2) 核心結束地址 <= wont_overwrite執行地址:無需搬移

(3) 核心起始地址– 16kB < 當前映象結束地址 && 核心結束地址 > wont_overwrite執行地址:需要搬移
仔細分析一下,這裡核心真正執行的地址是0x00004000,而現在程式碼的執行地址顯然已經在該地址之後了反彙編發現wont_overwrite的執行地址是0x00008000+0x00000168),而且核心解壓後的空間必然會覆蓋掉這裡(核心解壓後的大小大於0x00000168),所以這裡會執行程式碼搬移。

    /* 
     * Relocate ourselves past the end of the decompressed kernel. 
     *   r6  = _edata 
     *   r10 = end of the decompressed kernel 
     * Because we always copy ahead, we need to do it from the end and go 
     * backward in case the source and destination overlap. 
     */  
            /* 
             * Bump to the next 256-byte boundary with the size of 
             * the relocation code added. This avoids overwriting 
             * ourself when the offset is small. 
             */  
            add r10, r10, #((reloc_code_end - restart + 256) & ~255)  
            bic r10, r10, #255  

            /* Get start of code we want to copy and align it down. */  
            adr r5, restart  
            bic r5, r5, #31  

從這裡開始會將映象搬移到解壓的核心地址之後,首先將解壓後的核心結束地址進行擴充套件,擴充套件大小為程式碼段的大小(reloc_code_end定義在head.s的最後)儲存到r10中,即搬運目的起始地址,然後r5儲存了restart的起始地址,並進行對齊,即搬運的原起始地址。反彙編檢視這裡擴充套件的大小為0x800:

    11c:    e28aab02    add sl, sl, #2048   ; 0x800  
    120:    e3caa0ff    bic sl, sl, #255    ; 0xff  
    124:    e24f506c    sub r5, pc, #108    ; 0x6c  
    128:    e3c5501f    bic r5, r5, #31  
            sub r9, r6, r5      @ size to copy  
            add r9, r9, #31     @ rounded up to a multiple  
            bic r9, r9, #31     @ ... of 32 bytes  
            add r6, r9, r5  
            add r9, r9, r10  

    1:      ldmdb   r6!, {r0 - r3, r10 - r12, lr}  
            cmp r6, r5  
            stmdb   r9!, {r0 - r3, r10 - r12, lr}  
            bhi 1b  

            /* Preserve offset to relocated code. */  
            sub r6, r9, r6  

    #ifndef CONFIG_ZBOOT_ROM  
            /* cache_clean_flush may use the stack, so relocate it */  
            add sp, sp, r6  
    #endif  

這裡首先計算出需要搬運的大小儲存到r9中,搬運的原結束地址到r6中,搬運的目的結束地址到r9中。注意這裡只搬運程式碼段和資料段,並不包含bss、棧和堆空間。
接下來開始執行程式碼搬移,這裡是從後往前搬移,一直到r6 == r5結束,然後r6中儲存了搬移前後的偏移,並重定向棧指標(cache_clean_flush可能會使用到棧)。

    bl  cache_clean_flush  

    adr r0, BSYM(restart)  
    add r0, r0, r6  
    mov pc, r0  

首先呼叫cache_clean_flush清楚快取,然後將PC的值設定為搬運後restart的新地址,然後重新從restart開始執行。這次由於進行了程式碼搬移,所以會在檢查自覆蓋時進入wont_overwrite處執行。

    wont_overwrite:  
    /* 
     * If delta is zero, we are running at the address we were linked at. 
     *   r0  = delta 
     *   r2  = BSS start 
     *   r3  = BSS end 
     *   r4  = kernel execution address (possibly with LSB set) 
     *   r5  = appended dtb size (0 if not present) 
     *   r7  = architecture ID 
     *   r8  = atags pointer 
     *   r11 = GOT start 
     *   r12 = GOT end 
     *   sp  = stack pointer 
     */  
            orrs    r1, r0, r5  
            beq not_relocated  

這裡的註釋列出了現有所有暫存器值得含義,如果r0為0則說明當前執行的地址就是連結地址,無需進行重定位,跳轉到not_relocated執行,但是這裡執行的地址已經被移動到核心解壓地址之後,顯然不會是連結地址0x00000168(反彙編程式碼中得到),所以這裡需要重新修改GOT表中的變數地址來實現重定位。



            add r11, r11, r0  
            add r12, r12, r0  

    #ifndef CONFIG_ZBOOT_ROM  
            /* 
             * If we're running fully PIC === CONFIG_ZBOOT_ROM = n, 
             * we need to fix up pointers into the BSS region. 
             * Note that the stack pointer has already been fixed up. 
             */  
            add r2, r2, r0  
            add r3, r3, r0  

這裡更新GOT表的執行起始地址到r11和結束地址到r12中去,然後同樣更新BSS段的執行地址(需要修正BSS段的指標)。接下來開始執行重定位:

            /* 
             * Relocate all entries in the GOT table. 
             * Bump bss entries to _edata + dtb size 
             */  
    1:      ldr r1, [r11, #0]       @ relocate entries in the GOT  
            add r1, r1, r0      @ This fixes up C references  
            cmp r1, r2          @ if entry >= bss_start &&  
            cmphs   r3, r1          @       bss_end > entry  
            addhi   r1, r1, r5      @    entry += dtb size  
            str r1, [r11], #4       @ next entry  
            cmp r11, r12  
            blo 1b  

            /* bump our bss pointers too */  
            add r2, r2, r5  
            add r3, r3, r5  

通過r1獲取GOT表中的一項,然後對這一項的地址進行修正,如果修正後的地址 < BSS段的起始地址,或者在BSS段之中則再加上DTB的大小(如果不支援DTB則r5的值為0),然後再將值寫回GOT表中去。如此迴圈執行直到遍歷完GOT表。來看一下反彙編出來的GOT表,加深理解:

    006f2a58 <.got>:  
      6f2a58:   006f2a49    rsbeq   r2, pc, r9, asr #20  
      6f2a5c:   006f2a94    mlseq   pc, r4, sl, r2  ; <UNPREDICTABLE>  
      6f2a60:   0000466c    andeq   r4, r0, ip, ror #12  
      6f2a64:   006f2a90    mlseq   pc, r0, sl, r2  ; <UNPREDICTABLE>  
      6f2a68:   006f2a80    rsbeq   r2, pc, r0, lsl #21  
      6f2a6c:   0000090c    andeq   r0, r0, ip, lsl #18  
      6f2a70:   006f2a88    rsbeq   r2, pc, r8, lsl #21  
      6f2a74:   006f2a8c    rsbeq   r2, pc, ip, lsl #21  
      6f2a78:   006f2a84    rsbeq   r2, pc, r4, lsl #21  

以這裡的6f2a60: 0000466c為例,0000466c為input_data符號的連結地址,定義在arch/arm/boot/compressed/piggy.gzip.S中,可以算作是全域性變數,這裡在執行重定位時會將6f2a60地址處的值加上偏移,可得到input_data符號的執行地址,以此完成重定位工作,以後在核心的C程式碼中如果用到input_data符號就從這裡的地址載入。

    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  

在重定位完成後,繼續執行not_relocated部分程式碼,這裡迴圈清零BSS段。

    /* 
     * Did we skip the cache setup earlier? 
     * That is indicated by the LSB in r4. 
     * Do it now if so. 
     */  
    tst r4, #1  
    bic r4, r4, #1  
    blne    cache_on  

這裡檢測r4中的最低位,如果已經置位則說明在前面執行restart前並沒有執行cache_on來開啟快取(見前文),這裡補執行。

    /* 
     * The C runtime environment should now be setup sufficiently. 
     * Set up some pointers, and start decompressing. 
     *   r4  = kernel execution address 
     *   r7  = architecture ID 
     *   r8  = atags pointer 
     */  
            mov r0, r4  
            mov r1, sp          @ malloc space above stack  
            add r2, sp, #0x10000    @ 64k max  
            mov r3, r7  
            bl  decompress_kernel  

到此為止,C語言的執行環境已經準備就緒,設定一些指標就可以開始解壓核心了(這裡的核心解壓部分是使用C程式碼寫的)。

這裡r0~r3的4個暫存器是decompress_kernel()函式傳參用的,r0傳入核心解壓後的目的地址,r1傳入堆空間的起始地址,r2傳入堆空間的結束地址,r3傳入機器碼,然後就開始呼叫decompress_clean_flush()函式執行核心解壓操作:

    void  
    decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,  
            unsigned long free_mem_ptr_end_p,  
            int arch_id)  
    {  
        int ret;  

        output_data     = (unsigned char *)output_start;  
        free_mem_ptr        = free_mem_ptr_p;  
        free_mem_end_ptr    = free_mem_ptr_end_p;  
        __machine_arch_type = arch_id;  

        arch_decomp_setup();  

        putstr("Uncompressing Linux...");  
        ret = do_decompress(input_data, input_data_end - input_data,  
                    output_data, error);  
        if (ret)  
            error("decompressor returned an error");  
        else  
            putstr(" done, booting the kernel.\n");  
    }  

真正執行核心解壓的函式是do_decompress()->decompress()(該函式會根據不同的壓縮方式呼叫不同的解壓函式),在解壓前會在終端上列印“Uncompressing Linux…”,結束後會打印出“done, booting the kernel.\n”。(這裡有一點疑問:這裡會在終端輸出,那串列埠的驅動是在什麼時候初始化的?)

    bl  cache_clean_flush  
    bl  cache_off  
    mov r1, r7          @ restore architecture number  
    mov r2, r8          @ restore atags pointer  

解壓完成後就重新整理快取,然後將快取(包括MMU關閉),這裡之所以要開啟快取和MMU是為了加速核心解壓。

然後將機器碼和內啟動引數atags恢復到r1和r2暫存器中,為跳轉到解壓後的核心程式碼做準備。

b   __enter_kernel  
    __enter_kernel:  
            mov r0, #0          @ must be 0  
     ARM(       mov pc, r4  )       @ call kernel  
     THUMB(     bx  r4  )       @ entry point is always ARM  

補充:快取和MMU初始化cache_on的執行流程

    /* 
     * Turn on the cache.  We need to setup some page tables so that we 
     * can have both the I and D caches on. 
     * 
     * We place the page tables 16k down from the kernel execution address, 
     * and we hope that nothing else is using it.  If we're using it, we 
     * will go pop! 
     * 
     * On entry, 
     *  r4 = kernel execution address 
     *  r7 = architecture number 
     *  r8 = atags pointer 
     * On exit, 
     *  r0, r1, r2, r3, r9, r10, r12 corrupted 
     * This routine must preserve: 
     *  r4, r7, r8 
     */  
            .align  5  
    cache_on:   mov r3, #8          @ cache_on function  
            b   call_cache_fn  

註釋中說明了,為了開啟I Cache和D Cache,需要建立頁表(開啟MMU),而頁表使用的就是核心執行地址以下的16KB空間(對於我的環境來說地址就等於0x00004000~0x00008000)。同時在執行的過程中r0~r3以及r9、r10和r12暫存器會被使用。

這裡首先在r3中儲存開啟快取函式表項在cache操作表中的地址偏移(這裡為8,cache操作表見後文),然後跳轉到call_cache_fn中。

    /* 
     * Here follow the relocatable cache support functions for the 
     * various processors.  This is a generic hook for locating an 
     * entry and jumping to an instruction at the specified offset 
     * from the start of the block.  Please note this is all position 
     * independent code. 
     * 
     *  r1  = corrupted 
     *  r2  = corrupted 
     *  r3  = block offset 
     *  r9  = corrupted 
     *  r12 = corrupted 
     */  

    call_cache_fn:  adr r12, proc_types  
    #ifdef CONFIG_CPU_CP15  
            mrc p15, 0, r9, c0, c0  @ get processor ID  
    #else  
            ldr r9, =CONFIG_PROCESSOR_ID  
    #endif  
    1:      ldr r1, [r12, #0]       @ get value  
            ldr r2, [r12, #4]       @ get mask  
            eor r1, r1, r9      @ (real ^ match)  
            tst r1, r2          @       & mask  
     ARM(       addeq   pc, r12, r3     ) @ call cache function  
     THUMB(     addeq   r12, r3         )  
     THUMB(     moveq   pc, r12         ) @ call cache function  
            add r12, r12, #PROC_ENTRY_SIZE  
            b   1b  

首先儲存cache操作表的執行地址到r12暫存器中,proc_types定義在head.s中的825行:

    /* 
     * Table for cache operations.  This is basically: 
     *   - CPU ID match 
     *   - CPU ID mask 
     *   - 'cache on' method instruction 
     *   - 'cache off' method instruction 
     *   - 'cache flush' method instruction 
     * 
     * We match an entry using: ((real_id ^ match) & mask) == 0 
     * 
     * Writethrough caches generally only need 'on' and 'off' 
     * methods.  Writeback caches _must_ have the flush method 
     * defined. 
     */  
            .align  2  
            .type   proc_types,#object  

表中的每一類處理器都包含以下5項(如果不存在快取操作函式則使用“mov pc, lr”佔位):

(1) CPU ID

(2) CPU ID 位掩碼(用於匹配CPU型別用)

(3) 開啟快取“cache on”函式入口

(4) 關閉快取“cache off”函式入口

(5) 重新整理快取“cache flush”函式入口

其中我環境中使用到的ARMv6的cache操作表如下:

    .word   0x0007b000      @ ARMv6  
    .word   0x000ff000  
    W(b)    __armv6_mmu_cache_on  
    W(b)    __armv4_mmu_cache_off  
    W(b)    __armv6_mmu_cache_flush  

我的環境中由於配置了CPU_CP15條件編譯項,所以這裡將從CP15中獲取CPU型號而不是從核心配置項中獲取。

然後逐條對cache操作表中的CPU型別進行匹配,如果匹配上了就跳轉到相應的函式入口執行。

這裡通過反彙編程式碼來理解一下:

    0000044c <call_cache_fn>:  
         44c:   e28fc01c    add ip, pc, #28  
         450:   ee109f10    mrc 15, 0, r9, cr0, cr0, {0}  
         454:   e59c1000    ldr r1, [ip]  
         458:   e59c2004    ldr r2, [ip, #4]  
         45c:   e0211009    eor r1, r1, r9  
         460:   e1110002    tst r1, r2  
         464:   008cf003    addeq   pc, ip, r3  
         468:   e28cc014    add ip, ip, #20  
         46c:   eafffff8    b   454 <call_cache_fn+0x8>  

這裡首先在r12中獲取了proc_types的執行地址:pc + 0x8 + 0x1c:

00000470 <proc_types>:  

然後逐條匹配,最後會匹配到ARMv6部分,cache table中ARMv6部分的程式碼如下:

    5b0:    0007b000    andeq   fp, r7, r0  
    5b4:    000ff000    andeq   pc, pc, r0  
    5b8:    eaffff5c    b   330 <__armv6_mmu_cache_on>  
    5bc:    ea00001f    b   640 <__armv4_mmu_cache_off>  
    5c0:    ea00004f    b   704 <__armv6_mmu_cache_flush>  

於是PC暫存器就執行上面5b8地址處的內容,這條機器碼被翻譯為相對跳轉到__armv6_mmu_cache_on處執行:

00000330 <__armv6_mmu_cache_on>:  

這樣__armv6_mmu_cache_on就被呼叫執行了。

__armv6_mmu_cache_on中會通過寫暫存器來開啟MMU、I Cache和D Cache,這裡具體就不仔細分析了,其中為MMU建立頁表有必要分析一下:

前文已經分析過在核心最終執行地址r4下面有16KB的空間(我環境中是0x00004000~0x00008000),這就是用來存放頁表的,但是現在要建立的頁表在核心真正啟動後會被銷燬,只是用於零時存放。同時這裡將要建立的頁表對映關係是1:1對映(即虛擬地址 == 實體地址)。

    __setup_mmu:    sub r3, r4, #16384      @ Page directory size  
            bic r3, r3, #0xff       @ Align the pointer  
            bic r3, r3, #0x3f00  

首先在r3中儲存的是頁目錄表的基地址,需要將低14位清零進行16KB對齊。

    /* 
     * Initialise the page tables, turning on the cacheable and bufferable 
     * bits for the RAM area only. 
     */  
            mov r0, r3  
            mov r9, r0, lsr #18  
            mov r9, r9, lsl #18     @ start of RAM  
            add r10, r9, #0x10000000    @ a reasonable RAM size  

這裡建立頁表,只對物理RAM空間建立cache和buffer。然後通過將r0(r3)中的地址值右移18位再左移18位(即清零r3中地址的低18位),得到物理RAM空間的“初始地址”(其實是估計值)並儲存到r9(0x00000000)中去,然後將該地址加上256MB的大小作為物理RAM的“結束地址”(也是估計值)並儲存到r10(0x10000000)中去。這裡的另一個隱含意思也就是最多對映256MB大小的空間。

    mov r1, #0x12       @ XN|U + section mapping  
    orr r1, r1, #3 << 10  @ AP=11  
    add r2, r3, #16384  
            cmp r1, r9          @ if virt > start of RAM  
    cmphs   r10, r1         @   && end of RAM > virt  
    bic r1, r1, #0x1c       @ clear XN|U + C + B  
    orrlo   r1, r1, #0x10       @ Set XN|U for non-RAM  
    orrhs   r1, r1, r6      @ set RAM section settings  
    str r1, [r0], #4        @ 1:1 mapping  
    add r1, r1, #1048576  
    teq r0, r2  
    bne 1b  

ARM11的 MMU支援兩級分頁機制,使用者通過配置TTBCR可選對映方式,這裡只採用一級對映方式,每頁的大小為1MB,4GB線性空間佔4096個表項。ARM手冊中列出了對映的關係如下:
這裡寫圖片描述
其中Translation table base的值就是頁表存放的起始地址值0x00004000。這裡虛擬地址轉換到實體地址的方式如下:首先將虛擬地址的高12位(頁表中的索引)左移2位(正好為14位長度,補齊Translation table base中低14位的空白)得到在頁表中的偏移地址,該偏移值加上頁表基地址就得到了對應表項的實體地址,然後取出表項中的高12位值作為實體地址的基地址,最後以虛擬地址的低20位作為偏移地址值就得到了最終對映後的實體地址。

這裡First-level descriptor中的內容就是這裡程式中要填充的值,其中最低2位為0b10表示表項中儲存的是Section base address,而不是2級頁表的基地址。

來繼續分析程式碼,這裡首先r1 =0b 1100 0001 0010 = 0xC12(用於設定MMU區域表項的低12位狀態位),r2為頁表空間的結束地址,然後開始迴圈建立頁表項。

接著r1比較r9和r10以設定MMU區域表項狀態位:(其中r6中的值在前面__armv6_mmu_cache_on中賦值為0x1E)
(1)r1 > r9 && r1 <r10(r1的值在物理RAM地址範圍內):
設定RAM表項的C+B 位來開啟cache和buffer,同時清除XN表示可執行code
(2) r1 < r9 || r1 > r10(r1的值在物理RAM地址範圍外):

設定RAM表項的XN位並清除C+B位來關閉cache和buffer,不可執行code
在設定完狀態為後就要寫入頁表的相應地址中去了,然後將頁表的地址+4(指向下一個表項),實體地址空間+1M設定下一項(下一個需要對映實體地址的基地址),直到填完所有的4096表項。設定完後頁表項與對映關係如下:
這裡寫圖片描述


            
           

相關推薦

ARM Linux啟動流程-彙編第一階段

本文整理了ARM Linxu啟動流程的第一階段——核心自解壓,核心版本為3.12.35。我以手上的樹莓派b(ARM11)為平臺示例來分析uboot跳轉到Linux核心執行後做了哪些初始化動作,以及如何轉入真正的核心開始執行。 核心版本:Linux-3.12

ARM Linux啟動流程-彙編第二階段

本文整理了ARM Linxu啟動流程的第二階段——start_kernel前啟動階段(彙編部分),核心版本為3.12.35。我以手上的樹莓派b(ARM11)為平臺示例來分析Linux核心在自解壓後到跳轉執行start_kernel之前所做的主要初始化工作:包括引數有效性驗證、建立初始頁表和MMU初始化等。

ARM Linux啟動流程分析——start_kernel前啟動階段彙編部分)

本文整理了ARM Linxu啟動流程的第二階段——start_kernel前啟動階段(彙編部分),核心版本為3.12.35。我以手上的樹莓派b(ARM11)為平臺示例來分析Linux核心在自解壓後到跳轉執行start_kernel之前所做的主要初始化工作:包括引數有效性驗證

Linux啟動流程詳解

linux 詳解 啟動流程 grub mbr 內核 linux啟動流程第一部分 Linux啟動基礎知識1.1 linux centos6.8啟動流程圖 BIOS加電自檢à加載MBRà加載啟動grubà加載內核à啟動/sbin/i

linux啟動流程簡介

機器 配置文件 互聯網 local 相關信息 ade 通過 在操作 mbr 我們都知道,由於linux的穩定性,通常被作為服務器系統,要想稱為一個PHP的高手,linux是必修之課。那麽linux系統從開機到啟動,中間到底都發生了什麽?本文來簡單探討一下中間的神秘過程。 1

Linux 啟動流程

grub 指定 建立 cal wap module log images byte 面試遇到過兩家公司問這個問題,問的難一點的問題都答上來了,偏偏這個簡單的反而翻船了,這兩家都沒要我,痛定思痛,一定要把這個給記下來。 本次環境基於 RHEL 6 。RHEL

Linux 啟動流程學習

基礎知識Linux 啟動流程學習 開機自檢(加載BIOS) 無論什麽機器,在開機後都要進行通電自檢(硬件),如果硬件有問題,則無法繼續下去。 (例如當內存條松動時或者損壞,就會發出嘀嘀嘀警報聲)。接著開始加載BIOS(Basic Input Output System BIOS是一個寫入到主板上的一個軟件程序

Linux啟動流程和腳本服務-6

查看 lin 歡迎頁 restart 主機名 please 虛擬內存 linux系統啟動 inux 授課筆記:----------------------------------- linux系統啟動流程:一.初始化階段:1.grub引導界面2.識別硬件3.初始化驅動 二.

Linux 啟動流程及制作光盤鏡像

目的 清理 詳解 vml ESS initramfs load 裝載 歡迎信息 1、 簡述linux操作系統啟動流程 POST (加電自檢):自檢主要硬件設備如:CPU、內存、硬盤是否正常,以及輸入輸出設備是否存在問題等。BootSequence(BIOS)

arm linux 啟動後 can not find /dev/tty*

qemu + rootfs(buildroot) + linux3.18   實驗環境搭建參考部落格:https://blog.csdn.net/qq_24188351/article/details/77921653 (ntfs uboot 的方式沒搞定)  執行起來後

zynqMP LINUX 啟動流程和移植

最近花了幾天時間完成了zynqMP linux的移植工作,這裡記錄一下工作的流程。 zynqMP linux 啟動過程 U-BOOT製作 ATF編譯 BOOTBIN製作 L

Linux啟動流程及錯誤修復

系統啟動流程                                     

Linux啟動流程_LK流程_recovery/normal_boot(2.2)

深入,並且廣泛 -沉默犀牛 此篇部落格原部落格來自freebuf,原作者SetRet。原文連結:https://www.freebuf.com/news/135084.html 寫在前面的話 寫這篇文章之前,我只好假定你所知道的跟我一樣淺薄

Linux啟動流程_LK流程_aboot_init(不包含recovery boot)(2.1)

深入,並且廣泛 -沉默犀牛 此篇部落格原部落格來自freebuf,原作者SetRet。原文連結:https://www.freebuf.com/news/135084.html 寫在前面的話 寫這篇文章之前,我只好假定你所知道的跟我一樣淺薄

Linux啟動流程_LK流程_bootstrap2(1)

深入,並且廣泛 -沉默犀牛 此篇部落格原部落格來自freebuf,原作者SetRet。原文連結:https://www.freebuf.com/news/135084.html 寫在前面的話 寫這篇文章之前,我只好假定你所知道的跟我一樣淺薄(針對本文這一方

Linux啟動流程_LK流程_Kmain(0)

深入,並且廣泛 -沉默犀牛 此篇部落格原部落格來自freebuf,原作者SetRet。原文連結:https://www.freebuf.com/news/135084.html 寫在前面的話 寫這篇文章之前,我只好假定你所知道的跟我一樣淺薄(針對本文這一方

Linux啟動流程與模組管理(15)

系統的啟動其實是一項非常複雜的過程,因為核心得要檢測硬體並載入適當的驅動程式,接下來則必須要呼叫程式來準備好系統執行的環境,以讓使用者能夠順利的操作整臺主機系統,如果你能夠理解系統啟動的原理,那麼將有助於你在系統出問題時能夠很快速的修復系統,而且還能夠順利的配置多重作業系統的多重啟動問題,為了多重啟動的問題,

Linux 啟動流程 粗解(二)

另一個重要函式 在 start_kernel中最後呼叫的函式 0# 1#程序都是在這裡啟動的 static noinline void __init_refok rest_init(void) __releases(kernel_lock) {     int pid;

Linux啟動流程-bootloader至kernel的過程--android系統啟動流程

1 Bootloader 對於一般的ARM處理器,CPU上電或復位執行第一條指令所在地址,即第一段程式Bootloader的開始地址,Bootloader一般存於Nor-flash(XIP),支援晶片內執行。 Bootloader的功能可總結為:1)初始化CPU時鐘,記憶體

LINUX啟動流程簡析(以Debian為例)

半年前,我寫了《計算機是如何啟動的?》,探討BIOS和主引導記錄的作用。 那篇文章不涉及作業系統,只與主機板的板載程式有關。今天,我想接著往下寫,探討作業系統接管硬體以後發生的事情,也就是作業系統的啟動流程。 這個部分比較有意思。因為在BIOS階段,計算機的行為基本上被寫死了,程式