色婷婷AⅤ一区二区三区|亚洲精品第一国产综合亚AV|久久精品官方网视频|日本28视频香蕉

          新聞中心

          EEPW首頁 > 嵌入式系統(tǒng) > 設(shè)計應(yīng)用 > ARM linux解析之壓縮內(nèi)核zImage的啟動過程

          ARM linux解析之壓縮內(nèi)核zImage的啟動過程

          作者: 時間:2016-11-10 來源:網(wǎng)絡(luò) 收藏
          首先,我們要知道在zImage的生成過程中,是把a(bǔ)rch/arm/boot/compressed/head.s和解壓代碼misc.c,decompress.c加在壓縮內(nèi)核的最前面最終生成zImage的,那么它的啟動過程就是從這個head.s開始的,并且如果代碼從RAM運(yùn)行的話,是與位置無關(guān)的,可以加載到內(nèi)存的任何地方。

          下面以arch/arm/boot/compressed/head.s為主線進(jìn)行啟動過程解析。

          本文引用地址:http://cafeforensic.com/article/201611/317570.htm

          1.head.s的debug宏定義部分

          最開始的一段都是head.s的debug宏定義部分,這部分可以方便我們調(diào)試時使用。

          如下:

          #ifdefDEBUG

          #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

          mcrp14, 0, ch, c0, c5, 0

          .endm

          #elif defined(CONFIG_CPU_XSCALE)

          .macro loadsp, rb, tmp

          .endm

          .macro writeb, ch, rb

          mcrp14, 0, ch, c8, c0, 0

          .endm

          #else

          .macro loadsp, rb, tmp

          .endm

          .macro writeb, ch, rb

          mcrp14, 0, ch, c1, c0, 0

          .endm

          #endif

          #else

          #include

          .macro writeb, ch, rb

          senduart ch, rb

          .endm

          #if defined(CONFIG_ARCH_SA1100)

          .macro loadsp, rb, tmp

          movrb, #0x80@ physical base address

          #ifdef CONFIG_DEBUG_LL_SER3

          add rb, rb, #0x50 @ Ser3

          #else

          add rb, rb, #0x10 @ Ser1

          #endif

          .endm

          #elif defined(CONFIG_ARCH_S3C2410)

          .macro loadsp, rb, tmp

          movrb, #0x50

          add rb, rb, #0x4 * CONFIG_S3C_LOWLEVEL_UART_PORT

          .endm

          #else

          .macro loadsp, rb, tmp

          addruart rb, tmp

          .endm

          #endif

          #endif

          #endif

          如果開啟DEBUGging宏的話,這部分代碼分兩段CONFIG_DEBUG_ICEDCC是用ARMv6以上的加構(gòu)支持的ICEDCC技術(shù)進(jìn)行調(diào)試,DCC(Debug Communications Channel)是ARM的一個調(diào)試通信通道,在串口無法使用的時候可以使用這個通道進(jìn)行數(shù)據(jù)的通信,具體的技術(shù)參前ARM公司文檔《ARM Architecture Reference Manual》。

          第二部分首先#include ,這個文件定義位于arch/arm/mach-xxxx/include/mach/debug-macro.S里面,所以這個是和平臺相關(guān)的,里面定義了每個平臺的相關(guān)的串口操作,因這個時候系統(tǒng)還沒有起來,所以它所用的串口配置參數(shù)是依賴于前一級bootloader所設(shè)置好的,如我們使用的u-boot設(shè)置好所有的參數(shù)。如我們的EVB板ARM的實現(xiàn)如下:

          #include

          #include

          .macroaddruart, 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

          .macrosenduart,rd,rx

          strbrd, [rx, #(0x00)]@ Write to Transmitter Holding Register

          .endm

          .macrowaituart,rd,rx

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

          tst rd, #0x20@when TX FIFOFull, then wait

          bne 1001b

          .endm

          .macrobusyuart,rd,rx

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

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

          bne 1001b

          .endm

          主要實現(xiàn)addruart,senduart,waituart,busyuart這四個函數(shù)的具體實施。這個是調(diào)試函數(shù)打印的基礎(chǔ)。

          下面是調(diào)試打印用到的kputc和kphex

          .macrokputc,val

          movr0, val

          blputc

          .endm

          .macrokphex,val,len

          movr0, val

          movr1, #len

          blphex

          .endm

          它所調(diào)用的putc和phex是在head.s最后的一段定義的,如下

          #ifdef DEBUG

          .align2

          .typephexbuf,#object

          phexbuf:.space12

          .sizephexbuf, . - phexbuf

          上面是分配打印hex的buffer,下面是具體的實現(xiàn):

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

          phex:adr r3, phexbuf

          movr2, #0

          strbr2, [r3, r1]

          1:subsr1, r1, #1

          movmi r0, r3

          bmiputs

          and r2, r0, #15

          movr0, r0, lsr #4

          cmpr2, #10

          addger2, r2, #7

          add r2, r2, #0

          strbr2, [r3, r1]

          b1b

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

          puts:loadspr3, r1

          1:ldrbr2, [r0], #1

          teq r2, #0

          moveqpc, lr

          2:writebr2, r3

          movr1, #0x20

          3:subsr1, r1, #1

          bne 3b

          teq r2, #n

          moveqr2, #r

          beq 2b

          teq r0, #0

          bne 1b

          movpc, lr

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

          putc:

          movr2, r0

          movr0, #0

          loadspr3, r1

          b2b

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

          memdump:movr12, r0

          movr10, lr

          movr11, #0

          2:movr0, r11, lsl #2

          add r0, r0, r12

          movr1, #8

          blphex

          movr0, #:

          blputc

          1:movr0, #

          blputc

          ldrr0, [r12, r11, lsl #2]

          movr1, #8

          blphex

          and r0, r11, #7

          teq r0, #3

          moveqr0, #

          bleqputc

          and r0, r11, #7

          add r11, r11, #1

          teq r0, #7

          bne 1b

          movr0, #n

          blputc

          cmpr11, #64

          blt2b

          movpc, r10

          #endif

          嘿嘿,還有memdump這個函數(shù)可以用,不錯。

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

          .macrodebug_reloc_start

          #ifdef DEBUG

          kputc#n

          kphexr6, 8

          kputc#:

          kphexr7, 8

          #ifdef CONFIG_CPU_CP15

          kputc#:

          mrcp15, 0, r0, c1, c0

          kphexr0, 8

          #endif

          kputc#n

          kphexr5, 8

          kputc#-

          kphexr9, 8

          kputc#>

          kphexr4, 8

          kputc#n

          #endif

          .endm

          .macrodebug_reloc_end

          #ifdef DEBUG

          kphexr5, 8

          kputc#n

          movr0, r4

          blmemdump

          #endif

          .endm

          debug_reloc_start

          用來打印出一些代碼重定位后的信息,關(guān)于重定位,后面會說,debug_reloc_end

          用來把解壓后的內(nèi)核的256字節(jié)的數(shù)據(jù)dump出來,查看是否正確。很不幸的是,這個不是必須調(diào)用的,調(diào)試的時候,這些都是要自己把這些調(diào)試函數(shù)加上去的。好debug部分到這里就完了。

          2.head.s的.start部分,進(jìn)入或保持在svc模式,并關(guān)中斷

          繼續(xù)向下分析,下面是定義.start段,這段在鏈接時被鏈接到代碼的最開頭,那么zImage啟動時,最先執(zhí)行的代碼也就是下面這段代碼start開始的,如下:

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

          .align

          .arm@ Always enter in ARM state

          start:

          .typestart,#function

          .rept7

          movr0, r0

          .endr

          ARM(movr0, r0)

          ARM(b1f)

          THUMB(adr r12, BSYM(1f))

          THUMB(bxr12)

          .word0x016f2818@ Magic numbers to help the loader

          .wordstart@ absolute load/run zImage address

          .word_edata@ zImage end address

          THUMB(.thumb)

          1:movr7, r1@ save architecture ID

          movr8, r2@ save atags pointer

          #ifndef __ARM_ARCH_2__

          mrsr2, cpsr@ get current mode

          tstr2, #3@ not user?

          bne not_angel

          movr0, #0x17@ angel_SWIreason_EnterSVC

          ARM(swi 0x123456)@ angel_SWI_ARM

          THUMB(svc 0xab)@ angel_SWI_THUMB

          not_angel:

          mrsr2, cpsr@ turn off interrupts to

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

          msrcpsr_c, r2

          #else

          teqppc, #0x0c003@ turn off interrupts

          #endif

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

          .text : {

          _start = .;

          *(.start)

          *(.text)

          *(.text.*)

          *(.fixup)

          *(.gnu.warning)

          *(.rodata)

          *(.rodata.*)

          *(.glue_7)

          *(.glue_7t)

          *(.piggydata)

          . = ALIGN(4);

          }

          怎么樣,看到?jīng)],.text段最開始的一部分就是.start段,所以這就注定了它就是最先執(zhí)行的代碼。

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

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

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

          #ifdefCONFIG_THUMB2_KERNEL

          ......

          #defineARM(x...)

          #defineTHUMB(x...)x

          #ifdef __ASSEMBLY__

          #defineW(instr)instr.w

          #endif

          #defineBSYM(sym)sym + 1

          #else

          ......

          #defineARM(x...)x

          #defineTHUMB(x...)

          #ifdef __ASSEMBLY__

          #defineW(instr)instr

          #endif

          #defineBSYM(sym)sym

          #endif

          好的看到上面的定義你就會明白了,這里是為了兼容THUMB2指令的內(nèi)核。

          關(guān)于#defineARM(x...)里面的“...”,沒有見過吧,這個是C語言的C99的新標(biāo)準(zhǔn),變參宏,就是在x里,你可以隨便你輸入多少個參數(shù)。別急還沒有完,因為沒有看見文件里有什么方包含這個頭文件。是的文件中確實沒有包含,它的定義是在:arch/arm/makefile中加上的:

          KBUILD_AFLAGS+= -include asm/unified.h

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

          好了,再回來,讀取cpsr并判斷是否處理器處于supervisor模式——從u-boot進(jìn)入kernel,系統(tǒng)已經(jīng)處于SVC32模式;而利用angel進(jìn)入則處于user模式,還需要額外兩條指令。之后是再次確認(rèn)中斷關(guān)閉,并完成cpsr寫入。

          注:Angel是ARM公司的一種調(diào)試方法,它本身就是一個調(diào)試監(jiān)控程序,是一組運(yùn)行在目標(biāo)機(jī)上的程序,可以接收主機(jī)上調(diào)試器發(fā)送的命令,執(zhí)行諸如設(shè)置斷點(diǎn)、單步執(zhí)行目標(biāo)程序、觀察或修改寄存器、存儲器內(nèi)容之類的操作。與基于jtag的調(diào)試代理不同,Angel調(diào)試監(jiān)控程序需要占用一定的系統(tǒng)資源,如內(nèi)存、串行端口等。使用angel調(diào)試監(jiān)控程序可以調(diào)試在目標(biāo)系統(tǒng)運(yùn)行的arm程序或thumb程序。

          好了,里面有一句:teqppc, #0x0c003@ turn off interrupts

          是否很奇怪,不過大家千萬不要糾結(jié)它,因為它是ARMv2架構(gòu)以前的匯編方法,用于模式變換,和中斷關(guān)閉的,看不明白也沒關(guān)系,因為我們以后也用不到。這里知道一下有這個事就行了。

          行,到這里.start段就完了,代碼那么多,其實就是做一件事,保證運(yùn)行下面的代碼時已經(jīng)進(jìn)入了SVC模式,并保證中斷是關(guān)的,完了.start部分結(jié)束。

          3.。text段開始,先是內(nèi)核解壓地址的確定

          再往下看,代碼如下:

          .text

          #ifdefCONFIG_AUTO_ZRELADDR

          @ determine final kernel image address

          movr4, pc

          and r4, r4, #0xf8

          add r4, r4, #TEXT_OFFSET

          #else

          ldrr4, =zreladdr

          #endif

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

          如果你沒有定義的話,那么,就會去取zreladdr作為最后解壓的內(nèi)核運(yùn)行地。那么這個zreladdr是從哪里來的呢?答案是在:arch/arm/boot/compressed/Makefile中定義的

          # Supply ZRELADDR to the decompressor via a linker symbol.

          ifneq ($(CONFIG_AUTO_ZRELADDR),y)

          LDFLAGS_vmlinux += --defsymzreladdr=$(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)

          而里面的幾個參數(shù)是在每個arch/arm/Mach-xxx/ Makefile.boot里面定義的,內(nèi)容如下:

          zreladdr-y:= 0x28

          params_phys-y:= 0x20100

          initrd_phys-y:= 0x21

          這下知道了,繞了一大圈,終于知道r4存的是什么了,就是最后內(nèi)核解壓的起址,也是最后解壓后的內(nèi)核的運(yùn)行地址,記住,這個地址很重要。

          解壓內(nèi)核參數(shù)

          解壓時symbol

          解釋

          ZTEXTADDR

          千成不要看成ZTE啊,呵,這里是zImage的運(yùn)行的起始地址,當(dāng)內(nèi)核從nor flash中運(yùn)行的時候很重要,如果在ram中運(yùn)行,這個設(shè)為0

          ZBSSADDR

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

          ZRELADDR

          這個地址很重要,這個是解壓后內(nèi)核存放的地址,也是最后解壓后內(nèi)核的運(yùn)行起址。

          一般設(shè)為內(nèi)存起址的32K之后,如ARM: 0x28

          ZRELADDR = PHYS_OFFSET + TEXT_OFFSET

          INITRD_PHYS

          RAM disk的物理地址

          INITRD_VIRT

          RAM disk的虛擬地址

          __virt_to_phys(INITRD_VIRT) = INITRD_PHYS

          PARAMS_PHYS

          內(nèi)核參數(shù)的物理地址

          內(nèi)核參數(shù)

          PHYS_OFFSET

          實際RAM的物理地址

          對于當(dāng)前ARM來說,就是0x20

          PAGE_OFFSET

          內(nèi)核空間的如始虛擬地址,通常: 0xC0,高端1G

          __virt_to_phys(PAGE_OFFSET) = PHYS_OFFSET

          TASK_SIZE

          用戶進(jìn)程的內(nèi)存的最太值(以字節(jié)為單位)

          TEXTADDR

          內(nèi)核啟運(yùn)行的虛擬地址的起址,通常設(shè)為0xC8

          TEXTADDR = PAGE_OFFSET + TEXT_OFFSET

          __virt_to_phys(TEXTADDR) = ZRELADDR

          TEXT_OFFSET

          相對于內(nèi)存起址的內(nèi)核代碼存放的偏移,通常設(shè)為32k (0x8)

          DATAADDR

          這個是內(nèi)核數(shù)據(jù)段的虛擬地址的起址,當(dāng)用zImage的時候不要定義。

          表.1內(nèi)核參數(shù)解釋

          4.打開ARM系統(tǒng)的cache,為加快內(nèi)核解壓做好準(zhǔn)備

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

          blcache_on

          看起來很少,其實展開后內(nèi)容還是很多的。我們來看看這個cache_on在哪里,可以找到代碼如下:

          .align5

          cache_on:movr3, #8@ cache_on function

          bcall_cache_fn

          這里設(shè)計的很精妙的,只可意會,注意movr3, #8,不多解釋,跟進(jìn)去call_cache_fn:

          call_cache_fn:adr r12,proc_types

          #ifdefCONFIG_CPU_CP15

          mrcp15, 0, r9, c0, c0@ get processor ID

          #else

          ldrr9, =CONFIG_PROCESSOR_ID

          #endif

          1:ldrr1, [r12, #0]@ get value

          ldrr2, [r12, #4]@ get mask

          eor r1, r1, r9@ (real ^ match)

          tstr1, r2@& mask

          ARM(addeqpc, r12,r3) @ call cache function

          THUMB(addeqr12,r3)

          THUMB(moveqpc, r12) @ call cache function

          add r12, r12,#PROC_ENTRY_SIZE

          b1b

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

          proc_types:

          ......

          .word0xf0@ new CPU Id

          .word0xf0

          W(b)__armv7_mmu_cache_on

          W(b)__armv7_mmu_cache_off

          W(b)__armv7_mmu_cache_flush

          .......

          .word0@ unrecognised type

          .word0

          movpc, lr

          THUMB(nop)

          movpc, lr

          THUMB(nop)

          movpc, lr

          THUMB(nop)

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

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

          mrcp15, 0, r9, c0, c0@ get processor ID

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

          這里我們主要關(guān)心Architecture這項,我們的ARM這個值是: 0x410FB767,說明用的是r0p7的release。

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

          ( r9 ^r1)&r2,這里r1存是是表中的第一個CPU的ID值,r2是mask值,對于我們的ARM,結(jié)果如下:

          0x410FB767 ^ 0xf0 = 0x4100B767

          0x4100B767 & 0xf0 = 0

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

          ARM(addeqpc, r12,r3) @ call cache function

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

          W(b)__armv7_mmu_cache_on

          明白了,為何r3要等于0x8了吧,如果要調(diào)用cache_off,那么只要把r3設(shè)為0xC就可以了。精妙吧。行接著往下看__armv7_mmu_cache_on,如下:

          __armv7_mmu_cache_on:

          movr12, lr

          #ifdef CONFIG_MMU

          mrcp15, 0, r11, c0, c1, 4@ read ID_MMFR0

          tstr11, #0xf@VMSA見注:

          blne__setup_mmu

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

          首先是保存lr寄存器到r12中,因為我們馬上就要調(diào)用__setup_mmu了,最后返回也只要用r12就可以了。然后再查看cp15的c7,c10,4看是否支持VMSA,具體的見注解。我們在這里我們的ARM肯定是支持的,所以就要建立頁表,準(zhǔn)備打開MMU,從而可以使能cache。

          好了下面,就是跳到__setup_mmu進(jìn)行建產(chǎn)頁表的過程,代碼如下:

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

          bicr3, r3, #0xff@ Align the pointer

          bicr3, r3, #0x3f00

          movr0, r3

          movr9, r0, lsr #18

          movr9, r9, lsl #18@ start of RAM

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

          movr1, #0x12

          orr r1, r1, #3 << 10

          add r2, r3, #16384

          1:cmpr1, r9@ if virt > start of RAM

          #ifdef CONFIG_CPU_DCACHE_WRITETHROUGH

          orrhsr1, r1, #0x08@ set cacheable

          #else

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

          #endif

          cmpr1, r10@ if virt > end of RAM

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

          strr1, [r0], #4@ 1:1 mapping

          add r1, r1, #1048576

          teq r0, r2

          bne 1b

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

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

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

          圖.1段頁表項的具體內(nèi)容

          可以知道對于進(jìn)行mmu段映射這種方式,一共有4K個這樣的頁表項,點(diǎn)大小16K字節(jié)。在這里我們的16k頁表放哪呢?看程序第一句:

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

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

          圖.2 linux內(nèi)核地址空間

          (里面地址是用的是以我用的ARM為例的)

          好了,再回到MMU,從MMU_PAGE_BASE (0x24)建立好頁表后,ARM的cpu如何知道呢?這個就是要用到CP15的C2寄存器了,頁表基址就是存在這里面的,其中[31:14]為內(nèi)存中頁表的基址,[13:0]應(yīng)為0如下圖:

          圖.3 CP15的C2寄存器中的頁表項基址格式

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

          簡單解釋一下。首先,ARM的CPU從CP15的C2寄存器中找取出頁表基地址,然后把虛擬地址的最高12位左移兩位變?yōu)?4位放到頁表基址的低14位,組合成對應(yīng)1M空間的頁表項在MMU頁表中的地址。然后,再取出頁表項的值,檢查AP位,域,判斷是否有讀寫的權(quán)限,如果沒有權(quán)限測會拋出數(shù)據(jù)或指令異常,如果有權(quán)限,就把最高12位取出加上虛擬地址的低20位段內(nèi)偏移地址組合成最終的物理地址。到這里整個MMU從虛擬地址到物理地址的轉(zhuǎn)換過程就完成了。

          這段代碼里,只會開啟頁表所在代碼的開始的256K對齊的一個0x10(256M)空間的大小(這個空間必然包含解壓后的內(nèi)核),使能cache和write buffer,其他的4G-256M的空間不開啟。這里使用的是1:1的映射。到這里也很容易明白MMU和cache和write buffer的關(guān)系了,為什么不開MMU無法使用cache了。

          圖.4 MMU的段頁表的虛擬地址與物理地址的轉(zhuǎn)換過程

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

          movr1, #0x1e

          orr r1, r1, #3 << 10

          movr2, pc

          movr2, r2, lsr #20

          orr r1, r1, r2, lsl #20

          add r0, r3, r2, lsl #2

          strr1, [r0], #4

          add r1, r1, #1048576

          strr1, [r0]

          movpc, lr

          通過注釋就可以知道把當(dāng)前PC所在地址1M對齊的地方的2M空間開啟cache和write buffer為了加快代碼在nor flash中運(yùn)行的速度。然后反回,到這里16K的MMU頁表就完全建立好了。

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

          movr0, #0

          mcrp15, 0, r0, c7, c10, 4@ drain write buffer

          tstr11, #0xf@ VMSA

          mcrnep15, 0, r0, c8, c7, 0@ flush I,D TLBs

          #endif

          mrcp15, 0, r0, c1, c0, 0@ read control reg

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

          orr r0, r0, #0x5@ 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

          orrner0, r0, #1@ MMU enabled

          movner1, #-1

          mcrnep15, 0, r3, c2, c0, 0@ load page table pointer

          mcrnep15, 0, r1, c3, c0, 0@ load domain access control

          #endif

          mcrp15, 0, r0, c1, c0, 0@ load control register

          mrcp15, 0, r0, c1, c0, 0@ and read it back

          movr0, #0

          mcrp15, 0, r0, c7, c5, 4@ ISB

          movpc, r12

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

          restart:adr r0,LC0

          ldmiar0, {r1, r2, r3, r6, r10, r11, r12}

          ldrsp, [r0, #28]

          sub r0, r0, r1@ calculate the delta offset

          add r6, r6, r0@ _edata

          add r10, r10, r0@ inflated kernel size location

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

          .align2

          .typeLC0, #object

          LC0:.wordLC0@ r1

          .word__bss_start@ r2

          .word_end@ r3

          .word_edata@ r6

          .wordinput_data_end - 4 @ r10 (inflated size location)

          .word_got_start@ r11

          .word_got_end@ ip

          .word.L_user_stack_end@ sp

          .sizeLC0, . - LC0

          好吧,要理解它,再把a(bǔ)rch/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

          再加上最后一段代碼,關(guān)于stack的空間的大小分配:

          .L_user_stack:.space4096

          .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)/

          對應(yīng)到這里的話,就是BSS_START =ALIGN(8),這個替換過程會在vmlinux.lds.in到vmlinux.lds的過程中完成,這個過程主要是為了有些內(nèi)核在nor flash中運(yùn)行而設(shè)置的。

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

          圖.5 zImage各個段的位置

          從這里可以看到,zImage在RAM中運(yùn)行和在NorFlash中直接運(yùn)行是有些區(qū)別的,這就是為何前面要區(qū)分ZTEXTADDR和ZBSSADDR的原因了。

          好了,再看下面這兩句的區(qū)別,如果這個地方弄明白了,那么,下面的內(nèi)容就會變得很簡單,往下看:

          restart:adr r0,LC0

          addr0,pc,#0x10C

          LC0:.wordLC0@ r1

          dcd0x17C

          故可知,當(dāng)zImage加到0x28運(yùn)行時,PC值為:0x28070,這個時候r0=0x2817C

          而通過ldmiar0, {r1, r2, r3, r6, r10, r11, r12}加載內(nèi)存值后,r1=0x17C

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

          r0=0x2817C-0x17C =0x28

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

          ldrbr9, [r10, #0]

          ldrblr, [r10, #1]

          orr r9, r9, lr, lsl #8

          ldrblr, [r10, #2]

          ldrbr10, [r10, #3]

          orr r9, r9, lr, lsl #16

          orr r9, r9, r10, lsl #24

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

          #ifndef CONFIG_ZBOOT_ROM

          add sp, sp, r0

          add r10, sp, #0x10

          #else

          movr10, r6

          #endif

          我們這里在RAM中運(yùn)行,所以加上重定位SP的指針,加上偏移里,變成實際所在內(nèi)存的堆棧指針地址。這里主要是為了后面的檢查代碼是否要進(jìn)行重定位的時候所提前設(shè)置的,因為如果代碼不重定位,就不會再設(shè)堆棧指針了,重定位的話,則還要重設(shè)一次。然后再在堆棧指針的上面開辟一塊64K大小的空間,用于解壓內(nèi)核時的臨時buffer。

          再往下看:

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

          cmpr4, r10

          bhswont_overwrite

          add r10, r4, r9

          ARM(cmpr10, pc)

          THUMB(movlr, pc)

          THUMB(cmpr10, lr)

          blswont_overwrite

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

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

          第二種情況是加載運(yùn)行的zImage在上,而解壓的內(nèi)核運(yùn)行地址zreladdr在下面,只要最后解壓后的內(nèi)核的大小加上zreladdr不會到當(dāng)前pc值,則也不會出現(xiàn)代碼覆蓋的情況,這種情況下,也不用重位代碼,直接跳到wont_overwrite執(zhí)行就可以了。

          圖.6內(nèi)核的兩種解壓不要重定位的情況

          可以我們一般加載的zImage的地址,和最后解壓的zreladdr的地址是相同的,那么,就必然會發(fā)生代碼覆蓋的問題,這時候就要進(jìn)行代碼的自搬移和重定位。具體實現(xiàn)如下:

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

          bicr10, r10, #255

          adr r5, restart

          bicr5, r5, #31

          sub r9, r6, r5@ size to copy

          add r9, r9, #31@ rounded up to a multiple

          bicr9, r9, #31@ ... of 32 bytes

          add r6, r9, r5

          add r9, r9, r10

          1:ldmdbr6!, {r0 - r3, r10 - r12, lr}

          cmpr6, r5

          stmdbr9!, {r0 - r3, r10 - r12, lr}

          bhi 1b

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

          圖.7 zImage的代碼自搬移和內(nèi)核解壓的全程圖解

          圖.7中我已經(jīng)標(biāo)好了序號,代碼的自搬移和內(nèi)核解的整個過程都在這里面下面一步步來分解:

          ①.首先計算要搬移的代碼的.text段代碼的大小,從restart開始,到reloc_code_end結(jié)束,這個就是剩下的.text段的內(nèi)容,這段內(nèi)容是接在打開cache的函數(shù)之后的。然后把這段代碼搬到核實際解壓后256字節(jié)對齊的邊界,然后進(jìn)行搬移,搬移時一次搬運(yùn)32個字節(jié),故存有搬移大小的r9寄存器進(jìn)行了一下32字節(jié)對齊的擴(kuò)展。

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

          subr6, r9, r6

          #ifndef CONFIG_ZBOOT_ROM

          addsp, sp, r6

          #endif

          ③.然后進(jìn)行cache的flush,因為馬上要進(jìn)行代碼的跳轉(zhuǎn)了,接著就計算新的restart在哪里,接著跳過去執(zhí)行新的重定位后的代碼。

          blcache_clean_flush

          adrr0, BSYM(restart)

          addr0, r0, r6

          mov pc, r0

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

          teqr0, #0

          beqnot_relocated

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

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

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

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

          strr1, [r11], #4@ C references.

          cmpr11, r12

          blo 1b

          ⑤.下面就來初始化我們一直沒有進(jìn)行初始化的.bss段,其實就是清零,位置如圖所示。我雖畫了一個箭頭,但是其實并沒有進(jìn)行任何搬移動作,僅僅清零,代碼如下:

          not_relocated:movr0, #0

          1:strr0, [r2], #4@ clear bss

          strr0, [r2], #4

          strr0, [r2], #4

          strr0, [r2], #4

          cmpr2, r3

          blo 1b

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

          ⑥.設(shè)置好64K的解壓緩沖區(qū)在堆棧之后,代碼如下:

          mov r0, r4

          mov r1, sp@ malloc space above stack

          addr2, sp, #0x10@ 64k max

          mov r3, r7

          ⑦.進(jìn)行內(nèi)核的解壓過程

          bldecompress_kernel

          arch/arm/boot/compressed/misc.c

          voiddecompress_kernel(unsigned longoutput_start, unsigned longfree_mem_ptr_p,

          unsigned longfree_mem_ptr_end_p, intarch_id)

          這個函數(shù)是C下面的函數(shù),那些堆棧的設(shè)置啊,.got表啊,64k的解壓緩沖啊,都是為它準(zhǔn)備的。第一個參數(shù)是內(nèi)核解壓后所存放的地址,第二,第三參數(shù)是64k解壓緩沖起始地址和結(jié)束地址,最后一個參數(shù)ID號,這個由u-boot傳入。

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

          blcache_clean_flush

          blcache_off

          mov r0, #0@ must be zero

          mov r1, r7@ restore architecture number

          mov r2, r8@ restore atags pointer

          mov pc, r4@ call kernel

          這里先進(jìn)行cache的flush,然后關(guān)掉cache,再準(zhǔn)備好linux內(nèi)核要啟動的幾個參數(shù),最后跳到zreladdr處,進(jìn)入解壓后的內(nèi)核,到這里壓縮內(nèi)核的使命就完成了。但是它的功勞可不小啊。下面就是真真正正的linux內(nèi)核的啟動過程了,這里會進(jìn)入到arch/arm/kernel/head.s這個文件的stext這個地址開始執(zhí)行第一行代碼。



          評論


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

          關(guān)閉