ARM linux解析之壓縮內核zImage的啓動過程

ARM linux解析之壓縮內核zImage的啓動過程


首先,我們要知道在zImage的生成過程中,是把arch/arm/boot/compressed/head.s 和解壓代碼misc.cdecompress.c加在壓縮內核的最前面最終生成zImage的,那麼它的啓動過程就是從這個head.s開始的,並且如果代碼從RAM運行的話,是與位置無關的,可以加載到內存的任何地方。

下面以arch/arm/boot/compressed/head.s爲主線進行啓動過程解析。

1.   head.sdebug宏定義部分

最開始的一段都是head.sdebug宏定義部分,這部分可以方便我們調試時使用。

如下:

#ifdef DEBUG

#if defined(CONFIG_DEBUG_ICEDCC)

#if defined(CONFIG_CPU_V6) || defined(CONFIG_CPU_V6K) || defined(CONFIG_CPU_V7)

           .macro loadsp, rb, tmp

           .endm

           .macro writeb, ch, rb

           mcr      p14, 0, \ch, c0, c5, 0

           .endm

#elif defined(CONFIG_CPU_XSCALE)

           .macro loadsp, rb, tmp

           .endm

           .macro writeb, ch, rb

           mcr      p14, 0, \ch, c8, c0, 0

           .endm

#else

           .macro loadsp, rb, tmp

           .endm

           .macro writeb, ch, rb

           mcr      p14, 0, \ch, c1, c0, 0

           .endm

#endif

#else

#include <mach/debug-macro.S>

 

           .macro writeb, ch, rb

           senduart \ch, \rb

           .endm

#if defined(CONFIG_ARCH_SA1100)

           .macro loadsp, rb, tmp

           mov     \rb, #0x80000000  @ physical base address

#ifdef CONFIG_DEBUG_LL_SER3

           add \rb, \rb, #0x00050000 @ Ser3

#else

           add \rb, \rb, #0x00010000 @ Ser1

#endif

           .endm

#elif defined(CONFIG_ARCH_S3C2410)

           .macro loadsp, rb, tmp

           mov     \rb, #0x50000000

           add \rb, \rb, #0x4000 * CONFIG_S3C_LOWLEVEL_UART_PORT

           .endm

#else

           .macro loadsp, rb, tmp

           addruart \rb, \tmp

           .endm

#endif

#endif

#endif

如果開啓DEBUGging宏的話,這部分代碼分兩段CONFIG_DEBUG_ICEDCC是用ARMv6以上的加構支持的ICEDCC技術進行調試,DCCDebug Communications Channel)是ARM的一個調試通信通道,在串口無法使用的時候可以使用這個通道進行數據的通信,具體的技術參前ARM公司文檔《ARM Architecture Reference Manual》。

第二部分首先#include <mach/debug-macro.S>,這個文件定義位於arch/arm/mach-xxxx/include/mach/debug-macro.S裏面,所以這個是和平臺相關的,裏面定義了每個平臺的相關的串口操作,因這個時候系統還沒有起來,所以它所用的串口配置參數是依賴於前一級bootloader所設置好的,如我們使用的u-boot設置好所有的參數。如我們的EVBARM的實現如下:

#include <mach/hardware.h>

#include <mach/platform.h>

 

      .macro addruart, rp, rv

      ldr \rp, =ARM_EVB_UART0_BASE                       @ System peripherals (phys address)

      ldr \rv, =(IO_BASE+ ARM_EVB _UART0_BASE)           @ System peripherals (virt address)

      .endm

 

      .macro senduart,rd,rx

      strb      \rd, [\rx, #(0x00)]  @ Write to Transmitter Holding Register

      .endm

 

      .macro waituart,rd,rx

1001:   ldr \rd, [\rx, #(0x18)]                         @ Read Status Register

      tst \rd, #0x20                                         @when TX FIFO  Full, then wait

      bne 1001b

      .endm

 

      .macro busyuart,rd,rx

1001:   ldr \rd, [\rx, #(0x18)]        @ Read Status Register

      tst \rd, #0x08                         @ when uart is busy then wait

      bne 1001b

      .endm

主要實現 addruartsenduartwaituartbusyuart這四個函數的具體實施。這個是調試函數打印的基礎。

下面是調試打印用到的kputckphex

           .macro kputc,val

           mov     r0, \val

           bl   putc

           .endm

 

           .macro kphex,val,len

           mov     r0, \val

           mov     r1, #\len

           bl   phex

           .endm

它所調用的putc phex是在head.s最後的一段定義的,如下

 

#ifdef DEBUG

           .align   2

           .type    phexbuf,#object

phexbuf:   .space  12

           .size     phexbuf, . - phexbuf

上面是分配打印hexbuffer,下面是具體的實現:

@ phex corrupts {r0, r1, r2, r3}

phex:        adr r3, phexbuf

           mov     r2, #0

           strb      r2, [r3, r1]

1:        subs     r1, r1, #1

           movmi r0, r3

           bmi      puts

           and r2, r0, #15

           mov     r0, r0, lsr #4

           cmp     r2, #10

           addge   r2, r2, #7

           add r2, r2, #'0'

           strb      r2, [r3, r1]

           b    1b

 

@ puts corrupts {r0, r1, r2, r3}

puts:         loadsp  r3, r1

1:        ldrb      r2, [r0], #1

           teq r2, #0

           moveq  pc, lr

2:        writeb  r2, r3

           mov     r1, #0x00020000

3:        subs     r1, r1, #1

           bne 3b

           teq r2, #'\n'

           moveq  r2, #'\r'

           beq 2b

           teq r0, #0

           bne 1b

           mov     pc, lr

@ putc corrupts {r0, r1, r2, r3}

putc:

           mov     r2, r0

           mov     r0, #0

           loadsp  r3, r1

           b    2b

 

@ memdump corrupts {r0, r1, r2, r3, r10, r11, r12, lr}

memdump:    mov     r12, r0

           mov     r10, lr

           mov     r11, #0

2:        mov     r0, r11, lsl #2

           add r0, r0, r12

           mov     r1, #8

           bl   phex

           mov     r0, #':'

           bl   putc

1:        mov     r0, #' '

           bl   putc

           ldr  r0, [r12, r11, lsl #2]

           mov     r1, #8

           bl   phex

           and r0, r11, #7

           teq r0, #3

           moveq  r0, #' '

           bleq     putc

           and r0, r11, #7

           add r11, r11, #1

           teq r0, #7

           bne 1b

           mov     r0, #'\n'

           bl   putc

           cmp     r11, #64

           blt  2b

           mov     pc, r10

#endif

嘿嘿,還有memdump 這個函數可以用,不錯。

好了,言歸正傳,再往下看,代碼如下:

           .macro debug_reloc_start

#ifdef DEBUG

           kputc   #'\n'

           kphex   r6, 8         

           kputc   #':'

           kphex   r7, 8         

#ifdef CONFIG_CPU_CP15

           kputc   #':'

           mrc      p15, 0, r0, c1, c0

           kphex   r0, 8         

#endif

           kputc   #'\n'

           kphex   r5, 8         

           kputc   #'-'

           kphex   r9, 8         

           kputc   #'>'

           kphex   r4, 8         

           kputc   #'\n'

#endif

           .endm

 

           .macro debug_reloc_end

#ifdef DEBUG

           kphex   r5, 8         

           kputc   #'\n'

           mov     r0, r4

           bl   memdump      

#endif

           .endm

debug_reloc_start

用來打印出一些代碼重定位後的信息,關於重定位,後面會說,      debug_reloc_end

用來把解壓後的內核的256字節的數據dump出來,查看是否正確。很不幸的是,這個不是必須調用的,調試的時候,這些都是要自己把這些調試函數加上去的。好debug部分到這裏就完了。

2.   head.s.start部分,進入或保持在svc模式,並關中斷

繼續向下分析,下面是定義.start段,這段在鏈接時被鏈接到代碼的最開頭,那麼zImage啓動時,最先執行的代碼也就是下面這段代碼start開始的,如下:

           .section ".start", #alloc, #execinstr

 

           .align

           .arm                     @ Always enter in ARM state

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      )

 

           .word   0x016f2818           @ Magic numbers to help the loader

           .word   start                @ absolute load/run zImage address

           .word   _edata             @ zImage end address

 THUMB(         .thumb            )

1:        mov     r7, r1              @ save architecture ID

           mov     r8, r2              @ save atags pointer

 

#ifndef __ARM_ARCH_2__

          

           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

not_angel:

           mrs      r2, cpsr      @ turn off interrupts to

           orr r2, r2, #0xc0         @ prevent angel from running

           msr      cpsr_c, r2

#else

           teqp     pc, #0x0c000003         @ turn off interrupts

#endif

爲何這個會先執行呢?問的好。那麼來個中斷吧:這個是由arch/arm/boot/compressed/vmlinux.lds的鏈接腳本決定的,如下:

  .text : {

    _start = .;

    *(.start)

    *(.text)

    *(.text.*)

    *(.fixup)

    *(.gnu.warning)

    *(.rodata)

    *(.rodata.*)

    *(.glue_7)

    *(.glue_7t)

    *(.piggydata)

    . = ALIGN(4);

  }

怎麼樣,看到沒,.text段最開始的一部分就是.start段,所以這就註定了它就是最先執行的代碼。

好了,中斷結束,再回到先前面的代碼,這段代碼的最開始是會被編譯器編譯成8nop 這個是爲了留給ARM的中斷向量表的,但是整個head.s都沒有用到中斷啊,誰知道告訴我一下,謝了。

然後呢,把u-boot 傳過來的放在r1,r2的值,存在r7,r8中,r1存是的evb板的ID號,而r2存的是內核要用的參數地址,這兩個參數在解壓內核的時候不要用到,所以暫時保存一下,解壓內枋完了,再傳給linux內核。

再然後是幾個宏定義的解釋,ARM()BSYM()THUMB(),再加上 W()吧,這幾個個宏定義都是在arch/arm/include/asm/unified.h裏面定義的,好了,這裏也算箇中斷吧,如下:

#ifdef CONFIG_THUMB2_KERNEL

......

#define ARM(x...)

#define THUMB(x...)   x

#ifdef __ASSEMBLY__

#define W(instr)    instr.w

#endif

#define BSYM(sym)     sym + 1

#else  

......

#define ARM(x...)  x

#define THUMB(x...)

#ifdef __ASSEMBLY__

#define W(instr)    instr

#endif

#define BSYM(sym)     sym

#endif

好的看到上面的定義你就會明白了,這裏是爲了兼容THUMB2指令的內核。

關於#define ARM(x...) 裏面的“...”,沒有見過吧,這個是C語言的C99的新標準,變參宏,就是在x裏,你可以隨便你輸入多少個參數。別急還沒有完,因爲沒有看見文件裏有什麼方包含這個頭文件。是的文件中確實沒有包含,它的定義是在:arch/arm/makefile中加上的:

KBUILD_AFLAGS    += -include asm/unified.h

行,這些宏解釋到此,下面再出現,我就無視它了。

好了,再回來,讀取cpsr並判斷是否處理器處於supervisor模式——從u-boot進入kernel,系統已經處於SVC32模式;而利用angel進入則處於user模式,還需要額外兩條指令。之後是再次確認中斷關閉,並完成cpsr寫入。

注:AngelARM公司的一種調試方法,它本身就是一個調試監控程序,是一組運行在目標機上的程序,可以接收主機上調試器發送的命令,執行諸如設置斷點、單步執行目標程序、觀察或修改寄存器、存儲器內容之類的操作。與基於jtag的調試代理不同,Angel調試監控程序需要佔用一定的系統資源,如內存、串行端口等。使用angel調試監控程序可以調試在目標系統運行的arm程序或thumb程序。

好了,裏面有一句:teqp  pc, #0x0c000003         @ turn off interrupts

是否很奇怪,不過大家千萬不要糾結它,因爲它是ARMv2架構以前的彙編方法,用於模式變換,和中斷關閉的,看不明白也沒關係,因爲我們以後也用不到。這裏知道一下有這個事就行了。

      行,到這裏.start段就完了,代碼那麼多,其實就是做一件事,保證運行下面的代碼時已經進入了SVC模式,並保證中斷是關的,完了.start部分結束。

3.   text段開始,先是內核解壓地址的確定

      再往下看,代碼如下:

           .text

#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

~~~~ 不要小這一段代碼,東西好多啊。如哪入手呢?好吧,先從linux基本參數入手吧,見表.1,裏面我寫的很詳細,因爲表格我要放一頁,解釋我就寫在上面了。TEXT_OFFSET是代碼相對於物理內存的偏移,通常選爲32k=0x8000。這個是有原因的,具體的原因後面會說。先看CONFIG_AUTO_ZRELADDR這個宏所含的內容,它的意思是如果你不知道ZRELADDR地址要定在內存什麼地方,那麼這段代碼就可以幫你。看到0xf8000000了吧,那麼後面有多少個0呢?答案是27個,那麼227次方就是128M,這就明白了,只要你把解壓程序放在你最後解壓完成後的內核空間的128M之內的偏移的話,就可以自動設定好解壓後內核要運行的地址ZRELADDR

如果你沒有定義的話,那麼,就會去取zreladdr作爲最後解壓的內核運行地。那麼這個zreladdr是從哪裏來的呢?答案是在:arch/arm/boot/compressed/Makefile中定義的

# Supply ZRELADDR to the decompressor via a linker symbol.

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)

而裏面的幾個參數是在每個arch/arm/Mach-xxx/ Makefile.boot裏面定義的,內容如下:

   zreladdr-y    := 0x20008000

params_phys-y := 0x20000100

initrd_phys-y    := 0x21000000

這下知道了,繞了一大圈,終於知道r4存的是什麼了,就是最後內核解壓的起址,也是最後解壓後的內核的運行地址,記住,這個地址很重要。

 

解壓內核參數

解壓時symbol  

解釋

ZTEXTADDR

千成不要看成ZTE啊,呵,這裏是zImage的運行的起始地址,當內核從nor flash中運行的時候很重要,如果在ram中運行,這個設爲0

ZBSSADDR

這個地址也是一樣的,這個是BSS的地址,如果在nor中運行解壓的話,這個地址很重要。這個要放在RAM

ZRELADDR

這個地址很重要,這個是解壓後內核存放的地址,也是最後解壓後內核的運行起址。

一般設爲內存起址的32K之後,如ARM: 0x20008000

ZRELADDR = PHYS_OFFSET + TEXT_OFFSET

INITRD_PHYS

RAM disk的物理地址

INITRD_VIRT

RAM disk的虛擬地址

__virt_to_phys(INITRD_VIRT) = INITRD_PHYS

PARAMS_PHYS

內核參數的物理地址

內核參數

PHYS_OFFSET

實際RAM的物理地址

對於當前ARM來說,就是0x20000000

PAGE_OFFSET

內核空間的如始虛擬地址,通常: 0xC0000000,高端1G

__virt_to_phys(PAGE_OFFSET) = PHYS_OFFSET

TASK_SIZE

用戶進程的內存的最太值(以字節爲單位)

TEXTADDR

內核啓運行的虛擬地址的起址,通常設爲0xC0008000

TEXTADDR = PAGE_OFFSET + TEXT_OFFSET

__virt_to_phys(TEXTADDR) = ZRELADDR

TEXT_OFFSET

相對於內存起址的內核代碼存放的偏移,通常設爲 32k (0x8000)

DATAADDR

這個是內核數據段的虛擬地址的起址,當用zImage的時候不要定義。

.1 內核參數解釋

4.   打開ARM系統的cache,爲加快內核解壓做好準備

      可以看到,打開cache的就一個函數,如下:

bl   cache_on

看起來很少,其實展開後內容還是很多的。我們來看看這個cache_on在哪裏,可以找到代碼如下:

           .align   5

cache_on: mov    r3, #8             @ cache_on function

           b    call_cache_fn

這裏設計的很精妙的,只可意會,注意mov  r3, #8,不多解釋,跟進去call_cache_fn

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

首先看一下proc_types是什麼,定義如下:

proc_types:

        ......

 .word   0x000f0000           @ new CPU Id

           .word   0x000f0000

           W(b)    __armv7_mmu_cache_on

           W(b)    __armv7_mmu_cache_off

           W(b)    __armv7_mmu_cache_flush

       .......

            .word   0               @ unrecognised type

           .word   0

           mov     pc, lr

 THUMB(         nop                       )

           mov     pc, lr

 THUMB(         nop                       )

           mov     pc, lr

 THUMB(         nop                       )

可以看到這是一個以proc_types爲起始地址的表,上面我列出了第一個表項,和最後一個表項,如果查表不成功,則走最後一個表項返回。它實現的功能就是存兩個數據,三條跳轉指令,我們可以第一條是它的值,第二條是它的mask值,三條跳轉分別是:cache_on,cache_off,cache_flush

 

我想從ARMv4指令向下都是有CP15協處理器的吧,故:CONFIG_CPU_CP15是定義的,那下面我們來分析指令吧。

mrc      p15, 0, r9, c0, c0   @ get processor ID

這個意思是取得ARM處理器的ID,這個又要看《ARM Architecture Reference Manual》了,這裏我找了arm1176jzfs的架構手冊,也是我用的ARM所用的架構。裏面的解釋如下:

 

 

這裏我們主要關心 Architecture這項,我們的ARM這個值是: 0x410FB767,說明用的是r0p7release

好了讀取了這個值存入r9寄存器,然後使用算法(real ^ match) & mask,程序中:

( r9 ^r1)&r2這裏r1 存是是表中的第一個CPUID值,r2mask值,對於我們的ARM,結果如下:

0x410FB767 ^ 0x000f0000 = 0x4100B767

0x4100B767 & 0x000f0000 = 0

match上了,這個時候就會如下:

ARM(         addeq   pc, r12, r3       ) @ call cache function

我們知道r3的值是0x8,那麼r12表項的基址加上0x8就正好是表中的第一條跳轉指令:

W(b)    __armv7_mmu_cache_on

明白了,爲何r3要等於0x8了吧,如果要調用cache_off,那麼只要把r3設爲0xC就可以了。精妙吧。行接着往下看__armv7_mmu_cache_on,如下:

__armv7_mmu_cache_on:

           mov     r12, lr

#ifdef CONFIG_MMU

           mrc      p15, 0, r11, c0, c1, 4   @ read ID_MMFR0

           tst  r11, #0xf         @ VMSA  見注:

           blne    __setup_mmu

注:VMSA (Virtual Memory System Architecture),其實就是虛擬內存,通俗地地說就是否支持MMU

首先是保存lr寄存器到r12中,因爲我們馬上就要調用__setup_mmu了,最後返回也只要用r12就可以了。然後再查看cp15c7,c10,4看是否支持VMSA,具體的見註解。我們在這裏我們的ARM肯定是支持的,所以就要建立頁表,準備打開MMU,從而可以使能cache

好了下面,就是跳到__setup_mmu進行建產頁表的過程,代碼如下:

__setup_mmu:    sub r3, r4, #16384       @ Page directory size

           bic  r3, r3, #0xff          @ Align the pointer

           bic  r3, r3, #0x3f00

 

           mov     r0, r3

           mov     r9, r0, lsr #18

           mov     r9, r9, lsl #18        @ start of RAM

           add r10, r9, #0x10000000  @ a reasonable RAM size

           mov     r1, #0x12

           orr r1, r1, #3 << 10

           add r2, r3, #16384

1:        cmp     r1, r9              @ if virt > start of RAM

#ifdef CONFIG_CPU_DCACHE_WRITETHROUGH

           orrhs    r1, r1, #0x08         @ set cacheable

#else

           orrhs    r1, r1, #0x0c         @ set cacheable, bufferable

#endif

           cmp     r1, r10             @ if virt > end of RAM

           bichs    r1, r1, #0x0c         @ clear cacheable, bufferable

           str  r1, [r0], #4      @ 1:1 mapping

           add r1, r1, #1048576

           teq r0, r2

           bne 1b

關於MMU的知識又有好多啊,同樣可以參看《ARM Architecture Reference Manual》,還可以看《ARM體系架構與編程》關於MMU的部分,我這裏只簡單介紹一下我們這裏用到MMU。這裏只使用到了MMU的段映,故我只介紹與此相關的部分。

對於段頁的大小ARM中爲1M大小,對於32位的ARM,可尋址空間爲4G=4096M,故每一個頁表項表示1M空間的話,需要4096個頁表項,也就是4K大小,而每一個頁表項的大小是4字節,這就是說我們進行段映射的話,需要16K的大小存儲段頁表。

下面來看一下段頁表的格式,如下:

 

.1 段頁表項的具體內容

可以知道對於進行mmu段映射這種方式,一共有4K個這樣的頁表項,點大小16K字節。在這裏我們的16k頁表放哪呢?看程序第一句:

__setup_mmu:    sub r3, r4, #16384       @ Page directory size

我們知道r4存內核解壓後的基址,那麼這句就是把頁表放在解壓後的內核地址的前面16K空間如下圖所示:

 

.2 linux內核地址空間

(裏面地址是用的是以我用的ARM爲例的)

好了,再回到MMU,從MMU_PAGE_BASE (0x20004000)建立好頁表後,ARMcpu如何知道呢?這個就是要用到CP15C2寄存器了,頁表基址就是存在這裏面的,其中[31:14]爲內存中頁表的基址,[13:0]應爲0如下圖:

 

.3 CP15C2寄存器中的頁表項基址格式

所以我們初始化完段頁表後,就要把頁表基址MMU_PAGE_BASE (0x20004000)存入CP15C2寄存器,這樣ARM就知道到哪裏去找那些頁表項了。下面我們來看一下整個MMU的虛擬地址的尋址過程,如圖4所示。

簡單解釋一下。首先,ARMCPUCP15C2寄存器中找取出頁表基地址,然後把虛擬地址的最高12位左移兩位變爲14位放到頁表基址的低14位,組合成對應1M空間的頁表項在MMU頁表中的地址。然後,再取出頁表項的值,檢查AP位,域,判斷是否有讀寫的權限,如果沒有權限測會拋出數據或指令異常,如果有權限,就把最高12位取出加上虛擬地址的低20位段內偏移地址組合成最終的物理地址。到這裏整個MMU從虛擬地址到物理地址的轉換過程就完成了。

這段代碼裏,只會開啓頁表所在代碼的開始的256K對齊的一個0x10000000256M)空間的大小(這個空間必然包含解壓後的內核),使能cachewrite buffer,其他的4G-256M的空間不開啓。這裏使用的是11的映射。到這裏也很容易明白MMUcachewrite buffer的關係了,爲什麼不開MMU無法使用cache了。

 

.4 MMU的段頁表的虛擬地址與物理地址的轉換過程

這裏的4G空間全部映射完成之後,還會做一個映射,代碼如下:

 

           mov     r1, #0x1e

           orr r1, r1, #3 << 10

           mov     r2, pc

           mov     r2, r2, lsr #20

           orr r1, r1, r2, lsl #20

           add r0, r3, r2, lsl #2

           str  r1, [r0], #4

           add r1, r1, #1048576

           str  r1, [r0]

           mov     pc, lr

通過註釋就可以知道把當前PC所在地址1M對齊的地方的2M空間開啓cachewrite buffer 爲了加快代碼在 nor flash中運行的速度。然後反回,到這裏16KMMU頁表就完全建立好了。

然後再反回到建立頁表後的代碼,如下:

      mov     r0, #0

           mcr      p15, 0, r0, c7, c10, 4   @ drain write buffer

           tst  r11, #0xf         @ VMSA

           mcrne  p15, 0, r0, c8, c7, 0     @ flush I,D TLBs

#endif

           mrc      p15, 0, r0, c1, c0, 0     @ read control reg

           bic  r0, r0, #1 << 28    @ clear SCTLR.TRE

           orr r0, r0, #0x5000           @ I-cache enable, RR cache replacement

           orr r0, r0, #0x003c           @ write buffer

#ifdef CONFIG_MMU

#ifdef CONFIG_CPU_ENDIAN_BE8

           orr r0, r0, #1 << 25    @ big-endian page tables

#endif

           orrne    r0, r0, #1        @ MMU enabled

           movne  r1, #-1

           mcrne  p15, 0, r3, c2, c0, 0     @ load page table pointer

           mcrne  p15, 0, r1, c3, c0, 0     @ load domain access control

#endif

           mcr      p15, 0, r0, c1, c0, 0     @ load control register

           mrc      p15, 0, r0, c1, c0, 0     @ and read it back

           mov     r0, #0

           mcr      p15, 0, r0, c7, c5, 4     @ ISB

           mov     pc, r12

這段代碼就不具體解釋了,多數是關於CP15的控制寄存器的操作,主要是flush I-cache,D-cache, TLBSwrite buffer 然後存頁表基址啊,最後打開MMU這個是最後一步,前面所有東西都設好之後再使用MMU,否則系統就會掛掉。最後用保存在r12中的地址,反回到 BL cache_on的下一句代碼。如下:

restart:     adr r0, LC0

           ldmia   r0, {r1, r2, r3, r6, r10, r11, r12}

           ldr  sp, [r0, #28]

           

           sub r0, r0, r1         @ calculate the delta offset

           add r6, r6, r0         @ _edata

           add r10, r10, r0           @ inflated kernel size location

好了,先來看一下LC0是什麼東西吧。

           .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

           .size     LC0, . - LC0

好吧,要理解它,再把 arch/arm/boot/vmlinux.lds.in搬出來吧:

  _got_start = .;

  .got              : { *(.got) }

  _got_end = .;

  .got.plt         : { *(.got.plt) }

  _edata = .;

 

  . = BSS_START;

  __bss_start = .;

  .bss              : { *(.bss) }

  _end = .;

 

  . = ALIGN(8);          

  .stack           : { *(.stack) }

           .align

           .section ".stack", "aw", %nobits

再加上最後一段代碼,關於stack的空間的大小分配:

.L_user_stack:  .space  4096

.L_user_stack_end:

這裏不僅可以看到各個寄存器裏所存的值的意思,還可以看到. = BSS_START;在這裏的作用

arch/arm/boot/compressed/Makefile裏面:

ifeq ($(CONFIG_ZBOOT_ROM),y)

ZTEXTADDR     := $(CONFIG_ZBOOT_ROM_TEXT)

ZBSSADDR := $(CONFIG_ZBOOT_ROM_BSS)

else

ZTEXTADDR     := 0

ZBSSADDR := ALIGN(8)

endif

SEDFLAGS = s/TEXT_START/$(ZTEXTADDR)/;s/BSS_START/$(ZBSSADDR)/

對應到這裏的話,就是BSS_START = ALIGN(8)這個替換過程會在vmlinux.lds.in vmlinux.lds的過程中完成,這個過程主要是爲了有些內核在nor flash中運行而設置的。

好了,再次言歸正傳,從vmlinux.lds文件,可以看到鏈接後各個段的位置,如下。

 

.5 zImage各個段的位置

從這裏可以看到,zImageRAM中運行和在NorFlash中直接運行是有些區別的,這就是爲何前面要區分ZTEXTADDR ZBSSADDR 的原因了。

好了,再看下面這兩句的區別,如果這個地方弄明白了,那麼,下面的內容就會變得很簡單,往下看:

 

restart:     adr r0, LC0

      add  r0,pc,#0x10C  

LC0:    .word   LC0                 @ r1

      dcd  0x17C

故可知,zImage加到0x20008000運行時,PC值爲:0x20008070,這個時候r0=0x2000817C

而通過ldmia      r0, {r1, r2, r3, r6, r10, r11, r12}加載內存值後,r1=0x17C

那麼我們看一看這句:sub r0, r0, r1         @ calculate the delta offset的值是多少?如下:

r0= 0x2000817C  0x17C = 0x20008000

see~~~ 看出來什麼沒有,這個就是我們的加載zImage運行的內存起始地址,這個很重要,後面就要靠它知道我們當前的代碼在哪裏,搬移到哪裏。然後再下一條指令把堆棧指針設置好。然後再把實際代碼偏移量加在r6=_edata(r10=input_data_end-4)上面,這就是實際的內存中的地址。好繼續往下看:

          

           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

壓縮的工具會把所壓縮後的文件的最後加上用小端格式表示的4個字節的尾,用來存儲所壓內容的原始大小,這個信息很要,是我們後面分配空間,代碼重定位的重要依據。這裏爲何要一個字節,一個字節地取,只因爲要兼容ARM代碼使用大端編譯的情況,保證讀取的正確無誤。好了,再往下:

#ifndef CONFIG_ZBOOT_ROM

          

           add sp, sp, r0

           add r10, sp, #0x10000

#else

          

           mov     r10, r6

#endif

我們這裏在RAM中運行,所以加上重定位SP的指針,加上偏移裏,變成實際所在內存的堆棧指針地址。這裏主要是爲了後面的檢查代碼是否要進行重定位的時候所提前設置的,因爲如果代碼不重定位,就不會再設堆棧指針了,重定位的話,則還要重設一次。然後再在堆棧指針的上面開闢一塊64K大小的空間,用於解壓內核時的臨時buffer

再往下看:

 

           add r10, r10, #16384  //16K MMU頁表也不能被覆蓋哦,否則解壓到覆蓋後,ARM就掛了。

           cmp     r4, r10

           bhs wont_overwrite

           add r10, r4, r9

   ARM(           cmp     r10, pc       )

 THUMB(         mov     lr, pc          )

 THUMB(         cmp     r10, lr        )

 

           bls    wont_overwrite

這段的檢測有點繞人,兩種情況都畫個圖看一下,如圖.6所示,下面我們來看分析兩種不會覆蓋的情況:

第一種情況是加載運行的zImage在下,解壓後內核運行地址zreladdr在上,這種情況如果最上面的64k的解壓buffer不會覆蓋到內核前的16k頁表的話,就不用重定位代碼跳到wont_overwrite執行。

第二種情況是加載運行的zImage在上,而解壓的內核運行地址zreladdr在下面,只要最後解壓後的內核的大小加上zreladdr不會到當前pc值,則也不會出現代碼覆蓋的情況,這種情況下,也不用重位代碼,直接跳到wont_overwrite執行就可以了。 

 

.6內核的兩種解壓不要重定位的情況

一般加載的zImage的地址,和最後解壓的zreladdr的地址是相同的,那麼,就必然會發生代碼覆蓋的問題,這時候就要進行代碼的自搬移和重定位。具體實現如下:

           

           add r10, r10, #((reloc_code_end - restart + 256) & ~255)

           bic  r10, r10, #255

 

          

           adr r5, restart

           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

這段代碼就是實現代碼的自搬移,最開始兩句是取得所要搬移代碼的大小,進行了256字節的對齊,註釋上說了,爲了避免偏移很小時產生自我覆蓋(這個地方暫沒有想明白,不過不影響下面分析)。這裏還是再畫個圖表示一下整個搬移過程吧,以zImage 加載地下和zreladdr 都爲0x20008000爲例,其他的類似。

 

.7 zImage的代碼自搬移和內核解壓的全程圖解

.7中我已經標好了序號,代碼的自搬移和內核解的整個過程都在這裏面下面一步步來分解:

①.首先計算要搬移的代碼的.text段代碼的大小,從restart開始,到reloc_code_end結束,這個就是剩下的.text段的內容,這段內容是接在打開cache的函數之後的。然後把這段代碼搬到覈實際解壓後256字節對齊的邊界,然後進行搬移,搬移時一次搬運32個字節,故存有搬移大小的r9寄存器進行了一下32字節對齊的擴展。

②.搬移完成後,會保存一下新舊代碼間的offset值,存於r6中。再重新設置一下新的堆棧的地址,位置如圖所示,代碼如下:

             

              sub  r6, r9, r6

#ifndef CONFIG_ZBOOT_ROM

             

              add  sp, sp, r6

#endif

③.然後進行cacheflush,因爲馬上要進行代碼的跳轉了,接着就計算新的restart在哪裏,接着跳過去執行新的重定位後的代碼。

              bl    cache_clean_flush

 

              adr   r0, BSYM(restart)

              add  r0, r0, r6

              mov pc, r0

這個時候就又會到restart處執行,會把前面的代碼再執行一次,不過這次在執行時,會進入圖.6所示的代碼不用重定位的情況,意料之後的事,接着跳到wont_overwirte執行,如下:

 

              teq   r0, #0

              beq  not_relocated

這兩行代碼的意思是,看一下只什麼時候跳過來的,如果r0的值爲0,說明沒有進行代碼的重定位,那這個時候跳到no_relocated處執行,這段就會跳過.got符號表的搬移,因爲位置沒有變啊。代碼寫得好嚴謹啊,佩服。

④.我們這種經過代碼重定位的情況下,r0的值一定不會零,那麼這個時候就要進行.got表的重搬移,如圖中所示,代碼如下:

1:        ldr  r1, [r11, #0]          @ relocate entries in the GOT

           add r1, r1, r0         @ table.  This fixes up the

           str  r1, [r11], #4          @ C references.

           cmp     r11, r12

           blo 1b

⑤.下面就來初始化我們一直沒有進行初始化的.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

這裏看到我們可愛的not_relocated 標號了吧,這個標號就是前面所見到的如果沒有進行重定位,就直接跳過來進行bss的初始化。

⑥.設置好64K的解壓緩衝區在堆棧之後,代碼如下:

              mov r0, r4

              mov r1, sp                    @ malloc space above stack

              add  r2, sp, #0x10000    @ 64k max

              mov r3, r7

⑦.進行內核的解壓過程

           bl   decompress_kernel

arch/arm/boot/compressed/misc.c

void decompress_kernel(unsigned long output_start, unsigned long free_mem_ptr_p,

           unsigned long free_mem_ptr_end_p, int arch_id)

這個函數是C下面的函數,那些堆棧的設置啊,.got表啊,64k的解壓緩衝啊,都是爲它準備的。第一個參數是內核解壓後所存放的地址,第二,第三參數是64k解壓緩衝起始地址和結束地址,最後一個參數ID號,這個由u-boot 傳入。

⑧.這是最後一步了,終於到最後一步了。代碼如下:

              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

這裏先進行cacheflush,然後關掉cache,再準備好linux內核要啓動的幾個參數,最後跳到zreladdr處,進入解壓後的內核,到這裏壓縮內核的使命就完成了。但是它的功勞可不小啊。下面就是真真正正的linux內核的啓動過程了,這裏會進入到 arch/arm/kernel/head.s這個文件的stext這個地址開始執行第一行代碼。

發佈了76 篇原創文章 · 獲贊 139 · 訪問量 123萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章