新聞中心

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

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

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

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

本文引用地址:http://m.butianyuan.cn/article/201611/317570.htm

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

主要實(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í)行第一行代碼。



評論


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

關(guān)閉