ARM linux解析之壓縮內核zImage的啓動過程
首先,我們要知道在zImage的生成過程中,是把arch/arm/boot/compressed/head.s 和解壓代碼misc.c,decompress.c加在壓縮內核的最前面最終生成zImage的,那麼它的啓動過程就是從這個head.s開始的,並且如果代碼從RAM運行的話,是與位置無關的,可以加載到內存的任何地方。
下面以arch/arm/boot/compressed/head.s爲主線進行啓動過程解析。
1. head.s的debug宏定義部分
最開始的一段都是head.s的debug宏定義部分,這部分可以方便我們調試時使用。
如下:
#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技術進行調試,DCC(Debug 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設置好所有的參數。如我們的EVB板ARM的實現如下:
#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
主要實現 addruart,senduart,waituart,busyuart這四個函數的具體實施。這個是調試函數打印的基礎。
下面是調試打印用到的kputc和kphex
.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
上面是分配打印hex的buffer,下面是具體的實現:
@ 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段,所以這就註定了它就是最先執行的代碼。
好了,中斷結束,再回到先前面的代碼,這段代碼的最開始是會被編譯器編譯成8個nop, 這個是爲了留給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寫入。
注:Angel是ARM公司的一種調試方法,它本身就是一個調試監控程序,是一組運行在目標機上的程序,可以接收主機上調試器發送的命令,執行諸如設置斷點、單步執行目標程序、觀察或修改寄存器、存儲器內容之類的操作。與基於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個,那麼2的27次方就是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,說明用的是r0p7的release。
好了讀取了這個值存入r9寄存器,然後使用算法(real ^ match) & mask,程序中:
( r9 ^r1)&r2,這裏r1 存是是表中的第一個CPU的ID值,r2是mask值,對於我們的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就可以了。然後再查看cp15的c7,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)建立好頁表後,ARM的cpu如何知道呢?這個就是要用到CP15的C2寄存器了,頁表基址就是存在這裏面的,其中[31:14]爲內存中頁表的基址,[13:0]應爲0如下圖:
圖.3 CP15的C2寄存器中的頁表項基址格式
所以我們初始化完段頁表後,就要把頁表基址MMU_PAGE_BASE (0x20004000)存入CP15的C2寄存器,這樣ARM就知道到哪裏去找那些頁表項了。下面我們來看一下整個MMU的虛擬地址的尋址過程,如圖4所示。
簡單解釋一下。首先,ARM的CPU從CP15的C2寄存器中找取出頁表基地址,然後把虛擬地址的最高12位左移兩位變爲14位放到頁表基址的低14位,組合成對應1M空間的頁表項在MMU頁表中的地址。然後,再取出頁表項的值,檢查AP位,域,判斷是否有讀寫的權限,如果沒有權限測會拋出數據或指令異常,如果有權限,就把最高12位取出加上虛擬地址的低20位段內偏移地址組合成最終的物理地址。到這裏整個MMU從虛擬地址到物理地址的轉換過程就完成了。
這段代碼裏,只會開啓頁表所在代碼的開始的256K對齊的一個0x10000000(256M)空間的大小(這個空間必然包含解壓後的內核),使能cache和write buffer,其他的4G-256M的空間不開啓。這裏使用的是1:1的映射。到這裏也很容易明白MMU和cache和write 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空間開啓cache和write buffer 爲了加快代碼在 nor flash中運行的速度。然後反回,到這裏16K的MMU頁表就完全建立好了。
然後再反回到建立頁表後的代碼,如下:
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, TLBS,write 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各個段的位置
從這裏可以看到,zImage在RAM中運行和在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
③.然後進行cache的flush,因爲馬上要進行代碼的跳轉了,接着就計算新的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
這裏先進行cache的flush,然後關掉cache,再準備好linux內核要啓動的幾個參數,最後跳到zreladdr處,進入解壓後的內核,到這裏壓縮內核的使命就完成了。但是它的功勞可不小啊。下面就是真真正正的linux內核的啓動過程了,這裏會進入到 arch/arm/kernel/head.s這個文件的stext這個地址開始執行第一行代碼。