Linux内核启动阶段虚实地址映射

前言

编译kernel的时候,会通过链接脚本指定编译出来的image的布局以及image的运行地址,这里说的运行地址,是从CPU的视角来说的,是一个虚拟地址。CPU执行代码的时候,要么是顺序执行,要么是跳转到其他地址去执行,跳转的话又分为相对跳转和绝对跳转,顺序执行和相对跳转实际上都是相对PC当前位置做一个偏移,这种指令是位置无关指令,而绝对跳转是明确指定跳转到某个地址去,是位置相关指令,位置相关指令要跳转到的地址,是基于上诉所说的运行地址的。

从位置相关指令和位置无关指令的定义可以知道,位置相关指令即使不放在运行地址处,也是可以执行的,但是位置相关指令,必须放在运行地址处,否则CPU是无法寻址到该指令的。ARM64默认指定的运行地址是:ffff000008080000,这是一个虚拟地址。那么问题来了,由于CPU刚跳转到kernel的时候,MMU是关闭的,CPU只能直接通过物理地址寻址,那么如何保证CPU每次寻址都能找到正确的数据呢,这个其实很简单,只要保证在MMU开启之前所有的指令都是位置无关指令就可以了,一方面所有的跳转指令必须是相对跳转,另一方面,所有的load和store指令必须操作物理地址。在打开MMU之前,需要为虚拟地址到物理地址转换创建页表,虚拟地址的来源就是编译时指定的运行地址,而物理地址的来源更加简单,因为此时CPU寻址用的都是物理地址,所以很容易找到kernel的物理地址,详情下面会介绍。

宏定义

CPU不可能一直执行位置无关指令,所以必须将kernel本身的虚拟地址(运行地址)映射到它实际所在的物理地址,这个通过为MMU创建页表来实现,一旦页表创建完成,并打开了MMU,CPU发出的虚拟地址,可以由MMU转换成实际的物理地址,这样CPU执行kernel代码将不再受限。
在分析代码之前,有几个宏定义需要搞清楚:

  • KIMAGE_VADDR

定义:arch/arm64/include/asm/memory.h
#define KIMAGE_VADDR (MODULES_END)

Kernel Image的起始虚拟地址。
对于ARM64来说,虚拟地址的寻址能力一般是48 bits,当然也支持其他的寻址能力,我们这里按48 bits来作说明。按48 bits的寻址能力,分配给内核的起始虚拟地址是0xffff000000000000,前面会留一部分另做他用,如给KASAN,MODULE模块使用,后面才分给kernel image。我们这里假设不支持KASAN,则给MODULE预留128M的空间,128M以后给kernel image使用,也就是说kernel image的起始地址是0xffff000008000000。

  • TEXT_OFFSET

定义:arch/arm64/Makefile
TEXT_OFFSET := 0x00080000

为了简单起见,我们假设CONFIG_ARM64_RANDOMIZE_TEXT_OFFSET没有定义,那么TEXT_OFFSET就固定为0x00080000,这是kernel image的代码段偏移。所以kernel image的代码段地址是0xffff000008080000

  • KERNEL_START

定义:arch/arm64/include/asm/memory.h
#define KERNEL_START _text

_text定义在链接脚本arch/arm64/kernel/vmlinux.lds.S中,是代码段的起始地址,它的绝对地址是0xffff000008080000。链接脚本定义了kernel image的layout,如果当前PC指针存的是物理地址,对_text相对寻址,可以得到_text的物理地址,也就是kernel image代码段的起始物理地址。

  • KERNEL_END

定义:arch/arm64/include/asm/memory.h
#define KERNEL_END _end

_end定义在链接脚本中arch/arm64/kernel/vmlinux.lds.S中,是kernel image的结束地址,kernel image出来包括代码段之外,还包括其他段,如BSS段,数据段等等,而_end是整个kernel image的结束地址。

  • __PHYS_OFFSET

定义:arch/arm64/kernel/head.S
#define __PHYS_OFFSET (KERNEL_START - TEXT_OFFSET)

这个用于计算kernel image的起始地址,起始地址加上TEXT_OFFSET就是代码段的物理地址。

代码分析

下面分别创建页表的具体代码,为了减少代码量,非48 bits VA情况下的代码被删掉了。

__create_page_tables:
    mov x28, lr

    /*
     * Invalidate the idmap and swapper page tables to avoid potential
     * dirty cache lines being evicted.
     */
    adrp    x0, idmap_pg_dir
    adrp    x1, swapper_pg_dir + SWAPPER_DIR_SIZE
    bl  __inval_cache_range

    /*
     * Clear the idmap and swapper page tables.
     */
    adrp    x0, idmap_pg_dir
    adrp    x6, swapper_pg_dir + SWAPPER_DIR_SIZE
1:  stp xzr, xzr, [x0], #16
    stp xzr, xzr, [x0], #16
    stp xzr, xzr, [x0], #16
    stp xzr, xzr, [x0], #16
    cmp x0, x6
    b.lo    1b

    mov x7, SWAPPER_MM_MMUFLAGS

    /*
     * Create the identity mapping.
     */
    adrp    x0, idmap_pg_dir
    adrp    x3, __idmap_text_start      // __pa(__idmap_text_start)

    create_pgd_entry x0, x3, x5, x6
    mov x5, x3              // __pa(__idmap_text_start)
    adr_l   x6, __idmap_text_end        // __pa(__idmap_text_end)
    create_block_map x0, x7, x3, x5, x6

    /*
     * Map the kernel image (starting with PHYS_OFFSET).
     */
    adrp    x0, swapper_pg_dir
    mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
    add x5, x5, x23         // add KASLR displacement
    create_pgd_entry x0, x5, x3, x6
    adrp    x6, _end            // runtime __pa(_end)
    adrp    x3, _text           // runtime __pa(_text)
    sub x6, x6, x3          // _end - _text
    add x6, x6, x5          // runtime __va(_end)
    create_block_map x0, x7, x3, x5, x6

    /*
     * Since the page tables have been populated with non-cacheable
     * accesses (MMU disabled), invalidate the idmap and swapper page
     * tables again to remove any speculatively loaded cache lines.
     */
    adrp    x0, idmap_pg_dir
    adrp    x1, swapper_pg_dir + SWAPPER_DIR_SIZE
    dmb sy
    bl  __inval_cache_range

    ret x28
ENDPROC(__create_page_tables)

我们先对这部分代码的一些lable作一些说明,然后再来分别代码流程。

  • idmap_pg_dir

该lable定义了页表的起始地址,该页表用于为.idmap.text段的代码做虚实地址转换。.idmap.text段的代码有两种虚实地址转换机制,第一种是虚拟地址跟物理地址一致,第二种是编译时指定的虚拟地址转换到物理地址,这两种机制同时工作,也就是说,MMU开启之后,.idmap.text段的指令可以通过物理地址寻址到,也可以通过虚拟地址寻址到。那么为什么会有这种需求呢?试想一下这种情形,软重启的时候,我们希望PC指针载入的是物理地址,虚拟地址到物理地址的映射在启动过程中动态建立,这时候,负责重启的代码就必须可以通过物理地址寻址。

idmap_pg_dir存放的就是物理地址到物理地址的转换(其实是虚拟地址和物理地址一致),定义在链接脚本arch/arm64/kernel/vmlinux.lds.S中,其大小3个页(加上使用4K大小的页),采用三个页的意思是使用三级转换,建立三级页表PGD,PUD ,PMD,这样可以覆盖1G的虚拟地址空间。如果采用四级转换,建立四级页表PGD,PUD,PMD,PTE,则只能覆盖2M的虚拟地址空间,对于.idmap.text来说,2M足够了,可是由于整个kernel image也按照相同的方式建立转换表,2M对kernel来说,是不够的。

  • swapper_pg_dir

该lable定义了另外一个页表的起始地址,该页表用于为整个kernel image做虚拟地址到物理地址的转换。它也是定义在链接脚本arch/arm64/kernel/vmlinux.lds.S中,大小是3个页。
下面开始分析关键代码:

    adrp    x0, idmap_pg_dir
    adrp    x3, __idmap_text_start      // __pa(__idmap_text_start)

    create_pgd_entry x0, x3, x5, x6
    mov x5, x3              // __pa(__idmap_text_start)
    adr_l   x6, __idmap_text_end        // __pa(__idmap_text_end)


第一步:通过相对寻址获得页表的物理地址和.idmap.text段的物理地址,因为此时此刻,PC指针里面存放的是物理地址,那么相对寻址得到的也是物理地址。
第二步:创建页表,调用create_pgd_entry创建页表,这里是创建前两级页表,即PGD,PUD页表。这两级页表的建立非常简单,只需要提供页表物理地址及代码段的虚拟地址(当前上下文这里是代码段的物理地址)即可。
第三步:获得.idmap.text段的起始虚拟地址和结束虚拟地址,这里其实是物理地址。
第四步:创建物理地址到虚拟地址的映射,调用create_block_map,来创建PMD页表,每一条PMD表项覆盖2M的地址空间,对于.idmap.text代码段来说,一条PMD表项即可,而对于整个kernel image来说,可能需要若干条PMD表项才能覆盖完整的kernel image。

    adrp    x0, swapper_pg_dir
    mov_q   x5, KIMAGE_VADDR + TEXT_OFFSET  // compile time __va(_text)
    add x5, x5, x23         // add KASLR displacement
    create_pgd_entry x0, x5, x3, x6
    adrp    x6, _end            // runtime __pa(_end)
    adrp    x3, _text           // runtime __pa(_text)
    sub x6, x6, x3          // _end - _text
    add x6, x6, x5          // runtime __va(_end)
    create_block_map x0, x7, x3, x5, x6


第一步:通过相对寻址获得页表的物理地址。
第二步:获得kernel image代码段的虚拟地址,该地址在编译时指定。
第三步:调用create_pgd_entry创建PGD,PMD页表。
第四步:获得kernel image的起始虚拟地址和结束虚拟地址,以及起始物理地址。
第五步:建立虚拟地址到物理地址的映射。

至此,启动阶段的页表建立完成,但是由于目前并没有开启MMU,同时页表的物理地址并没有填充给TTBR0和TTBR1,所以在开启MMU之前,依然不能使用虚拟地址,只能使用物理地址。开始了MMU之后,可以使用虚拟地址。

猜你喜欢

转载自blog.csdn.net/liuhangtiant/article/details/80219994