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

转载地址:https://blog.csdn.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中我已经标好了序号,代码的自搬移和内核解压的整个过程都在这里面下面一步步来分解:

 
  1. /*

  2. * Relocate ourselves past the end of the decompressed kernel.

  3. * r6 = _edata

  4. * r10 = end of the decompressed kernel

  5. * Because we always copy ahead, we need to do it from the end and go

  6. * backward in case the source and destination overlap.

  7. */

  8. /*

  9. * Bump to the next 256-byte boundary with the size of

  10. * the relocation code added. This avoids overwriting

  11. * ourself when the offset is small.

  12. */

  13. add r10, r10, #((reloc_code_end - restart + 256) & ~255)

  14. bic r10, r10, #255 @ r10保存搬移的目的地址

  15.  
  16. /* Get start of code we want to copy and align it down. */

  17. adr r5, restart

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值