ARM linux解析之压缩内核zImage的启动过程

ARM linux解析之压缩内核zImage的启动过程

semilog@163.com

首先,我们要知道在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_onmov    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所用的架构。里面的解释如下:

ARM linux解析之压缩内核zImage的启动过程

ARM linux解析之压缩内核zImage的启动过程

这里我们主要关心 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的大小存储段页表。

下面来看一下段页表的格式,如下:

ARM linux解析之压缩内核zImage的启动过程

图.1 段页表项的具体内容

可以知道对于进行mmu段映射这种方式,一共有4K个这样的页表项,点大小16K字节。在这里我们的16k页表放哪呢?看程序第一句:

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

我们知道r4存内核解压后的基址,那么这句就是把页表放在解压后的内核地址的前面16K空间如下图所示:

ARM linux解析之压缩内核zImage的启动过程

图.2 linux内核地址空间

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

好了,再回到MMU,从MMU_PAGE_BASE (0x20004000)建立好页表后,ARM的cpu如何知道呢?这个就是要用到CP15的C2寄存器了,页表基址就是存在这里面的,其中[31:14]为内存中页表的基址,[13:0]应为0如下图:

ARM linux解析之压缩内核zImage的启动过程

图.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了。

ARM linux解析之压缩内核zImage的启动过程

图.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文件,可以看到链接后各个段的位置,如下。

ARM linux解析之压缩内核zImage的启动过程

图.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执行就可以了。

ARM linux解析之压缩内核zImage的启动过程

图.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为例,其他的类似。

ARM linux解析之压缩内核zImage的启动过程

图.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这个地址开始执行第一行代码。

上一篇:JavaScript函数继承


下一篇:Linux环境下MySql安装和常见问题的解决