浅沫记忆 发表于 2022-5-26 13:54:28

v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到main() | 百篇博客分析 OpenHarmony 源码

本篇关键词:内核重定位、MMU、SVC栈、热启动、内核映射表
内核汇编相关篇为:

[*]v74.01 鸿蒙内核源码分析(编码方式) | 机器指令是如何编码的
[*]v75.03 鸿蒙内核源码分析(汇编基础) | CPU上班也要打卡
[*]v76.04 鸿蒙内核源码分析(汇编传参) | 如何传递复杂的参数
[*]v77.01 鸿蒙内核源码分析(链接脚本) | 正在制作中 ...
[*]v78.01 鸿蒙内核源码分析(内核启动) | 从汇编到main()
[*]v79.01 鸿蒙内核源码分析(进程切换) | 正在制作中 ...
[*]v80.03 鸿蒙内核源码分析(任务切换) | 看汇编如何切换任务
[*]v81.05 鸿蒙内核源码分析(中断切换) | 系统因中断活力四射
[*]v82.06 鸿蒙内核源码分析(异常接管) | 社会很单纯 复杂的是人
[*]v83.01 鸿蒙内核源码分析(缺页中断) | 正在制作中 ...
这应该是系列篇最难写的一篇,全是汇编代码,需大量的底层知识,涉及协处理器,内核镜像重定位,创建内核映射表,初始化 CPU 模式栈,热启动,到最后熟悉的 main() 。
内核入口
在链接文件 liteos.ld 中可知内核的入口地址为 ENTRY(reset_vector) , 分别出现在reset_vector_mp.S (多核启动) 和 reset_vector_up.S(单核启动),系列篇研究多核启动的情况。代码可结合 (协处理器篇) 看更容易懂。
reset_vector: //鸿蒙开机代码
    /* clear register TPIDRPRW */
    mov   r0, #0//r0 = 0
    mcr   p15, 0, r0, c13, c0, 4//复位线程标识符寄存器TPIDRPRW , 不复位将导致系统不能启动
    /* do some early cpu setup: i/d cache disable, mmu disabled */
    mrc   p15, 0, r0, c1, c0, 0//System Control Register-SCTLR | 读取系统控制寄存器内容
    bic   r0, #(1<<12)//禁用指令缓存功能
    bic   r0, #(1<<2 | 1<<0)//禁用数据和TLB的缓存功能(bit2) | mmu功能(bit0)
    mcr   p15, 0, r0, c1, c0, 0//写系统控制寄存器

    /* enable fpu+neon 一些系统寄存器的操作
    | 使能浮点运算(floating point unit)和 NEON就是一种基于SIMD思想的ARM技术,相比于ARMv6或之前的架构,
    NEON结合了64-bit和128-bit的SIMD指令集,提供128-bit宽的向量运算(vector operations)*/
#ifndef LOSCFG_TEE_ENABLE      //Trusted Execution Environment   可信执行环境
    MRC    p15, 0, r0, c1, c1, 2 //非安全模式访问寄存器 (Non-Secure Access Control Register - NSACR)
    ORR    r0, r0, #0xC00      //使能安全和非安全访问协处理器10和11(Coprocessor 10和11)
    BIC    r0, r0, #0xC000       //设置bit15为0,不会影响修改CPACR.ASEDIS寄存器位(控制Advanced SIMD功能)| bit14 reserved
    MCR    p15, 0, r0, c1, c1, 2

    LDR    r0, =(0xF << 20)      //允许在EL0和EL1下,访问协处理器10和11(控制Floating-point和Advanced SIMD特性)
    MCR    p15, 0, r0, c1, c0, 2
    ISB
#endif
    MOV    r3, #0x40000000    //EN, bit 设置FPEXC的EN位来使能FPU
    VMSR   FPEXC, r3//浮点异常控制寄存器 (Floating-Point Exception Control register | B4.1.57)

    /* r11: delta of physical address and virtual address | 计算虚拟地址和物理地址之间的差值,目的是为了建立映射关系表 */
    adr   r11, pa_va_offset //获取pa_va_offset变量物理地址,由于这时候mmu已经被关闭,所以这个值就表示pa_va_offset变量的物理地址。
                              /*adr 是一条小范围的地址读取伪指令,它将基于PC的相对偏移的地址值读到目标寄存器中。
                               *编译源程序时,汇编器首先计算当前PC值(当前指令位置)到exper的距离,然后用一条ADD或者SUB指令替换这条伪指令,
                               *例如:ADD register,PC,#offset_to_exper 注意,标号exper与指令必须在同一代码段
                               */
    ldr   r0, //r0 = *r11 获取pa_va_offset变量虚拟地址
    sub   r11, r11, r0//物理地址-虚拟地址 = 映射偏移量 放入r11

    mrc   p15, 0, r12, c0, c0, 5      /* Multiprocessor Affinity Register-MPIDR */
    and   r12, r12, #MPIDR_CPUID_MASK //掩码过滤
    cmp   r12, #0                  //主控核0判断
    bne   secondary_cpu_init      //初始化CPU次核
/*
* adr是小范围的地址读取伪指令,它将基于PC寄存器相对偏移的地址值读取到寄存器中,
* 例如: 0x00000004: adr   r4, __exception_handlers
* 则此时PC寄存器的值为: 0x00000004 + 8(在三级流水线时,PC和执行地址相差8),
   * adr指令和标识__exception_handlers的地址相对固定,二者偏移量若为offset,
* 最后r4 = (0x00000004 + 8) + offset
*/

    /* if we need to relocate to proper location or not | 如果需要重新安装到合适的位置*/
    adr   r4, __exception_handlers            /* r4: base of load address | 加载基址*/
    ldr   r5, =SYS_MEM_BASE                   /* r5: base of physical address | 物理基址*/
    subs    r12, r4, r5                         /* r12: delta of load address and physical address | 二者偏移量*/
    beq   reloc_img_to_bottom_done            /* if we load image at the bottom of physical address | 不相等就需要重定位 */

    /* we need to relocate image at the bottom of physical address | 需要知道拷贝的大小*/
    ldr   r7, =__exception_handlers         /* r7: base of linked address (or vm address) | 链接地址基地址*/
    ldr   r6, =__bss_start                  /* r6: end of linked address (or vm address),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段,
       所以只需复制这段数据到内存基址处 */
    sub   r6, r7                              /* r6: delta of linked address (or vm address) | 内核镜像大小 */
    add   r6, r4                              /* r6: end of load address | 说明需拷贝[ r4,r4+r6 ] 区间内容到 [ r5,r5+r6 ]*/

reloc_img_to_bottom_loop://重定位镜像到内核物理内存基地址,将内核从加载地址拷贝到内存基址处
    ldr   r7, , #4// 类似C语言 *r5 = *r4 , r4++ , r5++
    str   r7, , #4// #4 代表32位的指令长度,此时在拷贝内核代码区内容
    cmp   r4, r6          /* 拷贝完成条件. r4++ 直到等于r6 (加载结束地址) 完成拷贝动作 */
    bne   reloc_img_to_bottom_loop
    sub   pc, r12                           /* 重新校准pc寄存器, 无缝跳到了拷贝后的指令地址处执行 r12是重定位镜像前内核加载基地址和内核物理内存基地址的差值 */
    nop// 注意执行完成sub       pc, r12后,新的PC寄存器也指向了 nop ,nop是伪汇编指令,等同于 mov r0 r0 通常用于控制时序的目的,强制内存对齐,防止流水线灾难,占据分支指令延迟
    sub   r11, r11, r12                     /* r11: eventual address offset | 最终地址映射偏移量, 用于构建MMU页表 */
//内核总大小 __bss_start - __exception_handlers
reloc_img_to_bottom_done:
#ifdef LOSCFG_KERNEL_MMU
    ldr   r4, =g_firstPageTable               /* r4: physical address of translation table and clear it
   内核页表是用数组g_firstPageTable存储 见于los_arch_mmu.c */
    add   r4, r4, r11                         //计算g_firstPageTable页表物理地址
    mov   r0, r4//因为默认r0 将作为memset_optimized的第一个参数
    mov   r1, #0//第二个参数,清0
    mov   r2, #MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS //第三个参数是L1表的长度
    bl      memset_optimized                  /* optimized memset since r0 is 64-byte aligned | 将内核页表空间清零*/

    ldr   r5, =g_archMmuInitMapping      //记录映射关系表
    add   r5, r5, r11                         //获取g_archMmuInitMapping的物理地址
init_mmu_loop:                              //初始化内核页表
    ldmia   r5!, {r6-r10}                     /* r6 = phys, r7 = virt, r8 = size, r9 = mmu_flags, r10 = name | 传参: 物理地址、虚拟地址、映射大小、映射属性、名称*/
    cmp   r8, 0                               /* if size = 0, the mmu init done | 完成条件 */
    beq   init_mmu_done                //标志寄存器中Z标志位等于零时跳转到 init_mmu_done处执行
    bl      page_table_build                //创建页表
    b       init_mmu_loop//循环继续
init_mmu_done:
    orr   r8, r4, #MMU_TTBRx_FLAGS            /* r8 = r4 and set cacheable attributes on translation walk | 设置缓存*/
    ldr   r4, =g_mmuJumpPageTable             /* r4: jump pagetable vaddr | 页表虚拟地址*/
    add   r4, r4, r11
    ldr   r4,
    add   r4, r4, r11                         /* r4: jump pagetable paddr | 页表物理地址*/

    /* build 1M section mapping, in order to jump va during turing on mmu:pa == pa, va == pa */
    /* 从当前PC开始建立1MB空间的段映射,分别建立物理地址和虚拟地址方式的段映射页表项
   * 内核临时页表在系统 使能mmu -> 切换到虚拟地址运行 这段时间使用
   */
    mov   r6, pc
    mov   r7, r6                              /* r7: pa (MB aligned)*/
    lsr   r6, r6, #20                         /* r6: pa l1 index */
    ldr   r10, =MMU_DESCRIPTOR_KERNEL_L1_PTE_FLAGS
    add   r12, r10, r6, lsl #20               /* r12: pa |flags */
    str   r12,       /* jumpTable = pt entry */
    rsb   r7, r11, r6, lsl #20                /* r7: va */
    str   r12,       /* jumpTable = pt entry */

    bl      mmu_setup                           /* set up the mmu | 内核映射表已经创建好了,此时可以启动MMU工作了*/
#endif
    /* clear out the interrupt and exception stack and set magic num to check the overflow
    |exc_stack|地址高位
    |svc_stack|地址低位
清除中断和异常堆栈并设置magic num检查溢出 */
    ldr   r0, =__svc_stack    //stack_init的第一个参数 __svc_stack表示栈顶
    ldr   r1, =__exc_stack_top//stack_init的第二个参数 __exc_stack_top表示栈底, 这里会有点绕, top表高地址位
    bl      stack_init            //初始化各个cpu不同模式下的栈空间
//设置各个栈顶魔法数字
    STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫"
    STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD   //异常栈底设成"烫烫烫烫烫烫"

warm_reset: //热启动 Warm Reset, warm reboot, soft reboot, 在不关闭电源的情况,由软件控制重启计算机
    /* initialize CPSR (machine state register) */
    mov    r0, #(CPSR_IRQ_DISABLE|CPSR_FIQ_DISABLE|CPSR_SVC_MODE) /* 禁止IRQ中断 | 禁止FIQ中断 | 管理模式-操作系统使用的保护模式 */
    msr    cpsr, r0//设置CPSR寄存器

    /* Note: some functions in LIBGCC1 will cause a "restore from SPSR"!! */
    msr    spsr, r0 //设置SPSR寄存器

    /* get cpuid and keep it in r12 */
    mrc   p15, 0, r12, c0, c0, 5//R12保存CPUID
    and   r12, r12, #MPIDR_CPUID_MASK //掩码操作获取当前cpu id

    /* set svc stack, every cpu has OS_EXC_SVC_STACK_SIZE stack | 设置 SVC栈 */
    ldr    r0, =__svc_stack_top //注意这是栈底,高地址位
    mov    r2, #OS_EXC_SVC_STACK_SIZE //栈大小
    mul    r2, r2, r12
    sub    r0, r0, r2                   /* 算出当前core的中断栈栈顶位置,写入所属core的sp */
    mov    sp, r0

    LDR    r0, =__exception_handlers   
    MCR    p15, 0, r0, c12, c0, 0       /* Vector Base Address Register - VBAR */

    cmp    r12, #0//CPU是否为主核
    bne    cpu_start                  //不相等就跳到从核处理分支

clear_bss:                            //主核处理.bss段清零
    ldr    r0, =__bss_start
    ldr    r2, =__bss_end
    mov    r1, #0
    sub    r2, r2, r0
    bl   memset
#if defined(LOSCFG_CC_STACKPROTECTOR_ALL) || \
    defined(LOSCFG_CC_STACKPROTECTOR_STRONG) || \
    defined(LOSCFG_CC_STACKPROTECTOR)
    bl   __stack_chk_guard_setup
#endif

#ifdef LOSCFG_GDB_DEBUG
    /* GDB_START - generate a compiled_breadk,This function will get GDB stubs started, with a proper environment */
    bl   GDB_START
    .word0xe7ffdeff
#endif

    bl   main                //带LR的子程序跳转, LR = pc - 4, 执行C层main函数   
解读

[*]第一步: 操作 CP15 协处理器 TPIDRPRW 寄存器,它被 ARM 设计保存当前运行线程的 ID值,在ARMv7 架构中才新出现,需PL1权限以上才能访问,而硬件不会从内部去改变它的值,也就是说这是一个直接暴露给工程师操作维护的一个寄存器,在鸿蒙内核中被用于记录线程结构体的开始地址,可以搜索 OsCurrTaskSet 来跟踪哪些地方会切换当前任务以便更好的理解内核。

[*]第二步: 系统控制寄存器(SCTLR),B4.1.130 SCTLR, System Control Register 它提供了系统的最高级别控制,高到了玉皇大帝级别,代码中将 0、2、12位写 0。对应关闭 MMU 、数据缓存 、指令缓存 功能。

[*]第三步: 对浮点运算FPU的设置,在安全模式下使用FPU,须定义NSACR、CPACR、FPEXC 三个寄存器

[*]第四步: 计算虚拟地址和物理地址的偏移量,为何要计算它呢 ? 主要目的是为了建立虚拟地址和物理地址的映射关系,因为在 MMU启动之后,运行地址(PC寄存器指向的地址)将变成虚拟地址,使用虚拟地址就离不开映射表,所以两个地址的映射关系需要在MMU启动前就创建好,而有了偏移量就可以创建映射表。但需先搞清楚 链接地址 和 运行地址 两个概念。
   
[*]链接地址 由链接器确定,链接器会将所有输入的 .o 文件链接成一个格式的 .bin 文件,它们都是ELF格式, 链接器给每条指令/数据都赋与一个地址,这个地址叫链接地址,它可以是相对的也可以是绝对的。但它们之间的内部距离是固定的,链接具体过程可翻看 (重定位篇) 和 (链接脚本篇)   
[*]运行地址 由加载器确定,内核镜像首先通过烧录工具将内核烧录到 flash 指定的位置,开机后由boot loader工具,例如uboot,将内核镜像加载到指定地址后开始执行真正的内核代码,这个地址叫运行地址。   
两个地址往往不一样,而内核设计者希望它们是一样的,那有没有办法检测二者是否一样呢? 答案是 : 当然有的 ,通过一个变量在链接时将其链接地址变成变量的内容 ,无论中间怎么加载变量的内容是不会变的,而获取运行地址是很容易获取的,其实就是PC寄存器的地址,二者一减,加载偏了多少不就出来了
pa_va_offset:
.word   . //定义一个4字节的pa_va_offset 变量, 链接器生成一个链接地址, . 表示 pa_va_offset = 链接地址 举例: 在地址 0x17321796 中保存了 0x17321796 值

adr   r11, pa_va_offset //代码已执行至此,指令将获取 pa_va_offset 的运行地址(可能不是`0x17321796`) 给r11
ldr   r0, // 中存的是链接地址 `0x17321796`, 它不会随加载器变化的
sub   r11, r11, r0 // 二者相减得到了偏移地址
   
[*]第五步: 将内核代码从 __exception_handlers 处移到 SYS_MEM_BASE处,长度是 __bss_start - __exception_handlers , __exception_handlers是加载后的开始地址, 由加载器决定, 而SYS_MEM_BASE 是系统定义的内存地址, 可由系统集成商指定配置, 他们希望内核从这里运行。 下图为内核镜像布局具体代码如下:
    /* if we need to relocate to proper location or not | 如果需要重新安装到合适的位置*/
adr   r4, __exception_handlers            /* r4: base of load address | 加载基址*/
ldr   r5, =SYS_MEM_BASE                   /* r5: base of physical address | 物理基址*/
subs    r12, r4, r5                         /* r12: delta of load address and physical address | 二者偏移量*/
beq   reloc_img_to_bottom_done            /* if we load image at the bottom of physical address | 不相等就需要重定位 */

/* we need to relocate image at the bottom of physical address | 需要知道拷贝的大小*/
ldr   r7, =__exception_handlers         /* r7: base of linked address (or vm address) | 链接地址基地址*/
ldr   r6, =__bss_start                  /* r6: end of linked address (or vm address),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段,
       所以只需复制这段数据到内存基址处 */
sub   r6, r7                              /* r6: delta of linked address (or vm address) | 内核镜像大小 */
add   r6, r4                              /* r6: end of load address | 说明需拷贝[ r4,r4+r6 ] 区间内容到 [ r5,r5+r6 ]*/

reloc_img_to_bottom_loop://重定位镜像到内核物理内存基地址,将内核从加载地址拷贝到内存基址处
      ldr   r7, , #4// 类似C语言 *r5 = *r4 , r4++ , r5++
      str   r7, , #4// #4 代表32位的指令长度,此时在拷贝内核代码区内容
      cmp   r4, r6          /* 拷贝完成条件. r4++ 直到等于r6 (加载结束地址) 完成拷贝动作 */
      bne   reloc_img_to_bottom_loop
      sub   pc, r12                           /* 重新校准pc寄存器, 无缝跳到了拷贝后的指令地址处执行 r12是重定位镜像前内核加载基地址和内核物理内存基地址的差值 */
      nop// 注意执行完成sub       pc, r12后,新的PC寄存器也指向了 nop ,nop是伪汇编指令,等同于 mov r0 r0 通常用于控制时序的目的,强制内存对齐,防止流水线灾难,占据分支指令延迟
      sub   r11, r11, r12                     /* r11: eventual address offset | 最终地址偏移量 */

[*]第六步: 在打开MMU必须要做好虚拟地址和物理地址的映射关系 , 需构建页表 , 关于页表可翻看 虚实映射篇, 具体代码如下
#ifdef LOSCFG_KERNEL_MMU
ldr   r4, =g_firstPageTable               /* r4: physical address of translation table and clear it
   内核页表是用数组g_firstPageTable存储 见于los_arch_mmu.c */
add   r4, r4, r11                         //计算g_firstPageTable页表物理地址
mov   r0, r4//因为默认r0 将作为memset_optimized的第一个参数
mov   r1, #0//第二个参数,清0
mov   r2, #MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS //第三个参数是L1表的长度
bl      memset_optimized                  /* optimized memset since r0 is 64-byte aligned | 将内核页表空间清零*/

ldr   r5, =g_archMmuInitMapping      //记录映射关系表
add   r5, r5, r11                         //获取g_archMmuInitMapping的物理地址
init_mmu_loop:                              //初始化内核页表
    ldmia   r5!, {r6-r10}                     /* r6 = phys, r7 = virt, r8 = size, r9 = mmu_flags, r10 = name | 物理地址、虚拟地址、映射大小、映射属性、名称*/
    cmp   r8, 0                               /* if size = 0, the mmu init done */
    beq   init_mmu_done                //标志寄存器中Z标志位等于零时跳转到 init_mmu_done处执行
    bl      page_table_build                //创建页表
    b       init_mmu_loop//循环继续
init_mmu_done:
    orr   r8, r4, #MMU_TTBRx_FLAGS            /* r8 = r4 and set cacheable attributes on translation walk | 设置缓存*/
    ldr   r4, =g_mmuJumpPageTable             /* r4: jump pagetable vaddr | 页表虚拟地址*/
    add   r4, r4, r11
    ldr   r4,
    add   r4, r4, r11                         /* r4: jump pagetable paddr | 页表物理地址*/

    /* build 1M section mapping, in order to jump va during turing on mmu:pa == pa, va == pa */
    /* 从当前PC开始建立1MB空间的段映射,分别建立物理地址和虚拟地址方式的段映射页表项
    * 内核临时页表在系统 使能mmu -> 切换到虚拟地址运行 这段时间使用
    */
    mov   r6, pc
    mov   r7, r6                              /* r7: pa (MB aligned)*/
    lsr   r6, r6, #20                         /* r6: pa l1 index */
    ldr   r10, =MMU_DESCRIPTOR_KERNEL_L1_PTE_FLAGS
    add   r12, r10, r6, lsl #20               /* r12: pa |flags */
    str   r12,       /* jumpTable = pt entry */
    rsb   r7, r11, r6, lsl #20                /* r7: va */
    str   r12,       /* jumpTable = pt entry */

    bl      mmu_setup                           /* set up the mmu | 内核映射表已经创建好了,此时可以启动MMU工作了*/
#endif

[*]第七步: 使能MMU, 有了页表就可以使用虚拟地址了
mmu_setup://启动MMU工作
    mov   r12, #0                           /* TLB Invalidate All entries - TLBIALL */
    mcr   p15, 0, r12, c8, c7, 0            /* Set c8 to control the TLB and set the mapping to invalid */
    isb
    mcr   p15, 0, r12, c2, c0, 2            /* Translation Table Base Control Register(TTBCR) = 0x0
                                                 :0 - Use the 32-bit translation system(虚拟地址是32位)
                                                :0 - use TTBR0和TTBR1
                                                :0 - TTBCR.N为0;
                                                例如:TTBCR.N为0,TTBR0 | VA | descriptor-type组成32位页表描述符的地址,
                                                      VA可以覆盖4GB的地址空间,所以TTBR0页表是16KB,不使用TTBR1;
                                                例如:TTBCR.N为1,TTBR0 | VA | descriptor-type组成32位页表描述符的地址,
                                                      VA可以覆盖2GB的地址空间,所以TTBR0页表是8KB,TTBR1页表是8KB(页表地址必须16KB对齐);
                                                */
    isb
    orr   r12, r4, #MMU_TTBRx_FLAGS//将临时页表属性和基地址放到r12
    mcr   p15, 0, r12, c2, c0, 0            /* Set attributes and set temp page table */
    isb
    mov   r12, #0x7                           /* 0b0111 */
    mcr   p15, 0, r12, c3, c0, 0            /* Set DACR with 0b0111, client and manager domian */
    isb
    mrc    p15, 0, r12, c1, c0, 1               /* ACTLR, Auxlliary Control Register */
    orr    r12, r12, #(1 << 6)                  /* SMP, Enables coherent requests to the processor. */
    orr    r12, r12, #(1 << 2)                  /* Enable D-side prefetch */
    orr    r12, r12, #(1 << 11)               /* Global BP Enable bit */
    mcr    p15, 0, r12, c1, c0, 1               /* ACTLR, Auxlliary Control Register */
    dsb
    /*
    * 开始使能MMU,使用的是内核临时页表,这时cpu访问内存不管是取指令还是访问数据都是需要经过mmu来翻译,
    * 但是在mmu使能之前cpu使用的都是内核的物理地址,即使现在使能了mmu,cpu访问的地址值还是内核的物理地址值(这里仅仅从数值上来看),
    * 而又由于mmu使能了,所以cpu会把这个值当做虚拟地址的值到页表中去找其对应的物理地址来访问。
    * 所以现在明白了为什么要在内核临时页表里建立一个内核物理地址和虚拟地址一一映射的页表项了吧,因为建立了一一映射,
    * cpu访问的地址经过mmu翻译得到的还是和原来一样的值,这样在cpu真正使用虚拟地址之前也能正常运行。
    */
    mrc   p15, 0, r12, c1, c0, 0
    bic   r12, #(1 << 29 | 1 << 28)         /* disable access flag,ap是访问权限位,支持全部的访问权限类型
                                                disable TEX remap,使用TEX与C Bbit控制memory region属性 */
    orr   r12, #(1 << 0)                      /* mmu enable */
    bic   r12, #(1 << 1)
    orr   r12, #(1 << 2)                     /* D cache enable */
    orr   r12, #(1 << 12)                  /* I cache enable */
    mcr   p15, 0, r12, c1, c0, 0            /* Set SCTLR with r12: Turn on the MMU, I/D cache Disable TRE/AFE */
    isb
    ldr   pc,=1f                            /* Convert to VA | 1表示标号,f表示forward(往下) - pc值取往下标识符“1”的虚拟地址(跳转到标识符“1”处)
                                                因为之前已经在内核临时页表中建立了内核虚拟地址和物理地址的映射关系,所以接下来cpu切换到虚拟地址空间 */
    1:
      mcr   p15, 0, r8, c2, c0, 0               /* Go to the base address saved in C2: Jump to the page table */
      isb                                       //r8中保存的是内核L1页表基地址和flags,r8写入到TTBR0实现临时页表和内核页表的切换
      mov   r12, #0
      mcr   p15, 0, r12, c8, c7, 0            /* TLB Invalidate All entries - TLBIALL(Invalidate all EL1&0 regime stage 1 and 2 TLB entries) */
      isb
      sub   lr,r11                            /* adjust lr with delta of physical address and virtual address |
                                                    lr中保存的是mmu使能之前返回地址的物理地址值,这时需要转换为虚拟地址,转换算法也很简单,虚拟地址 = 物理地址 - r11 */
      bx      lr                                  //返回

[*]第八步: 设置异常和中断栈 ,初始化栈内值和栈顶值
//初始化栈内值
    ldr   r0, =__svc_stack    //stack_init的第一个参数 __svc_stack表示栈顶
    ldr   r1, =__exc_stack_top//stack_init的第二个参数 __exc_stack_top表示栈底, 这里会有点绕, top表高地址位
    bl      stack_init            //初始化各个cpu不同模式下的栈空间
    //设置各个栈顶魔法数字
    STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫"
    STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD   //异常栈底设成"烫烫烫烫烫烫"
stack_init:
    ldr   r2, =OS_STACK_INIT//0xCACACACA
    ldr   r3, =OS_STACK_INIT
    /* Main loop sets 32 bytes at a time. | 主循环一次设置 32 个字节*/
stack_init_loop:
    .irp    offset, #0, #8, #16, #24
    strd    r2, r3,     /* 等价于strd r2, r3, , strd r2, r3, , ... , strd r2, r3, */
    .endr
    add   r0, #32//加跳32个字节,说明在地址范围上 r1 > r0 ==> __exc_stack_top > __svc_stack
    cmp   r0, r1//是否到栈底
    blt   stack_init_loop
    bx      lr

//初始化栈顶值
excstack_magic:
    mov   r3, #0 //r3 = 0
excstack_magic_loop:
    str   r2,    //栈顶设置魔法数字
    add   r0, r0, r1 //定位到栈底
    add   r3, r3, #1 //r3++
    cmp   r3, #CORE_NUM //栈空间等分成core_num个空间,所以每个core的栈顶需要magic num
    blt   excstack_magic_loop
    bx      lr
/* param0 is stack top, param1 is stack size, param2 is magic num */
.macro STACK_MAGIC_SET param0, param1, param2
    ldr   r0, =\param0
    mov   r1, \param1
    ldr   r2, =\param2
    bl      excstack_magic
.endm
STACK_MAGIC_SET __svc_stack, #OS_EXC_SVC_STACK_SIZE, OS_STACK_MAGIC_WORD //中断栈底设成"烫烫烫烫烫烫"
STACK_MAGIC_SET __exc_stack, #OS_EXC_STACK_SIZE, OS_STACK_MAGIC_WORD   //异常栈底设成"烫烫烫烫烫烫"

[*]第九步: 热启动
warm_reset: //热启动 Warm Reset, warm reboot, soft reboot, 在不关闭电源的情况,由软件控制重启计算机
/* initialize CPSR (machine state register) */
mov    r0, #(CPSR_IRQ_DISABLE|CPSR_FIQ_DISABLE|CPSR_SVC_MODE) /* 禁止IRQ中断 | 禁止FIQ中断 | 管理模式-操作系统使用的保护模式 */
msr    cpsr, r0

/* Note: some functions in LIBGCC1 will cause a "restore from SPSR"!! */
msr    spsr, r0

/* get cpuid and keep it in r12 */
mrc   p15, 0, r12, c0, c0, 5//R12保存CPUID
and   r12, r12, #MPIDR_CPUID_MASK //掩码操作获取当前cpu id

/* set svc stack, every cpu has OS_EXC_SVC_STACK_SIZE stack */
ldr    r0, =__svc_stack_top
mov    r2, #OS_EXC_SVC_STACK_SIZE
mul    r2, r2, r12
sub    r0, r0, r2                   /* 算出当前core的中断栈栈顶位置,写入所属core的sp */
mov    sp, r0

LDR    r0, =__exception_handlers
MCR    p15, 0, r0, c12, c0, 0       /* Vector Base Address Register - VBAR */

cmp    r12, #0
bne    cpu_start                  //从核处理分支

[*]第十步: 进入 C 语言的 main()
bl   main                //带LR的子程序跳转, LR = pc - 4, 执行C层main函数

LITE_OS_SEC_TEXT_INIT INT32 main(VOID)//由主CPU执行,默认0号CPU 为主CPU
{
      UINT32 ret = OsMain();
      if (ret != LOS_OK) {
          return (INT32)LOS_NOK;
      }

      CPU_MAP_SET(0, OsHwIDGet());//设置CPU映射,参数0 代表0号CPU

      OsSchedStart();//调度开始

      while (1) {
          __asm volatile("wfi");//WFI: wait for Interrupt 等待中断,即下一次中断发生前都在此hold住不干活
      }
}

百文说内核 | 抓住主脉络


[*]百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
[*]与代码需不断debug一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx 代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
[*]百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,鸿蒙研究站 | weharmonyos 中回复 百文 可方便阅读。
[*]
按功能模块:
基础知识    进程管理    任务管理    内存管理            双向链表 内核概念 源码结构 地址空间 计时单位 优雅的宏 钩子框架 位图管理 POSIX main函数    调度故事 进程控制块 进程空间 线性区 红黑树 进程管理 Fork进程 进程回收 Shell编辑 Shell解析    任务控制块 并发并行 就绪队列 调度机制 任务管理 用栈方式 软件定时器 控制台 远程登录 协议栈    内存规则 物理内存 内存概念 虚实映射 页表管理 静态分配 TLFS算法 内存池管理 原子操作 圆整对齐          通讯机制    文件系统    硬件架构    内核汇编          通讯总览 自旋锁 互斥锁 快锁使用 快锁实现 读写锁 信号量 事件机制 信号生产 信号消费 消息队列 消息封装 消息映射 共享内存    文件概念 文件故事 索引节点 VFS 文件句柄 根文件系统 挂载机制 管道文件 文件映射 写时拷贝    芯片模式 ARM架构 指令集 协处理器 工作模式 寄存器 多核管理 中断概念 中断管理    编码方式 汇编基础 汇编传参 链接脚本 内核启动 进程切换 任务切换 中断切换 异常接管 缺页中断          编译运行    调测工具                  编译过程 编译构建 GN语法 忍者无敌 ELF格式 ELF解析 静态链接 重定位 动态链接 进程映像 应用启动 系统调用 VDSO    模块监控 日志跟踪 系统安全 测试用例               百万注源码 | 处处扣细节


[*]百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。

[*]< gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码,鸿蒙研究站 | weharmonyos 中回复 百万 可方便阅读。


据说喜欢点赞分享的,后来都成了大神。:)

https://my.oschina.net/weharmony/blog/5531612
页: [1]
查看完整版本: v87.01 鸿蒙内核源码分析 (内核启动篇) | 从汇编到main() | 百篇博客分析 OpenHarmony 源码