ARM linux解析之压缩内核zImage的启动过程 三
转载地址:https://blog.****.net/coldsnow33/article/details/37728009
好了,再回到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
原因:通反汇编文件可以得到:
00000070 <restart>:
70: e28f00ec add r0, pc, #236 ; 0xec
74: e8901a4e ldm r0, {r1, r2, r3, r6, r9, fp, ip}
78: e590d01c ldr sp, [r0, #28]
7c: e0400001 sub r0, r0, r1
而通过ldmia r0, {r1, r2, r3, r6, r10, r11, r12}加载内存值后,r1=0x17C
那么我们看一看这句:sub r0, r0, r1 @ calculate the delta offset的值是多少?如下:
r0= 0x2000817C - 0x17C = 0x20008000 //就是LC0运行地址与链接地址的差,即为zImage的加载地址
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中我已经标好了序号,代码的自搬移和内核解压的整个过程都在这里面下面一步步来分解:
-
/*
-
* Relocate ourselves past the end of the decompressed kernel.
-
* r6 = _edata
-
* r10 = end of the decompressed kernel
-
* Because we always copy ahead, we need to do it from the end and go
-
* backward in case the source and destination overlap.
-
*/
-
/*
-
* Bump to the next 256-byte boundary with the size of
-
* the relocation code added. This avoids overwriting
-
* ourself when the offset is small.
-
*/
-
add r10, r10, #((reloc_code_end - restart + 256) & ~255)
-
bic r10, r10, #255 @ r10保存搬移的目的地址
-
/* Get start of code we want to copy and align it down. */
-
adr r5, restart
-
bic r5, r5, #31 @ r5保存搬移的起始地址
①.首先计算要搬移的代码的.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表的重搬移,如图中所示,代码如下:
add r2, r2, r0 @ 重定位BSS
add r3, r3, r0
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这个地址开始执行第一行代码。