ARM linux解析之壓縮內(nèi)核zImage的啟動過程
下面以arch/arm/boot/compressed/head.s為主線進(jìn)行啟動過程解析。
本文引用地址:http://m.butianyuan.cn/article/201611/317570.htm1.head.s的debug宏定義部分
最開始的一段都是head.s的debug宏定義部分,這部分可以方便我們調(diào)試時(shí)使用。
如下:
#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的一個(gè)調(diào)試通信通道,在串口無法使用的時(shí)候可以使用這個(gè)通道進(jìn)行數(shù)據(jù)的通信,具體的技術(shù)參前ARM公司文檔《ARM Architecture Reference Manual》。
第二部分首先#include
#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
主要實(shí)現(xiàn)addruart,senduart,waituart,busyuart這四個(gè)函數(shù)的具體實(shí)施。這個(gè)是調(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,下面是具體的實(shí)現(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這個(gè)函數(shù)可以用,不錯(cuò)。
好了,言歸正傳,再往下看,代碼如下:
.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出來,查看是否正確。很不幸的是,這個(gè)不是必須調(diào)用的,調(diào)試的時(shí)候,這些都是要自己把這些調(diào)試函數(shù)加上去的。好debug部分到這里就完了。
2.head.s的.start部分,進(jìn)入或保持在svc模式,并關(guān)中斷
繼續(xù)向下分析,下面是定義.start段,這段在鏈接時(shí)被鏈接到代碼的最開頭,那么zImage啟動時(shí),最先執(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
為何這個(gè)會先執(zhí)行呢?問的好。那么來個(gè)中斷吧:這個(gè)是由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個(gè)nop,這個(gè)是為了留給ARM的中斷向量表的,但是整個(gè)head.s都沒有用到中斷啊,誰知道告訴我一下,謝了。
然后呢,把u-boot傳過來的放在r1,r2的值,存在r7,r8中,r1存是的evb板的ID號,而r2存的是內(nèi)核要用的參數(shù)地址,這兩個(gè)參數(shù)在解壓內(nèi)核的時(shí)候不要用到,所以暫時(shí)保存一下,解壓內(nèi)枋完了,再傳給linux內(nèi)核。
再然后是幾個(gè)宏定義的解釋,ARM(),BSYM(),THUMB(),再加上W()吧,這幾個(gè)個(gè)宏定義都是在arch/arm/include/asm/unified.h里面定義的,好了,這里也算個(gè)中斷吧,如下:
#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...)里面的“...”,沒有見過吧,這個(gè)是C語言的C99的新標(biāo)準(zhǔn),變參宏,就是在x里,你可以隨便你輸入多少個(gè)參數(shù)。別急還沒有完,因?yàn)闆]有看見文件里有什么方包含這個(gè)頭文件。是的文件中確實(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)試方法,它本身就是一個(gè)調(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é)它,因?yàn)樗茿RMv2架構(gòu)以前的匯編方法,用于模式變換,和中斷關(guān)閉的,看不明白也沒關(guān)系,因?yàn)槲覀円院笠灿貌坏?。這里知道一下有這個(gè)事就行了。
行,到這里.start段就完了,代碼那么多,其實(shí)就是做一件事,保證運(yùn)行下面的代碼時(shí)已經(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ì),因?yàn)楸砀裎乙乓豁?,解釋我就寫在上面了。TEXT_OFFSET是代碼相對于物理內(nèi)存的偏移,通常選為32k=0x8。這個(gè)是有原因的,具體的原因后面會說。先看CONFIG_AUTO_ZRELADDR這個(gè)宏所含的內(nèi)容,它的意思是如果你不知道ZRELADDR地址要定在內(nèi)存什么地方,那么這段代碼就可以幫你??吹?xf8了吧,那么后面有多少個(gè)0呢?答案是27個(gè),那么2的27次方就是128M,這就明白了,只要你把解壓程序放在你最后解壓完成后的內(nèi)核空間的128M之內(nèi)的偏移的話,就可以自動設(shè)定好解壓后內(nèi)核要運(yùn)行的地址ZRELADDR。
如果你沒有定義的話,那么,就會去取zreladdr作為最后解壓的內(nèi)核運(yùn)行地。那么這個(gè)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)
而里面的幾個(gè)參數(shù)是在每個(gè)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)行地址,記住,這個(gè)地址很重要。
解壓內(nèi)核參數(shù) | 解壓時(shí)symbol | 解釋 |
ZTEXTADDR | 千成不要看成ZTE啊,呵,這里是zImage的運(yùn)行的起始地址,當(dāng)內(nèi)核從nor flash中運(yùn)行的時(shí)候很重要,如果在ram中運(yùn)行,這個(gè)設(shè)為0 | |
ZBSSADDR | 這個(gè)地址也是一樣的,這個(gè)是BSS的地址,如果在nor中運(yùn)行解壓的話,這個(gè)地址很重要。這個(gè)要放在RAM。 | |
ZRELADDR | 這個(gè)地址很重要,這個(gè)是解壓后內(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 | 實(shí)際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 | 這個(gè)是內(nèi)核數(shù)據(jù)段的虛擬地址的起址,當(dāng)用zImage的時(shí)候不要定義。 |
表.1內(nèi)核參數(shù)解釋
4.打開ARM系統(tǒng)的cache,為加快內(nèi)核解壓做好準(zhǔn)備
可以看到,打開cache的就一個(gè)函數(shù),如下:
blcache_on
看起來很少,其實(shí)展開后內(nèi)容還是很多的。我們來看看這個(gè)cache_on在哪里,可以找到代碼如下:
.align5
cache_on:movr3, #8@ cache_on function
bcall_cache_fn
這里設(shè)計(jì)的很精妙的,只可意會,注意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)
可以看到這是一個(gè)以proc_types為起始地址的表,上面我列出了第一個(gè)表項(xiàng),和最后一個(gè)表項(xiàng),如果查表不成功,則走最后一個(gè)表項(xiàng)返回。它實(shí)現(xiàn)的功能就是存兩個(gè)數(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
這個(gè)意思是取得ARM處理器的ID,這個(gè)又要看《ARM Architecture Reference Manual》了,這里我找了arm1176jzfs的架構(gòu)手冊,也是我用的ARM所用的架構(gòu)。里面的解釋如下:
這里我們主要關(guān)心Architecture這項(xiàng),我們的ARM這個(gè)值是: 0x410FB767,說明用的是r0p7的release。
好了讀取了這個(gè)值存入r9寄存器,然后使用算法(real ^ match) & mask,程序中:
( r9 ^r1)&r2,這里r1存是是表中的第一個(gè)CPU的ID值,r2是mask值,對于我們的ARM,結(jié)果如下:
0x410FB767 ^ 0xf0 = 0x4100B767
0x4100B767 & 0xf0 = 0
故match上了,這個(gè)時(shí)候就會如下:
ARM(addeqpc, r12,r3) @ call cache function
我們知道r3的值是0x8,那么r12表項(xiàng)的基址加上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),其實(shí)就是虛擬內(nèi)存,通俗地地說就是否支持MMU。
首先是保存lr寄存器到r12中,因?yàn)槲覀凂R上就要調(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,故每一個(gè)頁表項(xiàng)表示1M空間的話,需要4096個(gè)頁表項(xiàng),也就是4K大小,而每一個(gè)頁表項(xiàng)的大小是4字節(jié),這就是說我們進(jìn)行段映射的話,需要16K的大小存儲段頁表。
下面來看一下段頁表的格式,如下:
圖.1段頁表項(xiàng)的具體內(nèi)容
可以知道對于進(jìn)行mmu段映射這種方式,一共有4K個(gè)這樣的頁表項(xiàng),點(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如何知道呢?這個(gè)就是要用到CP15的C2寄存器了,頁表基址就是存在這里面的,其中[31:14]為內(nèi)存中頁表的基址,[13:0]應(yīng)為0如下圖:
圖.3 CP15的C2寄存器中的頁表項(xiàng)基址格式
所以我們初始化完段頁表后,就要把頁表基址MMU_PAGE_BASE (0x24)存入CP15的C2寄存器,這樣ARM就知道到哪里去找那些頁表項(xiàng)了。下面我們來看一下整個(gè)MMU的虛擬地址的尋址過程,如圖4所示。
簡單解釋一下。首先,ARM的CPU從CP15的C2寄存器中找取出頁表基地址,然后把虛擬地址的最高12位左移兩位變?yōu)?4位放到頁表基址的低14位,組合成對應(yīng)1M空間的頁表項(xiàng)在MMU頁表中的地址。然后,再取出頁表項(xiàng)的值,檢查AP位,域,判斷是否有讀寫的權(quán)限,如果沒有權(quán)限測會拋出數(shù)據(jù)或指令異常,如果有權(quán)限,就把最高12位取出加上虛擬地址的低20位段內(nèi)偏移地址組合成最終的物理地址。到這里整個(gè)MMU從虛擬地址到物理地址的轉(zhuǎn)換過程就完成了。
這段代碼里,只會開啟頁表所在代碼的開始的256K對齊的一個(gè)0x10(256M)空間的大?。ㄟ@個(gè)空間必然包含解壓后的內(nèi)核),使能cache和write buffer,其他的4G-256M的空間不開啟。這里使用的是1:1的映射。到這里也很容易明白MMU和cache和write buffer的關(guān)系了,為什么不開MMU無法使用cache了。
圖.4 MMU的段頁表的虛擬地址與物理地址的轉(zhuǎn)換過程
這里的4G空間全部映射完成之后,還會做一個(gè)映射,代碼如下:
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這個(gè)是最后一步,前面所有東西都設(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:
這里不僅可以看到各個(gè)寄存器里所存的值的意思,還可以看到. = 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),這個(gè)替換過程會在vmlinux.lds.in到vmlinux.lds的過程中完成,這個(gè)過程主要是為了有些內(nèi)核在nor flash中運(yùn)行而設(shè)置的。
好了,再次言歸正傳,從vmlinux.lds文件,可以看到鏈接后各個(gè)段的位置,如下。
圖.5 zImage各個(gè)段的位置
從這里可以看到,zImage在RAM中運(yùn)行和在NorFlash中直接運(yùn)行是有些區(qū)別的,這就是為何前面要區(qū)分ZTEXTADDR和ZBSSADDR的原因了。
好了,再看下面這兩句的區(qū)別,如果這個(gè)地方弄明白了,那么,下面的內(nèi)容就會變得很簡單,往下看:
restart:adr r0,LC0
addr0,pc,#0x10C
LC0:.wordLC0@ r1
dcd0x17C
故可知,當(dāng)zImage加到0x28運(yùn)行時(shí),PC值為:0x28070,這個(gè)時(shí)候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~~~看出來什么沒有,這個(gè)就是我們的加載zImage運(yùn)行的內(nèi)存起始地址,這個(gè)很重要,后面就要靠它知道我們當(dāng)前的代碼在哪里,搬移到哪里。然后再下一條指令把堆棧指針設(shè)置好。然后再把實(shí)際代碼偏移量加在r6=_edata和(r10=input_data_end-4)上面,這就是實(shí)際的內(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個(gè)字節(jié)的尾,用來存儲所壓內(nèi)容的原始大小,這個(gè)信息很要,是我們后面分配空間,代碼重定位的重要依據(jù)。這里為何要一個(gè)字節(jié),一個(gè)字節(jié)地取,只因?yàn)橐嫒軦RM代碼使用大端編譯的情況,保證讀取的正確無誤。好了,再往下:
#ifndef CONFIG_ZBOOT_ROM
add sp, sp, r0
add r10, sp, #0x10
#else
movr10, r6
#endif
我們這里在RAM中運(yùn)行,所以加上重定位SP的指針,加上偏移里,變成實(shí)際所在內(nèi)存的堆棧指針地址。這里主要是為了后面的檢查代碼是否要進(jìn)行重定位的時(shí)候所提前設(shè)置的,因?yàn)槿绻a不重定位,就不會再設(shè)堆棧指針了,重定位的話,則還要重設(shè)一次。然后再在堆棧指針的上面開辟一塊64K大小的空間,用于解壓內(nèi)核時(shí)的臨時(shí)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)繞人,兩種情況都畫個(gè)圖看一下,如圖.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ā)生代碼覆蓋的問題,這時(shí)候就要進(jìn)行代碼的自搬移和重定位。具體實(shí)現(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
這段代碼就是實(shí)現(xiàn)代碼的自搬移,最開始兩句是取得所要搬移代碼的大小,進(jìn)行了256字節(jié)的對齊,注釋上說了,為了避免偏移很小時(shí)產(chǎn)生自我覆蓋(這個(gè)地方暫沒有想明白,不過不影響下面分析)。這里還是再畫個(gè)圖表示一下整個(gè)搬移過程吧,以zImage加載地下和zreladdr都為0x28為例,其他的類似。
圖.7 zImage的代碼自搬移和內(nèi)核解壓的全程圖解
圖.7中我已經(jīng)標(biāo)好了序號,代碼的自搬移和內(nèi)核解的整個(gè)過程都在這里面下面一步步來分解:
①.首先計(jì)算要搬移的代碼的.text段代碼的大小,從restart開始,到reloc_code_end結(jié)束,這個(gè)就是剩下的.text段的內(nèi)容,這段內(nèi)容是接在打開cache的函數(shù)之后的。然后把這段代碼搬到核實(shí)際解壓后256字節(jié)對齊的邊界,然后進(jìn)行搬移,搬移時(shí)一次搬運(yùn)32個(gè)字節(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,因?yàn)轳R上要進(jìn)行代碼的跳轉(zhuǎn)了,接著就計(jì)算新的restart在哪里,接著跳過去執(zhí)行新的重定位后的代碼。
blcache_clean_flush
adrr0, BSYM(restart)
addr0, r0, r6
mov pc, r0
這個(gè)時(shí)候就又會到restart處執(zhí)行,會把前面的代碼再執(zhí)行一次,不過這次在執(zhí)行時(shí),會進(jìn)入圖.6所示的代碼不用重定位的情況,意料之后的事,接著跳到wont_overwirte執(zhí)行,如下:
teqr0, #0
beqnot_relocated
這兩行代碼的意思是,看一下只什么時(shí)候跳過來的,如果r0的值為0,說明沒有進(jìn)行代碼的重定位,那這個(gè)時(shí)候跳到no_relocated處執(zhí)行,這段就會跳過.got符號表的搬移,因?yàn)槲恢脹]有變啊。代碼寫得好嚴(yán)謹(jǐn)啊,佩服。
④.我們這種經(jīng)過代碼重定位的情況下,r0的值一定不會零,那么這個(gè)時(shí)候就要進(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段,其實(shí)就是清零,位置如圖所示。我雖畫了一個(gè)箭頭,但是其實(shí)并沒有進(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)號了吧,這個(gè)標(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)
這個(gè)函數(shù)是C下面的函數(shù),那些堆棧的設(shè)置啊,.got表啊,64k的解壓緩沖啊,都是為它準(zhǔn)備的。第一個(gè)參數(shù)是內(nèi)核解壓后所存放的地址,第二,第三參數(shù)是64k解壓緩沖起始地址和結(jié)束地址,最后一個(gè)參數(shù)ID號,這個(gè)由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)核要啟動的幾個(gè)參數(shù),最后跳到zreladdr處,進(jìn)入解壓后的內(nèi)核,到這里壓縮內(nèi)核的使命就完成了。但是它的功勞可不小啊。下面就是真真正正的linux內(nèi)核的啟動過程了,這里會進(jìn)入到arch/arm/kernel/head.s這個(gè)文件的stext這個(gè)地址開始執(zhí)行第一行代碼。
評論