arm-linux kernel启动过程分析(2)-start_kernel之前第二步

上篇博客分析了arm kernel启动的前几十条汇编,
今天接着分析,不过今天只分析stext中一条汇编,如下:
    bl  __create_page_tables

看看kernel启动初期,开启MMU之前如何初始化页表。

#ifdef CONFIG_ARM_LPAE
    /* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE 0x5000
#define PMD_ORDER   3
#else
#define PG_DIR_SIZE 0x4000
#define PMD_ORDER   2
#endif
.....
    .macro  pgtbl, rd, phys
    add \rd, \phys, #TEXT_OFFSET - PG_DIR_SIZE
    .endm
.....
__create_page_tables:
    //据上篇博文分析,r8存储着sdram的物理起始地址(我的板子0x80000000)
    //pgtbl宏获取0x80008000之下16K的地址空间作为页表空间
    //arm页表一页是4 bytes,完成虚拟地址空间4GB中1MB的映射,
    //一共需要4 x 4096 bytes的页表空间
    //可以看出,单页完成的是虚拟地址和物理地址高12位的转换。
    //低20位的地址(1M内的地址)偏移是一致的。
    pgtbl   r4, r8              @ page table address

    /*
     * Clear the swapper page table
     */
    //按照16bytes一页将16K页表空间清空
    mov r0, r4
    mov r3, #0
    add r6, r0, #PG_DIR_SIZE
1:  str r3, [r0], #4
    str r3, [r0], #4
    str r3, [r0], #4
    str r3, [r0], #4
    teq r0, r6
    bne 1b

    //如果定义CONFIG_ARM_LPAE,在PGD与PMD之前还要再加一级页表,这里不详解这种情景
#ifdef CONFIG_ARM_LPAE
    /*
     * Build the PGD table (first level) to point to the PMD table. A PGD
     * entry is 64-bit wide.
     */
    mov r0, r4
    add r3, r4, #0x1000         @ first PMD table address
    orr r3, r3, #3          @ PGD block type
    mov r6, #4              @ PTRS_PER_PGD
    mov r7, #1 << (55 - 32)     @ L_PGD_SWAPPER
1:  str r3, [r0], #4            @ set bottom PGD entry bits
    str r7, [r0], #4            @ set top PGD entry bits
    add r3, r3, #0x1000         @ next PMD table
    subs    r6, r6, #1
    bne 1b

    add r4, r4, #0x1000         @ point to the PMD tables
#endif
    //据上篇博文分析,r10中存储该CPU的processor_type_list(处理器信息结构体),获取该CPU的mmuflags
    ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

    /*
     * Create identity mapping to cater for __enable_mmu.
     * This identity mapping will be removed by paging_init().
     */
    //首先建立包含turn_mmu_on函数的1M空间的平映射(virt addr = phy addr)
    //turn_mmu_on距stext不远,所以实际完成0x8000000-0x81000000空间的平映射
    
    //老方法,上篇博文分析过,获取phy到virt的offset
    adr r0, __turn_mmu_on_loc
    ldmia   r0, {r3, r5, r6}
    sub r0, r0, r3          @ virt->phys offset
    //获取turn_mmu_on的首尾物理地址
    add r5, r5, r0          @ phys __turn_mmu_on
    add r6, r6, r0          @ phys __turn_mmu_on_end
    //因1页映射1M空间,所以SECTION_SHIFT为20
    //右移20位后,r5,r6代表该段地址空间在页表中下标(第几页)
    mov r5, r5, lsr #SECTION_SHIFT
    mov r6, r6, lsr #SECTION_SHIFT

    //r5左移20位,获取该页基地址,或上CPU的mmuflags,存在r3中
1:  orr r3, r7, r5, lsl #SECTION_SHIFT  @ flags + kernel base
    //将r3值存储在页表空间(r4起始)的(r5<<4)的页表中
    //因一页用4bytes表示,所以PMD_ORDER=4
    str r3, [r4, r5, lsl #PMD_ORDER]    @ identity mapping
    //r5与r6之前相距多个1M,则需要填写多个页表。
    //因turn_mmu_on函数很短,所以肯定在1M内,该处r5=r6
    cmp r5, r6
    addlo   r5, r5, #1          @ next section
    blo 1b

    /*
     * Now setup the pagetables for our kernel direct
     * mapped region.
     */
    //这里建立kernel整个镜像的线性映射,((0x80000000-0x80000000+kernel_end)-(0xc0000000-0xc0000000+kernel_end))
    //开启MMU之后就实现了链接地址(0xc0008000)与运行地址(0xc0008000)的统一
    mov r3, pc
    mov r3, r3, lsr #SECTION_SHIFT
    orr r3, r7, r3, lsl #SECTION_SHIFT
    //将该1M空间的物理起始地址存储到页表中相应虚拟地址页中
    add r0, r4,  #(KERNEL_START & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
    str r3, [r0, #((KERNEL_START & 0x00f00000) >> SECTION_SHIFT) << PMD_ORDER]!
    ldr r6, =(KERNEL_END - 1)
    add r0, r0, #1 << PMD_ORDER
    add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:  cmp r0, r6
    add r3, r3, #1 << SECTION_SHIFT
    strls   r3, [r0], #1 << PMD_ORDER
    bls 1b
#ifdef CONFIG_XIP_KERNEL
    /*
     * Map some ram to cover our .data and .bss areas.
     */
    add r3, r8, #TEXT_OFFSET
    orr r3, r3, r7
    add r0, r4,  #(KERNEL_RAM_VADDR & 0xff000000) >> (SECTION_SHIFT - PMD_ORDER)
    str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> (SECTION_SHIFT - PMD_ORDER)]!
    ldr r6, =(_end - 1)
    add r0, r0, #4
    add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
1:  cmp r0, r6
    add r3, r3, #1 << 20
    strls   r3, [r0], #4
    bls 1b
#endif

    /*
     * Then map boot params address in r2 or the first 1MB (2MB with LPAE)
     * of ram if boot params address is not specified.
     */
    //将包含boot params的1M地址空间做线性映射,方便start_kernel中对args进行分析
    //据上篇博文分析,r2中存储着bootloader传来的params基地址(我的板子在0x80000100)
    //所以该1M空间是0x80000000-0x81000000,映射到0xc0000000-0xc1000000
    mov r0, r2, lsr #SECTION_SHIFT
    movs    r0, r0, lsl #SECTION_SHIFT
    moveq   r0, r8
    sub r3, r0, r8
    add r3, r3, #PAGE_OFFSET
    add r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
    orr r6, r7, r0
    str r6, [r3]

//如果需要早期串口输出进行调试,在这里进行I/O空间的映射,从而实现可以对串口控制器的操作,这里不详解了。
#ifdef CONFIG_DEBUG_LL
#if !defined(CONFIG_DEBUG_ICEDCC) && !defined(CONFIG_DEBUG_SEMIHOSTING)
    /*
     * Map in IO space for serial debugging.
     * This allows debug messages to be output
     * via a serial console before paging_init.
     */
    addruart r7, r3, r0

    mov r3, r3, lsr #SECTION_SHIFT
    mov r3, r3, lsl #PMD_ORDER

    add r0, r4, r3
    rsb r3, r3, #0x4000         @ PTRS_PER_PGD*sizeof(long)
    cmp r3, #0x0800         @ limit to 512MB
    movhi   r3, #0x0800
    add r6, r0, r3
    mov r3, r7, lsr #SECTION_SHIFT
    ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
    orr r3, r7, r3, lsl #SECTION_SHIFT
#ifdef CONFIG_ARM_LPAE
    mov r7, #1 << (54 - 32)     @ XN
#else
    orr r3, r3, #PMD_SECT_XN
#endif
1:  str r3, [r0], #4
#ifdef CONFIG_ARM_LPAE
    str r7, [r0], #4
#endif
    add r3, r3, #1 << SECTION_SHIFT
    cmp r0, r6
    blo 1b

#else /* CONFIG_DEBUG_ICEDCC || CONFIG_DEBUG_SEMIHOSTING */
    /* we don't need any serial debugging mappings */
    ldr r7, [r10, #PROCINFO_IO_MMUFLAGS] @ io_mmuflags
#endif
#if defined(CONFIG_ARCH_NETWINDER) || defined(CONFIG_ARCH_CATS)
    /*
     * If we're using the NetWinder or CATS, we also need to map
     * in the 16550-type serial port for the debug messages
     */
    add r0, r4, #0xff000000 >> (SECTION_SHIFT - PMD_ORDER)
    orr r3, r7, #0x7c000000
    str r3, [r0]
#endif
#ifdef CONFIG_ARCH_RPC
    /*
     * Map in screen at 0x02000000 & SCREEN2_BASE
     * Similar reasons here - for debug.  This is
     * only for Acorn RiscPC architectures.
     */
    add r0, r4, #0x02000000 >> (SECTION_SHIFT - PMD_ORDER)
    orr r3, r7, #0x02000000
    str r3, [r0]
    add r0, r4, #0xd8000000 >> (SECTION_SHIFT - PMD_ORDER)
    str r3, [r0]
#endif
#endif
#ifdef CONFIG_ARM_LPAE
    sub r4, r4, #0x1000     @ point to the PGD table
#endif
    mov pc, lr
ENDPROC(__create_page_tables)
    .ltorg
    .align
__turn_mmu_on_loc:
    .long   .
    .long   __turn_mmu_on
    .long   __turn_mmu_on_end
create_page_table完成了3种地址映射的页表空间填写:
(1)turn_mmu_on所在1M空间的平映射
(2)kernel image的线性映射
(2)bootparams所在1M空间的线性映射


物理地址空间和虚拟地址空间映射关系图如下:



对于这3种地址空间映射,我觉得分别有3个值得思考的地方:


(1)为什么turn_mmu_on要做平映射?
turn_mmu_on我会在下一篇博文中分析,主要是完成开启MMU的操作。
那为什么将turn_mmu_on处做一个平映射?
可以想象,执行开启MMU指令之前,CPU取指是在0x80008000附近turn_mmu_on中。
如果只是做kernel image的线性映射,执行开启MMU指令后,CPU所看到的地址就全变啦。
turn_mmu_on对于CPU来说在0xc0008000附近,0x80008000附近对于CPU来说已经不可预知了。
但是CPU不知道这些,它只管按照地址一条条取指令,执行指令。
所以不做turn_mmu_on的平映射(virt addr = phy addr),turn_mmu_on在开启MMU后的运行是完全不可知。
完成turn_mmu_on的平映射,我们可以在turn_mmu_on末尾MMU已经开启稳定后,修改PC到0xc0008000附近,就可以解决从0x8xxxxxxx到0xcxxxxxxx的跳转。

(2)kernel image加载地址为什么会在0x****8000?
分析了kernel image线性映射部分,这个就好理解了,
kernel编译链接时的入口地址在0xc0008000(PAGE_OFFSET + TEXT_OFFSET),但其物理地址不等于其链接的虚拟地址,image的线性映射实现其运行地址等于链接地址。
kernel的每一页表映射1M,所以入口处在(0x80000000-->0xc0000000)映射页表中完成映射。物理地址和虚拟地址的1M内偏移必须一致呀。
kernel定义的TEXT_OFFSET = 0x8000.所以加载的物理地址必须为0x****8000.
这样,开启MMU后,访问0xc0008000附近指令,MMU根据TLB才能正确映射找到0x****8000附近的指令。

(3)bootparams跟kernel入口是在同一1M空间内,bootparams的线性映射操作是否多余?
根据第二个问题的分析,kernel image可以加载到任何sdram地址空间的0x****8000即可。
bootparams地址是有bootloader中指定,然后告诉kernel的。
那就有这样一种情况,加入sdram起始地址为0x80000000,bootparams起始地址为0x80000100。
但kernel image我加载到0x81008000,可以看出,这时bootparams跟kernel image就在不同一1M空间啦
bootparams单独的线性映射操作还是很有必要的。


这是我想到的关于create_page_table的3个疑问,大家如果有别的疑问,欢迎留言讨论,共同学习。


今天就分析到这,TLB已经准备就绪,只待开启MMU!


start_kernel之前剩余部分汇编如何开启mmu,下篇再分析。

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。