Skip to content

Latest commit

 

History

History
485 lines (367 loc) · 18.8 KB

28_RT-Smart 启动过程代码分析.md

File metadata and controls

485 lines (367 loc) · 18.8 KB

RT-Smart 启动过程源代码分析

在熟悉 RT-Smart 架构的过程中,研究其启动过程的是必不可少的,那么在系统正常运行之前,需要做哪些准备工作呢。本文将以 32 位 RT-Smart 的源代码为基础,讲解 RT-Smart 的启动过程。

内核地址空间

RT-Smart 与 RT-Thread 的一大区别是用户态和内核态的地址空间被隔离开来。内核运行在内核地址空间,用户进程运行在用户地址空间。由下图可知,RT-Smart 32 位内核运行在地址空间的高地址,而用户程序代码运行在低地址。

image-20210512173417892

系统初始化流程

上面说到 RT-Smart 将内核搬运到高地址空间运行,为了能让内核正常运行在内核地址空间,需要一些初始化对 MMU 进行逐步配置,初始化步骤如下:

  1. 使用实际物理地址设置栈,为调用 C 语言函数对 MMU 页表进行初始化作准备
  2. 建立从 0x600100000x60010000 的原地址映射
  3. 建立从 0x600100000xc0010000 的物理地址到内核地址空间的映射
  4. 使能 MMU,使地址映射生效,需要建立双重映射的原因是,如果只建立第三步的映射,此时程序还运行在 0x60010000 的物理空间上,此时开启 MMU,当前程序正在运行的地址空间变得无法访问,导致程序 fault 无法继续运行
  5. 切换到内核地址空间,在内核地址空间重新设置栈
  6. 解除 0x600100000x60010000 的原地址映射关系

启动过程代码详解(ARMv7)

系统启动前,内核程序被加载到 0x60010000 ,但内核在编译时被链接到 0xc0010000 的位置,此时 MMU 还没有开启。如果此时想要使用全局变量的,就需要将全局变量的地址加上 PV_OFFSET(物理地址减去虚拟地址的偏移量)获取实际的物理地址,才能正常访问该全局变量。

.equ UND_Stack_Size,     0x00000400
.equ SVC_Stack_Size,     0x00000400
.equ ABT_Stack_Size,     0x00000400
.equ RT_FIQ_STACK_PGSZ,  0x00000000
.equ RT_IRQ_STACK_PGSZ,  0x00000800
.equ USR_Stack_Size,     0x00000400

#define ISR_Stack_Size  (UND_Stack_Size + SVC_Stack_Size + ABT_Stack_Size + \
                 RT_FIQ_STACK_PGSZ + RT_IRQ_STACK_PGSZ)

.section .data.share.isr
/* stack */
.globl stack_start
.globl stack_top

stack_start:
.rept ISR_Stack_Size
.byte 0
.endr
stack_top:

/*
使用 1M 大小的 section 映射,描述符的类型为 unsigned int,占用 4 个字节内存,整个系统地址空间为 4GB,因此需要 4096 个描述符,总共占用内存 16kb。
*/
#ifdef RT_USING_USERSPACE
.data
.align 14
init_mtbl:
    .space 16*1024
#endif

.text
/* reset entry */
.globl _reset
_reset:

#ifdef RT_USING_USERSPACE
    ldr r5, =PV_OFFSET

    mov r7, #0x100000
    sub r7, #1
    mvn r8, r7     /* r8: 0xfff0_0000 */

    ldr r9, =KERNEL_VADDR_START

    ldr r6, =__bss_end
    add r6, r7
    and r6, r8          /* r6 end vaddr align up to 1M */
    sub r6, r9          /* r6 is size */

    ldr sp, =stack_top  
    add sp, r5          /* 使用栈的物理地址初始化栈 */

    ldr r0, =init_mtbl
    add r0, r5
    mov r1, r6
    mov r2, r5
    bl init_mm_setup    /* 初始化内存映射表,建立双重映射,即程序加载原地址映射与原地址到内核地址空间映射 */

    ldr lr, =after_enable_mmu
    ldr r0, =init_mtbl
    add r0, r5
    b enable_mmu        /* 使用初始化后的映射表使能 MMU */

after_enable_mmu:
#endif

    /* set the cpu to SVC32 mode and disable interrupt */
    cps #Mode_SVC

    /* disable the data alignment check */
    mrc p15, 0, r1, c1, c0, 0
    bic r1, #(1<<1)
    mcr p15, 0, r1, c1, c0, 0

    /* setup stack */
    bl      stack_setup  /* 使用内核空间栈的虚拟地址初始化栈 */

    /* clear .bss */
    mov r0,#0                   /* get a zero                       */
    ldr r1,=__bss_start         /* bss start                        */
    ldr r2,=__bss_end           /* bss end                          */

bss_loop:
    cmp r1,r2                   /* check if data to clear           */
    strlo r0,[r1],#4            /* clear 4 bytes                    */
    blo bss_loop                /* loop until done                  */

    /* initialize the mmu table and enable mmu */
    ldr r0, =platform_mem_desc
    ldr r1, =platform_mem_desc_size
    ldr r1, [r1]
    bl rt_hw_init_mmu_table

#ifdef RT_USING_USERSPACE
    ldr r0, =MMUTable    /* vaddr    */
    add r0, r5           /* to paddr */
    bl  switch_mmu
#endif

    /* call C++ constructors of global objects                      */
    ldr     r0, =__ctors_start__
    ldr     r1, =__ctors_end__

ctor_loop:
    cmp     r0, r1
    beq     ctor_end
    ldr     r2, [r0], #4
    stmfd   sp!, {r0-r1}
    mov     lr, pc
    bx      r2
    ldmfd   sp!, {r0-r1}
    b       ctor_loop
ctor_end:

    /* start RT-Thread Kernel */
    ldr     pc, _rtthread_startup
_rtthread_startup:
    .word rtthread_startup

双重地址映射函数详解

void init_mm_setup(unsigned int *mtbl, unsigned int size, unsigned int pv_off) {
    unsigned int va;

    for (va = 0; va < 0x1000; va++) {
        unsigned int vaddr = (va << 20);
        if (vaddr >= KERNEL_VADDR_START && vaddr - KERNEL_VADDR_START < size) {
            mtbl[va] = ((va << 20) + pv_off) | NORMAL_MEM;
        } else if (vaddr >= (KERNEL_VADDR_START + pv_off) && vaddr - (KERNEL_VADDR_START + pv_off) < size) {
            mtbl[va] = (va << 20) | NORMAL_MEM;
        } else {
            mtbl[va] = 0;
        }
    }
}

该函数初始化了内存映射表,从 0 地址开始,以 1M 的粒度扫描整个 4G 地址空间,建立两段映射关系:

  1. 如果发现虚拟地址在内核地址空间上,则建立从内核地址空间到内核程序加载地址的映射
  2. 如果发现虚拟地址在处于内核程序的加载地址,则建立相对应的原地址映射
  3. 其他地址配置成无效,如下图中的空白部分

配置的映射关系如下图所示:

image-20210513182917961

使能 MMU

.align 2
.global enable_mmu
enable_mmu:
    orr r0, #0x18
    mcr p15, 0, r0, c2, c0, 0 // ttbr0

    mov r0, #(1 << 5)         // PD1=1
    mcr p15, 0, r0, c2, c0, 2 // ttbcr

    mov r0, #1
    mcr p15, 0, r0, c3, c0, 0 // dacr

    // invalid tlb before enable mmu
    mov r0, #0
    mcr p15, 0, r0, c8, c7, 0
    mcr p15, 0, r0, c7, c5, 0   ; // iciallu
    mcr p15, 0, r0, c7, c5, 6   ; // bpiall

    mrc p15, 0, r0, c1, c0, 0
    orr r0, #(1 | 4)
    orr r0, #(1 << 12)
    mcr p15, 0, r0, c1, c0, 0
    dsb
    isb
    mov pc, lr

切换 MMU

.global switch_mmu
switch_mmu:
    orr r0, #0x18
    mcr p15, 0, r0, c2, c0, 0 //ttbr0

    // invalid tlb
    mov r0, #0
    mcr p15, 0, r0, c8, c7, 0
    mcr p15, 0, r0, c7, c5, 0   ;//iciallu
    mcr p15, 0, r0, c7, c5, 6   ;//bpiall

    dsb
    isb
    mov pc, lr

设置内核空间栈

stack_setup:
    ldr     r0, =stack_top     /* 获取内核地址空间下的栈地址,然后设置各模式下的栈 */

    @  Set the startup stack for svc
    mov     sp, r0

    @  Enter Undefined Instruction Mode and set its Stack Pointer
    msr     cpsr_c, #Mode_UND|I_Bit|F_Bit
    mov     sp, r0
    sub     r0, r0, #UND_Stack_Size

    @  Enter Abort Mode and set its Stack Pointer
    msr     cpsr_c, #Mode_ABT|I_Bit|F_Bit
    mov     sp, r0
    sub     r0, r0, #ABT_Stack_Size

    @  Enter FIQ Mode and set its Stack Pointer
    msr     cpsr_c, #Mode_FIQ|I_Bit|F_Bit
    mov     sp, r0
    sub     r0, r0, #RT_FIQ_STACK_PGSZ

    @  Enter IRQ Mode and set its Stack Pointer
    msr     cpsr_c, #Mode_IRQ|I_Bit|F_Bit
    mov     sp, r0
    sub     r0, r0, #RT_IRQ_STACK_PGSZ

    /* come back to SVC mode */
    msr     cpsr_c, #Mode_SVC|I_Bit|F_Bit
    bx      lr

与先前的 rt-thread 宏内核相比,整个 SMART 的启动过程主要多了对 MMU 的配置,这是因为 SMART 是一个区分用户态和内核态的操作系统。用户态进程与操作系统内核运行在各自私有的地址空间,为了实现这样的功能,要利用 MMU 提供的虚拟内存机制做更进一步的虚拟地址空间管理。

在 SMART 操作系统中,内存管理部分是比较复杂的,后续针对这一块还需要多多研究。

启动过程代码详解(ARMv8)

页表初始化过程

.L__in_el1:                        /* 清空早期使用的两个页表 */
    adr x1, __start                /* 将代码段的地址写入 x1,在 x1 中暂存该地址,该地址也是栈的顶端 */
    ldr x0, =~0x1fffff             /* 向 x0 存入 2M 地址对齐掩码  */
    and x0, x1, x0                 /* 将 x1 中存放的地址向下 2M 对齐,存入 x0 */     
    add x2, x0, #0x2000            /* 将 x0 增加 8k并存入 x2,注意:早期版本这里使用了 x1 存放计算后的值,会导致在本段代码最后一行读取 x1 的值作为栈时,导致设置了错误的栈,进而导致后续调用 c 函数的过程中程序运行错误 */
.L__clean_pd_loop:
    str     xzr, [x0], #8          /* 清空从 x0 - x2 之间 8k 的地址空间,用作页表*/
    cmp     x0, x2
    bne     .L__clean_pd_loop

    adr     x19, .L__in_el1
    ldr     x8, =.L__in_el1
    sub     x19, x19, x8            /* get PV_OFFSET            */
    mov     sp, x1                  /* in EL1. Set sp to _start */

ARMv8 架构的 MMU 初始化过程与上述内容稍有不同,原因是在 ARMv8 中可以利用 TTBR0_EL1 和 TTBR1_EL1 寄存器来区分对高位地址和低位地址的访问。在操作系统启动初期。

MMU 早期初始化

对 MMU 进行初始化,同 ARMv7 启动一样,也要进行两次映射,这样才能保证在开启 MMU 之后,后续指令可以正常执行。

/**
 * This function creates two early page tables, one for the mapping of high virtual addresses
 * to physical addresses and one for the one-to-one mapping of virtual addresses to physical addresses.  
 *
 * @param tbl0 user space address page table
 * @param tbl1 kernel space address page table
 * @param size mapping size
 * @param pv_off Offset from a virtual address to a physical address
 */
void rt_hw_mmu_setup_early(unsigned long *tbl0, unsigned long *tbl1, unsigned long size, unsigned long pv_off)
{
    int ret;
    unsigned long va = KERNEL_VADDR_START;
    unsigned long count = (size + ARCH_SECTION_MASK) >> ARCH_SECTION_SHIFT;
    unsigned long normal_attr = MMU_MAP_CUSTOM(MMU_AP_KAUN, NORMAL_MEM);

    /* 创建从高位虚拟地址(以 0xffff 起始的地址)到物理地址的映射 */
    ret = armv8_init_map_2M(tbl1 , va, va + pv_off, count, normal_attr);
    if (ret != 0)
    {
        while (1);
    }

    /* 创建物理地址到物理地址的一一对应映射,保证在跳转到链接高位的函数前,指令仍然可以继续执行 */
    ret = armv8_init_map_2M(tbl0, va + pv_off, va + pv_off, count, normal_attr);
    if (ret != 0)
    {
        while (1);
    }
}

很明显在 ARMv8 中建立双重映射的方式与 ARMv7 中不同,在 ARMv8 中使用了两张页表,而 v7 中只用了一张页表,这是架构差异导致的。在 ARMv8 中访问低位地址默认会使用 TTBR0_EL1 指向的页表,而访问高位地址默认会使用 TTBR1_EL1 指向的页表。

同时可以参考该函数被调用时的代码,可以知道在早期建立的虚拟地址映射的大小为 1G。

ldr x2, =0x40000000             /* map 1G memory for kernel space */
mov x3, x19                     /* set PV_OFFSET to x3 reg        */
bl rt_hw_mmu_setup_early

早期页表的映射是比较粗略的,在系统启动后内核还会创建更精细的页表,不过早期粗略的页表也不是完全没用了,后续该 CPU 的第二个核心启动时,就可以复用该页表而不需要再次初始化了。等到所有的核心都启动完毕后,早期的粗略页表就完全不会再次被使用了。

切换到虚拟地址运行

初始化的目的是,使得系统可以从物理地址运行逐步迁移到以 0xFFFF 起始的虚拟地址运行,这需要一个过度的过程,可以观察如下代码了解如何实现从物理地址到虚拟地址的切换。

/* jump to C code, should not return */
.L__jump_to_entry:
    bl get_free_page           /* 获取空闲页,用于存放页表 */
    mov x21, x0
    bl get_free_page           /* 获取空闲页,用于存放页表 */
    mov x20, x0

    mov x1, x21
    bl mmu_tcr_init            /* 配置 MMU 的基础属性,如虚拟地址位数、页大小、页属性等 */

    mov x0, x20
    mov x1, x21

    msr ttbr0_el1, x0          /* 将页表放入 ttbr0_el1 寄存器,配置低位地址映射 */
    msr ttbr1_el1, x1          /* 将页表放入 ttbr1_el1 寄存器,配置高位地址映射 */
    dsb sy                     /* 数据同步内存屏障,确保在下一个指令执行前,所有的存储器访问都已经完成 */

    ldr x2, =0x40000000        /* 为内核映射 1G 内存空间          */
    ldr x3, =0x1000060000000   /* 设置 PV_OFFSET 到 x3 寄存器    */
    bl rt_hw_mmu_setup_early   /* 调用 MMU 配置函数,初始化页表    */

    ldr x30, =after_mmu_enable /* 将 after_mmu_enable 函数的地址存入 LR 寄存器,这是一个高位虚拟地址 */
                              
    mrs x1, sctlr_el1         
    bic x1, x1, #(3 << 3)       /* dis SA, SA0 */
    bic x1, x1, #(1 << 1)       /* dis A */
    orr x1, x1, #(1 << 12)      /* I */
    orr x1, x1, #(1 << 2)       /* C */
    orr x1, x1, #(1 << 0)       /* M */
    msr sctlr_el1, x1           /* 使能 MMU,还可以执行下一条指令,因为低位地址进行了一一映射 */

    dsb sy                      /* 使能 MMU 后 */
    isb sy                      /* 指令屏障指令,在执行下一条指令前,所有的指令都已经完成 */
    ic ialluis                  /* 无效所有的指令缓存 */                       
    dsb sy
    isb sy
    tlbi vmalle1                /* 无效所有 el1 的 tlb 转换表 */
    dsb sy
    isb sy
    ret                         /* 返回到虚拟地址的 after_mmu_enable 函数,自此操作系统完成到虚拟地址的切换  */

after_mmu_enable:
    mrs x0, tcr_el1             /* 关闭 ttbr0 上的映射,操作系统将不再访问低位地址 */
    orr x0, x0, #(1 << 7)
    msr tcr_el1, x0
    msr ttbr0_el1, xzr
    dsb sy

    mov     x0, #1
    msr     spsel, x0
    adr     x1, __start
    mov     sp, x1              /* 设置 sp_el1 为 _start 符号地址 */

    b  rtthread_startup

初始化物理页管理器

rt_page_init 函数会初始化页表管理器,用于管理物理内存,从如下代码可以看出,物理内存的范围为内核地址空间的 16 M 到 128M 的位置,该函数的详细实现原理参考[《RT-Smart 物理页管理详解》](25_RT-Smart 物理页管理详解.md)。

#define HEAP_END        ((size_t)KERNEL_VADDR_START + 16 * 1024 * 1024)
#define PAGE_START      HEAP_END
#define PAGE_END        ((size_t)KERNEL_VADDR_START + 128 * 1024 * 1024)

rt_region_t init_page_region = {
  PAGE_START,
  PAGE_END,
};

这里使用的是虚拟地址,但实际上与物理地址是一一对应的,根据 PV_OFFSET 就可以很方便地找到对应的物理地址 。这里只是将可以用于被页管理器管理的地址空间加入到页管理器,并不牵扯到内存的映射和解映射,原因很简单,通过 PV_OFFSET 就可以很方便地解决问题。

系统中有很多地方会使用到物理页的申请和释放,这里有 128M 的限制,其实也就意味着不能申请到更多内存了。

内核页表再初始化

在系统切换到虚拟地址运行前,我们已经进行过 MMU 的早期初始化,但那时的映射方式是非常粗略的,后续继续使用该页表是不合适的,因此需要重新初始化内核态地址空间的页表。那用户态的页表怎样处理呢,暂时不用关心,因为每个进程都有独立的页表,后续创建相应的进程后,会给每个进程分配独立的页表。

早期 MMU 初始化帮助系统从物理地址切换到虚拟地址运行, rt_hw_mmu_setup 函数将重新初始化内核的页表,将内核地址空间前 256M 进行映射,作为内核的地址空间。

struct mem_desc platform_mem_desc[] = 
{
    {
        KERNEL_VADDR_START, KERNEL_VADDR_START + 0x0fffffff, 
        KERNEL_VADDR_START + PV_OFFSET, NORMAL_MEM
    }
};

void rt_hw_mmu_setup(struct mem_desc *mdesc, int desc_nr)
{
    /* set page table */
    for (; desc_nr > 0; desc_nr--)
    {
        rt_hw_mmu_setmtt(mdesc->vaddr_start, mdesc->vaddr_end,
                         mdesc->paddr_start, mdesc->attr);
        mdesc++;
    }
    rt_hw_cpu_dcache_ops(RT_HW_CACHE_FLUSH, (void *)MMUTable, sizeof MMUTable);
    kernel_mmu_switch((unsigned long)MMUTable);
}

从上面的实现可以看出,内核实际上只映射了 256 M 的地址空间。

动态申请地址空间初始化

内核中指定了部分地址空间用于给内核动态分配,这部分内存由 rt_hw_mmu_map_init 函数来指定,代码如下:

rt_hw_mmu_map_init(&mmu_info, (void *)0xfffffffff0000000, 0x10000000, MMUTable, PV_OFFSET);

从上面的代码可以看出,指定了内核地址空间一个高位地址 0xfffffffff0000000 开始的 256M 空间用作动态申请与释放。这部分地址空间后续可以用于 ioremap 以及 share memory 时使用。

需要申请地址空间时,就使用 rt_hw_mmu_maprt_hw_mmu_unmap 这两个函数来进行地址空间的申请和释放。

启动过程代码调试技巧

方法一

在启动代码的汇编部分,前几行代码执行汇编的 ret 函数返回指令,这时候如果代码执行了,就会返回到 uboot,你用这种方法可以知道是否正常执行了固件。

方法二

确定固件启动了之后,可以直接写一段汇编代码,向串口的 tx fifo 写字符,因为 uboot 都初始化好了,所以可以直接用改串口来打印。

方法三

mmu 开启后,会导致方法二无法使用,这时候需要将串口的地址空间范围加入到 MMU 的映射范围内,这样程序才能继续访问串口地址空间。

这时需要修改系统映射 mmu 映射范围更大,使得你可以在虚拟地址和物理地址上都访问串口寄存器的地址范围,同时将 rt_kprintf 配置成直接调用 puts 输出,这样做可以帮助你调试建立 mmu 粗映射到建立正式的系统映射这期间的代码,示例如下:

  • 加载地址 0x81080000
  • 内核链接地址 0xffff000000080000
  • PV_OFFSET 0x1000081000000
  • 串口地址 0x20008000

image-20220517175137186

上述代码片段,扩大了物理地址一一映射的范围,向前偏移了 1.75G,保证了串口的地址空间可以访问,后续就可以正常使用 rt_kprintf 打印调试。