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

语言: CN / TW / HK

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

内核汇编相关篇为:

这应该是系列篇最难写的一篇,全是汇编代码,需大量的底层知识,涉及协处理器,内核镜像重定位,创建内核映射表,初始化 CPU 模式栈,热启动,到最后熟悉的 main()

Uboot

PC寄存器 指向内核第一行指令之前,需要先将内核从 flash 加载到内存指定位置,这部分工作由引导程序 uboot (与硬件强相关) 完成,而引导代码并不在 kernel_liteos_a (与硬件弱相关) 工程中,而在工程 u-boot-2020.01中 ,前往 >> ,所以系列篇不对引导程序详细说明,后续有机会再分析。引导结束后舞台就交给了内核 reset_vector 处 ,一切从此开始。

内核入口

在链接文件 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[30] 设置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, [r11]		  //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),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段,
											       所以只需复制[__exception_handlers,__bss_start]这段数据到内存基址处 */
    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, [r4], #4	// 类似C语言 *r5 = *r4 , r4++ , r5++ 
    str     r7, [r5], #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, [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, [r4, r7, lsr #(20 - 2)]        /* jumpTable[paIndex] = pt entry */
    rsb     r7, r11, r6, lsl #20                /* r7: va */
    str     r12, [r4, r7, lsr #(20 - 2)]        /* jumpTable[vaIndex] = 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
    .word  0xe7ffdeff
#endif

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

解读

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

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

  • 第三步: 对浮点运算FPU的设置,在安全模式下使用FPU,须定义NSACRCPACRFPEXC 三个寄存器

  • 第四步: 计算虚拟地址和物理地址的偏移量,为何要计算它呢 ? 主要目的是为了建立虚拟地址和物理地址的映射关系,因为在 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, [r11] // [r11]中存的是链接地址 `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),由于目前阶段有用的数据是中断向量表+代码段+只读数据段+数据段,
    											       所以只需复制[__exception_handlers,__bss_start]这段数据到内存基址处 */
      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, [r4], #4	// 类似C语言 *r5 = *r4 , r4++ , r5++ 
          str     r7, [r5], #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, [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, [r4, r7, lsr #(20 - 2)]        /* jumpTable[paIndex] = pt entry */
        rsb     r7, r11, r6, lsl #20                /* r7: va */
        str     r12, [r4, r7, lsr #(20 - 2)]        /* jumpTable[vaIndex] = 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
                                                    [31] :0 - Use the 32-bit translation system(虚拟地址是32位)
                                                    [5:4]:0 - use TTBR0和TTBR1
                                                    [2:0]:0 - TTBCR.N为0;
                                                    例如:TTBCR.N为0,TTBR0[31:14-0] | VA[31-0:20] | descriptor-type[1:0]组成32位页表描述符的地址,
                                                            VA[31:20]可以覆盖4GB的地址空间,所以TTBR0页表是16KB,不使用TTBR1;
                                                    例如:TTBCR.N为1,TTBR0[31:14-1] | VA[31-1:20] | descriptor-type[1:0]组成32位页表描述符的地址,
                                                            VA[30:20]可以覆盖2GB的地址空间,所以TTBR0页表是8KB,TTBR1页表是8KB(页表地址必须16KB对齐);
                                                    */
        isb
        orr     r12, r4, #MMU_TTBRx_FLAGS			//将临时页表属性[6:0]和基地址[31:14]放到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[bit29],ap[0]是访问权限位,支持全部的访问权限类型
                                                    disable TEX remap[bit28],使用TEX[2:0]与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, [r0, \offset]    /* 等价于strd r2, r3, [r0, 0], strd r2, r3, [r0, 8], ... , strd r2, r3, [r0, 24] */
        .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, [r0]   //栈顶设置魔法数字
        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 中回复 百万 可方便阅读。

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

「其他文章」