Zircon 是 Google 新操作系统 Fuchsia 的内核,基于 LK - Little Kernel 演变而来。而 Little Kernel 前面一直作为 Android 系统的 Bootloader 的核心而存在。Zircon 在此基础上增加了 MMU,System Call 等功能。 Zircon 目前支持 X86/X64 和 ARM 两种 CPU 平台,下面我将以 ARM64 为例,一行行分析 Zircon 内核的早期启动过程,看一下 Zircon 和 ARM64 是如何完成平台初始化的,这部分由汇编实现。 需要事先声明的是,本人平时从事的是 Android 开发,对于 ARM 了解有限,此次源码阅读也会参考一些其他资料,其中难免会有一些错误,望广大读者谅解。
带注释的 Zircon 内核源码(未完成):1a4K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6Y4j5h3&6&6j5h3)9I4x3e0c8Q4x3V1k6*7K9i4u0U0L8$3&6Q4x3V1k6@1M7X3g2W2i4K6u0r3k6r3!0U0
首先需要简单过一下涉及到的 ARM64 背景,以前虽有简单接触过嵌入式的 ARM,但相比之下 ARM64 确实要复杂很多很多。。。
在 ARM32 中,我们使用 SVC 等 7 种特权模式来区分 CPU 的工作模式,操作系统等底层程序会运行在高特权模式,而普通用户程序则运行在低特权的用户模式。 而在 ARM64 中,其实也类似,不过在 ARM64 中统一成了 4 个异常等级 EL-0/1/2/3
EL 架构:
关于 4 个特权等级在系统软件中的实际使用: | EL | 用途 | |------|------| | EL0 | 运行用户程序 | | EL1 | 操作系统内核 | | EL2 | Hypervisor (可以理解为上面跑多个虚拟内核) | | EL3 | Secure Monitor(ARM Trusted Firmware) |
Secure State 的影响:
在多核心处理器下,ID = 0 的 CPU 内核为 prime 核心,或者被称为 BSP 引导处理器 - bootstrap processor,其他处理器则为 non - prime 核心,或者 AP 核心 - Application Processor,开机和内核初始化由 prime 核心完成,AP 核心只要完成自身的配置就可以了。
ARM MP 架构图:
仅说明本文所涉及的
有了上文对 ARM64 的简单介绍,我们就可以看懂代码中的一些代码了 以下是比较通用的代码
前两行取出 mpidr_el1 的 AFF01 放入 cpuid 第三行如果 cpuid = 0 则代表是 prime cpu 内核,并且也是第一个线程,虽然现在超线程没有实现就是了
这里需要解释一下,因为 kernel 在链接的时候是根据虚拟地址来的。而在内核引导的早期阶段,也就是本文所介绍的这个过程中,MMU 是处于关闭状态的,这段时间内核实际是跑在物理地址上的。 那么,这段代码就必须是 PIC 位置无关代码,除了尽量使用寄存器,在不得不访问内存时,这段代码还不能依赖链接器所给的地址,那么如果在这段代码中需要取到内存中的地址只能使用指令计算数据/Label的实际地址。
Zircon 将这一操作简化成了一个宏:
第一行得到得到包含 symbol 4K 内存页的基地址 第二行在基地址上加上偏移就是 symbol 的实际地址
等价于
启动早期,即内核在进入 C++ 世界之前,主要分为以下几步
在多核处理器架构中,很多初始化代码仅需要由 prime 处理器完成,其他处理器完成各自的配置即可。
<table> <tr> <td bgcolor=#00F5FF>prime 核心</td> <td bgcolor=#00F5FF>其他核心</td> </tr> <tr> <td>保存内核启动参数</td> <td>跳过</td> </tr> <tr> <td colspan="2">初始化 EL1 - EL3 的异常配置</td> </tr> <tr> <td colspan="2">初始化缓存</td> </tr> <tr> <td>修复 kernel base 地址 </td> <td>跳过</td> </tr> <tr> <td>检查并等待 .bss 段数据清除</td> <td>跳过</td> </tr> <tr> <td>创建启动阶段的页表</td> <td>自旋等待页表创建完成</td> </tr> <tr> <td colspan="2">打开 MMU 之前的准备工作</td> </tr> <tr> <td colspan="2">打开 MMU (以上代码运行在物理地址,以下代码运行在虚拟地址)</td> </tr> <tr> <td>重新配置内核栈指针</td> <td>配置其他 CPU 的栈指针</td> </tr> <tr> <td>跳转到 C 世界继续初始化</td> <td>休眠等待唤醒</td> </tr> </table>
start.S - _start
asm.S - arm64_elX_to_el1对各个 EL 的配置,需要 CPU 在对应的 EL 状态下才能配置
EL1 不需要配置直接返回
EL2 状态下主要配置了:
实际上 EL2 在 Zircon 中还没有具体用处,所以此处初始化基本上就是设一些空值 。
EL3 状态的主要任务就是配置 EL0/EL1 的 Secure State/HVC/运行指令集,其他的也是上面 EL2 一样付空值。
此工作由 prime cpu 完成,其他 cpu 开始进入自旋等待
首先要把也表中的内存清除:
在初始化阶段,需要映射三段地址:
因为页表映射调用到了 C 方法,所以需要提前为 CPU 配置 SP 指针:
打开 MMU 之前需要做一些配置
需要重置一下 MMU 和 Cache 的状态以清除里面的残余数据,在进入 Kernel 代码之前,Bootloader 可能使用过 MMU 和 Cache,所以 ICache 和 TLB 中可能还有前面留下来的残余垃圾数据。
Memory attributes 简单来说就是将 Memory 加上了几种属性,每种属性都会影响 Memory 的读写策略。
因为 Memory 读写策略是非常复杂的,比如一段内存区域指向的是一个 FIFO 设备,对内存的读写有严格的时序要求,则需要配置 Memory attributes 来禁止 CPU 读写重排,Cache 等等优化,因为这些对于这段 Memory 没有意义,还会影响数据的读写的正确性。
看一下 Zircon 的默认 Memory Attribute 配置:
这里打开 MMU 非常简单,打开之前,以上代码都在物理地址下运行,打开之后则在虚拟地址下运行了。
这里注意备份还原异常向量表 内存栅栏防止打开 MMU 前后代码乱序执行,造成逻辑错误
在此之前,重新设置栈指针,因为此时已经变成虚拟地址
配置 Stack Guard,其实就是在栈末尾设置一个页中断,如果程序读写到这里,代表栈溢出,触发异常。 防止编译期间没有启用栈保护
这部分大部分在 C/C++ 中完成,下一篇分析。
mrs cpuid, mpidr_el1
ubfx cpuid, cpuid, #0, #15 /* mask Aff0 and Aff1 fields */ //aff0 记录 cpu ID,aff1 记录是否支持超线程
cbnz cpuid, .Lno_save_bootinfo //如果不是 prim 核心(0 号核心),则不需要启动内核,也就不需要准备内核启动参数,直接执行核心初始化工作
.macro adr_global reg, symbol
//得到包含 symbol 4K 内存页的基地址
adrp \reg, \symbol
//第一个全局变量的地址
add \reg, \reg, #:lo12:\symbol
.endm
mrs x9, CurrentEL
cmp x9, #(0b01 << 2)
//不等于 0 时,说明不是在异常级别 1,跳转到 notEL1 代码
bne .notEL1
str xzr, [page_table1, tmp, lsl #3]
add tmp, tmp, #1
cmp tmp, #MMU_KERNEL_PAGE_TABLE_ENTRIES_TOP
bne .Lclear_top_page_table_loop
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课