四、内核启动(二)

4.1 MMU设置续

  上一节分析到调用 __armv4_mmu_cache_on,执行如下,这里我们要分析  set_mmu 函数

  

4.1.1 __setup_mmu

  前文已经分析过在内核最终运行地址r4下面有16KB的空间(我环境中是0x00004000~0x00008000),这就是用来存放页表的,但是现在要建立的页表在内核真正启动后会被销毁,只是用于零时存放。同时这里将要建立的页表映射关系是1:1映射(即虚拟地址 == 物理地址)。

  首先开始执行的 给页表留出空间,将页表的起始地址保存到 R3 中,R4 中保存的内容是 ZRELADDR,然后对齐页表,经过两次 bit 指令, R3 中的值的低 14 位均为0,实际上是对齐到了 16KB 边界。

1 __setup_mmu:    sub    r3, r4, #16384        @ Page directory size
2         bic    r3, r3, #0xff        @ Align the pointer
3         bic    r3, r3, #0x3f00

  

  

  • 常数 #16384 的由来:

  • 32 位的 RAM 系统,寻址空间为 4GB,此处每一个页表项代表 1MB,则需要 4096 个页表项。同时,每一个页表项的大小为 4Byte,那么就需要 4096 * 4Byte = 16384Byte = 16KB 的空间。

  • 此时页表项的每项对应 1MB 的内存空间,其格式为 段(section)页表项 ,如下图所示:

  

  图中 bit4 XN 为不可执行位, bit3 C 为 cacheable,bit2 B 为 bufferable

   注: L1页表项的格式有 4 种,分别为 Fault 页表项、Section 页表项、Page Table 页表项和 Supersection 页表项。详细内容参考 ARM 手册

  继续向下执行:

 1 /*
 2  * Initialise the page tables, turning on the cacheable and bufferable
 3  * bits for the RAM area only.
 4  */
 5         mov    r0, r3
 6         mov    r9, r0, lsr #18
 7         mov    r9, r9, lsl #18        @ start of RAM
 8         add    r10, r9, #0x10000000    @ a reasonable RAM size

  注释说的很清楚,初始化页表,打开 cacheable 和  bufferable 位

  对物理RAM空间建立cache和buffer。然后通过将r0(r3)中的地址值右移18位再左移18位(即清零r3中地址的低18位),得到物理RAM空间的“初始地址”(其实是估计值)并保存到r9(0x00000000)中去,然后将该地址加上256MB的大小作为物理RAM的“结束地址”(也是估计值)并保存到r10(0x10000000)中去。这里的另一个隐含意思也就是最多映射256MB大小的空间

  R0 = R3; R9 保存的实际是 R3 的高 14 位的内容,低 18 位全为0,对齐到了 256MB 的边界; R10 = R9 + 256MB.

  比较 R1 和 R9,若 R1 >= R9,则继续比较 R10 和 R1,之后 R1 = 0xC02,用于设置MMU区域表项的低12位状态位

1         mov    r1, #0x12        @ XN|U + section mapping
2         orr    r1, r1, #3 << 10    @ AP=11
3         add    r2, r3, #16384

  继续向下执行:

1 1:        cmp    r1, r9            @ if virt > start of RAM
2         cmphs    r10, r1            @   && end of RAM > virt
3         bic    r1, r1, #0x1c        @ clear XN|U + C + B
4         orrlo    r1, r1, #0x10        @ Set XN|U for non-RAM
5         orrhs    r1, r1, r6        @ set RAM section settings
6         str    r1, [r0], #4        @ 1:1 mapping
7         add    r1, r1, #1048576
8         teq    r0, r2
9         bne    1b

  接着r1比较r9和r10以设置MMU区域表项状态位:(其中r6中的值在前面__armv4_mmu_cache_on中赋值)

  (1)      r1 > r9 && r1 <r10 (r1的值在物理RAM地址范围内):

  设置RAM表项的C+B 位来开启cache和buffer,同时清除XN表示可执行code

  (2)      r1 < r9 || r1 > r10(r1的值在物理RAM地址范围外):

  设置RAM表项的XN位并清除C+B位来关闭cache和buffer,不可执行code

  在设置完状态为后就要写入页表的相应地址中去了,然后将页表的地址+4(指向下一个表项),物理地址空间+1M设置下一项(下一个需要映射物理地址的基地址),直到填完所有的4096表项。设置完后页表项与映射关系如下:

  

  如果代码不是运行在RAM中而是运行在FLASH中的,则映射2MB代码,如果运行在RAM中,则这部分代码重复前面的工作。

 1 /*
 2  * If ever we are running from Flash, then we surely want the cache
 3  * to be enabled also for our execution instance...  We map 2MB of it
 4  * so there is no map overlap problem for up to 1 MB compressed kernel.
 5  * If the execution is in RAM then we would only be duplicating the above.
 6  */
 7         orr    r1, r6, #0x04        @ ensure B is set for this
 8         orr    r1, r1, #3 << 10
 9         mov    r2, pc
10         mov    r2, r2, lsr #20
11         orr    r1, r1, r2, lsl #20
12         add    r0, r3, r2, lsl #2
13         str    r1, [r0], #4
14         add    r1, r1, #1048576
15         str    r1, [r0]
16         mov    pc, lr
17 ENDPROC(__setup_mmu)

  至此,cache on  分析结束,回到主干上继续执行,执行  restart 标签中的语句,继续是在not_angel中

4.2 restart

1 restart:    adr    r0, LC0
2         ldmia    r0, {r1, r2, r3, r6, r10, r11, r12}
3         ldr    sp, [r0, #28]

  通过前面LC0地址表的内容可见,这里r0中的内容就是编译时决定的LC0的实际运行地址(特别注意不是链接地址),然后调用ldmia命令依次将LC0地址表处定义的各个地址加载到r1、r2、r3、r6、r10、r11、r12和SP寄存器中去。执行之后各个寄存器中保存内容的意义如下:

  (1)      r0:LC0标签处的运行地址

  (2)      r1:LC0标签处的链接地址

  (3)      r2:__bss_start处的链接地址

  (4)      r3:_ednd处的链接地址(即程序结束位置)

  (5)      r6:_edata处的链接地址(即数据段结束位置)

  (6)      r10:压缩后内核数据大小位置

  (7)      r11:GOT表的启示链接地址

  (8)      r12:GOT表的结束链接地址

  (9)      sp:栈空间结束地址

  在获取了LC0的链接地址和运行地址后,就可以通过计算这两者之间的差值来判断当前运行的地址是否就是编译时的链接地址。

1         /*
2          * We might be running at a different address.  We need
3          * to fix up various pointers.
4          */
5         sub    r0, r0, r1        @ calculate the delta offset
6         add    r6, r6, r0        @ _edata
7         add    r10, r10, r0        @ inflated kernel size location

  将运行地址和链接地址的偏移保存到r0寄存器中,然后更新r6和r10中的地址,将其转换为实际的运行地址。

 1         /*
 2          * The kernel build system appends the size of the
 3          * decompressed kernel at the end of the compressed data
 4          * in little-endian form.
 5          */
 6         ldrb    r9, [r10, #0]
 7         ldrb    lr, [r10, #1]
 8         orr    r9, r9, lr, lsl #8
 9         ldrb    lr, [r10, #2]
10         ldrb    r10, [r10, #3]
11         orr    r9, r9, lr, lsl #16
12         orr    r9, r9, r10, lsl #24

  注释中说明了,内核编译系统在压缩内核时会在末尾处以小端模式附上未压缩的内核大小,这部分代码的作用就是将该值计算出来并保存到r9寄存器中去

 1 #ifndef CONFIG_ZBOOT_ROM
 2         /* malloc space is above the relocated stack (64k max) */
 3         add    sp, sp, r0
 4         add    r10, sp, #0x10000
 5 #else
 6         /*
 7          * With ZBOOT_ROM the bss/stack is non relocatable,
 8          * but someone could still run this code from RAM,
 9          * in which case our reference is _edata.
10          */
11         mov    r10, r6
12 #endif

  这里将镜像的结束地址保存到r10中去,我这里并没有定义ZBOOT_ROM(如果定义了ZBOOT_ROM则bss和stack是非可重定位的),这里将r10设置为sp结束地址上64kb处(这64kB空间是用来作为堆空间的)。

  接下来内核如果配置为支持设备树(DTB)会做一些特别的工作,我这里没有配置(#ifdef CONFIG_ARM_APPENDED_DTB),所以先跳过。

 1 /*
 2  * Check to see if we will overwrite ourselves.
 3  *   r4  = final kernel address (possibly with LSB set)
 4  *   r9  = size of decompressed image
 5  *   r10 = end of this image, including  bss/stack/malloc space if non XIP
 6  * We basically want:
 7  *   r4 - 16k page directory >= r10 -> OK
 8  *   r4 + image length <= address of wont_overwrite -> OK
 9  * Note: the possible LSB in r4 is harmless here.
10  */
11         add    r10, r10, #16384
12         cmp    r4, r10
13         bhs    wont_overwrite
14         add    r10, r4, r9
15         adr    r9, wont_overwrite
16         cmp    r10, r9
17         bls    wont_overwrite

  这部分代码用来分析当前代码是否会和最后的解压部分重叠,如果有重叠则需要执行代码搬移。首先比较内核解压地址r4-16Kb(这里是0x00004000,包括16KB的内核页表存放位置)和r10,如果r4 – 16kB >= r10,则无需搬移,否则继续计算解压后的内核末尾地址是否在当前运行地址之前,如果是则同样无需搬移,不然的话就需要进行搬移了。

  总结一下可能的3种情况:

  (1)      内核起始地址– 16kB >= 当前镜像结束地址:无需搬移

  (2)      内核结束地址 <= wont_overwrite运行地址:无需搬移

  (3)      内核起始地址– 16kB < 当前镜像结束地址 && 内核结束地址 > wont_overwrite运行地址:需要搬移

  仔细分析一下,这里内核真正运行的地址是0x00004000,而现在代码的运行地址显然已经在该地址之后了反汇编发现wont_overwrite的运行地址是0x00008000+0x00000168),而且内核解压后的空间必然会覆盖掉这里(内核解压后的大小大于0x00000168),所以这里会执行代码搬移。

 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
15 
16         /* Get start of code we want to copy and align it down. */
17         adr    r5, restart
18         bic    r5, r5, #31

  从这里开始会将镜像搬移到解压的内核地址之后,首先将解压后的内核结束地址进行扩展,扩展大小为代码段的大小(reloc_code_end定义在head.s的最后)保存到r10中,即搬运目的起始地址,然后r5保存了restart的起始地址,并进行对齐,即搬运的原起始地址。反汇编查看这里扩展的大小为0x800。

 1         sub    r9, r6, r5        @ size to copy
 2         add    r9, r9, #31        @ rounded up to a multiple
 3         bic    r9, r9, #31        @ ... of 32 bytes
 4         add    r6, r9, r5
 5         add    r9, r9, r10
 6 
 7 1:        ldmdb    r6!, {r0 - r3, r10 - r12, lr}
 8         cmp    r6, r5
 9         stmdb    r9!, {r0 - r3, r10 - r12, lr}
10         bhi    1b
11 
12         /* Preserve offset to relocated code. */
13         sub    r6, r9, r6
14 
15 #ifndef CONFIG_ZBOOT_ROM
16         /* cache_clean_flush may use the stack, so relocate it */
17         add    sp, sp, r6
18 #endif
19 
20         bl    cache_clean_flush
21 
22         badr    r0, restart
23         add    r0, r0, r6
24         mov    pc, r0

  这里首先计算出需要搬运的大小保存到r9中,搬运的原结束地址到r6中,搬运的目的结束地址到r9中。注意这里只搬运代码段和数据段,并不包含bss、栈和堆空间。

  接下来开始执行代码搬移,这里是从后往前搬移,一直到r6 == r5结束,然后r6中保存了搬移前后的偏移,并重定向栈指针(cache_clean_flush可能会使用到栈)。

  之后调用调用cache_clean_flush清楚缓存,然后将PC的值设置为搬运后restart的新地址,然后重新从restart开始执行。这次由于进行了代码搬移,所以会在检查自覆盖时进入wont_overwrite处执行。

 1 wont_overwrite:
 2 /*
 3  * If delta is zero, we are running at the address we were linked at.
 4  *   r0  = delta
 5  *   r2  = BSS start
 6  *   r3  = BSS end
 7  *   r4  = kernel execution address (possibly with LSB set)
 8  *   r5  = appended dtb size (0 if not present)
 9  *   r7  = architecture ID
10  *   r8  = atags pointer
11  *   r11 = GOT start
12  *   r12 = GOT end
13  *   sp  = stack pointer
14  */
15         orrs    r1, r0, r5
16         beq    not_relocated
17 
18         add    r11, r11, r0
19         add    r12, r12, r0

  这里的注释列出了现有所有寄存器值得含义,如果r0为0则说明当前运行的地址就是链接地址,无需进行重定位,跳转到not_relocated执行,但是这里运行的地址已经被移动到内核解压地址之后,显然不会是链接地址0x00000168(反汇编代码中得到),所以这里需要重新修改GOT表中的变量地址来实现重定位。

 1         add    r11, r11, r0
 2         add    r12, r12, r0
 3 
 4 #ifndef CONFIG_ZBOOT_ROM
 5         /*
 6          * If we're running fully PIC === CONFIG_ZBOOT_ROM = n,
 7          * we need to fix up pointers into the BSS region.
 8          * Note that the stack pointer has already been fixed up.
 9          */
10         add    r2, r2, r0
11         add    r3, r3, r0
12 
13         /*
14          * Relocate all entries in the GOT table.
15          * Bump bss entries to _edata + dtb size
16          */
17 1:        ldr    r1, [r11, #0]        @ relocate entries in the GOT
18         add    r1, r1, r0        @ This fixes up C references
19         cmp    r1, r2            @ if entry >= bss_start &&
20         cmphs    r3, r1            @       bss_end > entry
21         addhi    r1, r1, r5        @    entry += dtb size
22         str    r1, [r11], #4        @ next entry
23         cmp    r11, r12
24         blo    1b
25 
26         /* bump our bss pointers too */
27         add    r2, r2, r5
28         add    r3, r3, r5

  更新GOT表的运行起始地址到r11和结束地址到r12中去,然后同样更新BSS段的运行地址(需要修正BSS段的指针)。然后进入“1”标签中开始执行重定位。

  通过r1获取GOT表中的一项,然后对这一项的地址进行修正,如果修正后的地址 < BSS段的起始地址,或者在BSS段之中则再加上DTB的大小(如果不支持DTB则r5的值为0),然后再将值写回GOT表中去。如此循环执行直到遍历完GOT表。

  在重定位完成后,继续执行not_relocated部分代码,这里循环清零BSS段。

1 not_relocated:    mov    r0, #0
2 1:        str    r0, [r2], #4        @ clear bss
3         str    r0, [r2], #4
4         str    r0, [r2], #4
5         str    r0, [r2], #4
6         cmp    r2, r3
7         blo    1b

  这里检测r4中的最低位,如果已经置位则说明在前面执行restart前并没有执行cache_on来打开缓存(见前文),这里补执行。

1         /*
2          * Did we skip the cache setup earlier?
3          * That is indicated by the LSB in r4.
4          * Do it now if so.
5          */
6         tst    r4, #1
7         bic    r4, r4, #1
8         blne    cache_on
 1 /*
 2  * The C runtime environment should now be setup sufficiently.
 3  * Set up some pointers, and start decompressing.
 4  *   r4  = kernel execution address
 5  *   r7  = architecture ID
 6  *   r8  = atags pointer
 7  */
 8         mov    r0, r4
 9         mov    r1, sp            @ malloc space above stack
10         add    r2, sp, #0x10000    @ 64k max
11         mov    r3, r7
12         bl    decompress_kernel
13         bl    cache_clean_flush
14         bl    cache_off
15         mov    r1, r7            @ restore architecture number
16         mov    r2, r8            @ restore atags pointer

  到此为止,C语言的执行环境已经准备就绪,设置一些指针就可以开始解压内核了(这里的内核解压部分是使用C代码写的)。

  跳到 decompress_kernel 跳转到内核C代码运行。

  这里r0~r3的4个寄存器是decompress_kernel()函数传参用的,r0传入内核解压后的目的地址,r1传入堆空间的起始地址,r2传入堆空间的结束地址,r3传入机器码,然后就开始调用decompress_clean_flush()函数执行内核

  decompress_kernel()是用于解压内核

原文地址:https://www.cnblogs.com/kele-dad/p/8576923.html