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
存的是什么了,就是最后内核解压的起址,也是最后解压后的内核的运行地址,记住,这个地址很重要。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
表
.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
所在地址1
M
对齐的地方的
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
这个地址开始执行第一行代码。