内存初始化

"node" {
    "label": "kernel_map",
    "categories": ["mem"],
    "info": "kernel mem map init",
    "depends": []
}

我们已经介绍过了一些基础知识,本节介绍内核启动阶段内存管理的相关内容

前置基础

汇编指令

adrp指令: ADRP Xd,label利用当前PClabel的相对地址,计算label 内存地址的4KB基址

    //如果PC 当前指令地址为 0x1000 0000 ; data 相对 0x1000 0000 的偏移是 0x1234,
    //可以得到data的地址为0x1000 1234,他的内存基址就是 0x1000 1000
    // X0的值就为  0x1000 1000
    ADRP  X0, data;

内核布局描述

内核的地址分布描述定义在: arch/arm64/include/asm/memory.h 假设当前配置: 4K页(CONFIG_PAGE_SHIFT=12) VA地址是48BIT(256TB)

    /* 
     * STRUCT_PAGE_MAX_SHIFT 定义了一个 管理页表结构(struct page)的大小
     * PAGE_SHIT 是页表大小位移(比如 4K是12 16K是14 64K是16)
     * VMEMMAP_SHIFT 是用于计算线性地址大小的除数
     * 举例: 为了管理4GB大小的线性地址,需要使用 4GB/4KB = 1024 个页表, 每个页表大小如果占1B, 需要 1024 * 1B 的内存 
     * 因此页表所占内存计算公式为 : 需要映射的内存大小/页大小*页表内存 =   需要映射的内存大小/ (页大小 - 页表内存) 
    */

    #define VMEMMAP_SHIFT   (PAGE_SHIFT - STRUCT_PAGE_MAX_SHIFT) // 目前是: 12(4KB) - 6(64B) = 6
    // 计算管理128TB(0xffff800000000000 - 0xffff000000000000) 的线性内存 页表条目需要使用的内存大小    
    #define VMEMMAP_SIZE    ((_PAGE_END(VA_BITS_MIN) - PAGE_OFFSET) >> VMEMMAP_SHIFT) // 目前是 128TB/4KB*64B=2TB  

    #define VA_BITS                 (CONFIG_ARM64_VA_BITS)                           
    #define _PAGE_OFFSET(va)        (-(UL(1) << (va)))      //内核地址起始地址  0xffff000000000000                        
    #define PAGE_OFFSET             (_PAGE_OFFSET(VA_BITS)) //内核地址起始地址  0xffff 0000 0000 0000                            
    #define KIMAGE_VADDR            (MODULES_END)   //kernel image的VA地址 位于modules 结束 0xffff800007ffffff                                     
    #define MODULES_END             (MODULES_VADDR + MODULES_VSIZE)   //modules结束地址 0xffff800007ffffff                   
    #define MODULES_VADDR           (_PAGE_END(VA_BITS_MIN))  //modules起始地址 0xffff800000000000                                 
    #define MODULES_VSIZE           (SZ_128M)  //modules大小 128M                                       
    #define VMEMMAP_START           (-(UL(1) << (VA_BITS - VMEMMAP_SHIFT))) // fffffc0000000000        
    #define VMEMMAP_END             (VMEMMAP_START + VMEMMAP_SIZE) // 2TB大小: fffffdffffffffff                 
    #define PCI_IO_END              (VMEMMAP_START - SZ_8M)                          
    #define PCI_IO_START            (PCI_IO_END - PCI_IO_SIZE)                       
    #define FIXADDR_TOP             (VMEMMAP_START - SZ_32M) 

    #define _PAGE_END(va)           (-(UL(1) << ((va) - 1)))

下图以VA 39BIT 和 4K页做演示

Screenshot

下图以VA48 BIT 和 4K页做演示

Screenshot

内核内存管理的一部分工作,就是负责管理不同区域的内存的分配、释放

一阶段:内核镜像1:l映射

内核镜像我们简单也可以理解为是一个二进制的文件,arch/arm64/kernel/vmlinux.lds.S主要定义了代码段的布局情况, SECTIONS 描述了段的定义也可以直接通过 readelf -d vmlinux 获取内核链接后的文件 查看布局情况

Screenshot

当uboot 加载完成内核,并且跳转到内核起始位置的时候,此时MMU处于未打开的状态,因此此时CPU在执行内核代码是直接访问的物理内存; 这段代码执行期间,严格意义上来说不能够访问类似于全局变量函数等会涉及到虚拟内存地址的代码,只应该通过相对跳转访问

内存初始化会分几个阶段,第一阶段,使能mmu,为了保证MMU使能后内存还能被正确解析,需要先把内核的镜像代码建立 VAPA的映射

Screenshot

为什么是线性1:1映射?因为PC在刚开启MMU的时候, PC的地址依然是原先物理内存地址 因此需要先建立虚存1:1的映射

线性映射的页表准备

初始化阶段 只有静态页表可以使用 页表预留 init_idmap_pg_dir

    //arch/arm64/kernel/vmlinux.lds.S
    init_idmap_pg_dir = .;
    . += INIT_IDMAP_DIR_SIZE;
    init_idmap_pg_end = .;

INIT_IDMAP_DIR_SIZE 需要的大小

#define INIT_IDMAP_DIR_PAGES    (EARLY_PAGES(INIT_IDMAP_PGTABLE_LEVELS, KIMAGE_VADDR, _end, 1))
#define INIT_IDMAP_DIR_SIZE   INIT_IDMAP_DIR_PAGES

第一次线性映射,使用了4级页表映射,编译阶段根据内核镜像大小静态计算出需要的 内存页数量INIT_IDMAP_DIR_PAGES

// 下面代码用于计算 虚拟内存需要多少的内存
#define EARLY_ENTRIES(vstart, vend, shift, add)  
((((vend) - 1) >> (shift)) - ((vstart) >> (shift)) + 1 + add)

#define EARLY_PGDS(vstart, vend, add) (EARLY_ENTRIES(vstart, vend, PGDIR_SHIFT, add)) 
#define EARLY_PAGES(vstart, vend, add) ( 1 /* PGDIR page */ \
 + EARLY_PGDS((vstart), (vend), add) /* each PGDIR needs a next level page table */ \
 + EARLY_PUDS((vstart), (vend), add) /* each PUD needs a next level page table */ \
 + EARLY_PMDS((vstart), (vend), add)) /* each PMD needs a next level page table */
 #define INIT_DIR_SIZE (PAGE_SIZE * EARLY_PAGES(KIMAGE_VADDR, _end, EARLY_KASLR))
 /* the initial ID map may need two extra pages if it needs to be extended */
 #if VA_BITS < 48
 #define INIT_IDMAP_DIR_SIZE ((INIT_IDMAP_DIR_PAGES + 2) * PAGE_SIZE)
 #else  
 #define INIT_IDMAP_DIR_SIZE (INIT_IDMAP_DIR_PAGES * PAGE_SIZE)
 #endif

#define INIT_IDMAP_DIR_PAGES EARLY_PAGES(KIMAGE_VADDR, _end + MAX_FDT_SIZE + SWAPPER_BLOCK_SIZE, 1)

感兴趣可以研究一下

映射关键函数: map_range

此函数用于内存页表映射,特点:

  • 此函数在填充页表时不会动态申请页表

  • 认为提供的静态页表内存是连续数组,总是假设下一级页表位于前一个页表下标之后

  • 会尽可能使用段映射

  • 场景假设会阐述上述设计理念

/**
   * map_range - Map a contiguous range of physical pages into virtual memory
   *
   * @pte:                Address of physical pointer to array of pages to
   *                      allocate page tables from
   * @start:              Virtual address of the start of the range
   * @end:                Virtual address of the end of the range (exclusive)
   * @pa:                 Physical address of the start of the range
   * @prot:               Access permissions of the range
   * @level:              Translation level for the mapping
   * @tbl:                The level @level page table to create the mappings in
   * @may_use_cont:       Whether the use of the contiguous attribute is allowed
   * @va_offset:          Offset between a physical page and its current mapping
   *                      in the VA space
   */     
  void __init map_range(u64 *pte, u64 start, u64 end, u64 pa, pgprot_t prot,
                        int level, pte_t *tbl, bool may_use_cont, u64 va_offset)

下面是一个用户态映射的模拟代码,感兴趣可以研究一下

场景1: 假设虚存地址范围是0x0 - 0x200000 (2MB)

Q: 使用3级页表段映射(entry=2MB),3张内存页表,每级页表只需要填充1个entry

场景2: 假设虚存地址范围是0x0 - 0x300000 (3MB)

Q: 前2MB内存同场景1,后1MB额外使用1张4级内存页表,3级页表额外需要填充1个entry指向4级页表。4级页表需要填充256个entry(4k*256)

场景3: 假设虚存地址范围是0x100000 - 0x300000 (2MB)

Q:3级页表需要填充2个entry指向两个4级页表。前1MB使用1张4级内存页表,4级页表需要填充256个entry(4k*256);后1MB同样使用后一个4级页表

#include <stdint.h>
#include <stdio.h>

typedef unsigned long u64;
typedef u64 pteval_t;
 typedef u64 pmdval_t;
 typedef u64 pudval_t;
 typedef u64 p4dval_t;
 typedef u64 pgdval_t;

typedef struct { pgdval_t pgd; } pgd_t;
typedef struct { pteval_t pte; } pte_t;
typedef struct { pteval_t pgprot; } pgprot_t;


#define PAGE_SHIFT 12
#define PAGE_SIZE  (u64)(1 << PAGE_SHIFT)

#define U64_MAX         ((u64)~0ULL)
#define PAGE_MASK       (~((1 << PAGE_SHIFT) - 1))
#define PTRS_PER_PTE    512

/* align addr on a size boundary - adjust address up/down if needed */
#define _ALIGN_UP(addr, size)   (((addr)+((size)-1))&(~((typeof(addr))(size)-1)))
#define _ALIGN_DOWN(addr, size) ((addr)&(~((typeof(addr))(size)-1)))

/* align addr on a size boundary - adjust address up if needed */
#define _ALIGN(addr,size)     _ALIGN_UP(addr,size)

#define PAGE_ALIGN(addr)        _ALIGN(addr, PAGE_SIZE)

#define __pte(x) ((pte_t) { (x) } )

#define pte_none(pte)           (!(pte).pte)

#define min(a, b) ((a) < (b) ? (a) : (b))

void  map_range(u64 *pte, u64 start, u64 end, u64 pa,
                        int level, pte_t *tbl)
  {
          int lshift = (3 - level) * (PAGE_SHIFT - 3);
          u64 lmask = (PAGE_SIZE << lshift) - 1;
          start   &= PAGE_MASK;
          pa      &= PAGE_MASK;
          int tbl_index =  (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;
      printf("start map va: %lx  to pa %lx  size: %lx\n", start, pa, end-start);
      printf("tbl-ori: %p  index is %d  tbl-now: %p  \n",tbl, tbl_index,tbl+tbl_index);
          /* Advance tbl to the entry that covers start */
          tbl += (start >> (lshift + PAGE_SHIFT)) % PTRS_PER_PTE;

          while (start < end) {
                u64 next = min((start | lmask) + 1, PAGE_ALIGN(end));
          printf("start : %lx  next: %lx pa %lx \n",start, next, pa);
          printf("need next map: %lx \n",(start| next| pa) &lmask);
                  if (level < 3 && (start | next | pa) & lmask) {
              printf("enter next map \n");
                          /*
                           * This chunk needs a finer grained mapping. Create a
                           * table mapping if necessary and recurse.
                           */
                          if (!((*tbl).pte)) {
                                  *tbl = __pte(*pte);
                                  *pte += PTRS_PER_PTE * sizeof(pte_t);
                          }
                          map_range(pte, start, next, pa, level + 1,
                                    (pte_t *)((*tbl).pte));
                  } else {
                   printf("direct set tlb\n");
                          /* Put down a block or page mapping */
                          *tbl = __pte(pa);
                  }
                  pa += next - start;
                  start = next;
                  tbl++;
          }
 }

int  create_init_idmap(pgd_t *pg_dir)
{
        u64 ptep = (u64)pg_dir + PAGE_SIZE;
        map_range(&ptep, 0x100000, 0x300000, 0x0,
                  0, (pte_t *)pg_dir);

        return ptep;
}


static pgd_t page_mem[512][512][512] __attribute__((aligned(4096)));
int main(){
    create_init_idmap(page_mem);
}

映射实现

  asmlinkage u64 __init create_init_idmap(pgd_t *pg_dir, pteval_t clrmask)
  {
          u64 ptep = (u64)pg_dir + PAGE_SIZE;
          pgprot_t text_prot = PAGE_KERNEL_ROX;
          pgprot_t data_prot = PAGE_KERNEL;

          pgprot_val(text_prot) &= ~clrmask;
          pgprot_val(data_prot) &= ~clrmask;

          map_range(&ptep, (u64)_stext, (u64)__initdata_begin, (u64)_stext,
                    text_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);
          map_range(&ptep, (u64)__initdata_begin, (u64)_end, (u64)__initdata_begin,
                    data_prot, IDMAP_ROOT_LEVEL, (pte_t *)pg_dir, false, 0);

          return ptep;
  }

线性映射阶段 主要映射了两个段的内存,

  • _stext - init_data_begin : 只读可执行权限映射

  • init_data_begin - end: 可读写权限

映射关系: 虚拟内存完全等于物理地址 1:1映射

调用位于arch/arm64/kernel/head.S

     SYM_CODE_START(primary_entry)
          bl      record_mmu_state
          bl      preserve_boot_args

          adrp    x1, early_init_stack
          mov     sp, x1
          mov     x29, xzr
          adrp    x0, init_idmap_pg_dir
          mov     x1, xzr
          bl      __pi_create_init_idmap          

MMU开启

映射建立完成后就要准备开启MMU,代码依然位于 head.S

    SYM_FUNC_START_LOCAL(__primary_switch)
        adrp    x1, reserved_pg_dir
        adrp    x2, init_idmap_pg_dir  //加载tTBR 基址为 init_idmap_pg_dir
        bl      __enable_mmu

内核非线性映射

在开启MMU之后,内核可以正常访问物理地址(此时虚存地址和物理地址相同),但是还不意味着可以访问内核镜像的链接地址0xffffffxxxxxxx(KIMAGE_VADDR)这类地址 因此需要第二次映射,映射内核地址到实际的链接地址

Screenshot

页表准备

此时使用新的页表init_pg_dir

. = ALIGN(PAGE_SIZE);
init_pg_dir = .;
. += INIT_DIR_SIZE;
init_pg_end = .;

页表大小和上一个小节的IDMAP_DIR_SIZE类似

 #define INIT_DIR_SIZE (PAGE_SIZE * (EARLY_PAGES(SWAPPER_PGTABLE_LEVELS, KIMAGE_VADDR, _end, EXTRA_PAGE) \
                                      + EARLY_SEGMENT_EXTRA_PAGES))

映射建立

//arch/arm64/kernel/pi/map_kernel.c
static void map_kernel(u64 kaslr_offset, u64 va_offset, int root_level)
  // 映射各个内核内存段
 -> map_segment 
  // 设置ttbr_el1
 ->idmap_cpu_replace_ttbr1(init_pg_dir);

最终我们的MMU 基址被设置为: init_pg_dir