diff --git a/articles/20230615-hugepaged-linear-mapping.md b/articles/20230615-hugepaged-linear-mapping.md
new file mode 100644
index 0000000000000000000000000000000000000000..7d7a04cc09899d30e05c8cfb84d01b5f72343103
--- /dev/null
+++ b/articles/20230615-hugepaged-linear-mapping.md
@@ -0,0 +1,567 @@
+> Corrector: [TinyCorrect](https://gitee.com/tinylab/tinycorrect) v0.1 - [tables]
+> Author: sugarfillet
+> Date: 2023/06/15
+> Revisor: Falcon falcon@tinylab.org
+> Project: [RISC-V Linux 内核剖析](https://gitee.com/tinylab/riscv-linux)
+> Proposal: [RISC-V Linux SMP 技术调研与分析](https://gitee.com/tinylab/riscv-linux/issues/I5MU96)
+> Sponsor: PLCT Lab, ISCAS
+
+# RISC-V Linux 线性地址大页支持补丁解读
+
+## 前言
+
+Linux-v6.4-rc1 发布后带来了几个比较重要的变更,其中 commit (3335068f8721 "riscv: Use PUD/P4D/PGD pages for the linear mapping") 用来为内核线性地址空间提供的大页映射的支持,以获取更好的性能并回收了一些浪费的内存。然而,此补丁却带来不少的问题,比如:休眠场景下的 panic 以及 UEFI 启动过程中的 panic。
+
+本文对该 commit 及其背景知识进行介绍,并对其引发的两个问题进行分析。
+
+**说明**:
+
+- 本文的 Linux 版本采用 `Linux v6.4-rc1`
+- 本文采用的一些缩略词解释如下
+
+| 缩略词 | 全称 | 说明 |
+|-------------|---------------------------------|------------------------------------------------------------------|
+| PA | Physical Address | 物理地址 |
+| VA | Virtual Address | 虚拟地址 |
+| PPN | Physical Page Number | 物理页帧号,与页内偏移 page offset 构成物理地址 |
+| VPN | Virtual Page Number | 虚拟页帧号,与页内偏移 page offset 构成虚拟地址 |
+| PTE | Page Table Entry | 页表项,存放叶子页表或者页目录的 PPN,还有一层含义代表最后一级的页表 |
+| PGD | Page Global Directory | 根(页)目录 |
+| P4D/PUD/PMD | Page 4th/Upper/Middle Directory | 不同级别的页目录项,不同分页方案有不同页目录项,详见下文 |
+
+## RISC-V 地址翻译
+
+如 "RISC-V-Reader-Chinese-v2p1.pdf" 文档所述:
+
+> RISC-V S 模式提供了一种传统的虚拟内存系统,它将内存划分为固定大小的页来进行地址转换和对内存内容的保护。启用分页的时候,大多数地址(包括 load 和 store 的有效地址和
+PC 中的地址)都是虚拟地址。要访问物理内存,它们必须被转换为真正的物理地址,这通过遍历一种称为页表的高基数树实现。页表中的叶节点指示虚地址是否已经被映射到了真正的物理页面,如果是,则指示了哪些权限模式和通过哪种类型的访问可以操作这个页。访问未被映射的页或访问权限不足会导致页错误例外(page fault exception)。
+
+RISC-V MMU 可以将取指/load/store 等操作的的虚拟地址转化为物理地址,整个转换过程通过页表结构来实现。RISC-V 在 satp 寄存器的 Mode 字段定义所支持的分页方案,每种方案的区别主要体现在通过几个层级来映射不同长度的 VA 到不同长度的 PA,比如:Sv57 分页方案采用 5 级页表将 57 位的虚拟地址翻译为 56(44+12)位的物理地址:
+
+| stap.mode | SXLEN(pte_len) | VPNs | PPNs | page level | page tables |
+|-----------|----------------|-------------------|------------------|------------|---------------------|
+| Sv32 | 32 | 32 (10+10+12) | 22 (12+10) | 2 | PGD PTE |
+| Sv39 | 64 | 39 (9+9+9+12) | 44 (26+9+9) | 3 | PGD PMD PTE |
+| Sv48 | 64 | 48 (9+9+9+9+12) | 44 (17+9+9+9) | 4 | PGD PUD PMD PTE |
+| Sv57 | 64 | 57 (9+9+9+9+9+12) | 44 (8+9+9+9+9+9) | 5 | PGD P4D PUD PMD PTE |
+
+### MMU 地址翻译过程
+
+在 RISC-V 特权手册 "Virtual Address Translation Process" 一节中详细地描述了 Sv32 分页方案的地址翻译过程。这里在 Qemu 环境默认的 Sv57 的分页方案下,以内核的加载地址为例,结合 Linux 早期虚拟地址映射保留的每级页目录地址(early_p4d/early_pud/early_pmd),观察 MMU 如何将虚拟地址 -- `0xffffffff80000000` 翻译为其对应的物理地址:
+
+可参考此图阅读下文:
+
+
+
+首先将虚拟地址按照 Sv57 虚拟地址布局划分:
+
+```
+va=0xffffffff80000000
+
+0x[f] 111| 1[ff] | [ff] 1|111 [f] 10| 00 [0] 000|0 [00] |[000] // [] 内的为 16 进制表示,[]之外的为按照 bit 表示,下文亦是如此
+> vpn4 vpn3 vpn2 vpn1 vpn0 offset
+```
+
+通过 satp.PPN 获取根页表:`early_pg_dir = satp.PPN << 12`,按照如下步骤依次获取每级目录的页表项:
+
+1. offset = va.vpn4 (511); pte = early_pg_dir[offset] (0x0000000020380c01) // `01` is not leaf so pte.ppn <<12 => early_p4d
+2. offset = va.vpn3 (511); pte = early_p4d[offset] (0x0000000020380801) // pte.ppn <<12 => early_pud
+3. offset = va.vpn2 (510); pte = early_pud[offset] (0x0000000020381001) // pte.ppn <<12 => early_pmd
+4. offset = va.vpn1 (0); pte = early_pmd[offset] (0x00000000200800ef) // `ef` is leaf so
+
+在步骤 4 可以看到当前 VA 对应 earyly_pmd 中偏移为 0 的 PTE,此表项为叶子表项。对此 PTE 按照 PTE 格式划分如下,其中 ppn[4:0] 则为 44 位的物理地址表示,右移 10 位得到其对应的页帧号,再左移 12 位,填充 va.offset 则得到该地址对应的 56 位长度的物理地址。而这个物理地址 `0x00000080200000` 恰恰就是内核加载的物理地址,且映射存放在 PMD 上。
+
+```
+pte=0x00000000200800ef
+
+N PBMT |Reserved PPN | RSW D A G U X W R V
+
+0x[00]0 | 000[0000020080] 00 | 00[ef] // pte
+> ppn[4:0] page bits
+
+pte.ppn[4:0] = (0x00000000200800ef >>10) = 0x [000]00080200 (len is 44)
+
+pa = 0x00000080200[000] (ppt.ppn[4:0] <<12 | va.off)
+
+```
+
+```c
+(gdb) p/z early_pg_dir
+$31 = {{pgd = 0x0000000000000000} , {pgd = 0x00000000205c1401}, {
+ pgd = 0x0000000000000000} , {pgd = 0x0000000020380c01}} // 511
+(gdb) p/z early_p4d
+$35 = {{p4d = 0x0000000000000000} , {p4d = 0x0000000020380801}} // 511
+(gdb) p/z early_pud
+$36 = {{pud = 0x0000000000000000} , {pud = 0x0000000020381001}, {pud = 0x0000000000000000}} //510
+(gdb) p/z early_pmd
+$37 = {{pmd = 0x00000000200800ef}, {pmd = 0x00000000201000ef}, {pmd = 0x00000000201800ef}, {pmd = 0x00000000202000ef}, {pmd = 0x00000000202800ef}, {pmd = 0x00000000203000ef}, {pmd = 0x00000000203800ef}, {
+ pmd = 0x00000000204000ef}, {pmd = 0x00000000204800ef}, {pmd = 0x00000000205000ef}, {pmd = 0x00000000205800ef}, {pmd = 0x0000000000000000} } // 0
+```
+
+基于以上过程,S-mode 软件如果要开启虚拟内存,则需要建立页表并将根页表的 PFN 写入到 satp 寄存器中,同时需要执行 `sfence.vma` 指令用以同步当前所有的内存读写操作。
+
+### Linux 设置页表
+
+RISC-V Linux 中使用 `create_pgd_mapping()` 函数用于创建(根)页表,此函数的使用场景有:
+
+1. 初期的内存映射 `setup_vm()` 为 fix-mapping、内核镜像创建早期的临时页表 -- `early_pg_dir`
+2. 系统内存发现后的后期内存映射 `setup_vm_final()` 为内核镜像以及线性地址空间创建页表 -- `swapper_pg_dir`
+3. efi 为 runtime 内存创建 runtime 页表 -- `efi_mm`
+4. 在休眠唤醒过程中,需要对切换到休眠镜像中保存的页表,调用 `temp_pgtable_mapping()` 函数创建临时页表
+
+这里以内核线性地址的页表创建过程来做说明:在 `setup_vm_final()` 阶段,系统内存通过 dtb 或者 UEFI 内存映射表已添加到 `memblok.memory` 或者保留在 `memblock.reserved` 中,调用 `create_linear_mapping_page_table()` 将 `memblock.memory` 中的可用物理内存映射到 `PAGE_OFFSET` 开始的线性虚拟内存区域,调用 `create_pgd_mapping()` 创建页表,最终写入 `satp` 寄存器。调用 `create_pgd_mapping()` 传递的参数有:
+
+1. `swapper_pg_dir`: 根页表,后续以此变量写入 satp
+2. `va`: 要映射的虚拟地址
+3. `pa`: 要映射的物理地址
+4. `map_size`: 要映射的内存大小,在 `best_map_size()` 函数中通过物理地址是否对齐 PGDIR_SIZE/../PMD_SIZE 来决定映射大小(下文的 UEFI 启动 panic 就与此有关)
+
+ 比如:物理地址 `0x00000080200000` 对齐 `PMD_SIZE`,则返回 `PMD_SIZE`,物理地址 `0x00000080210000` 则返回 `PAGE_SIZE`
+
+5. `prot`: 定义当前映射 PTE 的保护位
+
+ 比如:内核代码段则为 `PAGE_KERNEL_READ_EXEC`
+
+```c
+// arch/riscv/mm/init.c : 1248
+
+static void __init setup_vm_final(void)
+ create_linear_mapping_page_table();
+ /* Map all memory banks in the linear mapping */
+ for_each_mem_range(i, &start, &end) {
+ if (start >= end)
+ break;
+ if (start <= __pa(PAGE_OFFSET) &&
+ __pa(PAGE_OFFSET) < end)
+ start = __pa(PAGE_OFFSET);
+ if (end >= __pa(PAGE_OFFSET) + memory_limit)
+ end = __pa(PAGE_OFFSET) + memory_limit;
+
+ create_linear_mapping_range(start, end);
+ }
+
+ csr_write(CSR_SATP, PFN_DOWN(__pa_symbol(swapper_pg_dir)) | satp_mode); // 写入根页表 PFN 到 satp
+ local_flush_tlb_all();
+
+static void __init create_linear_mapping_range(phys_addr_t start,
+ phys_addr_t end)
+{
+ phys_addr_t pa;
+ uintptr_t va, map_size;
+
+ for (pa = start; pa < end; pa += map_size) {
+ va = (uintptr_t)__va(pa);
+ map_size = best_map_size(pa, end - pa);
+
+ create_pgd_mapping(swapper_pg_dir, va, pa, map_size,
+ pgprot_from_va(va));
+ }
+}
+```
+
+`create_pgd_mapping()` 函数用于在页表树中迭代建立内存映射,大致的过程如下:
+
+1. `pgd_index(va)` 获取 VA 在 PGD 中的偏移(PGD 目录中的 PTE 数目为 `PTRS_PER_PGD -1`)
+2. 如果 `map_size` 为 `PGDIR_SIZE`,则表示此映射存放在 PGD 目录中对应索引的叶子 PTE,否则存放到下一级页目录
+3. 存放到下一级页目录
+ - 如果此 PTE 为空,则调用 `pt_ops` 结构中分配下一级页目录的函数,并保存在当前 PTE 中。`opt_ops` 在不同启动阶段采用的分配后端不同,可参考 (`pt_ops_set_{early,fixmap,late}`)
+ - 获取下一级页目录的(虚拟)地址,并调用下一级页目录的创建函数 `create_pgd_next_mapping()`,不同的分页方案调用不同的函数
+ - 如果指定的是最小的 `map_size` -- `PAGE_SIZE`,则最终调用 `create_pte_mapping()` 在 PTE 中存储该页物理地址的 PFN
+
+```c
+// arch/riscv/mm/init.c : 635
+
+void __init create_pgd_mapping(pgd_t *pgdp,
+ uintptr_t va, phys_addr_t pa,
+ phys_addr_t sz, pgprot_t prot)
+{
+ pgd_next_t *nextp;
+ phys_addr_t next_phys;
+ // 该虚拟地址在其对应的页目录中的索引
+ uintptr_t pgd_idx = pgd_index(va); /// (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1))
+
+ if (sz == PGDIR_SIZE) { // 存放在 PGD 中
+ if (pgd_val(pgdp[pgd_idx]) == 0)
+ pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(pa), prot);
+ return;
+ }
+
+ if (pgd_val(pgdp[pgd_idx]) == 0) { // 创建下一级页目录,并将其物理地址保存到此表项中
+ next_phys = alloc_pgd_next(va);
+ pt_ops.alloc_pmd(__va)
+
+ pgdp[pgd_idx] = pfn_pgd(PFN_DOWN(next_phys), PAGE_TABLE);
+
+ nextp = get_pgd_next_virt(next_phys); // 获取虚拟地址
+ pt_ops.get_pmd_virt
+
+ memset(nextp, 0, PAGE_SIZE);
+ } else {
+ next_phys = PFN_PHYS(_pgd_pfn(pgdp[pgd_idx])); // 如果表项不为空,取出物理地址
+ nextp = get_pgd_next_virt(next_phys);
+ }
+
+ create_pgd_next_mapping(nextp, va, pa, sz, prot); // 在下一级目录项中对此地址进行映射
+ create_pmd_mapping((pmd_t *)__nextp, __va, __pa, __sz, __prot)))
+ create_pte_mapping(ptep, va, pa, sz, prot)
+ ptep[pte_idx] = pfn_pte(PFN_DOWN(pa), prot);
+}
+```
+
+如果系统支持五级页表方案 (Sv57),则在 `map_size` 为 `PAGE_SIZE` 的情况下,依次调用 `create_p4d_mapping() => create_pud_mapping() => create_pmd_mapping() => create_pte_mapping()`;如果系统支持四级页表方案 (Sv48),则在 `map_size` 为 `PAGE_SIZE` 的情况下,依次调用 `create_pud_mapping() => create_pmd_mapping() => create_pte_mapping()`;如果系统支持三级页表方案(Sv39),则在 `map_size` 为 `PAGE_SIZE` 的情况下,依次调用 `create_pmd_mapping() => create_pte_mapping()`;如果是 32 位的二级页表方案(Sv32),则直接调用 `create_pte_mapping()`。
+
+```c
+// arch/riscv/mm/init.c : 611
+
+#define create_pgd_next_mapping(__nextp, __va, __pa, __sz, __prot) \
+ (pgtable_l5_enabled ? \
+ create_p4d_mapping(__nextp, __va, __pa, __sz, __prot) : \
+ (pgtable_l4_enabled ? \
+ create_pud_mapping((pud_t *)__nextp, __va, __pa, __sz, __prot) : \
+ create_pmd_mapping((pmd_t *)__nextp, __va, __pa, __sz, __prot)))
+
+create_pmd_mapping(pmd_t *pmdp, uintptr_t va, phys_addr_t pa, phys_addr_t sz, pgprot_t prot)
+ if (sz == PMD_SIZE)
+ pmdp[pmd_idx] = pfn_pmd(PFN_DOWN(pa), prot);
+ pte_phys = PFN_PHYS(_pmd_pfn(pmdp[pmd_idx]));
+ ptep = pt_ops.get_pte_virt(pte_phys);
+ create_pte_mapping(ptep, va, pa, sz, prot);
+ ptep[pte_idx] = pfn_pte(PFN_DOWN(pa), prot);
+
+```
+
+通过本节的介绍,相信大家对 MMU 的地址翻译,以及 Linux 如何创建页表有了一定的了解。这两个知识是理解本文要讨论的这个 commit 的背景知识,我们继续。
+
+## riscv: Use PUD/P4D/PGD pages for the linear mapping
+
+> 补丁原文就不贴了,可以结合具体的 commit id(3335068f8721)或者[邮件][1]来看
+
+这个补丁在 Linux v6.4-rc1 版本引入,主要带来两个变更:
+
+- kernel_map.va_pa_offset 从之前的 `PAGE_OFFSET - kernel_map.phys_addr` 变更到 `PAGE_OFFSET - phys_ram_base`。
+
+ va_pa_offset 用于 `__va()/__pa()` 的计算,比如:对于线性地址的 `__pa()` 等价于:`#define linear_mapping_va_to_pa(x) ((unsigned long)(x) - kernel_map.va_pa_offset)`。此变更就导致:线性地址空间的物理地址可以不从 `kernel_map.phys_addr` -- `0x00000080200000` 开始(这个地址根据前文所述,`map_size` 为 `PMD_SIZE` 即只能进行 2M 的 PMD 的映射),从而可以采用更大的页进行映射,比如:`0x00000000c0000000` 地址可映射到 1G 的页目录上。这样能带来可能的更好的 TLB 性能。
+
+- `MIN_MEMBLOCK_ADDR` 从 `__pa(PAGE_OFFSET)` 变更到 `0`
+
+ 此变量用于在早期的内存发现过程中,定义 DRAM 内存发现的下限。此变更导致:可以利用 `0x00000080200000` 地址之前的物理地址,避免的一定的内存浪费。
+
+> 注意:此修改使得 `setup_vm_setup()` 之前的 __va/__pa 操作都是无意义,如果你的代码执行了该操作,可能会报错
+>
+> 为描述方面,后文统一以“大页补丁”来称呼此补丁。
+
+## 休眠 panic
+
+此问题是我在对 Linux v6.4-rc1 的休眠特性进行分析(可参考社区对休眠的[三篇分析文章][2])的过程中发现的,在做了一些前期定位后,向社区发送了 [Bug Report][3] 邮件。本节结合邮件列表对此问题做个系统的梳理:
+
+> 你可以在邮件中找到问题的复现方法和系统日志
+
+在 `/sys/power/state` 中输入 `disk`,触发休眠过程中,出现 panic,关键日志如下:
+
+```sh
+[root@stage4 ~]# echo disk > /sys/power/state
+[ 448.600860] PM: hibernation: hibernation entry
+[ 448.633200] Filesystems sync: 0.023 seconds
+[ 448.637578] Freezing user space processes
+[ 448.642714] Freezing user space processes completed (elapsed 0.004 seconds)
+[ 448.643801] OOM killer disabled.
+[ 448.646150] PM: hibernation: Preallocating image memory
+[ 450.448810] PM: hibernation: Allocated 57556 pages for snapshot
+[ 450.449347] PM: hibernation: Allocated 230224 kbytes in 1.80 seconds (127.90 MB/s)
+[ 450.449950] Freezing remaining freezable tasks
+[ 450.453384] Freezing remaining freezable tasks completed (elapsed 0.003 seconds)
+[ 450.498622] Disabling non-boot CPUs ...
+[ 450.501293] CPU0 attaching NULL sched-domain.
+[ 450.501879] CPU1 attaching NULL sched-domain.
+[ 450.503247] CPU0 attaching NULL sched-domain.
+[ 450.503624] root domain span: 0 (max cpu_capacity = 1024)
+[ 450.514289] CPU1: off
+[ 450.525152] PM: hibernation: Creating image:
+[ 450.525152] PM: hibernation: Need to copy 56199 pages
+
+[ 450.525152] Oops - load access fault [#1]
+[ 450.525152] Modules linked in:
+[ 450.525152] CPU: 0 PID: 210 Comm: bash Not tainted 6.4.0-rc1-00004-gcce672326817 #18
+[ 450.525152] Hardware name: riscv-virtio,qemu (DT)
+[ 450.525152] epc : swsusp_save+0x2ee/0x45a
+[ 450.525152] ra : swsusp_save+0x2b2/0x45a
+[ 450.525152] epc : ffffffff809ac404 ra : ffffffff809ac3c8 sp : ff200000007dbc20
+[ 450.525152] gp : ffffffff815dc700 tp : ff6000000346b000 t0 : 65626968203a4d50
+[ 450.525152] t1 : 0000000000080000 t2 : 7265626968203a4d s0 : ff200000007dbc90
+[ 450.525152] s1 : 0000000000000001 a0 : 0000000000000001 a1 : ff5fffff80000000
+[ 450.525152] a2 : ff60000000000000 a3 : 0000000000001000 a4 : 0000000000000000
+[ 450.525152] a5 : ff60000000000000 a6 : ffffffff815eb000 a7 : ffffffffffff8000
+[ 450.525152] s2 : ff6000000ac22000 s3 : ffffffff815dbf45 s4 : 000000000000db87
+[ 450.525152] s5 : 0000000100000000 s6 : 0004000000000000 s7 : 0040000000000000
+[ 450.525152] s8 : ffffffff815dbf44 s9 : ff1c000002000000 s10: 0000000000080000
+[ 450.525152] s11: ffffffff81082060 t3 : 0000000000078000 t4 : ffffffff815f20c7
+[ 450.525152] t5 : ffffffff815f20c8 t6 : ff200000007dba28
+[ 450.525152] status: 0000000200000100 badaddr: ff60000000000000 cause: 0000000000000005
+[ 450.525152] [] swsusp_save+0x2ee/0x45a
+[ 450.525152] [] swsusp_arch_suspend+0x4a/0x98
+[ 450.525152] [] hibernation_snapshot+0x1cc/0x3e2
+[ 450.525152] [] hibernate+0x14e/0x236
+[ 450.525152] [] state_store+0x6a/0x72
+[ 450.525152] [] kobj_attr_store+0xe/0x1a
+[ 450.525152] [] sysfs_kf_write+0x32/0x3c
+[ 450.525152] [] kernfs_fop_write_iter+0xfa/0x164
+[ 450.525152] [] vfs_write+0x27c/0x31e
+[ 450.525152] [] ksys_write+0x68/0xda
+[ 450.525152] [] sys_write+0x1a/0x22
+[ 450.525152] [] do_trap_ecall_u+0xc2/0xd6
+[ 450.525152] [] do_trap_ecall_u+0xc2/0xd6
+[ 450.525152] [] ret_from_exception+0x0/0x64
+[ 450.525152] Code: 8f91 8f95 87b3 40fc 8799 07b2 97ae 6685 8633 00e7 (620c) 0633
+[ 450.525152] ---[ end trace 0000000000000000 ]---
+```
+
+从日志中可以看到,错误指令为 `epc: swsusp_save+0x2ee/0x45a`,对其执行反汇编后发现:在 `do_copy_page` 函数中,对寄存器 a2 中的地址执行 load 操作触发了 load access fault(此错误同时体现在 "Oops" 提示和 scause 寄存器的值中),可以判断这可能是一个访问 PMP 保护内存触发的异常。而发生错误的虚拟地址为 `0xff60000000000000`,正好的是内核线性地址的起始地址 -- `PAGE_OFFSET`,需要进一步确认该地址的映射的物理内存地址。
+
+```c
+
+1381 *dst++ = *src++;
+ 0xffffffff809ac400 <+738>: add a2,a5,a4
+ 0xffffffff809ac404 <+742>: ld a1,0(a2) // 0xff60000000000000
+ 0xffffffff809ac406 <+744>: add a2,s2,a4
+ 0xffffffff809ac40a <+748>: addi a4,a4,8
+ 0xffffffff809ac40c <+750>: sd a1,0(a2)
+```
+
+> 结合当前环境,介绍 Linux 内存发现过程(如何将 dtb 中描述的内存信息添加到 memblock)
+
+Linux 初期的内存发现过程中,以 `parse_dtb() => early_init_dt_scan_memory()` 调用 `memblock_add()`,保存完整的系统内存在 `memblock.memory`,(范围由 `MIN_MEMBLOCK_ADDR` = 0 控制)。在正式页表 `swapper_pg_dir` 建立之前,调用 `early_init_fdt_scan_reserved_mem()` 初始化保留内存,如果对应的 "reserved-memory" 节点中没有 "no-map" 属性,则直接调用 `memblock_reserve()`,而不调用 `memblock_mark_nomap()`。可以在如下日志中看到初始化保留内存的信息:
+
+```c
+[ 0.000000] OF: fdt: Looking for usable-memory-range property...
+[ 0.000000] OF: fdt: Reserved memory: reserved region for node 'mmode_resv0@80000000': base 0x0000000080000000, size 0 MiB
+[ 0.000000] OF: reserved mem: 0x0000000080000000..0x000000008003ffff (256 KiB) map non-reusable mmode_resv0@80000000
+```
+
+此内存区域 `0x0000000080000000..0x000000008003ffff` 正是 OpenSBI 的固件内存(mmode_resv0@80000000),被识别为 "map"、"non-reusable",则该区域同时存在于 `memblock.memory` 和 `memblock.reserved` 中,且无 `MEMBLOCK_NOMAP` 标志。在后续的线性映射过程 `create_linear_mapping_page_table()` 中,`for_each_mem_range` 会对该内存区域进行映射,从内核页表查询工具 -- `ptdump` 中可以看到,内核线性地址正好映射到 OpenSBI 的固件内存物理地址:
+
+```sh
+# cat /sys/kernel/debug/kernel_page_tables
+...
+---[ Linear mapping ]---
+0xff60000000000000-0xff60000000200000 0x0000000080000000 2M PMD D A G . . W R V // 固件内存
+0xff60000000200000-0xff60000000c00000 0x0000000080200000 10M PMD D A G . . . R V
+0xff60000000c00000-0xff60000001000000 0x0000000080c00000 4M PMD D A G . . W R V
+0xff60000001000000-0xff60000001600000 0x0000000081000000 6M PMD D A G . . . R V
+0xff60000001600000-0xff60000040000000 0x0000000081600000 1002M PMD D A G . . W R V
+0xff60000040000000-0xff60000100000000 0x00000000c0000000 3G PUD D A G . . W R V
+---[ Modules/BPF mapping ]---
+---[ Kernel mapping ]---
+0xffffffff80000000-0xffffffff80a00000 0x0000000080200000 10M PMD D A G . X . R V
+0xffffffff80a00000-0xffffffff80c00000 0x0000000080c00000 2M PMD D A G . . . R V
+0xffffffff80c00000-0xffffffff80e00000 0x0000000080e00000 2M PMD D A G . . W R V
+0xffffffff80e00000-0xffffffff81400000 0x0000000081000000 6M PMD D A G . . . R V
+0xffffffff81400000-0xffffffff81800000 0x0000000081600000 4M PMD
+```
+
+> 为何 OpenSBI 不在 dtb 中对此固件内存设置 "no-map" 属性呢?
+
+在 OpenSBI 的 v0.8 中引入 commit 6966ad0abe70 ("platform/lib: Allow the OS to map the regions that are protected by PMP"),此提交对 PMP 保护的内存(比如:固件内存)默认不再设置 "no-map" 属性,并允许操作系统对其进行映射,同时提供 platform_override 使得某个平台(比如:sifive,fu540)可手动设置 "no-map" 属性。而此补丁的出发点,与上节描述的大页补丁是一致的,都是为了更好的 TLB 性能。
+
+这样的话,panic 的原因就比较清晰了:
+
+1. OpenSBI 在 v0.8 之后对于固件内存默认不设置 "no-map" 属性
+2. 上节的大页补丁导致 OpenSBI 的固件内存被映射到线性地址空间
+3. 休眠过程中调用 `swsusp_save()` 拷贝当前系统内存页到休眠镜像,当对固件内存进行拷贝时,触发了 PMP 保护,进而 hart 发生 access fault 异常
+
+那针对以上三个原因可以有如下解决方案:
+
+1. OpenSBI 恢复设置 "no-map" 属性
+
+ 牺牲 TLB 性能,还要考虑向后的兼容性
+
+2. Linux 回退大页补丁
+
+ 回退会牺牲 TLB 性能(尽管此补丁的作者表示:对线性地址进行大页映射并不会带来更好的性能)
+
+3. 在休眠过程中跳过固件内存
+
+ 在启动早期对 "mmode_resv" 节点进行解析,并调用休眠的 `register_nosave_regions()` 接口保证此内存区域不会被休眠过程保存,实现可参考邮件中的[实验性补丁][4]。但此补丁不具有通用性,没办法处理非 "mmode_resv" 节点。
+
+4. 设置休眠选项 -- `ARCH_HIBERNATION_POSSIBLE` 为 `NONPORTABLE`
+
+ 休眠功能只能在 OpenSBI v8.0 之前(那些禁止 OS 映射固件内存的 SBI 实现)的系统中开启。而将休眠选项设置为 `NONPORTABLE`,用户可根据自己的系统配置,设置 `NONPORTABLE` 来开启或者关闭休眠功能。此方案在 commit (ed309ce52218 "RISC-V: mark hibernation as nonportable") 中实现。
+
+个人比较赞成方案 1,但是推动起来应该比较困难,简单谈谈我对这个几个方案的看法:
+
+- 方案 1:固件内存应该没有让内核对其映射的强烈需求,那么 OpenSBI 就应该将该区域设置为 "no-map",至于向后的兼容性,可通过文档的形式来描述
+- 方案 2:如果对线性地址进行大页映射并不会带来更好的性能,可以回退此补丁
+- 方案 3:其实此问题并非休眠的单点问题,邮件列表中对此方案的讨论从最初就有点偏差
+ - 比如:通过内核模块直接访问 `PAGE_OFFSET` 也会崩溃(虽然此访问没有经过内存分配器进行,但不代表某些组件(比如:[memory debugging stuff][5])不会这么做)
+- 方案 4: 为了 v6.4 版本稳定的规避方案,需要用户自己判断固件环境来选择开启或者关闭休眠功能
+
+最后,我会持续跟踪与此问题相关的一些内核/OpenSBI 的变更,让子弹再飞一会儿。
+
+## UEFI 启动 panic
+
+此问题是我在对 Linux UEFI 启动过程的分析(参考社区对 Linux UEFI [相关文章][6])中发现的,在做了一些前期定位后,向社区发送了 [Bug Report][7] 邮件,本节结合邮件列表对此问题做个系统的梳理:
+
+> 你可以在邮件中找到问题的复现方法和系统日志
+
+启动过程中内核 panic,关键日志如下:
+
+```sh
+[ 0.000000] Unable to handle kernel paging request at virtual address ff6000007fdb1000
+[ 0.000000] Oops [#1]
+[ 0.000000] Modules linked in:
+[ 0.000000] CPU: 0 PID: 0 Comm: swapper Not tainted 6.4.0-rc1-00007-g6966d7988c4f #65
+[ 0.000000] Hardware name: riscv-virtio,qemu (DT)
+[ 0.000000] epc : __memset+0x60/0xfc
+[ 0.000000] ra : memblock_alloc_try_nid+0x72/0x82
+[ 0.000000] epc : ffffffff8081d48c ra : ffffffff80a126e4 sp : ffffffff81403e80
+[ 0.000000] gp : ffffffff814fbb38 tp : ffffffff8140dac0 t0 : ff6000007fdb1000
+[ 0.000000] t1 : 0000000000000000 t2 : 5f6b636f6c626d65 s0 : ffffffff81403ec0
+[ 0.000000] s1 : 0000000000026000 a0 : ff6000007fdb1000 a1 : 0000000000000000
+[ 0.000000] a2 : 0000000000026000 a3 : ff6000007fdd7000 a4 : 0000000000000000
+[ 0.000000] a5 : ff5fffff7ffc0000 a6 : 0000000000000018 a7 : 0000000000000080
+[ 0.000000] s2 : ff6000007fdb1000 s3 : ffffffffffffffff s4 : 0000000000009e38
+[ 0.000000] s5 : ffffffffffffffff s6 : ff6000007fdd8000 s7 : 0000000000002000
+[ 0.000000] s8 : 00000000000071c8 s9 : 0000000000000000 s10: 0000000000000000
+[ 0.000000] s11: 0000000000000000 t3 : ffffffff80c0be40 t4 : ffffffff80c0be40
+[ 0.000000] t5 : ffffffff80c0bdb0 t6 : ffffffff80c0be40
+[ 0.000000] status: 0000000200000100 badaddr: ff6000007fdb1000 cause: 000000000000000f // Store/AMO page fault
+[ 0.000000] [] __memset+0x60/0xfc
+[ 0.000000] [] pcpu_embed_first_chunk+0x568/0x738
+[ 0.000000] [] setup_per_cpu_areas+0x22/0xb6
+[ 0.000000] [] start_kernel+0x1ce/0x57e
+[ 0.000000] Code: 1007 82b3 40e2 0797 0000 8793 00e7 8305 97ba 8782 (b023) 00b2
+[ 0.000000] ---[ end trace 0000000000000000 ]---
+[ 0.000000] Kernel panic - not syncing: Attempted to kill the idle task!
+[ 0.000000] ---[ end Kernel panic - not syncing: Attempted to kill the idle task! ]---
+```
+
+此问题与休眠问题,有如下不同:
+
+1. 异常类型不同
+
+ 此问题为 page fault 是系统开启分页后 MMU 相关的异常,而休眠问题是 access-fault,是有 PMAs 或者 PMP 引发的异常
+
+2. 休眠问题中固件内存映射到线性地址的情况,在 UEFI 环境中不存在
+
+ 对于 `reserved-memory` 节点中没有设置 "no-map" 且保留的固件内存,EDK2 (RiscVVirt) 将其保存在 `EfiReservedMemoryType`(下文会详细介绍)。而 Linux UEFI 初始化过程 `efi_init() => reserve_regions()` 会以 EFI memory mapping 重新构建 memblock,对于 `EfiReservedMemoryType` 内存,不会将其添加到 `memblock.memory` 中,而是在之后的 `early_init_fdt_scan_reserved_mem()` 函数中保存在 `memblock.reserved`,故而不会映射到线性地址。
+
+而发生 page fault 的错误地址 `0xff6000007fdb1000` 属于线性地址空间,调试一下线性地址空间的映射过程,看到如下日志:
+
+```
+song # lowmem region: [0x0000000081800000 -- 0x00000000ffe3d000], va: 0xff6000007fbc0000, pa: 0x00000000ffc00000, map_size: 200000 ,pg: e7
+song # lowmem region: [0x0000000081800000 -- 0x00000000ffe3d000], va: 0xff6000007fdc0000, pa: 0x00000000ffe00000, map_size: 1000 ,pg: e7
+```
+
+错误地址存在于 "va: 0xff6000007fbc0000"、"pa: 0x00000000ffc00000" 的 2M PMD 映射中,尝试对该映射做个手动分析:
+
+1. map_size = best_map_size(pa, end - pa)
+
+ 映射大小通过物理地址及其与 DRAM 最大内存地址进行计算,由于 "0x00000000ffc00000" 对齐 `PMD_SIZE` 则设置映射大小为 `PMD_SIZE`
+
+2. create_pgd_mapping(swapper_pg_dir, va, pa, map_size, pgprot_from_va(va));
+
+ 此函数在 "Linux 设置页表" 一节有详细介绍,迭代调用到 `create_pmd_mapping()` 并以 `pmd_index(va)` 为索引,设置当前物理地址的 PFN 到 PMD 表项中。而 "va: 0xff6000007fbc0000"(注意:此地址不是 2M 对齐的)在经过 `pmd_index(va)` 后,在最终的页表中实际映射到了 "va: 0xff6000007fa00000",参考下面代码块中的地址展开:
+
+ ```
+ va = 0xff6000007fbc0000
+
+ [ff6000007f]101|1[c0]|000
+ vpn0
+
+ // the va after pmd_index(va)
+
+ [ff6000007f]101|0[00]|000
+
+ real va : 0xff6000007fa00000
+
+ ```
+
+整个 2M PMD 的真实映射表示为:"va: [0xff6000007fa00000,0xff6000007fc00000)" => "pa: [0x00000000ffc00000,0x00000000ffe00000)",而在此虚拟地址范围之后且在下一个 4K PTE 映射的起始虚拟地址之间 -- `[0xff6000007fc00000,0xff6000007fdc0000` 存在一个虚拟地址空洞,如果对其进行访问,都会导致 page fault,而此问题的错误地址 `0xff6000007fdb1000` 正好就在这个区间。
+
+为了解决此虚拟地址空洞,应该在映射大小计算中考虑虚拟地址的与某个映射大小对齐,可参考如下代码,那么在此问题中,由于 va 不能对齐 `PMD_SIZE`,则此映射会以 `PAGE_SIZE` 进行。此修改在 riscv/fixes commit (49a0a3731596 "riscv: Check the virtual alignment before choosing a map size") 中实现。
+
+```c
+static uintptr_t __init best_map_size(phys_addr_t pa, uintptr_t va,
+ phys_addr_t size)
+{
+ if (!(pa & (PGDIR_SIZE - 1)) && !(va & (PGDIR_SIZE - 1)) && size >= PGDIR_SIZE)
+ return PGDIR_SIZE;
+
+ if (!(pa & (P4D_SIZE - 1)) && !(va & (P4D_SIZE - 1)) && size >= P4D_SIZE)
+ return P4D_SIZE;
+
+ if (!(pa & (PUD_SIZE - 1)) && !(va & (PUD_SIZE - 1)) && size >= PUD_SIZE)
+ return PUD_SIZE;
+
+ if (!(pa & (PMD_SIZE - 1)) && !(va & (PMD_SIZE - 1)) && size >= PMD_SIZE)
+ return PMD_SIZE;
+
+ return PAGE_SIZE;
+}
+
+```
+
+从这个问题,我们了解到:在调用 `create_pxd_mapping()` 接口设置页表时,需要保证虚拟地址 va 和物理地址 pa 在映射大小 map_size 上对齐。
+
+此邮件意外的展开了关于 `reserved-memory` 节点中设置 "no-map" 属性的内存区域应该如何在 UEFI 中保存的讨论:
+
+根据 DT 规范(devicetree-specification v0.4-rc1 3.5.4 "/reserved-memory and UEFI"),设置 "no-map" 的保留内存存放在 EfiReservedMemoryType,其他类型的保留内存存放在 BootServiceData(会被 OS 在 ExitBootServices 之后回收)。而在 EDK2 (RiscVVirt) 中对于固件内存(mmode_resv0)无视其 "no-map" 属性直接保存到 EfiReservedMemoryType,避免 OS 访问。相关代码及日志体现在:
+
+```c
+// edk2: OvmfPkg/RiscVVirt/Sec/Memory.c :200
+
+MemoryPeimInitialization()
+ Node = fdt_path_offset (FdtPointer, "/reserved-memory/mmode_resv0");
+ MmodeResvBase = fdt64_to_cpu (ReadUnaligned64 (RegProp));
+ MmodeResvSize = fdt64_to_cpu (ReadUnaligned64 (RegProp + 1));
+ InitializeRamRegions ( CurBase, CurSize, MmodeResvBase, MmodeResvSize);
+ AddReservedMemoryBaseSizeHob (MmodeResvBase, MmodeResvSize);
+
+// Linux 日志
+[ 0.000000] efi: 0x000080000000-0x00008003ffff [Reserved | | | | | | | | | | | | | |UC] // EfiReservedMemoryType
+[ 0.000000] memblock_reserve: [0x00000000f8fd1000-0x00000000f8fd1fff] efi_init+0x150/0x26c
+[ 0.000000] memblock_reserve: [0x0000000080200000-0x00000000817fffff] paging_init+0xee/0x5ae
+[ 0.000000] memblock_reserve: [0x00000000f2b61000-0x00000000f6760fff] reserve_initrd_mem+0x9a/0xfc
+[ 0.000000] memblock_reserve: [0x0000000080000000-0x000000008003ffff] early_init_fdt_scan_reserved_mem+0x242/0x2c6
+[ 0.000000] OF: reserved mem: 0x0000000080000000..0x000000008003ffff (256 KiB) map non-reusable mmode_resv0@80000000
+```
+
+但是在那些遵守 DT 标准的 UEFI 固件(比如 U-Boot)中,对于没有设置 "no-map" 属性的固件内存,则保留在 BootServiceData 区域,会被 OS 回收并映射,这样就会导致与休眠 panic 类似的问题。那么只能在 OpenSBI 中将固件内存设置为 "no-map",正如 Atish Patra 所述:
+
+```
+Let's have a no-map set for the reserved memory set for the firmware.
+The fallout would be anybody with kernel > 6.4 has to upgrade the firmware version that sets the no-map correctly
+if they care about hibernation or EFI booting.
+
+OpenSBI v1.3 is planned this month anyway.
+We can communicate the same to the rust-sbi project as well.
+```
+
+在我写这篇文章的时候,这个补丁已经提交了,参考 [platform/lib: Set no-map attribute on all PMP regions][8]。
+
+## 小结
+
+本文首先介绍了 RISC-V MMU 的地址翻译过程,并分析了 `create_pgd_mapping()` 接口是如何创建页表,之后介绍了 RISC-V Linux v6.4-rc1 大页补丁的实现,并对该补丁引发的两个 panic 进行分析,这里做个总结。
+
+在 OpenSBI v8.0 之后的固件为提升 TLB 性能默认不为固件内存设置 "no-map"属性,使得 OS 可以映射固件内存,Linux 大页补丁调整了物理内存发现的下限,使得固件内存映射到了线性地址空间,而休眠过程对其进行拷贝时发生 access-fault。内核目前在 commit (ed309ce52218 "RISC-V: mark hibernation as nonportable") 中以 `NONPORTABLE` 选项在没有为固件内存设置 "no-map" 属性的 OpenSBI 中临时关闭休眠功能。
+
+Linux 大页补丁在调用 `create_pgd_mapping()` 接口设置页表时,计算映射大小没有考虑虚拟地址对齐,进而产生了虚拟地址空洞,使得对空洞的访问触发 page fault,从而导致 UEFI 启动失败。此问题在 riscv/fixes commit (49a0a3731596 "riscv: Check the virtual alignment before choosing a map size") 中解决。
+
+第二个问题,同时引发了另外一个潜在问题:在没有为固件内存设置 "no-map"属性的 OpenSBI 并且遵守 DT 规范(/reserved-memory and UEFI)的固件环境中,OS 会映射固件内存,从而导致与休眠 panic 相似的问题。此问题最终推动 OpenSBI 恢复对固件内存设置 "no-map" 属性,预计在 OpenSBI v1.3 可以看到。
+
+如果你使用 RISC-V Linux v6.4-rc1 及其之后的内核版本,并遇到与上述两个问题,可降级到 OpenSBI 到 v0.8 之前的版本或者采用这个[补丁][8]对你的 OpenSBI 进行升级。
+
+## 参考资料
+
+- [RISC-V 休眠实现分析][2]
+- [RISC-V Linux 内核 UEFI 启动过程分析][6]
+- [Bug report: kernel paniced when system hibernates][3]
+- [Bug report: kernel paniced while booting with UEFI][7]
+
+[1]: https://lore.kernel.org/r/20230324155421.271544-4-alexghiti@rivosinc.com
+[2]: https://gitee.com/tinylab/riscv-linux/pulls/694
+[3]: https://lore.kernel.org/linux-riscv/CAAYs2=gQvkhTeioMmqRDVGjdtNF_vhB+vm_1dHJxPNi75YDQ_Q@mail.gmail.com/
+[4]: https://lore.kernel.org/linux-riscv/CAAYs2=jEPQLwe83UDVFStLuei4C+8ZuHJ98_J13RhobpjkGBVw@mail.gmail.com/
+[5]: https://lore.kernel.org/linux-kernel/20230530080425.18612-1-alexghiti@rivosinc.com/
+[6]: https://gitee.com/tinylab/riscv-linux/pulls/660
+[7]: https://lore.kernel.org/linux-riscv/tencent_7C3B580B47C1B17C16488EC1@qq.com/
+[8]: https://github.com/riscv-software-src/opensbi/commit/8153b2622b08802cc542f30a1fcba407a5667ab9
diff --git a/articles/images/riscv-linear-mapping/sv57_address_trans.png b/articles/images/riscv-linear-mapping/sv57_address_trans.png
new file mode 100644
index 0000000000000000000000000000000000000000..1dbc0bcf63f89af983a10658ecb0755f07b96377
Binary files /dev/null and b/articles/images/riscv-linear-mapping/sv57_address_trans.png differ