ARM64的启动过程之(二):创建启动阶段的页表

作者:linuxer 发布于:2015-10-13 18:18 分类:ARMv8A Arch

一、前言

本文主要描述了ARM64启动过程中,如何建立初始化阶段页表的过程。我们知道,从bootloader到kernel的时候,MMU是off的(顺带的负作用是无法打开data cache),为了提高性能,加快初始化速度,我们必须某个阶段(越早越好)打开MMU和cache,而在此之前,我们必须要设定好页表。

在初始化阶段,我们mapping三段地址,一段是identity mapping,其实就是把物理地址mapping到物理地址上去,在打开MMU的时候需要这样的mapping(ARM ARCH强烈推荐这么做的)。第二段是kernel image mapping,内核代码欢快的执行当然需要将kernel running需要的地址(kernel txt、dernel rodata、data、bss等等)进行映射了,第三段是blob memory对应的mapping。

在本文中,我们会混用下面的概念:page table和translation table、PGD和Level 0 translation table、PUD和Level 1 translation table、PMD和Level 2 translation table、Page Table和Level 3 translation table。最后,还是说明一下,本文来自4.1.10内核(部分来自4.4.6),有兴趣的读者可以下载来对照阅读本文。

二、基础知识

为了更好的理解__create_page_tables的代码,我们需要准备一些基础知识。由于ARM64太复杂了,各种exception level、各种stage translation、各种地址宽度配置等等让虚拟地址到物理地址的映射变得非常复杂,因此,本文focus在一种配置:Non-secure EL1和EL0、stage 1 translation、VA和PA的地址宽度都是48个bit。

1、虚拟地址空间的size是多少?

在32-bit的ARM时代,这个问题问的有点白痴,大家都是耳熟能详的一句话就是每一个进程都有4G的独立的虚拟地址空间(0x0~0xffffffff)。对于ARM64而言,进程需要完全使用2^64那么多的虚拟地址空间吗?如果需要,那么CPU的MMU单元需要能接受来自处理器发出的64根地址线的输入信号,并对其进行翻译,这也就意味着MMU需要更多的晶体管来支持64根地址线的输入,而CPU也需要驱动更多的地址线,但是实际上,在短期内,没有看出有2^64那么多的虚拟地址空间的需求,因此,ARMv8实际上提供了TCR_ELx (Translation Control Register (ELx)可以对MMU的输入地址(也就是虚拟地址)进行配置。为了不把问题复杂化,我们先不考虑TCR_EL2和TCR_EL3这两个寄存器。通过TCR_EL1寄存器中的TxSZ域可以控制虚拟地址空间的size。对于ARM64(是指处于AArch64状态的处理器)而言,最大的虚拟地址的宽度是48 bit,因此虚拟地址空间的范围是0x0000_0000_0000_0000 ~ 0x0000_FFFF_FFFF_FFFF,总共256TB。 当然,具体实现的时候可以选择如下的地址线数目:

config ARM64_VA_BITS
    int
    default 36 if ARM64_VA_BITS_36
    default 39 if ARM64_VA_BITS_39
    default 42 if ARM64_VA_BITS_42
    default 47 if ARM64_VA_BITS_47
    default 48 if ARM64_VA_BITS_48

在代码中,有一个宏定义如下:

#define VA_BITS            (CONFIG_ARM64_VA_BITS)

这个宏定义了虚拟地址空间的size。

2、物理地址空间的size是多少?

问过虚拟地址空间的size是多少这个问题之后,很自然的会考虑物理地址空间。基本概念和上一节类似,符合ARMv8的PE最大支持的物理地址宽度也是48个bit,当然,具体的实现可以自己定义(不能超过48个bit),具体的配置可以通过ID_AA64MMFR0_EL1 (AArch64 Memory Model Feature Register 0)这个RO寄存器获取。

3、和地址映射相关的宏定义

宏定义符号 描述
VA_START 内核地址空间的起始地址
TEXT_OFFSET bootloader会把kernel image从外设copy到RAM中,那么具体copy到什么位置呢?从RAM的起始地址开始吗?实际上是从TEXT_OFFSET开始的,偏移这么一小段内存估计是为了bootloader和kernel之间传递一些信息。所以,这里TEXT是指kernel text segment,而OFFSET是相对于RAM的首地址而言的。
TEXT_OFFSET必须要4K对齐并且TEXT_OFFSET的size不能大于2M。
PAGE_OFFSET kernel image的起始虚拟地址,一般而言也就是系统中RAM的首地址,在该地址TEXT_OFFSET之后保存了kernel image。
PAGE_OFFSET必须要2M对齐
TASK_SIZE 一般而言,用户地址空间从0开始,大小就是TASK_SIZE,因此,这个宏定义的全称应该是task userspace size。对于ARM64的用户空间进程而言,有两种,一种是运行在AArch64状态下,另外一种是运行在AArch32状态,因此,实际上代码中又定义了TASK_SIZE_32和TASK_SIZE_64两个宏定义。
PHYS_OFFSET 系统内存的起始物理地址。在系统初始化的过程中,会把PHYS_OFFSET开始的物理内存映射到PAGE_OFFSET的虚拟内存上去。

 

4、虚拟地址空间到物理地址空间的映射

MMU主要负责从VA(virutal address)到PA(Physical address)的翻译、memory的访问控制以及memory attribute的控制,这里我们暂时只关注地址翻译功能。不同的exception level和security state有自己独立的地址翻译过程,当然我们这里暂时只关注Non-secure EL1和EL0,在这种状态下,地址翻译可以分成两个stage,不过两个stage是为虚拟化考虑的,因此,为了简化问题,我们先只考虑一个stage。OK,做了这么多的简化之后,我们可以来看看地址翻译过程了(也就是Non-secure EL1和EL0 stage 1情况下的地址翻译过程)。

一个很有意思的改变(针对ARM32而言)是虚拟地址空间被分成了两个VA subrange:

Lower VA subrange : 从0x0000_0000_0000_0000 到 (2^(64-T0SZ) - 1)
Upper VA subrange : 从(2^64 - 2^(64-T1SZ)) 到 0xFFFF_FFFF_FFFF_FFFF

为什么呢?熟悉ARM平台的工程师都形成了固定的印象,当进程切换地址空间的时候,实际上切换了内核地址空间+用户地址空间(total 4G地址空间),而实际上,每次进程切换的时候,内核地址空间都是不变的,实际变化的只有userspace而已。如果硬件支持了VA subrange,那么我们可以这样使用:

Lower VA subrange : process-specific地址空间
Upper VA subrange : kernel地址空间

这样,当进程切换的时候,我们不必切换kernel space,只要切换userspace就OK了。

地址映射的粒度怎么配置呢?地址映射的粒度用通俗的语言讲就是page size(也可能是block size),传统的page size都是4K,ARM64的MMU支持4K、16K和64K的page size。除了地址映射的粒度还有一个地址映射的level的概念,在ARM32的时代,2 level或者3 level是比较常见的配置,对于ARM64,这和page size、物理地址和虚拟地址的宽度都是有关系的,具体请参考ARM ARM文档。

5、AArch64 Linux中虚拟地址空间的布局

把事情搞的太复杂了往往迷失了重点,我们这里再做一个简化就是固定page size是4K,并且VA宽度是48个bit,在这种情况下,虚拟地址空间的布局如下:

address space

具体的映射过程如下:

mapping level

整个地址翻译的过程是这样的:首先通过虚拟地址的高位可以知道是属于userspace还是kernel spce,从而分别选择TTBR0_EL1(Translation Table Base Register 0 (EL1))或者TTBR1_EL1(Translation Table Base Register 1 (EL1))。这个寄存器中保存了PGD的基地址,该地址指向了一个lookup table,每一个entry都是描述符,可能是Table descriptor、block descriptor或者是page descriptor。如果命中了一个block descriptor,那么地址翻译过程就结束了,当然对于4-level的地址翻译过程,PGD中当然保存的是Table descriptor,从而指向了下一节的Translation table,在kernel中称之PUD。随后的地址翻译概念类似,是一个PMD过程,最后一个level是PTE,也就是传说中的page table entry了,到了最后的地址翻译阶段。这时候PTE中都是一个个的page descriptor,完成最后的地址翻译过程。

三、代码分析

本文涉及的代码就是__create_page_tables这个函数。

1、initial translation tables的位置。

initial translation tables定义在链接脚本文件中(参考arch/arm64/kernel下的vmlinux.lds.S),如下:

. = ALIGN(PAGE_SIZE);
idmap_pg_dir = .;
. += IDMAP_DIR_SIZE;
swapper_pg_dir = .;
. += SWAPPER_DIR_SIZE;

ARM32的的时候,kernel image在RAM开始的位置让出了32KB的memory保存了bootloader到kernel传递的tag参数以及内核空间的页表。在刚开始的时候,ARM64沿用了ARM32的做法,将这些初始页表放到了PHYS_OFFSET和PHYS_OFFSET+TEXT_OFFSET之间(size是0x80000)。但是,其实这段内存是有可能被bootloader使用的,而且,这个时候,memory block模块(确定内核需要管理的memory block)没有ready,想要标记reservation memory也是不可能的。在这种情况下,假设bootloader在这段memory放了些数据,试图传递给kernel,但是kernel如果在这段memory上建立页表,那么就把有用数据给覆盖了。最后,initial translation tables被放到了kernel image的后面,位于bss段之后,从而解决了这个问题。

解决了位置问题之后,我们来看一看size,代码如下:

#if ARM64_SWAPPER_USES_SECTION_MAPS
#define SWAPPER_PGTABLE_LEVELS    (CONFIG_PGTABLE_LEVELS - 1)
#define IDMAP_PGTABLE_LEVELS    (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT) - 1)
#else
#define SWAPPER_PGTABLE_LEVELS    (CONFIG_PGTABLE_LEVELS)
#define IDMAP_PGTABLE_LEVELS    (ARM64_HW_PGTABLE_LEVELS(PHYS_MASK_SHIFT))
#endif

#define SWAPPER_DIR_SIZE    (SWAPPER_PGTABLE_LEVELS * PAGE_SIZE)
#define IDMAP_DIR_SIZE        (IDMAP_PGTABLE_LEVELS * PAGE_SIZE)

ARM64_SWAPPER_USES_SECTION_MAPS这个宏定义是说明了swapper/idmap的映射是否使用section map。什么是section map呢?我们用一个实际的例子来描述。假设VA是48 bit,page size是4K,那么,在地址映射过程中,地址被分成9(level 0) + 9(level 1) + 9(level 2) + 9(level 3) + 12(page offset),对于kernel image这样的big block memory region,使用4K的page来mapping有点得不偿失,在这种情况下,可以考虑让level 2的Translation table entry指向一个2M 的memory region,而不是下一级的Translation table。所谓的section map就是指使用2M的为单位进行映射。当然,不是什么情况都是可以使用section map,对于kernel image,其起始地址是2M对齐的,因此block size是2M的情况下才OK,对于PAGE SIZE是16K,其Block descriptor指向了一个32M的内存块,PAGE SIZE是64K的时候,Block descriptor指向了一个512M的内存块,因此,只有4K page size的情况下,才可以启用section map。


OK,我们回到具体的初始阶段页表大小这个问题上。原来ARM32的时候,一个page就OK了,对于ARM64,由于虚拟地址空间变大了,因此我们需要更多的page来完成启动阶段的initial translation tables的构建。我们仍然用VA是48 bit,page size是4K为例子进行说明。根据前面的描述,我们知道,内核空间的地址大小是256T,48 bit的地址被分成9 + 9 + 9 + 9 + 12,因此PGD(Level 0)、PUD(Level 1)、PMD(Level 2)、PT(Level 3)的translation table中的entry都是512项,每个描述符是8个byte,因此这些translation table都是4KB,恰好是一个page size。根据链接脚本中的定义,idmap和swapper page tables (或者叫做translation table)分别保留了3个page的页面。3个page分别是3个level的translation table。等等,读者可能会问:上面不是说48 bit VA加上4K page size需要4阶translation table吗?这里怎么只有3个level?实际上,3级映射是PGD/PUM/PMD(每个table占据一个page),只不过PMD的内容不是下一级的table descriptor,而是基于2M block的mapping(或者说PMD中的描述符是block descriptor)。

2、创建页表前的准备

  代码如下:

__create_page_tables:
    adrp    x25, idmap_pg_dir ------获取idmap的页表基地址(物理地址)
    adrp    x26, swapper_pg_dir -----获取kernel space的页表基地址(物理地址)
    mov    x27, lr ------保存lr


    mov    x0, x25 ----------准备要invalid cache的地址段的首地址
    add    x1, x26, #SWAPPER_DIR_SIZE -------准备要invalid cache的地址段的尾地址
    bl    __inval_cache_range ----将idmap和swapper页表地址段对应的cacheline设定为无效

    mov    x0, x25 -------这一段代码是将idmap和swapper页表内容设定为0
    add    x6, x26, #SWAPPER_DIR_SIZE ----x0是开始地址,x6是结束地址
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

这段代码没有什么特别要说明的,除了adrp这条指令。adrp是计算指定的符号地址到run time PC值的相对偏移(不过,这个offset没有那么精确,是以4K为单位,或者说,低12个bit是0)。在指令编码的时候,立即数(也就是offset)占据21个bit,此外,由于偏移计算是按照4K进行的,因此最后计算出来的符号地址必须要在该指令的-4G和4G之间。由于执行该指令的时候,还没有打开MMU,因此通过adrp获取的都是物理地址,当然该物理地址的低12个bit是全零的。此外,由于在链接脚本中idmap_pg_dir和swapper_pg_dir是page size aligned,因此使用adrp指令也是OK的。

为什么要调用__inval_cache_range来invalidate idmap_pg_dir和swapper_pg_dir对应页表空间的cache呢?根据boot protocol,代码执行到此,对于cache的要求是kernel image对应的那段空间的cache line是clean到PoC的,不过idmap_pg_dir和swapper_pg_dir对应页表空间不属于kernel image的一部分,因此其对应的cacheline很可能有一些旧的,无效的数据,必须要清理掉。

顺便再提一句,将idmap和swapper页表内容设定为0是有意义的。实际上这些translation table中的大部分entry都是没有使用的,PGD和PUD都是只有一个entry是有用的,而PMD中有效的entry数目是和mapping的地址size有关。将页表内容清零也就是意味着将页表中所有的描述符设定为invalid(描述符的bit 0指示是否有效,等于0表示无效描述符)。

3、创建identity mapping

identity mapping实际上就是建立了整个内核(从KERNEL_START到KERNEL_END)的一致性mapping,就是将物理地址所在的虚拟地址段mapping到物理地址上去。为什么这么做呢?ARM ARM文档中有一段话:

If the PA of the software that enables or disables a particular stage of address translation differs from its VA, speculative instruction fetching can cause complications. ARM strongly recommends that the PA and VA of any software that enables or disables a stage of address translation are identical if that stage of translation controls translations that apply to the software currently being executed.

由于打开MMU操作的时候,内核代码欢快的执行,这时候有一个地址映射ON/OFF的切换过程,这种一致性映射可以保证在在打开MMU那一点附近的程序代码可以平滑切换。具体的操作分成两个阶段,第一个阶段是通过create_pgd_entry建立中间level(也就是PGD和PUD)的描述符,第二个阶段是创建PMD的描述符,由于PMD的描述符是block descriptor,因此,完成PMD的设定后就完成了整个identity mapping页表的设定。具体代码如下:

    ldr    x7, =MM_MMUFLAGS 
    mov    x0, x25---------x0保存了idmap_pg_dir变量的物理地址
    adrp    x3, KERNEL_START---x3保存了内核image的物理地址

    create_pgd_entry x0, x3, x5, x6
    mov    x5, x3                // __pa(KERNEL_START)
    adr_l    x6, KERNEL_END            // __pa(KERNEL_END)
    create_block_map x0, x7, x3, x5, x6

create_pgd_entry用来在PGD(level 0 translation table)中创建一个描述符,如果需要下一级的translation table,也需要同时建立,最终的要求是能够完成所有中间level的translation table的建立(其实每个table中都是只建立了一个描述符),对于identity mapping,这里需要PGD和PUD就OK了。该函数需要四个参数:x0是pgd的地址,具体要创建哪一个地址的描述符由x3指定,x5和x6是临时变量,create_pgd_entry具体代码如下:

    .macro    create_pgd_entry, tbl, virt, tmp1, tmp2
    create_table_entry \tbl, \virt, PGDIR_SHIFT, PTRS_PER_PGD, \tmp1, \tmp2
    create_table_entry \tbl, \virt, TABLE_SHIFT, PTRS_PER_PTE, \tmp1, \tmp2
    .endm

create_table_entry这个宏定义主要是用来创建一个translation table的描述符,具体创建哪一个level的Translation table descriptor是由tbl参数指定的。怎么来创建描述符呢?如果是table descriptor,那么该描述符需要指向下一级页表基地址,当然,create_table_entry参数并没有给出,是在程序中hardcode实现:L(n)的translation table中的描述符指向的L(n+1) Translation table位于L(n)translation table所在page的下一个page(太拗口了,但是我也懒得画图了)。shift和ptrs这两个参数用来计算页表内的index,具体算法可以参考下面的代码:

.macro    create_table_entry, tbl, virt, shift, ptrs, tmp1, tmp2
lsr    \tmp1, \virt, #\shift----------------------------(1)
and    \tmp1, \tmp1, #\ptrs - 1 -------------------------(2)
add    \tmp2, \tbl, #PAGE_SIZE------------------------(3)
orr    \tmp2, \tmp2, #PMD_TYPE_TABLE--------------------(4)
str    \tmp2, [\tbl, \tmp1, lsl #3]------------------------(5)
add    \tbl, \tbl, #PAGE_SIZE -------------------------(6)
.endm

(1)如果是PGD,那么shift等于PGDIR_SHIFT,也就是39了。根据第二章的描述,我们知道L0 index(PGD index)使用虚拟地址的bit[47:39]。如果是PUD,那么shift等于PUD_SHIFT,也就是30了(注意:L1 index(PUD index)使用虚拟地址的bit[38:30])。要想找到virt这个地址(实际传入的是物理地址,当然,我们本来就是要建立和物理地址一样的虚拟地址的mapping)在translation table中的index,当然需要右移shift个bit了。

(2)除了右移操作,我们还需要mask操作(ptrs - 1实际上就是掩码)。对于PGD,其index占据9个bit,因此mask是0x1ff。同样的,对于PUD,其index占据9个bit,因此mask是0x1ff。至此,tmp1就是virt地址在translation table中对应的index了。

(3)如果是table描述符,需要指向另外一个level的translation table,在哪里呢?答案就是next page,读者可以自行回忆链接脚本中的3个连续的idmap_pg_dir的page定义。

(4)光有下一级translation table的地址不行,还要告知该描述符是否有效(set bit 0),该描述符的类型是哪一种类型(set bit 1表示是table descriptor),至此,描述符内容准备完毕,保存在tmp2中

(5)最关键的一步,将描述符写入页表中。之所以有“lsl #3”操作,是因为一个描述符占据8个Byte。

(6)将translation table的地址移到next level,以便进行下一步设定。

如果你足够细心,一定不会忽略这样的一个细节。获取KERNEL_START和KERNEL_END的代码是不一样的,对于KERNEL_START直接使用了adrp    x3, KERNEL_START,而对于KERNEL_END使用了adr_l    x6, KERNEL_END。具体使用哪一个是和该地址是否4K对齐相关的。KERNEL_START一定是4K对齐的,而KERNEL_END就不一定了,虽然在4.1.10中KERNEL_END也是4K对齐的,不过没有任何协议保证这一点,为了保险起见,代码使用了adr_l,确保获取正确的KERNEL_END的物理地址。

回到create_pgd_entry函数中,这个函数填充了内核image首地址对应的1G memory range所需要的Translation table描述符,听起来很吓人,不过就是两个描述符,一个是在PGD中,另外一个是在PUD中。虽然只有两个描述符,可以可以支持1G虚拟地址的mapping了。当然具体mapping多少(PMD中有多少entry),还是要看kernel image的size了。

OK,来到PMD部分的设定了,我们看看代码:

    .macro    create_block_map, tbl, flags, phys, start, end
    lsr    \phys, \phys, #BLOCK_SHIFT
    lsr    \start, \start, #BLOCK_SHIFT
    and    \start, \start, #PTRS_PER_PTE - 1    // table index
    orr    \phys, \flags, \phys, lsl #BLOCK_SHIFT    // table entry
    lsr    \end, \end, #BLOCK_SHIFT
    and    \end, \end, #PTRS_PER_PTE - 1        // table end index
9999:    str    \phys, [\tbl, \start, lsl #3]        // store the entry
    add    \start, \start, #1            // next entry
    add    \phys, \phys, #BLOCK_SIZE        // next block
    cmp    \start, \end
    b.ls    9999b
    .endm

create_block_map的名字起得不错,该函数就是在tbl指定的Translation table中建立block descriptor以便完成address mapping。具体mapping的内容是将start 到 end这一段VA mapping到phys开始的PA上去。其实这里的代码逻辑和上面类似,我们这里就不详述,需要提及的是PTE已经进入了最后一个level的mapping,因此描述符中除了地址信息之外(占据bit[47:21],还需要memory attribute和memory accesse的信息。对于这个场景,PMD中是block descriptor,因此描述符中还包括了block attribute域,分成upper block attribute[63:52]和lower block attribute[11:2]。对这些域的定义如下:

attribute

在代码中,block attribute是通过flags参数传递的,MM_MMUFLAGS定义如下:

#define MM_MMUFLAGS    PMD_ATTRINDX(MT_NORMAL) | PMD_FLAGS

#define PMD_FLAGS    PMD_TYPE_SECT | PMD_SECT_AF | PMD_SECT_S

MT_NORMAL表示该段内存的memory type是普通memory(对应AttrIndx[2:0]),而不是device什么的。PMD_TYPE_SECT 说明该描述符是一个有效的(bit 0等于1)的block descriptor(bit 1等于0)。PMD_SECT_AF中的AF是access flag的意思,表示该memory block(或者page)是否被最近被访问过。当然,这需要软件的协助。如果该bit被设置为0,当程序第一次访问的时候会产生异常,软件需要将给bit设置为1,之后再访问该page的时候,就不会产生异常了。不过当软件认为该page已经old enough的时候,也可以clear这个bit,表示最近都没有访问该page。这个flag是硬件对page reclaim算法的支持,找到最近不常访问的那些page。当然在这个场景下,我们没有必要enable这个特性,因此将其设定为1。PMD_SECT_S对应SH[1:0],描述memory的sharebility。这些内容和memory attribute相关,我们会在后续的文档中描述,这里就不偏离主题了。

广大人民群众最关心的当然也是最熟悉的是memory access control,这是通过AP[2:1]域来控制的。这里该域被设定为00b,表示EL1状态下是RW,EL0状态不可访问。UXN和PXN是用来控制可执行权限的,这里UXN和PXN都是0,表示EL1和EL0状态下都是excutable的。

4、创建kernel space mapping

要创建kernel space的页表了,遇到的第一个问题就是:mapping多少呢?kernel space辣么大,256T,不可能全部都mapping。OK,答案就是创建两部分的页表,一个从kernel image的开始地址(包括开始的那一段TEXT_OFFSET的保留区域)到kernel image的结束地址(内核的正常运行需要这段mapping),这一段覆盖了内核的正文段、各种data段、bss段、各种奇奇怪怪段等。还有一个就是bootloader传递过来的blob memory对应的页表。我们先看第一段kernel image的mapping:

mov    x0, x26-------------------------(1)
mov    x5, #PAGE_OFFSET-------------------(2)
create_pgd_entry x0, x5, x3, x6-----------------(3)
ldr    x6, =KERNEL_END------end address
mov    x3, x24                // phys offset
create_block_map x0, x7, x3, x5, x6---------------(4)

(1)swapper_pg_dir其实就是swapper进程(pid等于0的那个,其实就是idle进程)的地址空间,这时候,x0指向了内核地址空间的PGD的基地址。

(2)PAGE_OFFSET是kernel image的首地址,对于48bit的VA而言,该地址是0xffff8000-00000000。

(3)创建PAGE_OFFSET(即kernel image首地址)对应的PGD和PUD中的描述符。

(4)创建PMD中的描述符。x24保存了__PHYS_OFFSET,实际上也就是kernel image的首地址(物理地址)。

完成了kernel image的mapping,我们来看看blob mapping的建立。由于ARM64 boot protocol要求blob必须在内核空间开始的512MB内(同时要求8字节对齐,dtb image不能越过2M section size的边界),因此实际上PGD和PUD都不需要建立了,只要建立PMD的描述符就OK了。对应的PMD描述符的建立代码如下:

mov    x3, x21--------------FDT phys address
and    x3, x3, #~((1 << 21) - 1) ------2MB aligned
mov    x6, #PAGE_OFFSET-------kernel space start virtual address
sub    x5, x3, x24------------subtract kernel space start physical address

tst    x5, #~((1 << 29) - 1) --------within 512MB?
csel    x21, xzr, x21, ne ---------bad blob parameter and zero the FDT pointer
b.ne    1f
add    x5, x5, x6 ------------x5 equal blob virtual address
add    x6, x5, #1 << 21 ---------mapping 2M size
sub    x6, x6, #1 
create_block_map x0, x7, x3, x5, x6---create blob block descriptor in PMD

5、收尾

    mov    x0, x25-------再次invalid上文中建立page table memory对应的cache
    add    x1, x26, #SWAPPER_DIR_SIZE
    dmb    sy
    bl    __inval_cache_range

    mov    lr, x27------恢复lr
    ret-----------返回
ENDPROC(__create_page_tables)

由于页表中写了新的内容,而且是在没有打开cache的情况下写的,这时候,cache line的数据有可能被speculatively load,因此再次invalid是一个比较保险的做法。

四、参考文献

1、Documentation/arm64/memory.txt

2、ARM Architecture Reference Manual


change log:

1、2015-12-1,修正对PAGE_OFFSET的描述。

2、2016-7-12,(1)增加了和地址映射相关的几个宏定义的描述。(2)增加建立identity mapping的原因

3、2016-7-15,对initial translation tables的位置和size进行补充描述。

4、2016-9-9,修改DTB的限制。


原创文章,转发请注明出处。蜗窝科技

标签: __create_page_tables

评论:

hit201j
2017-11-13 01:22
错别字:
“还以一个就是bootloader传递过来的blob memory对应的页表”中:
还以 -> 还有
linuxer
2017-11-13 17:46
@hit201j:已经修改,多谢!
swan
2017-04-03 10:31
@linuxer:
你好!
非常感谢你的这一系列arm64分享。我最近开始接触arm64 linux的一些工作,读了你的这些文章对我梳理脉络非常有帮助。同时我也有一个问题想要咨询一下你:
我手上的项目需要写一个对kernel 地址的page table walk,我看手册上写ttbr1上的内容是kernel address对应的pgd的起点,可是我取出这个值后再用__va翻译成虚拟地址传给pgd_offset_raw这个函数进行对pud,pmd和pte的寻找时,总是会在pgd或者pmd上就指到一个空地址,不知道你有没有看过这方面的材料?
我把我的问题也放在了Stack Overflow上:http://stackoverflow.com/questions/42963312/arm64-linux-page-table-walk,若可以的话麻烦看一下我的代码并给我一点意见。

再次致谢!
祝好。
randy
2016-12-29 21:57
还是没弄明白到底为啥要identity mapping?
系统起来时候跑在物理地址上,在开启mmu之后有一个long jump
mov    r3, r13
ret    r3
r3是一个虚拟地址,如果pipeline,那么有可能跳转到r3时mmu还没开启成功,则会造成跳转到一个未经mmu翻译的虚拟地址造成指令fetch错误。但是我看到有isb保护着呢:
ENTRY(__turn_mmu_on)
    mov    r0, r0
    instr_sync
    mcr    p15, 0, r0, c1, c0, 0        @ write control reg
    mrc    p15, 0, r3, c0, c0, 0        @ read id reg
    instr_sync
    mov    r3, r3
    mov    r3, r13
    ret    r3

那到底为啥要identity mapping?上面贴的那段英文解释我没看懂
randy
2017-01-03 16:35
@randy:再打扰下,请问这个问题有人能稍作解答么?
wowo
2017-01-04 13:58
@randy:那段英文的意思是,MMU ON或者OFF的时候,如果代码所在的虚拟地址和物理地址不一样,ON或者OFF的取指可能有问题(speculative instruction fetching,投机的取指?)。而不是你指的后面的跳转指令。
至于为什么是这样子,我也不大清楚,估计要问问ARM的人,ARM也只是建议,我怀疑不是identity mapping的话,也不一定会出问题。
randy
2017-01-04 14:56
@wowo:speculative instruction fetching应该是指指令pipeline,应该是说MMU ON前后有指令还是按照老的方式在处理会有问题,我没想明白的是到底有啥问题,加了isb后貌似没问题
wowo
2017-01-04 15:34
@randy:我觉得MMU ON之前的指令,只要是位置无关的,都不会有问题。
而MMU ON之后的指令,就算加isb也不行,因为MMU ON的指令正在执行的时候,CPU就有可能再取后面的指令了(包括instr_sync)。因此在MMU ON的那一瞬间,可能会出问题。
tigger
2016-12-08 10:41
hi linuxer
假设kernel的代码非常的小,我能不能打开mmu,但是设置的都是identity mapping?
最近研究MMU相关,决定把你所有的文章都先研究一遍。轰炸式骚扰~~~哈哈
linuxer
2016-12-08 17:51
@tigger:欢迎骚扰,技术探讨都欢迎,除了解决问题那种类型的问题,那个估计是超出我的能力范围了,哈哈~~

你的问题暴露了你还是不是很理解identity mapping,哈哈,内核运行在PAGE_OFFSET开始的这个虚拟地址上(编译的时候就确定了),因此,无论如何,一旦打开MMU,kernel image mapping都是需要的。identity mapping是“地址值等于物理地址的那些虚拟地址”到“物理地址”的mapping,它的建立仅仅是为了打开MMU附近的代码准备的。
tigger
2016-12-08 18:09
@linuxer:MMU相关的很多东西我都不太理解。所以现在在看你的文章。看起来还是很吃力。
很多概念脑子里面没有任何的模型,而且你的文章写得很分散。不像普通的drive那么的系统。当然也可能是我能力不够。
我努努力,争取花一个月能够有点收获。
如果我只设置一级页表,TTBR0存放页表首地址。那么我这个页表的大小是多少??
我对这个非常的困惑。因为我接触的页表大小就是下面这3种。就不能有别的吗?
如果有别的,那我又怎么设置呢?
TG0, bits [15:14]
Granule size for the corresponding translation table base address register.
00 4KByte
01 64KByte
10 16KByte
===========================================
我是不是应该去讨论区里面启动一个帖子,然后在帖子里面写好对于哪篇文章的哪里不太懂?还是就在对于的文章下面提问题?你更倾向于哪种方式?
linuxer
2016-12-08 19:25
@tigger:在讨论区比较合适,可以起一个主题,不断的把关于MMU的Q和A整理出来也是一个不错的方案。
vachel
2016-10-10 16:04
请问ARMv8里面区分线性地址和虚拟地址吗?
比如kmalloc出来的地址通过virt_to_phys转换就能得到PA.但是Vmalloc就不能这么算.

测试方法, 将kmalloc和vmalloc的内存填好固定的值, 打印出对应的PA.  然后通过devmem PA 读对应的内容.

测试代码:
    //extern struct   mm_struct init_mm;
    pgd_t *pgd;
    pgd_t *pgd_temp;
    pmd_t *pmd;
    pte_t *pte;
    pud_t *pud;

    unsigned long va;
    int* n_kmalloc = (int*)kmalloc(32, GFP_KERNEL);
    int* n_vmalloc = (int*)vmalloc(sizeof(int));

    *n_kmalloc = 0x1234;
    *n_vmalloc = 0x4321;


    /**************************************************************test kmalloc**********************************************************/
    va = (unsigned long)n_kmalloc;
    printk(" Test 1, use virt_to_phys get kmalloc PA\n");
    printk("kmalloc virt_to_phy = %lx\n ", virt_to_phys(n_kmalloc));

    printk(" Test 2, use kernel pgd translation kmalloc to PA \n");
    // ffffffc000e88000 B swapper_pg_dir
    pgd =  0xffffffc000e88000;
    printk("pgd_index = 0x%x\n", pgd_index(va));

    pgd_temp = pgd+pgd_index(va);
    printk("[%p][%08lx] *pgd = %016llx, pgd = %p, ", n_kmalloc,va, pgd_val(*pgd_temp), pgd_temp);

    do {

        if (pgd_none(*pgd_temp) || pgd_bad(*pgd_temp))
            break;

        pud = pud_offset(pgd_temp, va);
        if (pud_none(*pud) || pud_bad(*pud))
            break;

        pmd = pmd_offset(pud, va);
        printk(", *pmd=%016llx", pmd_val(*pmd));
        if (pmd_none(*pmd) || pmd_bad(*pmd))
            break;

        pte = pte_offset_map(pmd, va);
        printk(", *pte=%016llx", pte_val(*pte));
    } while(0);

    printk("\n");

    printk("Test 3, use mm_struct pgd translation kmalloc to PA\n");
    pgd = current->mm->pgd;
    printk("pgd_index = 0x%x\n", pgd_index(va));

    pgd_temp = pgd+pgd_index(va);
    printk("[%p] [%08lx] *pgd = %016llx, pgd = %p, ", n_kmalloc, va, pgd_val(*pgd_temp), pgd_temp);

    do {

        if (pgd_none(*pgd_temp) || pgd_bad(*pgd_temp))
            break;

        pud = pud_offset(pgd_temp, va);
        if (pud_none(*pud) || pud_bad(*pud))
            break;

        pmd = pmd_offset(pud, va);
        printk(", *pmd=%016llx", pmd_val(*pmd));
        if (pmd_none(*pmd) || pmd_bad(*pmd))
            break;

        pte = pte_offset_map(pmd, va);
        printk(", *pte=%016llx", pte_val(*pte));
    } while(0);

    printk("\n");



    /**************************************************************test vmalloc**********************************************************/
    va = (unsigned long)n_vmalloc;
    printk(" Test 1, use virt_to_phys get vmalloc PA\n");
    printk("vmalloc virt_to_phy = %lx\n ", virt_to_phys(n_vmalloc));

    printk(" Test 2, use kernel pgd translation vmalloc to PA \n");
    // ffffffc000e88000 B swapper_pg_dir
    pgd =  0xffffffc000e88000;
    printk("pgd_index = 0x%x\n", pgd_index(va));

    pgd_temp = pgd+pgd_index(va);
    printk("[%p] [%08lx] *pgd = %016llx, pgd = %p, ", n_vmalloc, va, pgd_val(*pgd_temp), pgd_temp);

    do {

        if (pgd_none(*pgd_temp) || pgd_bad(*pgd_temp))
            break;

        pud = pud_offset(pgd_temp, va);
        if (pud_none(*pud) || pud_bad(*pud))
            break;

        pmd = pmd_offset(pud, va);
        printk(", *pmd=%016llx", pmd_val(*pmd));
        if (pmd_none(*pmd) || pmd_bad(*pmd))
            break;

        pte = pte_offset_map(pmd, va);
        printk(", *pte=%016llx", pte_val(*pte));
    } while(0);

    printk("\n");

    printk("Test 3, use mm_struct pgd translation vmalloc to PA\n");
    pgd = current->mm->pgd;
    printk("pgd_index = 0x%x\n", pgd_index(va));

    pgd_temp = pgd+pgd_index(va);
    printk("[%p] [%08lx]*pgd = %016llx, pgd = %p, ", n_vmalloc, va, pgd_val(*pgd_temp), pgd_temp);

    do {

        if (pgd_none(*pgd_temp) || pgd_bad(*pgd_temp))
            break;

        pud = pud_offset(pgd_temp, va);
        if (pud_none(*pud) || pud_bad(*pud))
            break;

        pmd = pmd_offset(pud, va);
        printk(", *pmd=%016llx", pmd_val(*pmd));
        if (pmd_none(*pmd) || pmd_bad(*pmd))
            break;

        pte = pte_offset_map(pmd, va);
        printk(", *pte=%016llx", pte_val(*pte));
    } while(0);

    printk("\n");


    //dump pgd
//    print_hex_dump(KERN_ERR, "PGD raw data:", DUMP_PREFIX_ADDRESS,16,1,pgd,4096,true);

    //panic
  //  *(int*)0x2 = 0;

   /**/ while(true){
    
    }
   /**/


测试结论:
# [ 4132.980000]  Test 1, use virt_to_phys get kmalloc PA
[ 4132.984000] kmalloc virt_to_phy = 32184ad40
  Test 2, use kernel pgd translation kmalloc to PA
[ 4132.992000] pgd_index = 0x102
[ 4132.996000] [ffffffc09d44ad40][ffffffc09d44ad40] *pgd = 000000005d1fa003, pgd = ffffffc000e88810, , *pmd=0040000321800711
[ 4133.008000] Test 3, use mm_struct pgd translation kmalloc to PA
[ 4133.012000] pgd_index = 0x102
[ 4133.016000] [ffffffc09d44ad40] [ffffffc09d44ad40] *pgd = 0000000000000000, pgd = ffffffc09ab62810,
[ 4133.024000]  Test 1, use virt_to_phys get vmalloc PA
[ 4133.032000] vmalloc virt_to_phy = ffffffc0260ed000
  Test 2, use kernel pgd translation vmalloc to PA
[ 4133.040000] pgd_index = 0x0
[ 4133.044000] [ffffff8005eed000] [ffffff8005eed000] *pgd = 000000033ac05003, pgd = ffffffc000e88000, , *pmd=00000000de236003, *pte=02e00003370e3713
[ 4133.056000] Test 3, use mm_struct pgd translation vmalloc to PA
[ 4133.060000] pgd_index = 0x0
[ 4133.064000] [ffffff8005eed000] [ffffff8005eed000]*pgd = 0000000000000000, pgd = ffffffc09ab62000,

busybox devmem  0x32184ad40                              
0x00001234
busybox devmem  0xffffffc0260ed000                        
devmem: mmap: Invalid argument
linuxer
2016-10-10 21:54
@vachel:我记得线性地址是X86平台特有的一个术语,在ARMv8中使用虚拟地址这个术语。

对于kmalloc,分配的虚拟地址是位于Directed mapped区域,这个区域的地址虚拟地址和物理地址有固定的线性偏移关系,因此可以使用virt_to_phys。

使用vmalloc,虚拟地址和物理地址没有线性关系。
vachel
2016-10-11 09:37
@linuxer:@linuxer, 感谢回复, 感谢纠正.
基于kernel的/Documentation/arm64/memory.txt 可以看到
AArch64 Linux memory layout with 4KB pages:

Start            End            Size        Use
-----------------------------------------------------------------------
0000000000000000    0000007fffffffff     512GB        user

ffffff8000000000    ffffffbbfffeffff    ~240GB        vmalloc

ffffffbbffff0000    ffffffbbffffffff      64KB        [guard page]

ffffffbc00000000    ffffffbdffffffff       8GB        vmemmap

ffffffbe00000000    ffffffbffbbfffff      ~8GB        [guard, future vmmemap]

ffffffbffa000000    ffffffbffaffffff      16MB        PCI I/O space

ffffffbffb000000    ffffffbffbbfffff      12MB        [guard]

ffffffbffbc00000    ffffffbffbdfffff       2MB        fixed mappings

ffffffbffbe00000    ffffffbffbffffff       2MB        [guard]

ffffffbffc000000    ffffffbfffffffff      64MB        modules

ffffffc000000000    ffffffffffffffff     256GB        kernel logical memory map

所以我能理解系统启动之后, 所有经过CPU的地址都是虚拟地址吧?
1. directed mapped区域是针对虚拟内存分布而言? 是哪部分呢. 从arch/arm64/mm/init.c打印的virtual kernel memory layout来看是属于[PAGE_OFFSET, (unsigned long)high_memory] 也就是memory 区间. 可以这么理解吗?

2. 拿到虚拟地址之后怎么通过页表换算为实际的物理地址呢? 因为无法知道TTBR1_EL1/TTBR0_EL1的值.  我用current->mm->pgd 和 swapper_pg_dir 分别去算都没有算对.

谢谢
swan
2017-04-03 10:38
@vachel:Hi @tigger,
你最后解决这个问题么?我用内联汇编取出了TTBR1的值可还是没办法把kmalloc的虚拟地址翻译成物理地址,所以也在寻求答案中..
逗你玩
2016-07-27 20:38
受教了。
"(2)PAGE_OFFSET是kernel image的首地址,对于48bit的VA而言,该地址是0xffff8000-00000000。"
这里是不是应该为0xffff0000-00000000?
hello
2016-07-28 18:04
@逗你玩:0xffff0000-00000000是kernel space的首地址,在ARM64中,kernel image并没有放到kernel space首地址的位置而是放在了中间,代码如下:
#define PAGE_OFFSET        (UL(0xffffffffffffffff) << (VA_BITS - 1))
wowo
2016-07-28 18:26
@hello:多谢分享,这两天有些忙,没有及时恢复,望见谅
hello
2016-07-28 19:16
@wowo:谢啥啊,我就是linuxer啊,只是懒得登录而已
tigger
2016-07-28 19:55
@hello:哈哈哈哈哈
wowo
2016-07-28 19:57
@tigger:疯了,名字竟然天天换。哈哈~~
passerby
2015-10-22 15:23
谢谢指点,查了一下ARM32确实有micro tlb和main tlb。但是main tlb做同步用的,它们的作用与arm64的tlb0 tlb1不一样。
第二个确实是这样的 0^0,因为一直看到异常向量表是建立在0XFFFF 0000上的,就一直以为设置为两个固定地址。
linuxer
2015-10-22 18:25
@passerby:对于ARM64的EL1状态,有两个Translation Table Base Register,叫做TTBR0_EL1和TTBR1_EL1,这两个寄存器分别指向了用户空间和内核空间的Translation table。

具体TLB的组织是和具体的processor相关,对于Cortex A72而言,其TLB包括两个level,共计3个,分别是L1 instruction TLB,L1 data TLB和L2 TLB。

ARM32的micro tlb类似L1 instruction TLB和L1 data TLB,Main TLB类似A72中的L2 TLB,其功能是一样的。
passerby
2015-10-22 19:19
@linuxer:受教了,谢谢O_O
KO户外
2015-10-20 11:07
谢谢分享
珠海影视制作
2015-10-19 07:50
好吧, 表示这些我都不懂的
passerby
2015-10-14 17:03
前段时间也在看aarch64,发现硬件的进步确实给软件带来很多优化。由于tbl有了两个,所以kernel和user的页表可以分离开了。并且arm32中的异常向量表只能放到0xffff 0000或者0x0000 0000,而aarch64有一个寄存器vbar_el1来指定来放异常向量表地址了。
另外我看的是高通的代码,感觉swapper_pg_dir的建立过程和你上面说的有所不同呢。
linuxer
2015-10-14 23:06
@passerby:我贴的代码是标准linux kernel4.1.10版本的代码,你可以贴出来高通的代码大家一起研究一下怎么不一样。
claud
2015-10-22 11:55
@passerby:@passerby
纠正一下哦:
1.arm32 TLB也是2个, Micro TLB & Main TLB
2.arm32(armv-7)的vector也可以设置到32字节对齐的任意地址哦:) 放在CP15 VBAR寄存器中

发表评论:

Copyright @ 2013-2015 蜗窝科技 All rights reserved. Powered by emlog