本篇关键词:内核重定位、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:
mov r0, #0
mcr p15, 0, r0, c13, c0, 4
mrc p15, 0, r0, c1, c0, 0
bic r0, #(1<<12)
bic r0, #(1<<2 | 1<<0)
mcr p15, 0, r0, c1, c0, 0
#ifndef LOSCFG_TEE_ENABLE
MRC p15, 0, r0, c1, c1, 2
ORR r0, r0, #0xC00
BIC r0, r0, #0xC000
MCR p15, 0, r0, c1, c1, 2
LDR r0, =(0xF << 20)
MCR p15, 0, r0, c1, c0, 2
ISB
#endif
MOV r3, #0x40000000
VMSR FPEXC, r3
adr r11, pa_va_offset
ldr r0, [r11]
sub r11, r11, r0
mrc p15, 0, r12, c0, c0, 5
and r12, r12, #MPIDR_CPUID_MASK
cmp r12, #0
bne secondary_cpu_init
adr r4, __exception_handlers
ldr r5, =SYS_MEM_BASE
subs r12, r4, r5
beq reloc_img_to_bottom_done
ldr r7, =__exception_handlers
ldr r6, =__bss_start
sub r6, r7
add r6, r4
reloc_img_to_bottom_loop:
ldr r7, [r4], #4
str r7, [r5], #4
cmp r4, r6
bne reloc_img_to_bottom_loop
sub pc, r12
nop
sub r11, r11, r12
reloc_img_to_bottom_done:
#ifdef LOSCFG_KERNEL_MMU
ldr r4, =g_firstPageTable
add r4, r4, r11
mov r0, r4
mov r1, #0
mov r2, #MMU_DESCRIPTOR_L1_SMALL_ENTRY_NUMBERS
bl memset_optimized
ldr r5, =g_archMmuInitMapping
add r5, r5, r11
init_mmu_loop:
ldmia r5!, {r6-r10}
cmp r8, 0
beq init_mmu_done
bl page_table_build
b init_mmu_loop
init_mmu_done:
orr r8, r4, #MMU_TTBRx_FLAGS
ldr r4, =g_mmuJumpPageTable
add r4, r4, r11
ldr r4, [r4]
add r4, r4, r11
mov r6, pc
mov r7, r6
lsr r6, r6, #20
ldr r10, =MMU_DESCRIPTOR_KERNEL_L1_PTE_FLAGS
add r12, r10, r6, lsl #20
str r12, [r4, r7, lsr #(20 - 2)]
rsb r7, r11, r6, lsl #20
str r12, [r4, r7, lsr #(20 - 2)]
bl mmu_setup
#endif
ldr r0, =__svc_stack
ldr r1, =__exc_stack_top
bl stack_init
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:
mov r0, #(CPSR_IRQ_DISABLE|CPSR_FIQ_DISABLE|CPSR_SVC_MODE)
msr cpsr, r0
msr spsr, r0
mrc p15, 0, r12, c0, c0, 5
and r12, r12, #MPIDR_CPUID_MASK
ldr r0, =__svc_stack_top
mov r2, #OS_EXC_SVC_STACK_SIZE
mul r2, r2, r12
sub r0, r0, r2
mov sp, r0
LDR r0, =__exception_handlers
MCR p15, 0, r0, c12, c0, 0
cmp r12, #0
bne cpu_start
clear_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
bl GDB_START
.word 0xe7ffdeff
#endif
bl main
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
解读
-
第一步: 操作 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寄存器指向的地址)CPU会将理解成虚拟地址,使用虚拟地址就离不开映射表,所以这两个地址的映射关系需要在MMU启动前就创建好,而有了偏移量就可以创建映射表。但需先搞清楚 链接地址 和 运行地址 两个概念。
- 链接地址 由链接器确定,链接器会将所有输入的 .o 文件链接成一个 .bin 文件,它们都是ELF格式, 链接器给每条指令/数据都赋与一个地址,这个地址叫链接地址,它可以是相对的也可以是绝对的。但它们之间的内部距离是固定的,链接具体过程可翻看 (重定位篇) 和 (链接脚本篇)。
- 运行地址 由加载器确定,内核镜像首先通过烧录工具将内核烧录到 flash 指定的位置,开机后由boot loader工具,例如uboot,将内核镜像加载到指定地址后开始执行真正的内核代码,这个地址叫运行地址。
两个地址值往往不一样,而内核设计者希望它们是一样的,那有没有办法检测二者是否一样呢?答案是: 有,那必须的 。通过一个变量在链接时将其链接地址变成变量的内容 ,无论中间怎么加载变量的内容是不会变的,而获取运行地址是很容易获取的,其实就是PC寄存器的地址,二者相减,加载后偏了多少可不就出来了。别看说了这么一大串,实现却只需4行代码,但如果没有上面的废话每一行都会把您整懵逼。
-
第五步: 将内核代码从 __exception_handlers 处移到 SYS_MEM_BASE处,长度是 __bss_start - __exception_handlers , __exception_handlers是加载后的开始地址, 由加载器决定, 而SYS_MEM_BASE 是系统定义的内存地址, 可由系统集成商指定配置, 他们希望内核从这里运行。 下图为内核镜像布局

具体代码如下:
-
第六步: 在打开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
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
-
第七步: 使能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 //返回
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
-
第八步: 设置异常和中断栈 ,初始化栈内值和栈顶值
-
第九步: 热启动
-
第十步: 进入 C 语言的 main()
百文说内核 | 抓住主脉络
- 百文相当于摸出内核的肌肉和器官系统,让人开始丰满有立体感,因是直接从注释源码起步,在加注释过程中,每每有心得处就整理,慢慢形成了以下文章。内容立足源码,常以生活场景打比方尽可能多的将内核知识点置入某种场景,具有画面感,容易理解记忆。说别人能听得懂的话很重要! 百篇博客绝不是百度教条式的在说一堆诘屈聱牙的概念,那没什么意思。更希望让内核变得栩栩如生,倍感亲切。
- 与代码需不断
debug
一样,文章内容会存在不少错漏之处,请多包涵,但会反复修正,持续更新,v**.xx
代表文章序号和修改的次数,精雕细琢,言简意赅,力求打造精品内容。
- 百文在 < 鸿蒙研究站 | 开源中国 | 博客园 | 51cto | csdn | 知乎 | 掘金 > 站点发布,百篇博客系列目录如下。

按功能模块:
百万注源码 | 处处扣细节
-
百万汉字注解内核目的是要看清楚其毛细血管,细胞结构,等于在拿放大镜看内核。内核并不神秘,带着问题去源码中找答案是很容易上瘾的,你会发现很多文章对一些问题的解读是错误的,或者说不深刻难以自圆其说,你会慢慢形成自己新的解读,而新的解读又会碰到新的问题,如此层层递进,滚滚向前,拿着放大镜根本不愿意放手。
-
< gitee | github | coding | gitcode > 四大码仓推送 | 同步官方源码。

据说喜欢 点赞 + 分享 的,后来都成了大神。😃
大佬的知识面太广了,膜拜一波!
收下我的膝盖
膜拜大神!!
附件下载不了了