最新Java微信公众号+微信支付入门开发项目实战(完整)

  1. BSS_SECTION(0, 0, 0)

  2. . = ALIGN(PAGE_SIZE);

  3. idmap_pg_dir = .;

  4. . += IDMAP_DIR_SIZE;

  5. swapper_pg_dir = .;

  6. . += SWAPPER_DIR_SIZE;

从链接脚本中可以看到预留6个页面存储页表项。紧跟在bss段后面。idmap_pg_dir是identity mapping使用的页表。swapper_pg_dir是kernel image mapping初始阶段使用的页表。请注意,这里的内存是一段连续内存。也就是说页表(PGD/PUD/PMD)都是连在一起的,地址相差PAGE_SIZE(4k)。

如何填充页表的页表项

从链接脚本vmlinux.lds.S文件中可以找到kernel代码起始代码段是".head.text"段,因此kernel的代码起始位置位于arch/arm64/kernel/head.S文件_head标号。在head.S文件中有三个宏定义和创建地址映射相关。分别是:create_table_entrycreate_pgd_entrycreate_block_map

create_table_entry实现如下。

 
 
  1. /*

  2. * Macro to create a table entry to the next page.

  3. *

  4. * tbl: 页表基地址

  5. * virt: 需要创建地址映射的虚拟地址

  6. * shift: #imm page table shift

  7. * ptrs: #imm pointers per table page

  8. *

  9. * Preserves: virt

  10. * Corrupts: tmp1, tmp2

  11. * Returns: tbl -> next level table page address

  12. */

  13. .macro create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2

  14. lsr \tmp1, \virt, #\shift

  15. and \tmp1, \tmp1, #\ptrs - 1    // table index

  16. add \tmp2, \tbl, #PAGE_SIZE

  17. orr \tmp2, \tmp2, #PMD_TYPE_TABLE // address of next table and entry type

  18. str \tmp2, [\tbl, \tmp1, lsl #3]

  19. add \tbl, \tbl, #PAGE_SIZE    // next level table page

  20. .endm

这里是汇编中的宏定义。汇编中宏定义是以.macro开头,以.endm结尾。宏定义中以\x来引用宏定义中的参数x。该宏定义的作用是创建一个level的页表项(PGD/PUD/PMD)。具体是哪个level是由virt、shift和ptrs参数决定。我总是喜欢帮你翻译成C语言的形式。C语言如果不懂的话,我也没办法了。既然汇编你不熟悉,没关系,下面帮你转换成C语言的宏定义。

 
 
  1. #define PAGE_SIZE            (1 << 12)

  2. #define PMD_TYPE_TABLE       (3 << 0)

  3. #define create_table_entry(tbl, virt, shift, ptrs, tmp1, tmp2) do { \

  4. tmp1 = virt >> shift;                     /* 1 */           \

  5. tmp1 &= ptrs - 1;                         /* 1 */           \

  6. tmp2 = tbl + PAGE_SIZE;                   /* 2 */           \

  7. tmp2 |= PMD_TYPE_TABLE;                   /* 3 */           \

  8. *((long *)(tbl + (tmp1 << 3))) = tmp2;    /* 4 */           \

  9. tbl += PAGE_SIZE;                         /* 5 */           \

  10. } while (0)

  1. 根据virt和ptrs参数计算该虚拟地址virt的页表项在页表中的index。例如计算virt地址在PGD也表中的indedx,可以传递shift = PGDIR_SHIFT,ptrs = PTRS_PER_PGD,tbl传递PGD页表基地址。所以,宏定义是一个创建中间level的页表项。

  2. 既然要填充当前level的页表项就需要告知下一个level页表的基地址,这里就是计算下一个页表的基地址。还记得上面说的idmap_pg_dir和swapper_pg_dir吗?页表(PGD/PUD/PMD)都是连在一起的,地址相差PAGE_SIZE。

  3. 告知MMU这是一个中间level页表并且是有效的。

  4. 页表项的真正填充操作,tmp1 << 3是因为ARM64的地址占用8bytes。

  5. 更新tbl,也就只指向下一个level页表的地址,可以方便再一次调用create_table_entry填充下一个level页表项而不用自己更新tbl。

create_pgd_entry的实现如下。

 
 
  1. /*

  2. * Macro to populate the PGD (and possibily PUD) for the corresponding

  3. * block entry in the next level (tbl) for the given virtual address.

  4. *

  5. * Preserves: tbl, next, virt

  6. * Corrupts: tmp1, tmp2

  7. */

  8. .macro create_pgd_entry, tbl, virt, tmp1, tmp2

  9. create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2

  10. create_table_entry \tbl, \virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2

  11. .endm

create_pgd_entry可以用来填充PGD、PUD、PMD等中间level页表对应页表项。虽然名字是创建PGD的描述符,但是实际上是一级一级的创建页表项,最终只留下最后一级页表没有填充页表项。老规矩转换成C语言分析。

 
  1. #define SWAPPER_TABLE_SHIFT PUD_SHIFT

  2. #define create_pgd_entry(tbl, virt, tmp1, tmp2) do {                                          \

  3. create_table_entry(tbl, virt, PGDIR_SHIFT, PTRS_PER_PGD, tmp1, tmp2);         /* 1 */ \

  4. create_table_entry(tbl, virt, SWAPPER_TABLE_SHIFT, PTRS_PER_PTE, tmp1, tmp2); /* 2 */ \

  5. } while (0)

  1. 这里的tbl参数相当于PGD页表地址,调用create_table_entry创建PGD页表中virt地址对应的页表项。

  2. 填充下一个level的页表项。这里是PUD页表。由于使用了ARM64初期使用section mapping,因此PUD页表就是最后一个中间level的页表,所以只剩下PMD页表的页表项没有填充,virt地址对应的PMD页表项最终会填充block descriptor。假设这里使用4级页表,那么下面还会创建PMD页表的页表项,也就是只留下PTE页表。所以,宏定义是创建所有中间level的页表项,只留下最后一级页表。

在经过create_pgd_entry宏的调用后,就填充好了从PGD开始的所有中间level的页表的页表项的填充操作。现在是不是只剩下PTE页表的页表项没有填充呢?所以最后一个create_block_map就是完成这个操作的。

 
 
  1. /*

  2. * Macro to populate block entries in the page table for the start..end

  3. * virtual range (inclusive).

  4. *

  5. * Preserves: tbl, flags

  6. * Corrupts: phys, start, end, pstate

  7. */

  8. .macro create_block_map, tbl, flags, phys, start, end

  9. lsr \phys, \phys, #SWAPPER_BLOCK_SHIFT

  10. lsr \start, \start, #SWAPPER_BLOCK_SHIFT

  11. and \start, \start, #PTRS_PER_PTE - 1               // table index

  12. orr \phys, \flags, \phys, lsl #SWAPPER_BLOCK_SHIFT  // table entry

  13. lsr \end, \end, #SWAPPER_BLOCK_SHIFT

  14. and \end, \end, #PTRS_PER_PTE - 1                   // table end index

  15. 9999: str \phys, [\tbl, \start, lsl #3]               // store the entry

  16. add \start, \start, #1                              // next entry

  17. add \phys, \phys, #SWAPPER_BLOCK_SIZE               // next block

  18. cmp \start, \end

  19. b.ls 9999b

  20. .endm

create_block_map宏的作用是创建虚拟地址(从start到end)区域映射到到phys物理地址。传入5个参数,分别如下意思。

  • tbl:页表基地址

  • flags:将要填充页表项的flags

  • phys:创建映射的物理地址

  • start:创建映射的虚拟地址起始地址

  • end:创建映射的虚拟地址结束地址

我们还是依然翻译成C语言分析。

 
 
  1. #define SWAPPER_BLOCK_SHIFT PMD_SHIFT

  2. #define SWAPPER_BLOCK_SIZE (1 << PMD_SHIFT)

  3. #define create_block_map(tbl, flags, phys, start, end) do {  \

  4. phys >>= SWAPPER_BLOCK_SHIFT;                /* 1 */ \

  5. start >>= SWAPPER_BLOCK_SHIFT;               /* 2 */ \

  6. start &= PTRS_PER_PTE - 1;                   /* 2 */ \

  7. phys = flags | (phys << SWAPPER_BLOCK_SHIFT);/* 3 */ \

  8. end >>= SWAPPER_BLOCK_SHIFT;                 /* 4 */ \

  9. end &= PTRS_PER_PTE - 1;                     /* 4 */ \

  10.                                                             \

  11. while (start != end) {                       /* 5 */ \

  12. *((long *)(tbl + (start << 3))) = phys;  /* 6 */ \

  13. start++;                                 /* 7 */ \

  14. phys += SWAPPER_BLOCK_SIZE;              /* 8 */ \

  15. }                                                    \

  16. } while (0)

  1. 针对phys的低SWAPPER_BLOCK_SHIFT位进行清零,和第三步骤的phys << SWAPPER_BLOCK_SHIFT收尾呼应。相当于对齐(这里的情况是2M对齐)。

  2. 计算起始地址start的页目录项的index。

  3. 构造描述符。

  4. 计算结束地址end的页目录项的index。

  5. 循环填充start到end的页目录项。

  6. 根据页表基地址tbl和当前的start变量填充对应的页表项。start << 3是因为ARM64地址占用8 bytes。

  7. 更新下一个页表项。

  8. 更新下一个block的物理地址。

如何使用上述三个接口创建映射关系呢?其实很简单,首先我们需要先调用create_pgd_entry宏填充PGD以及所有中间level的页表项。最后的PMD页表的填充可以调用create_block_map宏来完成操作。

如何创建页表

在汇编代码阶段的head.S文件中,负责创建映射关系的函数是create_page_tables。create_page_tables函数负责identity mapping和kernel image mapping。前文提到identity mapping主要是打开MMU的过度阶段,因此对于identity mapping不需要映射整个kernel,只需要映射操作MMU代码相关的部分。如何区分这部分代码呢?当然是利用linux中常用手段自定义代码段。自定义的代码段的名称是".idmap.text"。除此之外,肯定还需要在链接脚本中声明两个标量,用来标记代码段的开始和结束。可以从vmlinux.lds.S中找到答案。

 
 
  1. #define IDMAP_TEXT                             \

  2. . = ALIGN(SZ_4K);                          \

  3. VMLINUX_SYMBOL(__idmap_text_start) = .;    \

  4. *(.idmap.text)                             \

  5. VMLINUX_SYMBOL(__idmap_text_end) = .;

从链接脚本中可以看出idmap_text_start和idmap_text_end分别是.idmap.text段的起始和结束地址。在创建identity mapping的时候会使用。另外我们同样从链接脚本中得到_text和_end两个变量,分别是kernel代码链接的开始和结束地址。编译器的链接地址实际上就是最后代码期望运行的地址。在KASLR关闭的情况下就是kernel image需要映射的虚拟地址。当我们编译kernel后,可以根据符号表System.map文件查看哪些函数被放在".idmap.text"段。当然你也可以看代码,但是我觉得没有这种方法简单。

 
 
  1. ffff200008fbc000 T __idmap_text_start

  2. ffff200008fbc000 T kimage_vaddr

  3. ffff200008fbc008 T el2_setup

  4. ffff200008fbc054 t set_hcr

  5. ffff200008fbc118 t install_el2_stub

  6. ffff200008fbc16c t set_cpu_boot_mode_flag

  7. ffff200008fbc190 T secondary_holding_pen

  8. ffff200008fbc1b4 t pen

  9. ffff200008fbc1c8 T secondary_entry

  10. ffff200008fbc1d4 t secondary_startup

  11. ffff200008fbc1e4 t __secondary_switched

  12. ffff200008fbc218 T __enable_mmu

  13. ffff200008fbc26c t __no_granule_support

  14. ffff200008fbc290 t __primary_switch

  15. ffff200008fbc2b0 T cpu_resume

  16. ffff200008fbc2d0 T cpu_do_resume

  17. ffff200008fbc340 T idmap_cpu_replace_ttbr1

  18. ffff200008fbc370 T __cpu_setup

  19. ffff200008fbc3f0 t crval

  20. ffff200008fbc408 T __idmap_text_end

create_page_tables的汇编代码比较简单,就不转换成C语言讲解了。create_page_tables实现如下。

 
 
  1. __create_page_tables:

  2. mov x7, SWAPPER_MM_MMUFLAGS

  3. /*

  4. * Create the identity mapping.

  5. */

  6. adrp x0, idmap_pg_dir                                             /* 1 */

  7. adrp x3, __idmap_text_start          // __pa(__idmap_text_start)  /* 2 */

  8. create_pgd_entry x0, x3, x5, x6                                      /* 3 */

  9. mov x5, x3                              // __pa(__idmap_text_start)  /* 4 */

  10. adr_l x6, __idmap_text_end            // __pa(__idmap_text_end)

  11. create_block_map x0, x7, x3, x5, x6                                  /* 5 */

  12. /*

  13. * Map the kernel image.

  14. */

  15. adrp x0, swapper_pg_dir                                           /* 6 */

  16. mov_q x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)

  17. add x5, x5, x23                         // add KASLR displacement    /* 7 */

  18. create_pgd_entry x0, x5, x3, x6                                      /* 8 */

  19. adrp x6, _end                        // runtime __pa(_end)

  20. adrp x3, _text                       // runtime __pa(_text)

  21. sub x6, x6, x3                          // _end - _text

  22. add x6, x6, x5                          // runtime __va(_end)

  23. create_block_map x0, x7, x3, x5, x6                                  /* 9 */

  1. x0寄存器PGD页表基地址,这里是idmap_pg_dir,是为了创建identity mapping。

  2. adrp指令可以获取__idmap_text_start符号的实际运行物理地址。

  3. 填充PGD及中间level页表的页表项。

  4. 因为我们为了创建虚拟地址和物理地址相等的映射,因此这里的x5和x3值相等。

  5. 调用create_block_map创建identity mapping,注意这里传递的参数物理地址(x3)和虚拟地址(x5)相等。

  6. 创建kernel image mapping,PGD页表基地址是swapper_pg_dir。

  7. KASLR默认关闭的情况下,x23的值为0。

  8. 填充PGD及中间level页表的页表项。

  9. 填充PMD页表项。因为采用的是section mapping,所以一个页表项对应2M大小。注意汇编中的注释,va()代表得到的事虚拟地址,pa()得到的是物理地址。

经过以上初始化,页表就算是初始化完成。kernel映射区域从先行映射区域迁移到VMALLOC区域在哪里体现呢?答案就是KIMAGE_VADDR宏定义。KIMAGE_VADDR是kernel的虚拟地址。其定义在arch/arm64/mm/memory.h文件。

 
  1. #define VA_BITS         (CONFIG_ARM64_VA_BITS)

  2. #define VA_START        (UL(0xffffffffffffffff) - (UL(1) << VA_BITS) + 1)

  3. #define PAGE_OFFSET     (UL(0xffffffffffffffff) - (UL(1) << (VA_BITS - 1)) + 1)

  4. #define KIMAGE_VADDR    (MODULES_END)

  5. #define VMALLOC_START   (MODULES_END)

  6. #define VMALLOC_END     (PAGE_OFFSET - PUD_SIZE - VMEMMAP_SIZE - SZ_64K)

  7. #define MODULES_END     (MODULES_VADDR + MODULES_VSIZE)

  8. #define MODULES_VADDR   (VA_START + KASAN_SHADOW_SIZE)

  9. #define MODULES_VSIZE   (SZ_128M)

  10. #define VMEMMAP_START   (PAGE_OFFSET - VMEMMAP_SIZE)

  11. #define PCI_IO_END      (VMEMMAP_START - SZ_2M)

  12. #define PCI_IO_START    (PCI_IO_END - PCI_IO_SIZE)

  13. #define FIXADDR_TOP     (PCI_IO_START - SZ_2M)

  14. #define TASK_SIZE_64    (UL(1) << VA_BITS)

猜你喜欢

转载自blog.csdn.net/weixin_44942475/article/details/89736048