Linux内存管理学习2 —— head.S中的段页表的建立

作者

彭东林

pengdonglin137@163.com

平台

TQ2440

Qemu+vexpress-ca9

Linux-4.10.17

正文

继续分析head.S:

此时r2存放的是设备树镜像的物理起始地址,r8是物理内存的起始地址,r9是从CP15的C0中读到的cpu id,r10是与该cpu id匹配的proc_info_list的物理地址

TQ2440:

r8: 0x3000_0000,r9: 0x41129200

vexpress:

r8: 0x6000_0000,r9: 0x414FC091

8、第30行调用__create_page_tables建立段式页表。

下面是__create_page_tables的定义: 其中涉及到几个宏定义:

PG_DIR_SIZE: 0x4000

PROCINFO_MM_MMUFLAGS:0x8

SECTION_SHIFT:20

PMD_ORDER:2

 1 /*
 2  * Setup the initial page tables.  We only setup the barest
 3  * amount which are required to get the kernel running, which
 4  * generally means mapping in the kernel code.
 5  *
 6  * r8 = phys_offset, r9 = cpuid, r10 = procinfo
 7  *
 8  * Returns:
 9  *  r0, r3, r5-r7 corrupted
10  *  r4 = physical page table address
11  */
12 __create_page_tables:
13     pgtbl    r4, r8                @ page table address
14 
15     /*
16      * Clear the swapper page table
17      */
18     mov    r0, r4
19     mov    r3, #0
20     add    r6, r0, #PG_DIR_SIZE
21 1:    str    r3, [r0], #4
22     str    r3, [r0], #4
23     str    r3, [r0], #4
24     str    r3, [r0], #4
25     teq    r0, r6
26     bne    1b
27 
28     ldr    r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
29 
30     /*
31      * Create identity mapping to cater for __enable_mmu.
32      * This identity mapping will be removed by paging_init().
33      */
34     adr    r0, __turn_mmu_on_loc
35     ldmia    r0, {r3, r5, r6}
36     sub    r0, r0, r3            @ virt->phys offset
37     add    r5, r5, r0            @ phys __turn_mmu_on
38     add    r6, r6, r0            @ phys __turn_mmu_on_end
39     mov    r5, r5, lsr #SECTION_SHIFT
40     mov    r6, r6, lsr #SECTION_SHIFT
41 
42 1:    orr    r3, r7, r5, lsl #SECTION_SHIFT    @ flags + kernel base
43     str    r3, [r4, r5, lsl #PMD_ORDER]    @ identity mapping
44     cmp    r5, r6
45     addlo    r5, r5, #1            @ next section
46     blo    1b
47 
48     /*
49      * Map our RAM from the start to the end of the kernel .bss section.
50      */
51     add    r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
52     ldr    r6, =(_end - 1)
53     orr    r3, r8, r7
54     add    r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
55 1:    str    r3, [r0], #1 << PMD_ORDER
56     add    r3, r3, #1 << SECTION_SHIFT
57     cmp    r0, r6
58     bls    1b
59 
60     /*
61      * Then map boot params address in r2 if specified.
62      * We map 2 sections in case the ATAGs/DTB crosses a section boundary.
63      */
64     mov    r0, r2, lsr #SECTION_SHIFT
65     movs    r0, r0, lsl #SECTION_SHIFT
66     subne    r3, r0, r8
67     addne    r3, r3, #PAGE_OFFSET
68     addne    r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
69     orrne    r6, r7, r0
70     strne    r6, [r3], #1 << PMD_ORDER
71     addne    r6, r6, #1 << SECTION_SHIFT
72     strne    r6, [r3]
73 
74     ret    lr
75 ENDPROC(__create_page_tables)
76     .ltorg
77     .align
78 __turn_mmu_on_loc:
79     .long    .
80     .long    __turn_mmu_on
81     .long    __turn_mmu_on_end

第13行,计算段式页表在物理内存当中的起始地址,方法如下:

    .macro    pgtbl, rd, phys
    add    
d, phys, #TEXT_OFFSET
    sub    
d, 
d, #PG_DIR_SIZE
    .endm

r4 =  r8 + 0x8000 - 0x4000,通过前文知道r8存放的是物理内存的起始地址,所以对于TQ2440,r4为0x3000_4000,对于vexpress是0x6000_4000,也就是段式页表占据了kernel代码段(0x3000_8000或0x6000_8000)之前的16KB的物理内存

第18到第26行,将段式页表的占据的那16KB的物理内存清零

第28行,加载MMU Flags到r7中。通过之前的分析知道,r10中存放的是与cpu id匹配的proc_info_list的首地址,这里访问的是proc_info_list->__cpu_mm_mmu_flags。

对于TQ2440,是0x00000c1e, 对于vexpress来说是0x00011c0e。下面以TQ2440为例说明。参考手册 ARM920T Technical Reference Manual 的 3.3.3 Level one descriptor

对于段式页表,bit[1:0]是0x10,对于TQ2440的0xC1E,满足这一条件,所以是段式页表。具体段式页表的页表项的含义如下:

每个bit位的含义如下:

 对于段式页表,只需要一级,地址转换过程如下:

理解了段式页表地址翻译的过程,也就容易理解接下来建立段式页表的代码了。

可以现在start_kernel的入口处将段式页表中的内容打印出来:

diff --git a/init/main.c b/init/main.c
index 09beb7f..fa6edd6 100644
--- a/init/main.c
+++ b/init/main.c
@@ -484,6 +484,18 @@ asmlinkage __visible void __init start_kernel(void)
     char *command_line;
     char *after_dashes;
 
+
+    unsigned int i, addr, value;
+    for (i=0; i < 0x1000; i++) {
+        addr = 0xC0004000 + i*4;
+        value = *((unsigned int *)addr);
+        if (value) {
+            pr_notice("0x%04x: hw: 0x%08x, virt: 0x%08x, value: 0x%08x map: [0x%08x ~ 0x%08x ==> 0x%08x ~ 0x%08x]
",
+                 i, __pa(addr), addr, *((int *)addr), i*(1<<20), i*(1<<20)+(1<<20)-1,
+                 ((value>>20)<<20), ((value>>20)<<20)+(1<<20)-1);
+        }
+    }
+
     set_task_stack_end_magic(&init_task);
     smp_setup_processor_id();
     debug_objects_early_init();

上面加的代码会将段式表中建立了映射的段表项的编号、存放每个段表项的物理地址和虚拟地址以及段表项的内容、该段表实现的虚拟地址跟物理地址之间的映射关系都打印出来。

对于TQ2440,段表内容如下:

 1 0x0300: hw: 0x30004c00, virt: 0xc0004c00, value: 0x30000c1e map: [0x30000000 ~ 0x300fffff ==> 0x30000000 ~ 0x300fffff]
 2 0x0c00: hw: 0x30007000, virt: 0xc0007000, value: 0x30000c1e map: [0xc0000000 ~ 0xc00fffff ==> 0x30000000 ~ 0x300fffff]
 3 0x0c01: hw: 0x30007004, virt: 0xc0007004, value: 0x30100c1e map: [0xc0100000 ~ 0xc01fffff ==> 0x30100000 ~ 0x301fffff]
 4 0x0c02: hw: 0x30007008, virt: 0xc0007008, value: 0x30200c1e map: [0xc0200000 ~ 0xc02fffff ==> 0x30200000 ~ 0x302fffff]
 5 0x0c03: hw: 0x3000700c, virt: 0xc000700c, value: 0x30300c1e map: [0xc0300000 ~ 0xc03fffff ==> 0x30300000 ~ 0x303fffff]
 6 0x0c04: hw: 0x30007010, virt: 0xc0007010, value: 0x30400c1e map: [0xc0400000 ~ 0xc04fffff ==> 0x30400000 ~ 0x304fffff]
 7 0x0c05: hw: 0x30007014, virt: 0xc0007014, value: 0x30500c1e map: [0xc0500000 ~ 0xc05fffff ==> 0x30500000 ~ 0x305fffff]
 8 0x0c06: hw: 0x30007018, virt: 0xc0007018, value: 0x30600c1e map: [0xc0600000 ~ 0xc06fffff ==> 0x30600000 ~ 0x306fffff]
 9 0x0c07: hw: 0x3000701c, virt: 0xc000701c, value: 0x30700c1e map: [0xc0700000 ~ 0xc07fffff ==> 0x30700000 ~ 0x307fffff]
10 0x0c3a: hw: 0x300070e8, virt: 0xc00070e8, value: 0x33a00c1e map: [0xc3a00000 ~ 0xc3afffff ==> 0x33a00000 ~ 0x33afffff]
11 0x0c3b: hw: 0x300070ec, virt: 0xc00070ec, value: 0x33b00c1e map: [0xc3b00000 ~ 0xc3bfffff ==> 0x33b00000 ~ 0x33bfffff]
12 0x0f70: hw: 0x30007dc0, virt: 0xc0007dc0, value: 0x50000c12 map: [0xf7000000 ~ 0xf70fffff ==> 0x50000000 ~ 0x500fffff]

对于vexpress,段表内容如下:

 1 0x0601: hw: 0x60005804, virt: 0xc0005804, value: 0x60111c0e map: [0x60100000 ~ 0x601fffff ==> 0x60100000 ~ 0x601fffff]
 2 0x0c00: hw: 0x60007000, virt: 0xc0007000, value: 0x60011c0e map: [0xc0000000 ~ 0xc00fffff ==> 0x60000000 ~ 0x600fffff]
 3 0x0c01: hw: 0x60007004, virt: 0xc0007004, value: 0x60111c0e map: [0xc0100000 ~ 0xc01fffff ==> 0x60100000 ~ 0x601fffff]
 4 0x0c02: hw: 0x60007008, virt: 0xc0007008, value: 0x60211c0e map: [0xc0200000 ~ 0xc02fffff ==> 0x60200000 ~ 0x602fffff]
 5 0x0c03: hw: 0x6000700c, virt: 0xc000700c, value: 0x60311c0e map: [0xc0300000 ~ 0xc03fffff ==> 0x60300000 ~ 0x603fffff]
 6 0x0c04: hw: 0x60007010, virt: 0xc0007010, value: 0x60411c0e map: [0xc0400000 ~ 0xc04fffff ==> 0x60400000 ~ 0x604fffff]
 7 0x0c05: hw: 0x60007014, virt: 0xc0007014, value: 0x60511c0e map: [0xc0500000 ~ 0xc05fffff ==> 0x60500000 ~ 0x605fffff]
 8 0x0c06: hw: 0x60007018, virt: 0xc0007018, value: 0x60611c0e map: [0xc0600000 ~ 0xc06fffff ==> 0x60600000 ~ 0x606fffff]
 9 0x0c07: hw: 0x6000701c, virt: 0xc000701c, value: 0x60711c0e map: [0xc0700000 ~ 0xc07fffff ==> 0x60700000 ~ 0x607fffff]
10 0x0c08: hw: 0x60007020, virt: 0xc0007020, value: 0x60811c0e map: [0xc0800000 ~ 0xc08fffff ==> 0x60800000 ~ 0x608fffff]
11 0x0c09: hw: 0x60007024, virt: 0xc0007024, value: 0x60911c0e map: [0xc0900000 ~ 0xc09fffff ==> 0x60900000 ~ 0x609fffff]
12 0x0c0a: hw: 0x60007028, virt: 0xc0007028, value: 0x60a11c0e map: [0xc0a00000 ~ 0xc0afffff ==> 0x60a00000 ~ 0x60afffff]
13 0x0c80: hw: 0x60007200, virt: 0xc0007200, value: 0x68011c0e map: [0xc8000000 ~ 0xc80fffff ==> 0x68000000 ~ 0x680fffff]
14 0x0c81: hw: 0x60007204, virt: 0xc0007204, value: 0x68111c0e map: [0xc8100000 ~ 0xc81fffff ==> 0x68100000 ~ 0x681fffff]
15 0x0f80: hw: 0x60007e00, virt: 0xc0007e00, value: 0x10000c12 map: [0xf8000000 ~ 0xf80fffff ==> 0x10000000 ~ 0x100fffff]

第34行到第36行,用于将__turn_mmu_on到__turn_mmu_on_end的虚拟地址转换成物理地址,然后将得到的物理地址当做虚拟地址建立页表。这样做的目的是什么呢?这两个标号之间的代码完成的任务是开启MMU,问题来了,再打开MMU那一刻,紧接着后面的地址都会被认为虚拟地址,都需要经过MMU进行转换,而在开启MMU的那条指令前后的PC是连续的,开启之前PC存放的是物理地址(0x30008000或者0x6000_8000级别的),开启之后,如果没有特殊的操作,紧邻的指令和数据的地址也是0x30008000或0x60008000级别,而此时的地址被认为是虚拟地址。如果不对这部分虚拟地址建立页表,程序里立刻出问题。

以TQ2440为例,在上面的打印的log中的第1行就是映射__turn_mmu_on到__turn_mmu_on_end之间代码地址的段表项的信息:

1 0x0300: hw: 0x30004c00, virt: 0xc0004c00, value: 0x30000c1e map: [0x30000000 ~ 0x300fffff ==> 0x30000000 ~ 0x300fffff]

 我们按照代码的计算方法手动计算一下该段表项:

首先我们打开System.map文件,该文件中列出了kernel里一些符号对应的虚拟地址,我们找到__turn_mmu_on和__turn_mmu_on_end对应的虚拟地址:

c0008200 T __turn_mmu_on
c0008200 T _stext
c0008218 t __turn_mmu_on_end

然后将该虚拟地址转成对应的物理地址,可以看到这两个标号之间相差很小,在1MB以内,由于每个段表项都可以映射1MB的地址空间,所以只需要一个段表项即可。对于TQ2440,就是0x30008200和0x30008218,然后右移20位就都变成了0x300,然后再左移20位并或上0x00000c1e,就变成了0x3000_0c1e,这个就是段表项的内容,该表项的物理地址是多少呢?0x30004000 + 0x300<<2 = 0x30004C00,画出图的话:

 第51行到58行完成的任务是对0xC000_0000到_end标号之间的虚拟地址进行映射,映射到物理内存的起始地址处,查询一下System.map,可以发现标号_end的虚拟地址是0xc07404e0, 需要的段表项的个数是:(0xc07404e0 - 0xc0000000 + 0x100000)/ 0x100000 = 8

在上面打印出来的log中下面的8个段表项就负责完成这部分的映射:

 2 0x0c00: hw: 0x30007000, virt: 0xc0007000, value: 0x30000c1e map: [0xc0000000 ~ 0xc00fffff ==> 0x30000000 ~ 0x300fffff]
 3 0x0c01: hw: 0x30007004, virt: 0xc0007004, value: 0x30100c1e map: [0xc0100000 ~ 0xc01fffff ==> 0x30100000 ~ 0x301fffff]
 4 0x0c02: hw: 0x30007008, virt: 0xc0007008, value: 0x30200c1e map: [0xc0200000 ~ 0xc02fffff ==> 0x30200000 ~ 0x302fffff]
 5 0x0c03: hw: 0x3000700c, virt: 0xc000700c, value: 0x30300c1e map: [0xc0300000 ~ 0xc03fffff ==> 0x30300000 ~ 0x303fffff]
 6 0x0c04: hw: 0x30007010, virt: 0xc0007010, value: 0x30400c1e map: [0xc0400000 ~ 0xc04fffff ==> 0x30400000 ~ 0x304fffff]
 7 0x0c05: hw: 0x30007014, virt: 0xc0007014, value: 0x30500c1e map: [0xc0500000 ~ 0xc05fffff ==> 0x30500000 ~ 0x305fffff]
 8 0x0c06: hw: 0x30007018, virt: 0xc0007018, value: 0x30600c1e map: [0xc0600000 ~ 0xc06fffff ==> 0x30600000 ~ 0x306fffff]
 9 0x0c07: hw: 0x3000701c, virt: 0xc000701c, value: 0x30700c1e map: [0xc0700000 ~ 0xc07fffff ==> 0x30700000 ~ 0x307fffff]

我们可以拿0xC000_0000到0xC00F_FFFF映射为例,段表项的地址:(0xC000_0000 >> 18 ) + 0xC000_4000 = 0xC000_7000,段表项的内容: r8 | r7 = 0x3000_0000 | 0xC1E = 0x3000_0C1E

对应的映射图如下:

 第64到72行做的工作是对r2指向的设备树镜像文件所在的物理地址空间镜像映射,这里kernel为其建立了两个段表项,一共可以映射2MB的空间。

还是以TQ2440为例,对应的就是上面log中这两个:

10 0x0c3a: hw: 0x300070e8, virt: 0xc00070e8, value: 0x33a00c1e map: [0xc3a00000 ~ 0xc3afffff ==> 0x33a00000 ~ 0x33afffff]
11 0x0c3b: hw: 0x300070ec, virt: 0xc00070ec, value: 0x33b00c1e map: [0xc3b00000 ~ 0xc3bfffff ==> 0x33b00000 ~ 0x33bfffff]

在TQ2440启动的时候uboot将设备树dtb加载到了物理内存0x33aa7000处:

## Booting kernel from Legacy Image at 30008000 ...
   Image Name:   Linux-4.10.17+
   Created:      2017-11-11  10:15:24 UTC
   Image Type:   ARM Linux Kernel Image (uncompressed)
   Data Size:    3487280 Bytes = 3.3 MiB
   Load Address: 30008000
   Entry Point:  30008000
   Verifying Checksum ... OK
## Flattened Device Tree blob at 32000000
   Booting using the fdt blob at 0x32000000
   Loading Kernel Image ... OK
   Loading Device Tree to 33aa7000, end 33aac168 ... OK

Starting kernel ...

Uncompressing Linux... done, booting the kernel.

下面按照上面的方法,手动计算一下: 段表项的内容:(0x33aa7000 >> 20)<< 20 | 0x00000c1e = 0x33a0_0c1e   段表项的存放的地址: ((0x33aa7000 >> 20)<<20 - 0x30000000 + 0xC0000000) >> 18 + 0x3000_4000 = 0x30e8 + 0x3000_4000 = 0x3000_70e8

此时,dtb占用的地址空间的映射图如下:

 

未完待续。

原文地址:https://www.cnblogs.com/pengdonglin137/p/7818922.html