课前准备:Avatar OS 前置知识
"Indeed, the best way to understand how computers work is to build one from scratch." — Noam Nisan & Shimon Schocken 出自《The Elements of Computing Systems: Building a Modern Computer from First Principles》(中文译名:《计算机系统要素:从零开始构建现代计算机》)前言。
从 C 语言跳转到 OS,跨度很大,Unit 5 的实验难度较高。在开始前,请做足知识和环境上的准备。
NOTE
课程仓库:opencamp/learning-nccl/Unit-5-C-Kernel 基于 Avatar OS 真实内核代码的 24 节由浅入深的课程,涵盖 AArch64 / RISC-V 64 / x86_64 三个架构。
课程总览
五个阶段 · 24 课(L97–L120)
| 阶段 | Lesson | 主题 | 关键文件 |
|---|---|---|---|
| 一、裸机基础 | 97 | Bare-Metal Hello 裸机第一声 | boot_hello.S |
| 98 | Freestanding Types 独立类型系统 | include/types.h | |
| 99 | Kernel Logging 内核日志系统 | include/klog.h, lib/klog.c | |
| 100 | Linker Script 链接脚本 | boot/*/link.ld | |
| 101 | Boot Assembly 引导汇编 | boot/*/boot.S | |
| 102 | Architecture Abstraction 架构抽象层 | include/arch.h, include/barrier.h | |
| 二、内存与同步 | 103 | Memory Barriers 内存屏障 | include/barrier.h |
| 104 | Spinlocks 自旋锁 | include/spinlock.h | |
| 105 | Physical Memory Manager 物理内存管理 | kernel/mm/pmm.c | |
| 106 | Page Tables 页表结构 | include/aarch64/mmu.h | |
| 107 | MMU Enable 启用 MMU | kernel/mm/*/mmu.S | |
| 108 | Kernel Address Space 内核地址空间 | include/mm_vm.h | |
| 三、中断与定时器 | 109 | Exception Vectors 异常向量表 | boot/*/exception.S |
| 110 | Exception Handling 异常处理 | boot/*/exception.c | |
| 111 | Timer Driver 定时器驱动 | driver/timer/timer.c | |
| 112 | Interrupt Controllers 中断控制器 | driver/irq/gicv3.c, plic.c, lapic.c | |
| 四、多任务 | 113 | Task Control Block 任务控制块 | kernel/task/task.h |
| 114 | Context Switch 上下文切换 | kernel/task/*/switch.S | |
| 115 | Round-Robin Scheduler 轮转调度 | kernel/task/sched.c | |
| 116 | Preemptive Scheduling 抢占式调度 | kernel/task/preempt.c | |
| 五、用户态与多核 | 117 | User Mode 用户态 | kernel/task/task.c |
| 118 | System Calls 系统调用 | kernel/syscall/syscall.c | |
| 119 | ELF Loader ELF 加载器 | kernel/loader/elf_loader.c | |
| 120 | SMP Multicore 多核启动与调度 | kernel/task/cpu.c |
里程碑
| 里程碑 | 对应 Lesson | 能力描述 |
|---|---|---|
| M1 | L105 | 最小内核基础设施:UART + 日志 + 栈 + 锁 + 物理内存 |
| M2 | L108 | 虚拟内存完备:页表 + MMU + 地址空间隔离 |
| M3 | L116 | 抢占式多任务:定时器驱动的上下文切换 |
| M4 | L120 | 完整操作系统:用户态 + 系统调用 + 多核 |
三架构支持
每课涉及的概念均在三个架构上实现,可通过 ARCH 变量切换:
make ARCH=aarch64 run # AArch64(ARM 64位)
make ARCH=riscv64 run # RISC-V 64
make ARCH=x86_64 run # x86_64构建与评测
# 编译运行
make -C kernel clean
make -C kernel build
make -C kernel qemu
# 评测指定 Lesson
bash grading/grade.sh -l 97
# 评测全部
bash grading/grade.sh每课 README 中的「预期输出」描述了自动评测匹配的关键内容,通过即代表该课实验通过。
一个好用的 AI 助手
Unit 5 与之前的 C 语言单元有一个本质区别:你将面对大量陌生概念——汇编指令、MMU、页表、异常向量、原子操作、内存屏障…… 这些知识很难一次性全部记住,也不应该在还没开始实验时就试图"彻底搞懂"。
下面的前置知识章节(计算机组成原理、进阶 C 特性、汇编详解)是按需查阅的参考手册,不是要求背诵的教科书。更好的学习方式是:
- 从 L97 直接开始实验
- 遇到不懂的指令或概念 → 回到本文档对应章节查阅
- 查阅后仍有疑问 → 用 AI 助手即时提问
举个具体例子:你在 L100 的 boot.S 里看到 movz x19, #0x0000 后面紧跟着 movk x19, #0xFFFF, lsl #48,你可能会问"为什么不用一条 mov 写 64 位地址?"——这时把这段汇编贴给 AI,它能立刻告诉你 ARMv8 指令编码限制和 movz+movk 的工作原理,比翻手册快 10 倍。
类似的场景贯穿整个课程:
dsb/dmb/isb到底有什么区别?(L107 MMU 启用)ldaxr+stxr为什么失败后要重试?(L103 自旋锁)- 异常向量表为什么要
.balign 0x80?(L109) - 上下文切换为什么只保存 x19-x30?(L114)
这些都是 AI 助手最擅长回答的问题——它不需要你写出完整的上下文,只要贴一段代码 + 一句话提问即可。
NOTE
CNB 官方为学习提供了一些 AI 额度,可在 WebIDE、VS Code 等环境中使用。安装 Tencent Cloud CodeBuddy,模型建议选 DeepSeek V4 Pro。
一、计算机组成原理与操作系统核心概念
汇编语言直接操作硬件,如果不理解计算机的基本组成,看汇编指令就像看天书。本节帮你建立基本的硬件心智模型,为后续汇编学习和实验打好基础。
1.1 CPU 基础:寄存器与程序计数器
CPU 的核心组件:运算器(ALU) + 控制器 + 寄存器组。
| 概念 | 说明 | 课程对应 |
|---|---|---|
| 寄存器(Register) | CPU 内部最快的存储单元,直接参与 ALU 运算 | L97 起全程使用 |
| 通用寄存器 | ARMv8 有 x0-x30 共 31 个 64 位通用寄存器 | L97:strb w1, [x0] 输出字符 |
| 程序计数器 PC | 存放下一条将要执行指令的地址 | L100:理解 bl/ret 如何通过修改 PC 实现函数调用与返回 |
| 链接寄存器 LR(x30) | 函数调用时自动保存返回地址 | L100:bl 将返回地址写入 x30 |
| 栈指针 SP | 指向当前栈顶,栈用于保存局部变量和返回地址 | L100:Boot 阶段设置栈 |
NOTE
寄存器 vs 内存:寄存器的访问速度是内存的上百倍,但数量极少(AArch64 只有 31 个通用寄存器)。这正是汇编编程的核心挑战——如何高效地使用有限的寄存器。
1.2 内存层级:从寄存器到外存
| 层级 | 速度 | 容量 | 典型大小 |
|---|---|---|---|
| 寄存器 | 最快 | 最小 | ~31 个 × 64-bit |
| L1 Cache | ↓ | ↓ | 32–64 KB |
| L2 Cache | ↓ | ↓ | 256–512 KB |
| L3 Cache | ↓ | ↓ | 2–32 MB |
| 主存(RAM) | ↓ | ↓ | 4–64 GB |
| 外存(Flash/磁盘) | 最慢 | 最大 | 128 GB–数 TB |
| 概念 | 说明 | 课程对应 |
|---|---|---|
| Cache(缓存) | CPU 与主存之间的高速缓冲,最小单位是 Cache Line(通常 64 字节) | L103:理解为什么 Store 不立即对其他核可见 |
| Cache Line | 缓存与主存交换数据的最小单位 | L103-L104:ldaxr/stxr 以 Cache Line 为独占监视粒度 |
| TLB(地址翻译缓存) | MMU 的"缓存",缓存最近用过的虚拟地址→物理地址映射 | L107:页表修改后必须用 tlbi 刷新 TLB |
| MMIO 不走 Cache | 外设寄存器映射到物理地址,每次读写必须直达硬件 | L97:UART 寄存器必须标记 volatile |
1.3 地址空间:物理地址与虚拟地址
| 概念 | 说明 | 课程对应 |
|---|---|---|
| 物理地址(PA) | 硬件总线上的真实地址,直接对应内存芯片的某个位置 | L97:UART MMIO 基址 0x09000000 |
| 虚拟地址(VA) | 程序看到的地址,由 MMU 翻译成物理地址 | L99:链接脚本中的 VMA |
| MMU 地址翻译 | VA → 查多级页表 → PA | L107:MMU 启用流程 |
| 内核高半区 vs 用户低半区 | 内核映射到高地址(0xFFFF...),用户程序在低地址(0x0000...) | L108:地址空间隔离 |
1.4 特权级:CPU 的安全分层
| 概念 | 说明 | 课程对应 |
|---|---|---|
| EL0(用户态) | 最低特权级,不能访问系统寄存器 | L117:用户程序运行在 EL0 |
| EL1(内核态) | OS 内核运行级别,可访问所有系统资源 | L97 起大部分代码运行在 EL1 |
| EL2(Hypervisor) | 虚拟机监控器级别 | 本课程不涉及 |
| EL3(Secure Monitor) | 安全监控级别 | 本课程不涉及 |
svc/eret 切换特权级 | svc 从 EL0 进入 EL1,eret 从 EL1 返回 EL0 | L117-L118:系统调用实现 |
TIP
为什么需要特权级? 如果用户程序能随意修改页表、关闭中断,整个系统就会崩溃。特权级确保了只有可信的内核代码才能操作关键硬件资源。
1.5 中断与异常:CPU 如何响应外部事件
| 概念 | 说明 | 课程对应 |
|---|---|---|
| 同步异常 | 由指令执行直接触发(如系统调用 svc、缺页异常) | L109:svc 触发同步异常 |
| 异步中断 | 由外部硬件触发(如定时器、键盘、网卡) | L111-L112:定时器 + GIC 中断控制器 |
| 异常向量表 | 一张固定布局的跳转表,CPU 根据异常类型自动跳转到对应入口 | L109:VBAR_EL1 指向 2KB 的向量表 |
| 中断屏蔽 | 通过 DAIF 寄存器控制哪些中断被屏蔽 | L100:msr daifset 屏蔽中断 |
二、进阶 C 特性
以下 C 语言特性在裸机/内核开发中高频出现。如果对某个概念不熟,建议先回顾 Unit 1-4 的相关实验。
2.1 指针进阶
| 知识点 | 说明 | 课程对应 |
|---|---|---|
void* 泛型指针 | 通用指针类型,需要时强制转换 | L105:PMM 分配器返回 void* |
多级指针 void** | 指针的指针 | L113:TCB 的 sp 字段保存"栈指针的地址" |
| 函数指针 | 指向函数的指针,用于回调/分发表 | L117:系统调用分发表 syscall_table[] |
container_of 宏 | 从结构体成员指针反推结构体首地址 | L115:从链表节点获取包含它的 task_struct |
| 成员对齐 & 首地址相等 | 结构体第一个成员偏移为 0 | L113:sp 必须在 TCB 偏移 0 |
2.2 宏编程
// container_of — 内核中最经典的宏之一
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - offsetof(type, member)))| 知识点 | 说明 | 课程对应 |
|---|---|---|
#define 函数宏 | 用宏模拟内联函数 | L108 phys_to_virt() / L115 container_of() |
do { } while(0) 包装 | 让多语句宏在 if/else 中安全展开 | L98:kprintf 宏 |
##__VA_ARGS__ | 可变参数宏 | L98:kprintf 格式化字符串 |
__typeof__ | 编译期类型推导 | L115:container_of 类型安全检查 |
# 字符串化 / ## 拼接 | 宏参数转字符串 / 标识符拼接 | L98:日志级别宏模板 |
#if/#ifdef 条件编译 | 架构相关代码选择性编译 | L102:ARCH_AARCH64 vs ARCH_RISCV64 |
TIP
为什么宏要用 do { ... } while(0) 包装? 考虑这个场景:
// 错误写法:只有 stmt1 受 if 控制
#define BAD_MACRO stmt1; stmt2;
if (cond) BAD_MACRO // → if (cond) stmt1; stmt2;
// 正确写法:整个宏作为一条语句
#define GOOD_MACRO do { stmt1; stmt2; } while(0)
if (cond) GOOD_MACRO // → if (cond) do { stmt1; stmt2; } while(0);2.3 static / inline / volatile / extern
| 知识点 | 说明 | 课程对应 |
|---|---|---|
static 文件级作用域 | 函数/变量不对外暴露,避免符号冲突 | L97:static void uart_putc() 驱动函数内部可见 |
static inline | 零开销内联 + 文件级作用域 | L103:Spinlock 函数——频繁调用,必须内联 |
extern 外部符号 | 引用其他文件或链接脚本定义的符号 | L100:引用 __bss_start[]、_stack_top |
volatile | 告诉编译器"每次都从内存读取,不要优化" | L97:UART MMIO 寄存器 / L103:Spinlock |
TIP
volatile 为什么在 MMIO 中必不可少? 编译器看到 *p = 'A'; *p = 'B'; 时,可能会优化成只做 *p = 'B'。但对于 UART 设备寄存器,每次写入都是一次真实的硬件操作——不加 volatile,你的字符就丢了。
2.4 位运算与内存模型
| 知识点 | 说明 | 课程对应 |
|---|---|---|
位运算 << >> & | ~ ^ | 对二进制位直接操作 | 贯穿全程 |
| Bitmap 位图 | 用 1 个 bit 表示资源是否占用 | L105:物理内存分配器 |
| 位字段组装与提取 | 从 64 位值中提取/设置特定字段 | L107:页表描述符构建 / 虚拟地址分解 |
sizeof / offsetof / 对齐 | 类型大小 / 成员偏移 / 地址对齐要求 | L113 TCB 布局 / L106 页表对齐 / L109 异常向量表对齐 |
-nostdlib -ffreestanding | 无 libc 的裸机环境编译参数 | L99:freestanding 环境,一切从零构建 |
2.5 工具链知识
| 知识 | 说明 | 课程对应 |
|---|---|---|
make / make clean | 构建系统 | L97 起:每日构建 |
objdump -d / nm / readelf | 反汇编 / 符号表 / ELF 解析 | L100:检查 ELF 结构 / L119:解析 ELF |
gdb + QEMU 远程调试 | 源码级调试裸机程序 | L100 起 |
| 链接脚本 VMA vs LMA | 虚拟地址 vs 加载地址 | L99-L100:段布局 |
段:.text .rodata .data .bss | 代码段 / 只读数据 / 已初始化数据 / 零初始化数据 | L99:段分类 / L100:BSS 清零 |
2.6 链接器(Linker)是什么
编译四阶段中,前三个阶段(预处理 → 编译 → 汇编)将每个 .c / .S 文件变成独立的 .o(目标文件),而 链接器 负责最后一步:把多个 .o 文件合并成一个可执行文件。
| 阶段 | 输入 | 工具 | 输出 |
|---|---|---|---|
| 预处理 | hello.c | cpp | hello.i |
| 编译 | hello.i | cc1 | hello.s |
| 汇编 | hello.s | as | hello.o |
| ... | world.c → ... → world.o | (同上) | |
| 链接 | hello.o + world.o | ld(链接器) | kernel.elf |
链接器做了三件关键的事:
符号解析(Symbol Resolution):每个
.o文件里有"我提供什么符号"和"我需要什么符号"。链接器把所有.o的符号表合并,把"需要"匹配到"提供"。例如boot.S里声明了.globl _start(提供),link.ld里用ENTRY(_start)找到它(需要)。段合并(Section Merging):所有
.o的.text段合并成一个大的.text,所有.data合并成一个大的.data,依次类推。最终一个可执行文件内部是整齐排列的段,而不是散落的碎片。地址分配(Address Assignment):链接脚本(linker script,
*.ld)告诉链接器每个段应该放在哪个虚拟地址(VMA)。例如".text放在0xFFFF000000000000,.bss放在.data之后"。这一步决定了程序运行时的内存布局。
TIP
和裸机开发有什么关系? 在普通 C 程序中,链接器用的是系统默认脚本,你感受不到它的存在。但在裸机/内核开发中,你必须自己写链接脚本——因为没有操作系统帮你布局内存。ENTRY(_start) 告诉 CPU 从哪里开始执行;.bss 段需要手动清零(因为没有 C 运行时帮你做)。
这正是 L99(链接脚本)和 L100(BSS 清零) 的核心内容。
三、前置汇编知识:ARMv8/AArch64 GAS 汇编详解
看到汇编不用怕——在这个阶段,能看懂就可以了。
以下是 Avatar OS 课程中使用的所有汇编指令的详细讲解,按类别组织。这份参考手册 + AI 助手,就是你攻克 Unit 5 的操作系统之旅的"左膀右臂"。
NOTE
课程基于 AArch64 执行态(64 位 ARM 架构)。不要与 AArch32(32 位 ARM 模式)混淆——两者的寄存器宽度、指令集和系统寄存器命名都不同。
0. 必须先掌握的核心概念
在看具体指令之前,请先理解下面这些概念。它们在整个汇编部分会反复出现,花 5 分钟读一遍,后面学指令会顺畅 10 倍。
0.1 三种操作数:汇编的数据来源
一条汇编指令需要"对什么数据做什么操作"。汇编中有且仅有三种数据来源:
以 add x0, x1, #8 为例,三个操作数分别来自三种来源:
| 操作数 | 来源 | 说明 | 速度 |
|---|---|---|---|
x0(目标) | 寄存器 | CPU 内部的存储,极快 | 极快 |
x1(源1) | 寄存器 | 同上 | 极快 |
#8(源2) | 立即数 | 常数直接编码在指令里 | 最快 |
| 类型 | 写法 | C 语言等价 | 速度 |
|---|---|---|---|
| 立即数 | #42、#0xFF | 常量 42、0xFF | 最快(在指令编码里) |
| 寄存器值 | x0、w1、sp | 变量 int a | 极快(在 CPU 内部) |
| 内存值 | [x0]、[sp, #8] | 指针解引用 *ptr | 较慢(需访问 RAM) |
NOTE
立即数前缀 #:ARM 汇编中,立即数必须带 #。mov x0, 42 是错的,必须是 mov x0, #42。
间接寻址 []:方括号意为"把这个寄存器的值当成内存地址"。[x1] 不是访问 x1 本身,而是访问 x1 指向的那块内存。等价于 C 的 *ptr。
0.2 指令格式:目标操作数永远在最前面
ARM GAS 汇编的指令格式固定为:
指令 目标操作数, 源操作数1, 源操作数2add x0, x1, x2 @ x0 = x1 + x2 (目标 x0,源 x1、x2)
ldr x0, [x1] @ x0 = *x1 (目标 x0,源 [x1])
str x2, [x0] @ *x0 = x2 (目标 [x0],源 x2)永远记这条规则:第一个操作数是被写入的目标,后面的是只读的源。
WARNING
不要和 x86 的 AT&T 语法混淆! x86 AT&T 的 add %eax, %ebx 是 ebx += eax——源在前。ARM 正好相反:add x0, x1, x2 是 x0 = x1 + x2——目标在前。如果你在网上看到不同的操作数顺序,先确认它针对的是 ARM 还是 x86。
0.3 寄存器一览
AArch64(ARM 64 位)有 31 个通用寄存器 + 若干特殊寄存器:
| 寄存器 | 宽度 | 用途 |
|---|---|---|
| x0 – x30 | 64 位 | 通用寄存器,随便用 |
| w0 – w30 | 32 位 | x 寄存器的低 32 位视图,写入 w 时高 32 位自动清零 |
| sp | 64 位 | Stack Pointer,栈指针,始终指向栈顶 |
| lr(x30) | 64 位 | Link Register,bl 调用时自动存放返回地址 |
| xzr / wzr | 64/32 位 | Zero Register,读永远返回 0,写丢弃(比 mov x0, #0 更高效) |
| pc | 64 位 | Program Counter,存放下条指令地址(不能直接读写,由跳转指令间接修改) |
NOTE
x 与 w 的关系:w0 是 x0 的低 32 位视图,两者共享同一个物理寄存器。写入 w0 时,x0 的高 32 位自动清零。
| 寄存器前缀 | 物理宽度 | 读写行为 |
|---|---|---|
x0 – x30 | 64 位 | 完整读写 64 位 |
w0 – w30 | 32 位 | 读写低 32 位,写入时高 32 位自动清零 |
TIP
xzr 是神器:想给寄存器清零,直接 mov x0, xzr,不需要 mov x0, #0(省了一条编码立即数的空间)。
0.4 栈指针 sp 与函数调用
栈是内存中一块按"后进先出"规则使用的区域。sp 始终指向栈顶,栈向低地址方向增长:
| 地址方向 | 内容 | 说明 |
|---|---|---|
| 高地址 | 旧数据 | 已使用的栈空间 |
| ↑ | sp 初始位置 | 栈从高地址开始 |
| ↓ | 可用空间 | sp 指向当前栈顶 |
| 低地址 | (栈底) | 栈向低地址增长 |
栈用来保存:局部变量、返回地址、临时寄存器值。
IMPORTANT
ARM 没有 x86 那样的 push/pop 指令。所有栈操作由 stp/ldp(Store/Load Pair)配合手动地址偏移完成。
0.5 标签、符号与 .globl
汇编中,label: 形式的文本叫标签(Label),代表某个内存地址。bl label 就是"跳转到 label 对应的地址"。
.globl(Global) 声明该标签对链接器可见。不加 .globl,标签只在当前 .S 文件内有效。
.globl _start @ 导出 _start,让链接器能找到入口
_start: @ _start 标签,代表一个内存地址
mov x0, #42
my_helper: @ 没有 .globl,外部文件看不到这个标签
ret0.6 伪指令(Directives):给汇编器的指令
伪指令以 . 开头,不是 CPU 指令——它们是告诉汇编器"怎么布局代码和数据"的元信息。
| 伪指令 | 作用 | 示例 |
|---|---|---|
.globl / .global | 声明全局符号 | .globl _start |
.section | 指定代码/数据段 | .section .text |
.align N | 按 2^N 字节对齐 | .align 4 → 16 字节 |
.balign N | 按 N 字节对齐 | .balign 2048 |
.ascii | 定义字符串(无 \0) | .ascii "hello\n" |
.macro / .endm | 定义宏 | .macro save_all_regs ... .endm |
.space N | 保留 N 字节 | .space 65536 |
.equ | 定义符号常量 | .equ MY_CONST, 42 |
.section .text @ 以下内容放代码段
.globl _start @ 导出入口供链接器使用
_start:
// 代码写这里
.section .rodata @ 以下内容放只读数据段
.align 4 @ 16 字节边界对齐
msg:
.ascii "hello, OK\n"TIP
.ascii vs .asciz:.ascii 不自动加 \0(NUL 终止符),.asciz 会。本课程用 .ascii 精确控制字符串长度——裸机环境没有 C 标准库替你处理字符串终止。
1. 数据移动指令
这是最核心、最高频的指令族,相当于 C 语言中的赋值运算符 = 和解引用 *ptr。
1.1 立即数加载 → 寄存器
mov
全称 Move(移动)。
mov x0, #1 @ x0 = 1(加载立即数)
mov x2, #10 @ x2 = 10
mov sp, x0 @ sp = x0(寄存器到寄存器)
mov x2, #0 @ x2 = 0WARNING
mov 只能加载 16 位立即数(支持移位扩展)。这是因为 ARMv8 的指令编码中,立即数字段只有约 16 位宽。想要加载 64 位地址怎么办?往下看。
TIP
为什么指令编码只给 16 位? ARMv8 每条指令固定 32 位宽。扣掉操作码、寄存器编号等字段后,留给立即数的空间只有约 16 位。这是固定长度指令集的经典权衡——指令长度统一,但立即数空间有限。
movz + movk
全称 Move with Zero(清零移动)/ Move with Keep(保留移动)。
@ 构建 64 位立即数:0xFFFF000000000000
movz x19, #0x0000 @ x19 = 0x0000,低16位清零
movk x19, #0xFFFF, lsl #48 @ x19[63:48] = 0xFFFF,保持其他位不变这条指令组合在启动代码中用于构建内核高半区地址偏移。
| 指令 | 含义 | 行为 |
|---|---|---|
movz | Move with Zero | 设目标位段,其余位填 0 |
movk | Move with Keep | 设目标位段,其余位 保持不变 |
以构建 0xFFFF000000000000 为例,64 位寄存器 x19 的变化过程:
| 步骤 | 指令 | x19[63:48] | x19[47:16] | x19[15:0] | 说明 |
|---|---|---|---|---|---|
| 初始 | — | ? | ? | ? | 任意值 |
| 1 | movz x19, #0x0000 | 0 | 0 | 0x0000 | 全部清零,低 16 位设为目标值 |
| 2 | movk x19, #0xFFFF, lsl #48 | 0xFFFF | 0(保持) | 0x0000(保持) | 仅替换 bits[63:48],其余位不变 |
TIP
为什么不直接写 mov x0, #0xFFFF000000000000? 因为 ARMv8 的 mov 指令编码无法容纳 64 位立即数。movz + movk 组合是构建大常量的标准方式,最多需要 4 条指令填满 64 位。
ldr x, =label
全称 Load Register(伪指令)。
ldr x0, =_stack_top @ x0 = _stack_top 的链接地址
ldr x0, =__bss_start @ x0 = BSS 段起始地址
ldr x0, =exception_vector_baseIMPORTANT
这是伪指令(pseudo-instruction),不是真实的 CPU 指令。汇编器会在后台自动将其转换为 PC 相对加载或通过文字池(literal pool)访问。它加载的是编译时链接地址(VMA),而非运行时地址。
adr
全称 Address of label at PC-relative offset(PC 相对地址加载)。
adr x1, msg @ x1 = msg 标签的当前运行时地址(±1MB 范围)
adr x0, high_half @ x0 = high_half 的地址NOTE
adr 是真实指令,通过 PC + 有符号偏移 计算地址,范围 ±1MB。适合短距离地址加载——它返回的是运行时地址,而非链接时地址。
adr vs ldr = 核心对比
| 维度 | adr | ldr = |
|---|---|---|
| 本质 | 真实 CPU 指令 | 伪指令(由汇编器展开) |
| 范围 | ±1MB(PC 相对) | 64 位全范围(字面池) |
| 返回地址 | 运行时刻 PC 相对地址 | 链接时绝对地址(VMA) |
| 代码密度 | 1 条指令 | 1 条指令 + 1 个字面池条目 |
| 适用场景 | 附近标签的运行时地址 | 任意位置的链接地址 |
TIP
什么时候用哪个? 在位置无关代码(PIC)和 Trampoline 中,必须用 adr 获取运行时地址,因为链接时的 VMA 和实际运行的物理地址不同。在静态链接的启动代码中,两者通常等价,ldr = 更直观。
1.2 内存读写
ldr / str
全称 Load Register(加载)/ Store Register(存储),64 位操作。
ldr x0, [x1] @ x0 = *(x1 指向的内存),加载 8 字节
str x2, [x0] @ *(x0 指向的内存) = x2,存储 8 字节后索引寻址
str x2, [x0], #8 @ *(x0) = x2; x0 += 8; (写后地址递增)这在 BSS 清零循环中使用:
clear_bss:
cmp x0, x1
b.ge clear_bss_done
str x2, [x0], #8 @ 写入 8 字节零,然后 x0 += 8(相当于 *ptr++ = 0)
b clear_bss前索引寻址
在上下文切换中经常看到这种形式:
stp x29, x30, [sp, #-16]! @ sp -= 16; *(sp+0)=x29; *(sp+8)=x30! 表示先修改再操作:
sp = sp - 16(栈向下生长,先分配空间)- 写入
x29到sp+0 - 写入
x30到sp+8
ldrb / strb
全称 Load Register Byte(加载字节)/ Store Register Byte(存储字节)。
strb w1, [x0] @ *(x0 指向的内存) = w1 的低 8 位UART 输出使用字节写入:
movz x0, #0x0900, lsl #16 @ UART MMIO 基址
mov w1, #'B'
strb w1, [x0] @ 向 UART 数据寄存器写入 'B'TIP
为什么用 w1 而不是 x1? w1 是 x1 的低 32 位视图。字节写入只需要 8 位数据,用 w1 可以避免汇编器报类型不匹配。strb 会自动取 w1 的最低 8 位。
ldp / stp
全称 Load Pair(加载寄存器对)/ Store Pair(存储寄存器对)。
stp x29, x30, [sp, #-16]! @ 前索引推栈:sp-=16; *(sp)=x29; *(sp+8)=x30
ldp x29, x30, [sp], #16 @ 后索引弹栈:x29=*(sp); x30=*(sp+8); sp+=16IMPORTANT
AArch64 不提供 push/pop 指令(这点和 x86 不同),所有栈操作都通过 stp/ldp(Store/Load Pair)+ 手动偏移实现。
1.3 寄存器宽度约定
AArch64 的寄存器有"别名"——同一个物理寄存器可以用不同宽度来访问:
| 寄存器前缀 | 宽度 | 说明 |
|---|---|---|
x0-x30 | 64 位 | 完整 64 位寄存器 |
w0-w30 | 32 位 | 对应 x 寄存器的低 32 位,写入 w 时高 32 位自动清零 |
xzr | 64 位 | 零寄存器(读永远返回 0,写丢弃) |
wzr | 32 位 | 32 位零寄存器 |
mov x0, xzr @ x0 = 0(64 位清零的标准方式)
mov w0, wzr @ w0 = 0(32 位清零,同时清零 x0 高 32 位)TIP
神器 xzr/wzr:这是 ARM 架构的精妙设计——不需要浪费一条指令加载 #0,直接"读"零寄存器即可。
2. 系统寄存器访问指令
在 ARMv8 中,CPU 的控制和状态寄存器(如页表基址、异常配置)不能通过普通 ldr/str 访问——它们不在统一的内存地址空间中,必须用专用指令。
TIP
为什么系统寄存器不能当普通内存读写? 系统寄存器(System Registers)位于 CPU 内部的一个独立地址空间,与普通内存地址空间完全隔离。这种设计有三重好处:
- 性能:访问走专用数据通路,不需经过 Cache/TLB/总线
- 安全:访问受异常级别(EL)控制,普通
ldr/str无法绕开权限检查 - 原子性:
msr可以原子地修改控制位,避免竞态
2.1 mrs
全称 Move from System Register(读系统寄存器)。
mrs x0, sctlr_el1 @ x0 = SCTLR_EL1(系统控制寄存器)
mrs x0, esr_el1 @ x0 = ESR_EL1(异常综合寄存器)
mrs x0, far_el1 @ x0 = FAR_EL1(故障地址寄存器)
mrs x0, currentel @ x0 = CurrentEL(当前异常级别)
mrs x0, cntfrq_el0 @ x0 = 系统定时器频率
mrs x0, cntpct_el0 @ x0 = 物理计数器值2.2 msr
全称 Move to System Register(写系统寄存器)。
msr ttbr0_el1, x0 @ TTBR0_EL1 = x0(用户页表基址)
msr ttbr1_el1, x0 @ TTBR1_EL1 = x0(内核页表基址)
msr vbar_el1, x0 @ VBAR_EL1 = x0(异常向量基址)
msr spsr_el1, x0 @ SPSR_EL1 = x0(保存的处理器状态)
msr elr_el1, x0 @ ELR_EL1 = x0(异常链接寄存器)
msr sp_el0, x0 @ SP_EL0 = x0(EL0 栈指针)
msr cntv_tval_el0, x0 @ CNTV_TVAL_EL0 = x0(定时器倒计时值)
msr cnthctl_el2, x0 @ CNTHCTL_EL2(Hypervisor 层定时器控制)NOTE
系统寄存器命名遵循 name_ELx 约定,其中 ELx 指该寄存器归属的异常级别:
_EL0:用户态可用_EL1:内核态(OS 内核运行在此级别)_EL2:Hypervisor 层_EL3:Secure Monitor 层
本课程主要操作 EL1 寄存器。
2.3 msr daifset / daifclr — 中断屏蔽控制
msr daifset, #0xf @ 屏蔽 D / A / I / F 四种异常
msr daifclr, #0xf @ 打开所有中断| 位 | 含义 | 全称 |
|---|---|---|
| D | Debug exceptions | 调试异常 |
| A | Asynchronous (SError) | 异步错误(如 ECC 错误) |
| I | IRQ | 普通中断请求 |
| F | FIQ | 快速中断请求 |
TIP
#0xf = 二进制 0b1111,每一位对应一种异常类型。写入 0xf 屏蔽全部四种,写入 0x0 打开全部。
3. 原子操作与自旋锁
在多核系统中,多个 CPU 核心可能同时访问同一块内存。原子操作保证"读-改-写"操作不会被其他核心打断。
TIP
为什么需要原子操作? 考虑两个核心同时尝试获取锁:
Core 0: 读 lock → 看到 0(空闲)
Core 1: 读 lock → 看到 0(空闲)
Core 0: 写 lock = 1
Core 1: 写 lock = 1 ← 两个核心都认为拿到了锁!这就是竞态条件(race condition)。ldaxr/stxr 通过硬件级的"独占监视"机制解决了这个问题。
3.1 ldaxr
全称 Load-Acquire eXclusive Register(获取独占加载)。
ldaxr w1, [x0] @ w1 = *(x0 指向的内存); 标记该地址为「独占监视」- 带 Acquire 语义(后续读写不能重排到此指令之前)
- 设置硬件对
[x0]所在 cache line 的「独占监视」标志
3.2 stxr
全称 Store eXclusive Register(独占存储)。
stxr w2, w3, [x0] @ if (独占监视有效) *(x0) = w3; w2 = 0;
@ else w2 = 1(失败); 不写入- 带 Release 语义(之前的读写不能重排到此指令之后)
- 如果
ldaxr到stxr之间该地址被其他核修改,独占监视自动失效,stxr不写入
IMPORTANT
ldaxr + stxr 不是 CAS! 它和 x86 的 CMPXCHG 不同:
CMPXCHG比较值是否匹配,匹配才写stxr不比较值——它只检查独占监视是否还有效- ARM 的原子操作依赖"试写"而非"比较",失败后必须重新
ldaxr读取
3.3 stlr
全称 Store-Release Register(释放存储)。
stlr w0, [x1] @ *(x1) = w0; Release 语义(之前操作不可后延)3.4 自旋锁原理
自旋锁的核心思想:用 ldaxr + stxr 实现一条原子的"读-检查-写"链路。
加锁流程:
ldaxr读取锁值 → 如果是 0(空闲),继续;否则自旋等待stxr尝试写入 1 → 如果独占监视有效(没被抢),成功;否则回到步骤 1 重试
解锁流程:
stlr写入 0 → Release 语义确保锁释放前的所有操作对其他核可见
NOTE
具体的 C 内联汇编实现在 L103-L104 实验中展开。这里只需要理解:自旋锁 = ldaxr(读 + 声明独占)+ 判断 + stxr(试写,失败就重试) + stlr(写 0 释放)。
4. 内存屏障指令
ARMv8 采用弱内存序模型(Weakly-Ordered Memory Model):CPU 可以按不同于程序顺序的实际顺序执行内存访问。这对单核程序透明,但多核场景下可能出问题。
TIP
弱内存序的直观理解:假设 Core 0 执行:
data = 42; // Store A
flag = 1; // Store BCore 1 执行:
while (flag == 0); // Load B
print(data); // Load A在弱内存序下,Core 1 可能在 flag == 1 后读到 data == 0(旧值)——因为 Store B 先于 Store A 到达 Core 1 的缓存!内存屏障用来防止这类重排。
4.1 dmb
全称 Data Memory Barrier(数据内存屏障)。
dmb sy @ 全系统数据屏障 — 前后内存操作不可交叉
dmb ish @ 可共享域内屏障 — 对同一 Inner Shareable 域可见
dmb ishst @ 仅 Store 屏障
dmb ishld @ 仅 Load 屏障4.2 dsb
全称 Data Synchronization Barrier(数据同步屏障)。
dsb sy @ 严格同步 — 之前所有内存访问完成后才开始后面指令
dsb ishst @ Store 同步(写 MMU 寄存器前必须用)4.3 isb
全称 Instruction Synchronization Barrier(指令同步屏障)。
isb @ 冲刷流水线 — 之后的指令重新取指NOTE
dmb vs dsb vs isb 的区别,直观理解:
dmb:禁止屏障前后的内存操作交叉。相当于一个"请排队"的提示。dsb:屏障前的所有内存操作必须完成,才能执行屏障后的指令。相当于"等前面全部做完"。isb:冲刷 CPU 流水线,从新上下文重新取指。相当于"之前的状态立刻生效,之后的指令重新来过"。
4.4 课程中的应用场景
| 场景 | 需要的屏障 | 原因 |
|---|---|---|
| 修改页表 | dsb ishst + isb | 页表写入必须对其他核可见,且 TLB 需要看到新条目 |
| 启用/禁用 MMU | dsb ishst + isb | MMU 开启后所有地址翻译立即生效 |
| 修改异常向量基址 | isb | 异常处理路径必须立即使用新向量表 |
| 跨核同步数据结构 | dmb ish | 保证共享数据的写入顺序 |
5. 分支与控制流指令
分支指令修改 PC(程序计数器),控制程序的执行路径。
5.1 无条件跳转
ARM 的分支指令由一个"根指令" b 加上不同后缀组合而成。理解后缀的含义,比死记硬背所有变体更高效:
| 指令 | 全称 | 行为 | 关键变化 |
|---|---|---|---|
b | Branch | 直接跳转到 label(±128MB) | 最基础:只改 PC |
bl | Branch with Link | 跳转 + 自动将返回地址写入 x30(lr) | 加了 l:多了"记下回家的路" |
br | Branch to Register | 跳转到寄存器中的地址 | r 替换 label:目标地址来自寄存器而非固定标签 |
blr | Branch with Link to Register | 跳转到寄存器中的地址 + 自动保存返回地址到 x30 | l + r 叠加:既有动态目标,又记返回地址 |
ret | Return | 等价于 br lr,从函数返回 | b → br lr 的语义封装 |
记忆口诀:
l= Link(链接):加l意味着"自动把返回地址写入x30",这样被调函数执行完可以ret回来r= Register(寄存器):加r意味着"跳转目标来自寄存器",而不是硬编码的 label,适用于函数指针、虚表分发等动态场景blr=bl+br:既有 link(保存返回地址),又从寄存器取目标——是 C 中函数指针调用的底层实现
b label @ 无条件跳转(不保存返回地址)
bl function @ 函数调用(x30 = 下一条指令的地址)
blr x0 @ 通过函数指针调用(x30 = 返回地址,目标 = x0)
br x0 @ 跳转到 x0,不保存返回地址(常用于 switch-case 跳转表)
ret @ 等价于 br lr,函数返回NOTE
b vs bl 的本质区别:b 是"一去不回"的跳转(如循环末尾跳回循环头),bl 是"去了还要回来"的调用(如函数调用)。bl 在跳转前悄悄把下一条指令的地址写入 x30(Link Register),配合 ret(=br lr)就构成了完整的函数调用/返回框架。这相当于 x86 的 CALL + RET,但更透明:你可以直接看到 x30 就是返回地址。
5.2 条件跳转
cmp x0, x1 @ **C**o**m**p**are:比较 x0 和 x1,设置条件标志(NZCV)
b.ge label @ Branch if Greater or Equal(有符号)
b.lt label @ Branch if Less Than
b.eq label @ Branch if EQual
b.ne label @ Branch if Not Equal
b.gt label @ Branch if Greater Than
b.le label @ Branch if Less or EqualBSS 清零中的条件跳转:
cmp x0, x1
b.ge clear_bss_done @ x0 >= x1 时结束循环
str x2, [x0], #8
b clear_bssTIP
条件后缀速记:eq = equal, ne = not equal, ge = greater or equal, lt = less than, gt = greater than, le = less or equal。注意 ARM 的条件跳转区分有符号比较和无符号比较(无符号用 hs/lo/hi/ls)。
5.3 cbnz / cbz
全称 Compare and Branch if Not Zero(非零则跳转)/ if Zero(为零则跳转)。
cbnz x0, label @ 若 x0 != 0,跳转到 label
cbz x0, label @ 若 x0 == 0,跳转到 label无需先 cmp,直接判断寄存器是否为 0——这比 cmp + b.eq 少一条指令。
5.4 wfe / wfi
全称 Wait For Event(等待事件)/ Wait For Interrupt(等待中断),用于低功耗等待。
wfe @ 低功耗等待,直到被 SEV 唤醒
wfi @ 低功耗等待,直到中断唤醒
hang:
wfe
b hang @ 原地等待,不消耗 CPU(进入低功耗态)NOTE
wfe vs wfi 的区别:wfe(Wait For Event)可用 SEV 指令唤醒,常用于多核间的轻量级同步;wfi(Wait For Interrupt)只能被硬件中断唤醒,用于真正的空闲等待。
6. 异常处理机制
异常是 CPU 处理"意外事件"的机制——同步异常(如系统调用 svc、缺页)和异步中断(如定时器、外设)都通过异常向量表分发。
6.1 svc
全称 Supervisor Call(系统调用)。
svc #0 @ 触发系统调用,从 EL0 切换到 EL1- 从 EL0 用户态切换到 EL1 内核态
#0是 24 位立即数参数,可通过ESR_EL1读取- 系统调用约定:
x8= 系统调用号,x0-x5= 参数,x0= 返回值
用户程序示例:
mov x8, #64 @ syscall number: SYS_WRITE
svc #0 @ 触发系统调用6.2 eret
全称 Exception Return(异常返回)。
eret @ PC = ELR_EL1, PSTATE = SPSR_EL1- 将
ELR_EL1加载到 PC(返回到中断前的下一条指令) SPSR_EL1恢复 CPU 状态(中断前的特权级等)- 用于从内核态返回用户态或从异常处理返回
NOTE
eret 做了两件事:恢复 PC(从哪里中断的)和恢复 CPU 状态(中断前是什么特权级)。这两个信息在异常发生时分别被硬件自动保存到 ELR_EL1 和 SPSR_EL1。
6.3 异常向量表原理
ARMv8 异常向量表有 16 个入口(4 组 × 4 种异常类型),共 2KB(16 × 0x80),由 VBAR_EL1 寄存器指向基址。
| 偏移 | 组 0:同 EL,SP0 | 组 1:同 EL,SPx | 组 2:低 EL,AArch64 | 组 3:低 EL,AArch32 |
|---|---|---|---|---|
+0x000 | Sync | Sync | Sync | Sync |
+0x080 | IRQ | IRQ | IRQ | IRQ |
+0x100 | FIQ | FIQ | FIQ | FIQ |
+0x180 | SError | SError | SError | SError |
四组分别对应:
- 组 0:同 EL、使用 SP_EL0(栈指针 0)
- 组 1:同 EL、使用 SP_ELx(当前级别的栈指针)
- 组 2:从低 EL 进入,AArch64 模式(本课程最常用)
- 组 3:从低 EL 进入,AArch32 模式
每组 4 种异常类型:Sync(同步异常)、IRQ(普通中断)、FIQ(快速中断)、SError(系统错误)
每个入口间隔 0x80 字节(128 字节),足够放一条分支指令跳转到实际的 C 处理函数。
IMPORTANT
设置异常向量表的关键步骤:
- 定义 2KB 对齐的向量表(
.balign 0x800) - 在每个入口填入跳转指令(
b handler) - 用
msr vbar_el1, x0设置VBAR_EL1 - 调用
isb确保新向量表立即生效
具体的向量表代码实现和异常现场保存/恢复宏在 L109-L110 实验中展开。
7. MMU / 页表相关操作
MMU(Memory Management Unit)将虚拟地址翻译为物理地址,是实现虚拟内存、进程隔离和保护的基础。
7.1 页表项结构(原理)
每条页表项是 64 位描述符,以 Block Descriptor 为例:
| 位域 | 名称 | 含义 |
|---|---|---|
[63] | — | 保留 |
[54] | UXN | EL0 不可执行 |
[53:48] | — | 保留 |
[47:30] | 输出物理地址 | block descriptor 输出物理地址 / table descriptor 下级页表基址 |
[29:11] | — | 保留 |
[10] | AF | Access Flag:硬件访问后自动置 1 |
[9:7] | — | 保留 |
[6:4] | AttrIndx | 内存属性索引(普通内存 / 设备内存 / 不可缓存等) |
[3:2] | — | 保留 |
[1:0] | 描述符类型 | 0b01 = block, 0b11 = table |
7.2 tlbi
全称 TLB Invalidate(TLB 刷新)。
tlbi vmalle1is @ 清空 EL1 所有 TLB 条目(跨核广播)页表修改后必须刷新 TLB,否则 MMU 可能继续使用旧的地址映射。
7.3 MMU 启用原理
MMU 的启用通过设置 SCTLR_EL1 寄存器的 bit 0 来完成:
1. 配置 TCR_EL1(页表粒度、地址宽度)
2. 设置 TTBR0_EL1 / TTBR1_EL1(页表基址)
3. tlbi + dsb + isb(刷新 TLB、同步)
4. 设置 SCTLR_EL1[0] = 1(启用 MMU)
5. isb(立即同步——后续所有取指经过 MMU 翻译)WARNING
启用 MMU 后,地址立即改变! 如果 SCTLR_EL1[0] 从 0 变为 1,下一条指令的 PC 就会经过 MMU 翻译。如果你的页表没有为当前代码段建立正确映射,CPU 会立刻触发 Translation Fault。这就是为什么需要 isb——确保流水线中的指令使用新的 MMU 状态。
具体的 MMU 启用代码在 L107 实验中展开。
8. 上下文切换
上下文切换是操作系统任务调度的核心——保存当前任务的所有寄存器状态,恢复下一个任务的状态。
核心原理
上下文切换只保存 callee-saved 寄存器(x19-x30):
- x0-x18 是 caller-saved(调用者保存),编译器在调用函数时自动推栈
- x19-x30 是 callee-saved(被调用者保存),必须手动保存和恢复
关键设计:sp(栈指针)保存在 TCB 结构体偏移 0 处,即 &task->sp == &task。因此 TCB 指针直接指向栈指针的存储位置,无需额外偏移计算。
切换流程概要
1. 将当前任务的 x19-x30 压入其栈中
2. 将当前 sp 存入 TCB
3. 加载新任务的 sp
4. 从新任务栈中恢复 x19-x30
5. ret → 自然返回到新任务的上下文NOTE
具体的上下文切换汇编代码在 L114 实验中展开。
9. GCC 内联汇编(Inline Assembly)
GCC 内联汇编让你在 C 代码中嵌入汇编指令,是内核开发中访问 CPU 特性不可或缺的工具。
基本格式
asm volatile(
"汇编指令模板"
: 输出操作数列表 // output operands
: 输入操作数列表 // input operands
: 破坏列表 // clobber list
);TIP
每一部分的含义:
- 模板:汇编指令字符串,
%0、%1等是操作数的占位符 - 输出操作数:C 变量 ← 汇编结果,格式
"约束"(变量名) - 输入操作数:汇编 ← C 变量值,格式
"约束"(表达式) - 破坏列表:告诉编译器哪些寄存器/内存被修改,防止编译器误优化
约束符速查
| 约束 | 含义 | 示例 |
|---|---|---|
r | 任意通用寄存器 | "r"(lock) |
+r | 可读写的寄存器 | "+r"(tmp) |
=r | 只写的寄存器 | "=r"(val) |
%w0 | 操作数的 W(32 位)视图 | %w0 |
%0 | 操作数的 X(64 位)视图 | %0 |
memory | 声明内存会被修改(clobber 专用) | : "memory" |
IMPORTANT
"+r" vs "=r" vs "r" 的细微区别:
"="表示纯输出(只写),编译器不会把旧值传给你"+"表示读写,编译器会把变量值加载到寄存器,执行完后写回- 不加前缀表示纯输入(只读)
实战示例
// 读取系统寄存器
static inline uint64_t read_sctlr_el1(void) {
uint64_t val;
asm volatile("mrs %0, sctlr_el1" : "=r"(val));
return val;
}
// 内存同步屏障
static inline void dsb_sy(void) {
asm volatile("dsb sy" ::: "memory");
}
// 读取故障地址
static inline uintptr_t read_far(void) {
uintptr_t val;
asm volatile("mrs %0, far_el1" : "=r"(val));
return val;
}WARNING
"memory" clobber 很重要!它告诉编译器"汇编代码可能读写你不认识的任意内存位置"。不加 "memory",编译器可能会把内存访问重排到内联汇编之前或之后,在多核/中断场景下导致难以排查的 bug。
10. 汇编指令速查卡
| 类别 | 指令 |
|---|---|
| 数据移动 | mov, movz, movk, ldr, str |
| 字节操作 | ldrb, strb, ldrsw |
| 寄存器对 | stp, ldp |
| 地址生成 | adr, ldr = |
| 系统寄存器 | mrs(读), msr(写) |
| 中断控制 | msr daifset / daifclr |
| 异常控制 | svc, eret |
| 原子操作 | ldaxr, stxr, stlr |
| 内存屏障 | dmb, dsb, isb |
| 分支 | b, bl, blr, br, ret, cbnz, cbz |
| 比较 | cmp, b.eq/ne/ge/lt/gt/le |
| 等待 | wfe, wfi |
| TLB 管理 | tlbi vmalle1is |
这套汇编体系是课程的核心底盘。建议从数据移动指令开始理解,然后逐步深入系统寄存器、原子操作和异常处理,这些会在课程各阶段反复使用并加深理解。
参考链接
- Avatar OS 课程仓库 — 源码、README、评测脚本
- 夏令营活动页面 — 课程时间安排与公告
- ARM Architecture Reference Manual — ARMv8 官方架构手册