从单片机到操作系统学习笔记

前言

之前学习了二进制,最近打算向单片机方向进军,于是学着学着莫名其妙学到了系统安全上,顺手写了个操作系统,现在系统学到了kmalloc,想着写点笔记记录一下学习过程。

前置知识:

  • asm汇编基础
  • C语言基础
  • rust基础

一、从单片机开始

我使用的单片机是经典的STM32F103C8T6,俗称蓝丁板(或蓝药丸),它的主频内置64KB闪存和20KB SRAM,使用STLink烧写。

1.1 点个灯吧

作为单片机界的“Hello World”,点亮一个LED灯是最基础的入门项目。但是我不想用教程自带的Kill9,(加上我是MacOS环境),打算用rust+vscode来完成这个任务。首先是亿点环境配置,这个根据网上的教程就可以搞定了,主要是安装rustup,然后添加thumbv7m-none-eabi的编译目标:

rustup target add thumbv7m-none-eabi

请注意是要用rustup安装的rust,而不是brew安装的,否则会出现找不到target的问题

然后准备一个新项目

cargo new --bin led_blink
cd led_blink

下一步我们要改造这个项目让其可以跑在单片机上,具体来说要准备这些东西:

.
|- Cargo.toml
|- build.rs
|- memory.x
|- src
|  |- main.rs
|- svd
|  |- STM32F103.svd
|- .cargo
|  |- config.toml
|- .vscode
|  |- launch.json
  1. Cargo.toml:项目配置文件

一个个说,第一个是Cargo.toml,这个文件是rust项目的配置文件,我们需要在里面添加和去除一些依赖和配置:

[package]
name = "rusty-blink"
version = "0.1.0"
edition = "2024"

[dependencies]
embedded-hal = "1.0.0"
nb = "1"
cortex-m = "0.7.7"
cortex-m-rt = "0.7.5"
# Panic behaviour, see https://crates.io/keywords/panic-impl for alternatives
panic-halt = "1.0.0"

[dependencies.stm32f1xx-hal]
version = "0.11.0"
features = ["stm32f103", "medium"]

总之就是一些单片机的特有依赖

  1. build.rs:构建脚本

这个文件是rust的构建脚本,我们需要它来告诉编译器使用我们的内存布局文件memory.x

use std::env;
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

fn main() {
    // Put `memory.x` in our output directory and ensure it's
    // on the linker search path.
    let out = &PathBuf::from(env::var_os("OUT_DIR").unwrap());
    File::create(out.join("memory.x"))
        .unwrap()
        .write_all(include_bytes!("memory.x"))
        .unwrap();
    println!("cargo:rustc-link-search={}", out.display());

    // By default, Cargo will re-run this build script whenever
    // any file in the project changes. If we aren't changing
    // `memory.x`, we can tell Cargo to only re-run if
    // `memory.x` changes.
    println!("cargo:rerun-if-changed=memory.x");
}
  1. memory.x:内存布局文件

这个文件定义了单片机的内存布局,告诉链接器如何分配内存:

MEMORY
{
  FLASH : ORIGIN = 0x08000000, LENGTH = 64K
  RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
  1. src/main.rs:主程序文件
    这是我们的主程序文件,我们将在这里编写点亮LED的代码:
#![no_std]
#![no_main]

use nb::block;
use panic_halt as _; // 必须的 panic handler

use cortex_m_rt::entry;
use stm32f1xx_hal::{pac, prelude::*, timer::Timer};

#[entry]
fn main() -> ! {
    // Get access to the core peripherals from the cortex-m crate
    let cp = cortex_m::Peripherals::take().unwrap();
    // Get access to the device specific peripherals from the peripheral access crate
    let dp = pac::Peripherals::take().unwrap();

    let mut rcc = dp.RCC.constrain();

    // Acquire the GPIOC peripheral
    let mut gpioc = dp.GPIOC.split(&mut rcc);

    // Configure gpio C pin 13 as a push-pull output. The `crh` register is passed to the function
    // in order to configure the port. For pins 0-7, crl should be passed instead.
    let mut led = gpioc.pc13.into_push_pull_output(&mut gpioc.crh);
    // Configure the syst timer to trigger an update every second
    let mut timer = Timer::syst(cp.SYST, &rcc.clocks).counter_hz();
    timer.start(1.Hz()).unwrap();

    // Wait for the timer to trigger an update and change the state of the LED
    loop {
        block!(timer.wait()).unwrap();
        led.set_high();
        block!(timer.wait()).unwrap();
        led.set_low();
    }
}
  1. svd/STM32F103.svd:设备描述文件
    这个文件是单片机的设备描述文件,包含寄存器和外设的信息,可以从这里下载。

  2. .cargo/config.toml:Cargo配置文件
    这个文件配置了编译器的选项,告诉它使用我们的构建脚本和内存布局文件:

[target.thumbv7m-none-eabi]
runner = "probe-rs run --chip STM32F103C8"
rustflags = ["-C", "link-arg=-Tlink.x"]

[build]
target = "thumbv7m-none-eabi"
  1. .vscode/launch.json:VSCode调试配置文件
    这个文件配置了VSCode的调试选项,方便我们在IDE中调试
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Cortex Debug (OpenOCD)",
      "type": "cortex-debug",
      "request": "launch",
      "servertype": "openocd",
      "cwd": "${workspaceFolder}",
      "executable": "target/thumbv7m-none-eabi/debug/rusty-blink",
      "device": "STM32F103C8",
      "interface": "swd",
      "runToEntryPoint": "main",
      "configFiles": [
        "interface/stlink.cfg",
        "target/stm32f1x.cfg"
      ],
      "svdFile": "${workspaceFolder}/svd/STM32F103.svd",
      "postLaunchCommands": [
        "set output-radix 16"
      ]
    }
  ]
}

处理好后,我们就可以编译并烧录程序了:

cargo build
cargo run

理论上就可以看到LED灯开始一闪一闪了(=゚ω゚)ノ

恭喜你达成成就:点灯!

1.2 让我们来写汇编吧

rust的确很好用,但是不懂汇编就永远会被高级语言束缚住,想要真正成为第一调用者,必须用汇编语言来和硬件打交道。

实际上汇编没有想象中难,真正难和无聊的部分编译器早就帮我们处理好了,我们只要知道在某些关键地方如何接管汇编即可。换言之:不需要我们从头写一个完整的汇编程序,只需要在关键地方接管汇编代码即可。

新开一个项目

mkdir asm_blink
cd asm_blink

然后我们需要准备这些文件:

.
├── firmware.elf
├── linker.ld
├── run.sh
├── startup.o
├── startup.s
└── svd
    └── STM32F103.svd
  1. linker.ld:链接脚本
    这个文件定义了内存布局,告诉链接器如何分配内存:
MEMORY
{
    FLASH (rx) : ORIGIN = 0x08000000, LENGTH = 64K
    RAM   (rwx): ORIGIN = 0x20000000, LENGTH = 20K
}

_stack_top = ORIGIN(RAM) + LENGTH(RAM);

SECTIONS
{
    .isr_vector : { KEEP(*(.isr_vector)) } > FLASH

    .text : { *(.text*) } > FLASH

    .user_elf :
    {
        __user_text_load = LOADADDR(.user_elf);
        PROVIDE(__user_text_start = ADDR(.user_elf));
        PROVIDE(__user_text_end = .);
        *(.user_elf*)
    } > RAM AT > FLASH

    .user_data :
    {
        __user_data_load = LOADADDR(.user_data);
        PROVIDE(__user_data_start = ADDR(.user_data));
        PROVIDE(__user_data_end = .);
        *(.user_data*)
    } > RAM AT > FLASH
}

这个文件分为两部分,第一部分是定义内存区域,第二部分是定义各个段的布局。

因为还记得最开始我们说的:

我使用的单片机是经典的STM32F103C8T6,俗称蓝丁板(或蓝药丸),它的主频内置64KB闪存和20KB SRAM,使用STLink烧写。

所以我们在MEMORY部分定义了FLASHRAM两个区域,分别对应单片机的闪存和SRAM。

上面的MEMORY比较好理解,大概就是:把这些东西烧录到对应位置即可。

下面的SECTIONS部分定义了各个段的布局,但是暂时不用细究,后面会讲到。

如果你对二进制比较熟悉,可以点开看看:

点击查看详细内容

本质上,ELF文件是由多个段(section)组成的,每个段包含不同类型的数据或代码。链接脚本通过定义这些段的布局,告诉链接器如何将它们放置在内存中。

经典的比方说.data.text.bss等段,分别存放已初始化的数据、代码和未初始化的数据。

但其实这些名字只是约定俗成的,链接脚本可以定义任意名称的段,并指定它们的属性和位置。比方说我们在打二进制攻防和二进制补丁时候,会直接增加一个段.fox_code,用来存放我们自己的代码。这里的.user_elf.user_data段就是类似的概念。

至于里面乱七八糟的LOADADDRADDRPROVIDE等指令,都是链接脚本的语法,用来定义符号和地址。具体可以参考GNU链接器脚本文档

不用管,因为我们在下一章就要手写他们了。

  1. startup.s:启动汇编代码

这个文件包含了启动代码,负责初始化系统并跳转到主程序:

.syntax unified
.cpu cortex-m3
.thumb

.section .isr_vector, "a", %progbits
.global _stack_top
.global Reset_Handler
.global SVC_Handler
.global HardFault_Handler
 
_stack_top = 0x20005000 + 0x1000  // 栈顶地址
.word _stack_top
.word Reset_Handler
.word 0
.word HardFault_Handler
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler

// 默认的 HardFault 处理器,防止掉入未知状态
.type HardFault_Handler, %function
HardFault_Handler:
    // 保存当前堆栈指针
    mrs r0, msp               // 获取 MSP
    push {r0-r3, lr}          // 保存寄存器

    // 打印出故障信息(或使用调试器查看)
    ldr r1, =0xE000ED2C       // HFSR 地址
    ldr r1, [r1]              // 读取 HFSR
    ldr r2, =0xE000ED28       // CFSR 地址
    ldr r2, [r2]              // 读取 CFSR
    ldr r3, =0xE000ED34       // BFAR 地址
    ldr r3, [r3]              // 读取 BFAR

    // 这里可以把 r1, r2, r3 打印出来,或者在调试器中查看它们的值

    // 持续停留在这里,防止程序继续执行
    b .

// 硬件复位入口
.section .text.Reset_Handler
.thumb_func
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
// 上面的别改//
// ======================================//

    // 初始化内核堆栈指针
    ldr r0, =_stack_top
    msr msp, r0
    // 调用加载用户 elf 到 RAM 并退出
    svc #1
    // 永远循环,防止程序继续执行
    b .



// 特权调用
.section .text.SVC_Handler
.thumb_func
.global SVC_Handler
.type SVC_Handler, %function
SVC_Handler:
    // 检查调用号
    // 0. 保留
    // 1. 初始化后进入非特权态
    // 2. 把r0设置为0x12345678
    tst lr, #4              // 检查EXC_RETURN的第2位,确定使用主栈还是进程栈
    ite eq
    mrseq r0, msp          // 如果为0,使用主栈指针
    mrsne r0, psp          // 如果为1,使用进程栈指针
    ldr r1, [r0, #24]      // 获取SVC指令地址
    ldrb r1, [r1, #-2]     // 获取SVC调用号
    cmp r1, #1
    beq load_user_elf
    cmp r1, #2
    beq svc_set_r0_value
    bx lr                  // 异常返回,理论上不会到达这里

.global load_user_elf
load_user_elf:
    // 初始化后切换到非特权态
    // 用户代码区域 0x20000000 - 0x20001FFF (8KB)
    // 用户data区域 0x20002000 - 0x20003FFF (4KB)
    // 用户栈区域 0x20004000 - 0x20004FFF (4KB)
    // 内核栈区域 0x20005000 - 0x20005FFF (4KB)

    // 1. 拷贝Flash中__user_text_start地址的代码到用户代码区域
    ldr r2, =__user_text_start
    ldr r3, =0x20000000          // 用户代码区起始
    ldr r4, =__user_text_end
    subs r4, r2                  // 计算代码大小
1:  // 循环拷贝
    cmp r4, #0
    beq 2f
    ldrb r5, [r2], #1
    strb r5, [r3], #1
    subs r4, #1
    b 1b
2:  // 拷贝Flash中__user_data_start地址的数据到用户数据区域
    ldr r2, =__user_data_start
    ldr r3, =0x20002000          // 用户数据区起始
    ldr r4, =__user_data_end
    subs r4, r2                  // 计算数据大小
3:  // 循环拷贝
    cmp r4, #0
    beq 4f
    ldrb r5, [r2], #1
    strb r5, [r3], #1
    subs r4, #1
    b 3b
4:  // 设置进程栈指针(PSP)到用户栈区域顶端
    ldr r0, =0x20005000          // 内核栈底
    sub r0, #0x1000              // 用户栈顶
    msr psp, r0
    // 伪造调用结构体
    movs r0, #0                  // xPSR
    ldr r1, =user_mode_entry     // PC
    movs r2, #0                  // LR
    movs r3, #0                  // R0
    push {r0-r3}                 // 压入栈
    // 切换到进程栈并且非特权态
    movs r0, #0x03               // 非特权态
    msr control, r0
    isb                          // 指令同步屏障
    // 异常返回,跳转到用户代码
    movs r0, #0xFFFFFFFD        // EXC_RETURN: 使用PSP,返回线程模式
    bx r0

.global svc_set_r0_value
svc_set_r0_value:
    // 2. 把r0设置为0x12345678,模拟特权指令调用
    movs r0, #0x78
    lsl r0, r0, #8
    movs r1, #0x56
    lsl r1, r1, #8
    orrs r0, r0, r1
    movs r1, #0x34
    lsl r1, r1, #8
    orrs r0, r0, r1
    movs r1, #0x12
    orrs r0, r0, r1
    bx lr                  // 异常返回

// 将要被加载的用户代码
.section .user_elf, "a", %progbits
.global __user_text_start
.global __user_text_end
.global __user_data_start
.global __user_data_end
.global user_mode_entry
__user_text_start:
user_mode_entry = __user_text_start
    // 加载数据段的值到r2
    ldr r2, =__user_data_start 
    ldr r2, [r2]
    // 特权调用SVC指令,设置r0为0x12345678
    svc #2
    // 永远循环
1:  b 1b
__user_text_end:

__user_data_start:
    .word 0x12345678
__user_data_end:

这里面的代码比较多,我们分块来看。
首先是中断向量表部分:

.syntax unified // 使用统一语法
.cpu cortex-m3  // 指定处理器类型
.thumb          // 使用Thumb指令集

.section .isr_vector, "a", %progbits // 定义中断向量表段
.global _stack_top                   // 定义栈顶符号
.global Reset_Handler                // 定义复位处理器符号
.global SVC_Handler                  // 定义SVC处理器符号
.global HardFault_Handler            // 定义硬件故障处理器符号
 
_stack_top = 0x20005000 + 0x1000     // 栈顶地址
.word _stack_top                     // 初始栈指针
.word Reset_Handler                  // 复位处理器地址
.word 0
.word HardFault_Handler              // 硬件故障处理器地址
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word 0
.word SVC_Handler                  // SVC处理器地址

很多,很眼花撩乱,但是这一部分不需要动,我们直接照搬即可。

因为单片机的逻辑很简单:上电,复位,然后硬件自动把固定位置作为向量表放入(即我们定义的中断向量表),PC指向复位处理器(Reset_Handler,实际上不一定叫这个名字,准确来说是向量表中复位处理器的地址,即第12位)地址,然后开始执行。其他放了0的地方是我们暂时不需要处理的中断。

然后接下来就能看懂了,这里是HardFault_Handler,即硬件故障处理器,发生对应中断时会跳转到这里执行。这个处理器的用处目前只有一个:持续停留在这里,防止程序继续执行。

// 默认的 HardFault 处理器,防止掉入未知状态
.type HardFault_Handler, %function
HardFault_Handler:
    // 保存当前堆栈指针
    mrs r0, msp               // 获取 MSP
    push {r0-r3, lr}          // 保存寄存器

    // 打印出故障信息(或使用调试器查看)
    ldr r1, =0xE000ED2C       // HFSR 地址
    ldr r1, [r1]              // 读取 HFSR
    ldr r2, =0xE000ED28       // CFSR 地址
    ldr r2, [r2]              // 读取 CFSR
    ldr r3, =0xE000ED34       // BFAR 地址
    ldr r3, [r3]              // 读取 BFAR

    // 这里可以把 r1, r2, r3 打印出来,或者在调试器中查看它们的值

    // 持续停留在这里,防止程序继续执行
    b .

接下来是复位处理器Reset_Handler,这个处理器在单片机上电或复位时会被调用。它的主要任务是初始化系统,人话就是:真第一个运行的代码。

标准一大堆建议别改的地方别改,我们照搬即可,反正后面都是自动生成,下面是我们要改的部分:
(其实硬要理解也不是不行,但是这里有点八股文了,理解了也对主要逻辑没太大帮助)

// 硬件复位入口
.section .text.Reset_Handler
.thumb_func
.global Reset_Handler
.type Reset_Handler, %function
Reset_Handler:
// 上面的别改//
// ======================================//

    // 初始化内核堆栈指针
    ldr r0, =_stack_top
    msr msp, r0
    // 永远循环,防止程序继续执行
    b .

然后我们编译执行
编译命令:
1.汇编 → 目标文件 .o

arm-none-eabi-as -mcpu=cortex-m3 -mthumb -g startup.s -o startup.o

说明:
- g → 生成调试符号,GDB 能识别 Reset_Handler 等符号

  • mcpu=cortex-m3 -mthumb → 针对蓝药丸 CPU
  1. 链接 → 可执行 ELF 文件
arm-none-eabi-gcc -mcpu=cortex-m3 -mthumb -g -nostdlib -T linker.ld startup.o -o firmware.elf

说明:

  • nostdlib → 不要 libc/CRT,完全裸机
  • T linker.ld → 使用你写的链接脚本
    输出 ELF 文件:firmware.elf,可供 GDB 调试或烧录
  1. 烧录
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg \
  -c "adapter speed 100" \
  -c "init" \
  -c "reset halt" \
  -c "flash erase_address 0x08000000 0x10000" \
  -c "program firmware.elf verify reset exit"

理论上在烧录完成后,单片机会运行我们写的汇编代码,你可以使用GDB连接调试断点

1.3 让我们来玩特权异常吧

在上一章我们提到了SVC异常处理器SVC_Handler,这个处理器用于处理特权调用(Supervisor Call),即用户代码请求内核执行特权操作。

这个异常实际上是通过svc指令来引起的,只要在代码中执行svc #n,就会触发SVC异常,cpu就会保存现场,跳转到SVC_Handler处理器执行对应的操作。

此时就有小伙伴DNA动了:这怎么感觉那么像Liunx的syscall呢?其实并非,硬要说的话general idea是类似的,但是实现细节完全不同。Liunx的syscall是通过软中断指令int 0x80syscall来实现的,而SVC是ARM架构特有的指令。

然后在SVC_Handler中,我们可以根据传入的参数来执行不同的特权操作。

.global load_user_elf
load_user_elf:
    // 初始化后切换到非特权态
    // 用户代码区域 0x20000000 - 0x20001FFF (8KB)
    // 用户data区域 0x20002000 - 0x20003FFF (4KB)
    // 用户栈区域 0x20004000 - 0x20004FFF (4KB)
    // 内核栈区域 0x20005000 - 0x20005FFF (4KB)

    // 1. 拷贝Flash中__user_text_start地址的代码到用户代码区域
    ldr r2, =__user_text_start
    ldr r3, =0x20000000          // 用户代码区起始
    ldr r4, =__user_text_end
    subs r4, r2                  // 计算代码大小
1:  // 循环拷贝
    cmp r4, #0
    beq 2f
    ldrb r5, [r2], #1
    strb r5, [r3], #1
    subs r4, #1
    b 1b
2:  // 拷贝Flash中__user_data_start地址的数据到用户数据区域
    ldr r2, =__user_data_start
    ldr r3, =0x20002000          // 用户数据区起始
    ldr r4, =__user_data_end
    subs r4, r2                  // 计算数据大小
3:  // 循环拷贝
    cmp r4, #0
    beq 4f
    ldrb r5, [r2], #1
    strb r5, [r3], #1
    subs r4, #1
    b 3b
4:  // 设置进程栈指针(PSP)到用户栈区域顶端
    ldr r0, =0x20005000          // 内核栈底
    sub r0, #0x1000              // 用户栈顶
    msr psp, r0
    // 伪造调用结构体
    movs r0, #0                  // xPSR
    ldr r1, =user_mode_entry     // PC
    movs r2, #0                  // LR
    movs r3, #0                  // R0
    push {r0-r3}                 // 压入栈
    // 切换到进程栈并且非特权态
    movs r0, #0x03               // 非特权态
    msr control, r0
    isb                          // 指令同步屏障
    // 异常返回,跳转到用户代码
    movs r0, #0xFFFFFFFD        // EXC_RETURN: 使用PSP,返回线程模式
    bx r0

.global svc_set_r0_value
svc_set_r0_value:
    // 2. 把r0设置为0x12345678,模拟特权指令调用
    movs r0, #0x78
    lsl r0, r0, #8
    movs r1, #0x56
    lsl r1, r1, #8
    orrs r0, r0, r1
    movs r1, #0x34
    lsl r1, r1, #8
    orrs r0, r0, r1
    movs r1, #0x12
    orrs r0, r0, r1
    bx lr                  // 异常返回

在上面的代码中,我们定义了两个特权操作:

  1. load_user_elf:加载用户代码和数据到指定内存区域
  2. svc_set_r0_value:将寄存器r0设置为特定值0x12345678

在用户代码中,我们可以通过执行svc #1来调用load_user_elf,通过执行svc #2来调用svc_set_r0_value

同样的,要进入用户态,我们需要伪造一个调用栈,并切换到进程栈指针(PSP)和非特权态(control寄存器设置为3),然后通过异常返回(EXC_RETURN)跳转到用户代码执行。

这里解答一下可能会蒙的问题:

  1. 什么是特权态和非特权态?
    从cpu视角下,本质上就是允不允许访问某些特定寄存器或执行某些特权指令。特权态可以访问所有资源,而非特权态则受到限制。

  2. 什么是进程栈指针(PSP)和主栈指针(MSP)?
    MSP是系统默认使用的栈指针,通常用于内核态和中断处理。而PSP则是为用户态设计的栈指针,允许用户代码使用独立的栈空间。

  3. 为什么要伪造调用栈而不是直接跳转?
    因为你直接跳转过去不还是特权吗,跳了个寂寞(笑)。本质上要通过cpu定义的异常返回机制(EXC_RETURN)来切换到用户态,这个机制会自动恢复寄存器状态,包括程序计数器(PC)和栈指针(PSP)。那么栈哪里来呢,当然是伪造一个放在用户栈空间里咯。(第一次进入用户态,后面用真的即可),返回地址就填用户程序入口地址,异常返回时候自然执行用户程序。

题外话:
其实进入内核态也是类似的,cpu自动保存上下文,但是你在自己栈上其实可以伪造一个上下文,然后通过异常返回跳转到内核态执行特权代码,达到利用目的。恭喜你发明了内核pwn的基本原理。

你可能会想,为什么理论上内核态代码不能被用户态代码利用呢?答案是:设计不当。只要设计不当,内核态代码就有可能被用户态代码利用。

还有例外情况:有没有想过ELF标准内存模型(忘了的我贴个图)为什么不能直接跳转到内核代码执行呢,打过pwn的都知道:直接跳到内核代码执行会直接被kill掉,但是从现在这个角度来看,理论上来说都在一个内存,只要我不利用某些特权代码,我就可以执行吧。。。当然,在当前的设计下是可能的,但是下一章我们会看到,现代操作系统通过内存隔离等机制来防止这种情况发生。
image

回到正题:
调用特权异常后,要经过分流,根据调用的栈类型和调用号分流,或者换一个更熟悉的说法:“门结构”进行分类跳转。

恭喜你解锁了成就:特权异常!

理论上你可以把剩下的异常全部写个遍,然后写一个简单的内存分配器,加载用户代码就是最早期的操作系统了。ψ(`∇´)ψ

二、从单片机到操作系统

在上一章我们写了特权异常,但是有一个问题我们没有解决一个问题:如果用户可以直接访问到内核代码,尽管调用不了某些寄存器,但是依然可以通过特权异常利用内核代码,而且可以直接刷写内存(非特权),以及读写IO端口(非特权),这岂不是很危险?

第一个想法:有没有什么办法让内核只读不可执行呢?

有的,有些cpu自带了一种叫做XN(eXecute Never)的机制,可以标记某些内存区域为不可执行,或者只读等权限,从而防止代码在这些区域执行。

你开开心心的去试了,结果发现开不起来,对的,因为STM32F103C8T6并不支持XN机制,只有一些高端的ARM处理器才支持这个特性。

咱们先不考虑硬件支持,从理论分析,哪怕你把内核区域进入用户态设置为不可执行,也没什么卵用,因为用户态代码依然可以通过特权异常利用内核代码,毕竟内核代码是可读的嘛,只是限制了任意POR,但是本身就能读啊,为什么要POR?而且内核你始终要处理异常调用,不可能一直阻止用户态代码访问内核态代码。

第二个想法:内存隔离

那有没有什么办法能让用户态代码完全访问不到内核态代码呢?
有的,这就是内存隔离(Memory Isolation),为了实现内存隔离,我们需要硬件和软件的配合。

硬件方面,我们需要MMU(Memory Management Unit,内存管理单元)来实现内存隔离。

当你开开心心去搜索符合要求的开发版,发现都要大几百,甚至上千,实在是太贵了,有没有什么开发版是支持MMU,而且便宜或者已经有了,而且很熟悉且方便调试的呢?

有的,而且已经在你手上了,对的,看看你手上正在碰着什么,你的电脑!通过系统虚拟化技术,我们可以在电脑上模拟一个支持MMU的所有硬件环境,然后在这个环境中运行我们的操作系统。

那么用什么硬件模拟器呢?答案是QEMU。vmware和VirtualBox也行,但是QEMU更灵活,而且本身支持很多架构,包括ARM,x86等。而且本体轻量和支持gdb调试。

在这里我选择了armv8aarch64)架构,因为它支持64位,功能更强大,也更接近现代处理器的架构,而且它的页表结构也比较简单,适合我们学习。

三、QEMU?启动!

这里需要科普一下qume的模拟,它分为两种模式:

  1. 用户模式模拟(User Mode Emulation):这种模式下,QEMU只模拟用户空间的应用程序,不涉及内核态代码。适合运行单个应用程序。
  2. 系统模式模拟(System Mode Emulation):这种模式下,QEMU模拟整个系统,包括CPU、内存、外设等,可以运行完整的操作系统。适合操作系统开发和调试。

在系统模拟中,又分为三种启动方式:

  • BIOS启动:通过模拟BIOS固件来引导系统,类似传统PC的启动方式。
  • UEFI启动:通过模拟UEFI固件来引导系统,适合现代操作系统。
  • Kernel直接启动:直接加载内核映像文件启动,适合嵌入式系统和自定义内核。

很显然我们不想用BIOS和UEFI启动,因为我们要自己写内核,所以我们选择Kernel直接启动。

我们的启动命令定为:

exec qemu-system-aarch64 \
    -M virt \
    -cpu cortex-a53 \
    -nographic \
    -kernel kernel8.img \
    -m 512M \
    -S \
    -gdb tcp::1234 \
    -serial mon:stdio

说明:

  • -M virt:指定模拟的机器类型为virt,这是QEMU提供的一个通用虚拟平台,适合运行各种操作系统。
  • -cpu cortex-a53:指定模拟的CPU类型为cortex-a53,这是ARMv8架构的CPU,支持64位和MMU。
  • -nographic:禁用图形输出,所有输出都通过串口进行,适合命令行操作。
  • -kernel kernel8.img:指定要加载的内核映像文件,这里我们假设内核文件名为kernel8.img
  • -m 512M:分配512MB内存给虚拟机。
  • -S:启动时暂停CPU,等待GDB连接,方便调试。
  • -gdb tcp::1234:启用GDB服务器,监听TCP端口1234,方便GDB连接调试。
  • -serial mon:stdio:将串口输出重定向到标准输入输出,方便查看日志。

看不懂没关系,反正后面就知道为什么了

3.1 准备项目

现在要写一个真正的操作系统了,全部asm写那你真是超人了,我们还是用rust来写内核吧。但是在关键的地方我们用汇编来接管。

项目结构如下:

.
├── .cargo
│   └── config.toml
├── .vscode
│   |── launch.json
│   └── tasks.json
├── src
│   |── main.rs
│   └── boot.S
├── build.rs
├── aarch64.ld
├── build_img.sh
├── Cargo.toml
├── run_qemu_gdb.sh
  1. Cargo.toml:项目配置文件
[package]
name = "rust_system"
version = "0.1.0"
edition = "2024"

[build-dependencies]
cc = "1.0"  # 添加构建依赖

[dependencies]
fdt = "0.1.5"
linked_list_allocator = "0.10.5"

[profile.dev]
panic = "abort"
opt-level = "s"
debug = true

[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1
debug = true

是不是发现突然干净了很多?因为我们不需要那些单片机的外设驱动了,直接操作内存和页表即可,而且我们将会禁用std库。

  1. build.rs:构建脚本
// build.rs
use std::env;

fn main() {
    println!("cargo:rerun-if-changed=src/boot.S");
    println!("cargo:rerun-if-changed=aarch64.ld");

    let target = env::var("TARGET").unwrap();

    if target.contains("aarch64") {
        // 使用cc crate编译汇编文件
        cc::Build::new()
            .file("src/early/boot.S")
            .target(&target)
            .compile("boot");
    }
}
  1. aarch64.ld:链接脚本(建议别抄,之后会讲)

这个脚本就大有讲究了,后面会详细讲解:

ENTRY(_start)

KERNEL_VIRT_BASE = 0x40080000;
KERNEL_PHYS_BASE = 0x40080000;
VIRT_TO_PHYS_OFFSET = 0;

SECTIONS {
    . = KERNEL_VIRT_BASE;

    _kernel_virt_start = .;

    .kernel_text : ALIGN(0x1000) {
        _kernel_text_start = .;
        *(.text.boot)
        *(.text .text.*)
        _kernel_text_end = .;
    }

    .kernel_rodata : ALIGN(0x1000) {
        _kernel_rodata_start = .;
        *(.rodata .rodata.*)
        _kernel_rodata_end = .;
    }

    .kernel_data : ALIGN(0x1000) {
        _kernel_data_start = .;
        *(.data .data.*)
        _kernel_data_end = .;
    }

    .kernel_bss : ALIGN(0x1000) {
        _kernel_bss_start = .;
        *(.bss .bss.*)
        *(COMMON)
        _kernel_bss_end = .;
    }

    .kernel_stack : ALIGN(0x1000) {
        _kernel_stack_start = .;
        . += 0x4000;
        _kernel_stack_end = .;
    }

    .kernel_usable_regions : ALIGN(0x1000) {
        _kernel_usable_regions_start = .;
        . += 0x1000;
        _kernel_usable_regions_end = .;
    }

    .kernel_reserved_regions : ALIGN(0x1000) {
        _kernel_reserved_regions_start = .;
        . += 0x1000;
        _kernel_reserved_regions_end = .;
    }

    .kernel_pmm_bitmap : ALIGN(0x1000) {
        _kernel_pmm_bitmap_start = .;
        . += 0x4000;
        _kernel_pmm_bitmap_end = .;
    }

    .printk_buf : ALIGN(0x1000) {
        _printk_buf_start = .;
        . += 0x4000;
        _printk_buf_end = .;
    }

    .kernel_mmu_tables : ALIGN(0x1000) {
        _kernel_mmu_tables_start = .;
        . += 0x1000;
        _kernel_mmu_tables_end = .;
    }

    _kernel_virt_end = .;
    _kernel_phys_end = . - VIRT_TO_PHYS_OFFSET;
}
  1. src/boot.S:启动汇编代码(建议别抄,之后会讲)
.section ".text.boot"

// --------------------------------------------------
// 入口点
// --------------------------------------------------
.global _start
_start:
    // 用 x19(callee-saved)保存 fdt_addr
    mov x19, x0
    // 设置异常向量表基址 (VBAR_EL1)
    adrp x0, exception_vector_table
    add x0, x0, :lo12:exception_vector_table
    msr VBAR_EL1, x0

    // 设置栈指针(确保链接脚本中 _kernel_stack_end 已定义)
    ldr x0, =_kernel_stack_end
    mov sp, x0

    // 清零 BSS 段
    ldr x0, =_kernel_bss_start
    ldr x1, =_kernel_bss_end
    mov x2, #0
clear_bss:
    cmp x0, x1
    b.hs clear_done
    str x2, [x0], #8
    b clear_bss
clear_done:
    // 恢复到 x0,作为第一个参数
    mov x0, x19
    // 跳转到 Rust 主 init 函数
    bl init

    // 如果返回,死循环
halt:
    brk #1
    wfi
    b halt


// --------------------------------------------------
// TODO 调试桩:所有未实现异常跳转到这里
// --------------------------------------------------
.global todo_debug
todo_debug:
    // 你可以在这里加 brk、wfi 或其他调试手段
    brk #0xFFFF      // 触发一个高编号 BRK,便于 GDB 识别
    b todo_debug


// --------------------------------------------------
// 异常向量表(必须 2048 字节 = 0x800 对齐)
// ARMv8-A: 16 个 entry,每个 entry 起始地址间隔 0x80
// --------------------------------------------------
.align 11  // 2^11 = 2048 = 0x800

.global exception_vector_table
exception_vector_table:

// ------------------------------------------------------------------
// 1. Current EL with SP0 (MRS/MSR 使用 SP0) — 通常不使用
// ------------------------------------------------------------------
.org exception_vector_table + 0x000
current_el_sp0_sync:
    bl rust_exception_current_sp0_sync
    b .

.org exception_vector_table + 0x080
current_el_sp0_irq:
    b todo_debug

.org exception_vector_table + 0x100
current_el_sp0_fiq:
    b todo_debug

.org exception_vector_table + 0x180
current_el_sp0_serror:
    b todo_debug


// ------------------------------------------------------------------
// 2. Current EL with SPx (使用当前 SP_ELx) ← 这是你 panic 会进来的!
// ------------------------------------------------------------------
.org exception_vector_table + 0x200
current_el_spx_sync:
    mrs x0, ESR_EL1
    mrs x1, FAR_EL1
    bl rust_exception_current_spx_sync   // ← 重点:panic!() 会到这里
    b .

.org exception_vector_table + 0x280
current_el_spx_irq:
    b todo_debug

.org exception_vector_table + 0x300
current_el_spx_fiq:
    b todo_debug

.org exception_vector_table + 0x380
current_el_spx_serror:
    b todo_debug


// ------------------------------------------------------------------
// 3. Lower EL using AArch64 (从 EL0 进入 EL1)
// ------------------------------------------------------------------
.org exception_vector_table + 0x400
lower_el_aarch64_sync:
    b todo_debug

.org exception_vector_table + 0x480
lower_el_aarch64_irq:
    b todo_debug

.org exception_vector_table + 0x500
lower_el_aarch64_fiq:
    b todo_debug

.org exception_vector_table + 0x580
lower_el_aarch64_serror:
    b todo_debug


// ------------------------------------------------------------------
// 4. Lower EL using AArch32 (AArch32 状态,可忽略)
// ------------------------------------------------------------------
.org exception_vector_table + 0x600
lower_el_aarch32_sync:
    b todo_debug

.org exception_vector_table + 0x680
lower_el_aarch32_irq:
    b todo_debug

.org exception_vector_table + 0x700
lower_el_aarch32_fiq:
    b todo_debug

.org exception_vector_table + 0x780
lower_el_aarch32_serror:
    b todo_debug


// 确保总大小为 0x800
.org exception_vector_table + 0x800


// --------------------------------------------------
// 预留的 Rust 异常处理函数(需在 Rust 中实现)
// --------------------------------------------------
// 这些是弱符号,防止链接错误(可选)
.weak rust_exception_current_sp0_sync
rust_exception_current_sp0_sync:
    b todo_debug

.weak rust_exception_current_spx_sync
rust_exception_current_spx_sync:
    b todo_debug
  1. .cargo/config.toml:Cargo 配置文件
[build]
target = "aarch64-unknown-none"
rustflags = [
    "-C", "link-arg=-Taarch64.ld",
    "-C", "linker=rust-lld",
    "-C", "panic=abort",
    "-C", "relocation-model=static"
]

# 可选:为不同配置文件设置不同优化
[profile.dev]
opt-level = "s"  # 优化大小,便于调试

[profile.release]
opt-level = "z"  # 最小化大小
lto = true
codegen-units = 1
debug = true
  1. .vscode/launch.json:VSCode 调试配置
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Debug Kernel (QEMU)",
      "type": "cppdbg",
      "request": "launch",
      "program": "${workspaceFolder}/target/aarch64-unknown-none/debug/rust_system",
      "miDebuggerServerAddress": "localhost:1234",
      "miDebuggerPath": "pwndbg",
      "cwd": "${workspaceFolder}",
      "MIMode": "gdb",
      "stopAtEntry": true,
      "externalConsole": false,
      "preLaunchTask": "Run QEMU (GDB Stub)",
      "setupCommands": [
        {
          "description": "Set output to hexadecimal",
          "text": "set output-radix 16",
          "ignoreFailures": true
        },
        {
          "description": "Set input to hexadecimal (optional)",
          "text": "set input-radix 16",
          "ignoreFailures": true
        }
      ]
    }
  ]
}

注意:

  • "program" 改为你的内核可执行文件路径
  • "miDebuggerPath" 改为你的 GDB 可执行文件路径
  1. .vscode/tasks.json:VSCode 任务配置
{
  "version": "2.0.0",
  "tasks": [
    {
      "label": "Kill_QEMU",
      "type": "shell",
      "command": "pkill -f 'qemu-system-aarch64' || pkill -f 'run_qemu_gdb.sh' || true"
    },
    {
      "label": "Build Kernel",
      "type": "shell",
      "command": "cargo build",
      "group": "build",
      "dependsOn": "Kill_QEMU",
      "problemMatcher": [
        "$rustc"
      ]
    },
    {
      "label": "Pack Image",
      "type": "shell",
      "command": "./build_img.sh",
      "dependsOn": "Build Kernel"
    },
    {
      "label": "Run QEMU (GDB Stub)",
      "type": "shell",
      "command": "./run_qemu_gdb.sh",
      "isBackground": true,
      "dependsOn": "Pack Image",
      "problemMatcher": {
        "pattern": {
          "regexp": "QEMU ready for GDB on port 1234",
          "file": 1,
          "location": 2,
          "message": 3
        },
        "background": {
          "activeOnStart": true,
          "beginsPattern": ".*",
          "endsPattern": "QEMU ready for GDB on port 1234"
        }
      }
    }
  ]
}

因为qume会一直运行,所以我们需要一个任务来杀掉之前的qemu进程,然后再启动新的。

最后我们用一个脚本来启动qemu,在系统最开始没有串口输出时候会卡住

  1. run_qemu_gdb.sh:启动QEMU脚本
#!/bin/bash

# run_qemu_gdb.sh
# 启动 QEMU 并立即声明 GDB stub 就绪,解决无输出导致的 task hang 问题

echo "QEMU ready for GDB on port 1234"

exec qemu-system-aarch64 \
    -M virt \
    -cpu cortex-a53 \
    -nographic \
    -kernel kernel8.img \
    -m 512M \
    -S \
    -gdb tcp::1234 \
    -serial mon:stdio
  1. build_img.sh:构建内核映像脚本
#!/bin/bash

echo "=== 构建 ARMv8 内核 ==="
# 转换为原始二进制
echo "生成内核镜像..."
rust-objcopy \
    -O binary \
    target/aarch64-unknown-none/debug/rust_system \
    kernel8.img

echo ""
echo "=== 构建完成 ==="
echo "内核镜像: kernel8.img"
echo "ELF文件: target/aarch64-unknown-none/debug/rust_system"

3.2 准备启动

先说一下aarch64.ld,我们截取一段

ENTRY(_start)

KERNEL_VIRT_BASE = 0x40080000;
KERNEL_PHYS_BASE = 0x40080000;
VIRT_TO_PHYS_OFFSET = 0;

SECTIONS {
    . = KERNEL_VIRT_BASE;

    _kernel_virt_start = .;

    .kernel_text : ALIGN(0x1000) {
        _kernel_text_start = .;
        *(.text.boot)
        *(.text .text.*)
        _kernel_text_end = .;
    }

    .kernel_usable_regions : ALIGN(0x1000) {
        _kernel_usable_regions_start = .;
        . += 0x1000;
        _kernel_usable_regions_end = .;
    }
}

我们先说整个ld文件是干什么的:

  1. 把段放到该放的地方,比方说.kernel_text就是把所有的代码段放到这里(比方说.text.boot*.text.text.*),然后对齐到0x1000(4KB)边界。在某些情况下预留空间. += 0x1000;意思是预留1页(4KB)空间。

  2. 定义一些符号,比如_kernel_text_start_kernel_text_end,这些符号在代码中可以用来获取对应段的起始和结束地址。

对于第二点,我说一个例子:__malloc_hook__free_hook,这些符号就是在链接脚本中定义的,然后在代码中引用。

简单理解为用符号定位的锚点,方便我们在代码中引用特定的内存地址。

然后我们来写boot.S,这个文件是内核启动的汇编代码,主要任务是确保关键的位置是我们定义的,整个项目要写的汇编80%都在这里了(除了驱动),然后把控制权交给Rust代码。

.section ".text.boot"

// --------------------------------------------------
// 入口点
// --------------------------------------------------
.global _start
_start:
    // 用 x19(callee-saved)保存 fdt_addr
    mov x19, x0
    // 设置异常向量表基址 (VBAR_EL1)
    adrp x0, exception_vector_table
    add x0, x0, :lo12:exception_vector_table
    msr VBAR_EL1, x0

    // 设置栈指针(确保链接脚本中 _kernel_stack_end 已定义)
    ldr x0, =_kernel_stack_end
    mov sp, x0

    // 清零 BSS 段
    ldr x0, =_kernel_bss_start
    ldr x1, =_kernel_bss_end
    mov x2, #0
clear_bss:
    cmp x0, x1
    b.hs clear_done
    str x2, [x0], #8
    b clear_bss
clear_done:
    // 恢复到 x0,作为第一个参数
    mov x0, x19
    // 跳转到 Rust 主 init 函数
    bl init

    // 如果返回,死循环
halt:
    brk #1
    wfi
    b halt


// --------------------------------------------------
// TODO 调试桩:所有未实现异常跳转到这里
// --------------------------------------------------
.global todo_debug
todo_debug:
    // 你可以在这里加 brk、wfi 或其他调试手段
    brk #0xFFFF      // 触发一个高编号 BRK,便于 GDB 识别
    b todo_debug


// --------------------------------------------------
// 异常向量表(必须 2048 字节 = 0x800 对齐)
// ARMv8-A: 16 个 entry,每个 entry 起始地址间隔 0x80
// --------------------------------------------------
.align 11  // 2^11 = 2048 = 0x800

.global exception_vector_table
exception_vector_table:

// ------------------------------------------------------------------
// 1. Current EL with SP0 (MRS/MSR 使用 SP0) — 通常不使用
// ------------------------------------------------------------------
.org exception_vector_table + 0x000
current_el_sp0_sync:
    bl rust_exception_current_sp0_sync
    b .

.org exception_vector_table + 0x080
current_el_sp0_irq:
    b todo_debug

.org exception_vector_table + 0x100
current_el_sp0_fiq:
    b todo_debug

.org exception_vector_table + 0x180
current_el_sp0_serror:
    b todo_debug


// ------------------------------------------------------------------
// 2. Current EL with SPx (使用当前 SP_ELx) ← 这是你 panic 会进来的!
// ------------------------------------------------------------------
.org exception_vector_table + 0x200
current_el_spx_sync:
    mrs x0, ESR_EL1
    mrs x1, FAR_EL1
    bl rust_exception_current_spx_sync   // ← 重点:panic!() 会到这里
    b .

.org exception_vector_table + 0x280
current_el_spx_irq:
    b todo_debug

.org exception_vector_table + 0x300
current_el_spx_fiq:
    b todo_debug

.org exception_vector_table + 0x380
current_el_spx_serror:
    b todo_debug


// ------------------------------------------------------------------
// 3. Lower EL using AArch64 (从 EL0 进入 EL1)
// ------------------------------------------------------------------
.org exception_vector_table + 0x400
lower_el_aarch64_sync:
    b todo_debug

.org exception_vector_table + 0x480
lower_el_aarch64_irq:
    b todo_debug

.org exception_vector_table + 0x500
lower_el_aarch64_fiq:
    b todo_debug

.org exception_vector_table + 0x580
lower_el_aarch64_serror:
    b todo_debug


// ------------------------------------------------------------------
// 4. Lower EL using AArch32 (AArch32 状态,可忽略)
// ------------------------------------------------------------------
.org exception_vector_table + 0x600
lower_el_aarch32_sync:
    b todo_debug

.org exception_vector_table + 0x680
lower_el_aarch32_irq:
    b todo_debug

.org exception_vector_table + 0x700
lower_el_aarch32_fiq:
    b todo_debug

.org exception_vector_table + 0x780
lower_el_aarch32_serror:
    b todo_debug


// 确保总大小为 0x800
.org exception_vector_table + 0x800


// --------------------------------------------------
// 预留的 Rust 异常处理函数(需在 Rust 中实现)
// --------------------------------------------------
// 这些是弱符号,防止链接错误(可选)
.weak rust_exception_current_sp0_sync
rust_exception_current_sp0_sync:
    b todo_debug

.weak rust_exception_current_spx_sync
rust_exception_current_spx_sync:
    b todo_debug

里面重要的部分我已经加了注释,主要是设置异常向量表基址,设置栈指针,清零BSS段,然后跳转到Rust的init函数。

尤其是同步异常和中断异常的处理,我们预留了rust_exception_current_spx_sync函数,这个函数会在Rust中实现,用于处理同步异常(包括panic)。这个可太重要了,接下来你会跑飞无数次,都是会停在这里。

其他的和之前的单片机差不多,最多是一些位置上的差异,比如栈的位置,内存布局等。

有的师傅就要问了:你的特权异常呢?别急,后面会讲,因为我们要实现内存隔离,特权异常的实现会有所不同,之后再定义。(实际上到了这个教程结束才需要考虑这些事情)

3.3 main.rs

和传统的项目不同,我们第一个代码已经在boot.S_start实现了,所以main.rs中的init函数才是内核的真正入口。

#![no_std]
#![no_main]

mod early;
mod kernel;

use core::panic::PanicInfo;

#[unsafe(no_mangle)]
pub extern "C" fn init(fdt_addr: u64) -> ! {
    unsafe {
        panic!("Returned from init_mapping!");
    }
}

// 异常捕获
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    unsafe { core::arch::asm!("brk #1") }
    loop {}
}

// 同步异常
#[unsafe(no_mangle)]
pub extern "C" fn rust_exception_current_spx_sync(esr: u64, far: u64) -> ! {
    let ec = (esr >> 26) & 0x3F;
    unsafe {
        core::arch::asm!("wfi");
        core::arch::asm!("brk #1")
    };
    loop {}
}

这里我们实现了init函数作为内核入口,异常处理函数panic_handlerrust_exception_current_spx_sync函数。

同时我们使用

#![no_std]
#![no_main]

来禁用标准库和main

同时我们发现了一些特殊的函数

#[unsafe(no_mangle)]
pub extern "C" fn
  • #[no_mangle]:告诉编译器不要对函数名进行重命名,这样我们在汇编中就可以通过函数名直接调用它。
  • extern "C":指定函数使用C语言的调用约定,确保函数在汇编和Rust之间正确调用。
  • unsafe:表示这个函数包含不安全代码,需要调用者保证安全性。

好了,主要的准备工作都完成了。

四、从内存开始(PMM)

理论上我们刚刚的操作和单片机上没有任何区别,顶多复杂了一点点,但是我们并没有实现内存隔离。要实现内存隔离,首先我们要知道内存有多少、在哪里、有没有使用。

这个时候就有长得帅的小伙伴跳出来甩出来一张图:

image

这我知道啊,可用的空间不就是总内存 - 内核占用 - 已分配 - 不可用吗?

的确,但是问题是:

  1. 总内存多少?
  2. 内核占用多少?
  3. 已分配多少?
  4. 不可用多少?

对的,就是四个都不知道,哪怕通过汇编符号获取内核占用的地址范围,也不知道总内存是多少,更别说已分配和不可用了。而且,给出的这张内存分布图真的正确吗,我就问一个问题好了,正常的用户程序一般运行在0x0 - 0x0000_7fff_ffff_f000,我们稍微计算一下这个数字是多少GB?

>>> hex(0x0000_7fff_ffff_f000)
'0x7fffffff f000'
>>> 0x0000_7fff_ffff_f000 / (1024**3)
8191.999999761581

对的,8192GB,也就是8TB,很显然这不可能是对的,8TB内存的服务器都少见,更别说普通的电脑了。

哪怕我们指定了-M 512MB,但是开一下调试器或者逆向看看pc最开始应该等于0x40080000,再次计算一下:

>>> hex(0x40080000)
'0x40080000'
>>> 0x40080000 / (1024**3)
1.0073741824

对的,1GB,很显然,这也不对。

可能有师傅注意到了ld定义

KERNEL_VIRT_BASE = 0x40080000;
KERNEL_PHYS_BASE = 0x40080000;
VIRT_TO_PHYS_OFFSET = 0;

是不是这里的问题?不是的,这里只是定义了内核的虚拟地址和物理地址的映射关系,并没有定义总内存大小,而且你可以试试看更换这个数字(比如改成0x80000000),你会发现内核依然可以启动。

但是更多情况是改到一个随机数字启动不起来,这就说明这个数字是有要求的。

说了这么多,到底总内存是多少呢?
答案是:通过设备树获取

4.1 设备树(Device Tree)

设备树是一种数据结构,用于描述硬件设备的布局和配置。它通常以树形结构表示,包含节点和属性,每个节点代表一个硬件设备或组件,属性则描述设备的特性和配置参数。

说人话就是:这里告诉了你系统的内存布局。

4.2 获取设备树

ok,我们回到boot.S,会发现_start函数的第一个参数就是fdt_addr,这个地址就是设备树的地址。
解析设备树已经有现成的函数库了,rust有一个叫fdt的库,可以方便地解析设备树,不必我们手写,这个不是我们重点。

假设你已经解析好了,那么应该怎么存储这些内存区域呢?
很显然我们需要一个数据结构来存储这些内存区域,我们可以定义一个MemoryRegion结构体:

pub struct MemoryRegion {
    pub start: u64,
    pub end: u64,
    pub region_type: MemoryRegionType,
}

然后很自然而然的标注使用区域....对。。对吗?

这个结构体你要放在那里呢?我的意思是说,要把这些内存区域存储在哪里呢?这里涉及到了一个问题:被管理的内存区域不能和管理内存区域重叠,否则会出现问题,而且管理内存区域在被管理区域之中。

有人就想出了一个解决办法是:那我自己在ld中预留一块区域专门存储内存区域不就行了吗?
对的,这也是一种办法,我们可以在ld中预留一块区域,比如:

.kernel_usable_regions : ALIGN(0x1000) {
    _kernel_usable_regions_start = .;
    . += 0x1000;
    _kernel_usable_regions_end = .;
}

然后在代码中使用这些符号来获取这块区域的地址和大小。然后遇到了第二个问题:Vec用不了了(笑)

理论上我们写一个管理类,然后用静态数组来存储内存区域也行,但是这样就失去了动态扩展的能力,总之就是记录好全部内存,全部分配和不可用区域,全部丢进Vec多好,但是很显然我们没有std库,Vec用不了。

那么就拿出C语言功底,手动写一个链表?对不起,链表依赖于malloc/free,我们还没有实现内存分配器呢。

那就静态数组?对不起,静态数组需要预定义大小,万一内存区域太多怎么办?

=========

我给出的解决办法是:

use core::cell::UnsafeCell;
use core::sync::atomic::{AtomicBool, AtomicPtr, Ordering};

unsafe extern "C" {
    fn _kernel_usable_regions_start();
    fn _kernel_usable_regions_end();
    fn _kernel_reserved_regions_start();
    fn _kernel_reserved_regions_end();
    fn _kernel_pmm_bitmap_start();
    fn _kernel_pmm_bitmap_end();
    fn _kernel_virt_start();
    fn _kernel_virt_end();
}

pub static FRAME_ALLOCATOR: AtomicPtr<GlobalPMM> = AtomicPtr::new(core::ptr::null_mut());
pub static PMM_ALLOCATOR: GlobalPMM = GlobalPMM(UnsafeCell::new(PMMManager::empty()));
pub static PMM_ALLOCATOR_INIT: AtomicBool = AtomicBool::new(false);

type PageFrameNumber = u64;
pub struct GlobalPMM(UnsafeCell<PMMManager>);
unsafe impl Sync for GlobalPMM {}
impl GlobalPMM {
    fn get(&self) -> &PMMManager {
        unsafe { &*self.0.get() }
    }
}

pub struct PMMManager {
    pmm_data_addr: u64,         // 物理内存管理数据区起始地址
    page_size: PageFrameNumber, // 总页数(即4K页长度)
    init_no_use: u64,           // 初始内核占用数据地址,不允许分配,最开始为空
    reserve_addr: u64,          // 不允许释放页索引(本质上是 u64)
    reserve_size: u64,          // 不允许释放长度(本质上是 u64)
    base_addr: u64,             // 物理页编号的基地址(第0页对应的物理起始地址)
}

所有的都是静态分配的,然后通过链接脚本预留内存区域,然后通过符号获取地址,而且采用裸地址操作,全局化唯一一个实例,通过AtomicPtr来保证线程安全。

那么接下来一个问题是:怎么知道这些内存区域的数量呢?万一很多不会回到刚刚的问题吗?

请看VCR:


impl PMMManager {
    /// 初始化管理器
    /// - `pmm_data_addr` 物理内存管理数据区起始地址
    /// - `total_pages` 总页数
    /// - `reserve_addr` 不允许释放页索引
    /// - `reserve_size` 不允许释放长度
    /// - `base_addr` 物理页编号的基地址
    fn init(
        &mut self,
        pmm_data_addr: u64,
        total_pages: PageFrameNumber,
        reserve_addr: u64,
        reserve_size: u64,
        base_addr: u64,
    ) {
        self.pmm_data_addr = pmm_data_addr;
        self.page_size = total_pages;
        self.reserve_addr = reserve_addr;
        self.reserve_size = reserve_size;
        self.base_addr = base_addr;
    }

    const fn empty() -> Self {
        Self {
            pmm_data_addr: 0,
            page_size: 0,
            init_no_use: 0,
            reserve_addr: 0,
            reserve_size: 0,
            base_addr: 0,
        }
    }

/// 初始化物理内存管理器
/// 物理内存按 4KB 页面管理
/// 初始化物理内存管理器
pub unsafe fn init_PMM(fdt_addr: u64) {
    if PMM_ALLOCATOR_INIT.load(Ordering::Acquire) {
        return;
    }

    let fdt = unsafe { Fdt::from_ptr(fdt_addr as *const u8).expect("Invalid FDT") };

    // 收集所有物理内存区域(usable_regions)
    let usable_regions = _kernel_usable_regions_start as u64;
    let usable_regions_end = _kernel_usable_regions_end as u64;
    for i in (usable_regions..usable_regions_end).step_by(16) {
        unsafe {
            let ptr = i as *mut u8;
            core::ptr::write_volatile(ptr, 0);
        }
    }
    // unsable_count 记录 usable_regions 中的区域数量
    let mut usable_count = 0;
    for node in fdt.find_all_nodes("/memory") {
        if let Some(reg) = node.reg() {
            for range in reg {
                let start = range.starting_address as u64;
                let end = start.wrapping_add(range.size.unwrap_or(0) as u64);
                if end > start && usable_count < 64 {
                    unsafe {
                        let ptr = usable_regions as *mut (u64, u64);
                        core::ptr::write_volatile(ptr.add(usable_count), (start, end));
                    }
                    usable_count += 1;
                } else if usable_count > 64 {
                    panic!("Too many usable memory regions in FDT");
                } else {
                    printk!(
                        "Ignoring invalid memory region: start=0x{:x}, end=0x{:x}\n",
                        start,
                        end
                    );
                }
            }
        }
    }

    // total_memory_size 记录总内存大小
    let mut total_memory_size = 0u64;
    // memory_base_addr 记录物理内存基地址
    let mut memory_base_addr = 0u64;
    // 遍历 usable_regions,计算总内存大小和基地址
    for i in 0..usable_count {
        let ptr = usable_regions as *const (u64, u64);
        let (start, end) = unsafe { core::ptr::read_volatile(ptr.add(i)) };
        total_memory_size = total_memory_size.wrapping_add(end.wrapping_sub(start));
        if memory_base_addr == 0 || start < memory_base_addr {
            memory_base_addr = start;
        }
    }

    // 收集不可使用的内存区域(reserved_regions)
    let mut reserved_regions = _kernel_reserved_regions_start as u64;
    let reserved_regions_end = _kernel_reserved_regions_end as u64;

    for i in (reserved_regions..reserved_regions_end).step_by(16) {
        unsafe {
            let ptr = i as *mut u8;
            core::ptr::write_volatile(ptr, 0);
        }
    }
    let mut reserved_count = 0;

    for node in fdt.find_all_nodes("/reserved-memory") {
        if let Some(reg) = node.reg() {
            for range in reg {
                let start = range.starting_address as u64;
                let end = start.wrapping_add(range.size.unwrap_or(0) as u64);
                if end > start && reserved_count < 64 {
                    unsafe {
                        let ptr = reserved_regions as *mut (u64, u64);
                        core::ptr::write_volatile(ptr.add(reserved_count), (start, end));
                    }
                    reserved_count += 1;
                } else if reserved_count > 64 {
                    panic!("Too many reserved memory regions in FDT");
                } else {
                    panic!(
                        "Ignoring invalid reserved memory region: start=0x{:x}, end=0x{:x}\n",
                        start, end
                    );
                }
            }
        }
    }

    // 在可用内存区域中寻找一块足够大的区域
    let total_pages = total_memory_size / 0x1000;
    let curr_addr = _kernel_pmm_bitmap_start as u64;
    let end_addr = _kernel_pmm_bitmap_end as u64;
    let mut pmm_data_addr = curr_addr;

    // 标记已使用(BitSet),标记保留区域:
    for i in 0..reserved_count {
        let ptr = reserved_regions as *const (u64, u64);
        let (start, end) = unsafe { core::ptr::read_volatile(ptr.add(i)) };
        let mut addr = start;
        while addr < end {
            let page_index = (addr - memory_base_addr) / 0x1000;
            let byte_index = page_index / 8;
            let bit_index = page_index % 8;
            unsafe {
                let bitset_ptr = (pmm_data_addr + byte_index) as *mut u8;
                let byte = core::ptr::read_volatile(bitset_ptr);
                core::ptr::write_volatile(bitset_ptr, byte | (1 << bit_index));
            }
            addr += 0x1000;
        }
    }
    // 标记内核区域:
    let kernel_start = &_kernel_virt_start as *const _ as u64;
    let kernel_end = &_kernel_virt_end as *const _ as u64;
    let mut addr = kernel_start;
    while addr < kernel_end {
        let page_index = (addr - memory_base_addr) / 0x1000;
        let byte_index = page_index / 8;
        let bit_index = page_index % 8;
        unsafe {
            let bitset_ptr = (pmm_data_addr + byte_index) as *mut u8;
            let byte = core::ptr::read_volatile(bitset_ptr);
            core::ptr::write_volatile(bitset_ptr, byte | (1 << bit_index));
        }
        addr += 0x1000;
    }

    // 初始化 PMM 管理器
    let pmm = unsafe { &mut *PMM_ALLOCATOR.0.get() };
    pmm.init(
        pmm_data_addr,
        0x4000, // 512 MB / 0x1000 = 0x4000 pages
        reserved_regions,
        reserved_regions_end - reserved_regions,
        memory_base_addr,
    );
    PMM_ALLOCATOR_INIT.store(true, Ordering::Release);
    FRAME_ALLOCATOR.store(
        &PMM_ALLOCATOR as *const GlobalPMM as *mut GlobalPMM,
        Ordering::SeqCst,
    );
}

答案是:

  1. 我们先解决总内存的问题,通过解析设备树获取所有的内存区域,然后把这些区域存储到预留的内存区域中。这一块比较小,我们假设最多64个内存区域(够用了),然后通过遍历设备树中的/memory节点,获取所有的内存区域,存储到预留的内存区域中。
  2. 然后保留保留内存区域,这一块也比较小,我们同样假设最多64个保留内存区域,然后通过遍历设备树中的/reserved-memory节点,获取所有的保留内存区域,存储到预留的内存区域中。
  3. 最后我们计算总内存大小和基地址,然后初始化物理内存管理器,使用bitset一个bit存储一页(0x1000 = 4KB),标记已使用的内存区域。

通过这种方式,我们就可以动态地获取内存区域的数量,而不需要预定义一个固定大小的数组。在这里我避免bitset过大,直接假设512MB内存。

对的,解决问题的方法就是这么简单粗暴:我就假设512MB内存,够用就行,如果有多的内存,我启动到一定阶段后再次去获取内存区域,然后动态扩展内存管理器。

总之一通操作下来,我们可以管理512MB内存了。然后我们写一个简单的alloc_pagefree_page函数,就可以分配物理内存页了,此时直接分配一页4KB的内存,回收也是回收一页4KB的内存,不需要子结构,因为整个内存的管理都是基于页的。

    /// 分配一页(0x1000)返回物理地址
    fn alloc_page(&mut self) -> Option<u64> {
        let table = self.pmm_data_addr;
        let total_pages = self.page_size;

        for page_index in 0..total_pages {
            let byte_index = page_index / 8;
            let bit_index = page_index % 8;
            unsafe {
                let bitset_ptr = (table + byte_index) as *mut u8;
                let byte = core::ptr::read_volatile(bitset_ptr);
                if (byte & (1 << bit_index)) == 0 {
                    // 标记为已使用
                    core::ptr::write_volatile(bitset_ptr, byte | (1 << bit_index));
                    let phys_addr = self.base_addr + page_index * 0x1000;
                    // 清空页面内容
                    let page_ptr = phys_addr as *mut u8;
                    for i in 0..0x1000 {
                        core::ptr::write_volatile(page_ptr.add(i), 0);
                    }
                    return Some(phys_addr);
                }
            }
        }
        None
    }

    // 回收一页
    fn free_page(&mut self, phys_addr: u64) {
        if phys_addr == 0 {
            return;
        }
        if self.is_reserved(phys_addr) {
            panic!(
                "Attempt to free a reserved physical page at address 0x{:x}",
                phys_addr
            );
        }
        let page_index = (phys_addr - self.base_addr) / 0x1000;
        let byte_index = page_index / 8;
        let bit_index = page_index % 8;
        unsafe {
            let bitset_ptr = (self.pmm_data_addr + byte_index) as *mut u8;
            let byte = core::ptr::read_volatile(bitset_ptr);
            core::ptr::write_volatile(bitset_ptr, byte & !(1 << bit_index));
        }
    }

这里我选择直接清空内存,不是为了安全,而是如果分配到非法内存会直接炸而不是在后续使用时炸。这里对于内存处理的方法十分简单粗暴,直接错误就panic,因为这个pmm是最底层的,没人能给它错误错处理,不如直接painc出来保证安全。

这样子加上一下辅助方法,我们就完成了一个简单的物理内存管理器(PMM)。

五、打印串口

对着调试器调试,哪怕是PMM这种底层模块,实在是太痛苦了,我们需要一个打印串口的功能。对于qume,我们可以以下驱动来打印:

use core::ptr;

use crate::printk;

/// PL011 UART 基地址(QEMU virt 机器)
const PL011_BASE: *mut u32 = 0x0900_0000 as *mut u32;

// 寄存器偏移(单位:u32,所以除以 4)
const DR: usize = 0x00; // Data Register
const FR: usize = 0x18 / 4; // Flag Register
const IBRD: usize = 0x24 / 4; // Integer Baud Rate Divisor
const FBRD: usize = 0x28 / 4; // Fractional Baud Rate Divisor
const LCR_H: usize = 0x2C / 4; // Line Control Register
const CR: usize = 0x30 / 4; // Control Register
const IFLS: usize = 0x34 / 4; // Interrupt FIFO Level Select (可选)
const IMSC: usize = 0x38 / 4; // Interrupt Mask Set/Clear

/// 初始化 PL011 UART(波特率 115200,8N1)
pub fn init_TTYS() {
    unsafe {
        // 1. 禁用 UART(CR.UARTEN = 0)
        ptr::write_volatile(PL011_BASE.add(CR), 0x0);

        // 2. 设置波特率:115200
        //    公式:BRD = UARTCLK / (16 * BAUD_RATE)
        //    UARTCLK = 24,000,000 Hz (QEMU virt)
        //    BAUD_RATE = 115200
        //    BRD = 24e6 / (16 * 115200) = 13.020833...
        //    IBRD = 13
        //    FBRD = round((0.020833...) * 64) = round(1.333) = 1
        ptr::write_volatile(PL011_BASE.add(IBRD), 13);
        ptr::write_volatile(PL011_BASE.add(FBRD), 1);

        // 3. 设置数据格式:8位、无校验、1停止位 + 使能 FIFO
        //    LCR_H:
        //      WLEN = 0b11 → 8 bits (bits 5:6 = 0x3 << 5 = 0x60)
        //      FEN  = 1    → enable FIFO (bit 4)
        ptr::write_volatile(PL011_BASE.add(LCR_H), (0x3 << 5) | (1 << 4));

        // 4. (可选)设置 FIFO 触发级别
        // ptr::write_volatile(PL011_BASE.add(IFLS), 0x0); // 默认即可

        // 5. 禁用所有中断(调试阶段不需要)
        ptr::write_volatile(PL011_BASE.add(IMSC), 0x0);

        // 6. 使能 UART:TXE, RXE, UARTEN
        //    CR: TXE=1 (bit 8), RXE=1 (bit 9), UARTEN=1 (bit 0)
        ptr::write_volatile(PL011_BASE.add(CR), (1 << 8) | (1 << 9) | 1);
    }
    printk!("Cyber_Kaiyo System 26H1 0.0.1 \n");
    printk!("TTYS (PL011 UART) initialized.\n");
    printk!("Hello, UART!\n");
}

/// 阻塞发送一个字节
pub fn putc(byte: u8) {
    // 等待 TX FIFO 未满:FR.TXFF == 0(bit 5 = 0 表示未满)
    while unsafe { (ptr::read_volatile(PL011_BASE.add(FR)) & (1 << 5)) != 0 } {
        // 可加 cpu_relax(),但早期可空转
    }
    unsafe {
        ptr::write_volatile(PL011_BASE.add(DR), byte as u32);
    }
}

/// 实现 core::fmt::Write 以便使用 println!
pub struct Uart;

impl core::fmt::Write for Uart {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        for c in s.bytes() {
            if c == b'\n' {
                putc(b'\r'); // 将 \n 转为 \r\n,适配终端
            }
            putc(c);
        }
        Ok(())
    }
}

原理写在了说明书上,但是不是我们学习的重点,我们只需要知道怎么用就行了。我们注意它使用到了一个地址:0x0900_0000这个是保留地址,我们在-M virt中指定的QEMU机器中,这个地址是PL011 UART的地址。

然后我们实现一个prink!宏,先放入内核缓冲区,再打印,无论炸不炸都不会丢失调试信息,至于缓冲区怎么来的,答案是ld预留的,这个模块比PMM还要早初始化。

use crate::early::ttys;
use core::cell::UnsafeCell;
use core::fmt;

unsafe extern "C" {
    fn _printk_buf_start();
    fn _printk_buf_end();
}

static PRINTK: GlobalPrintk = GlobalPrintk(UnsafeCell::new(Printk::empty()));
static mut PRINTK_INIT: bool = false;

struct GlobalPrintk(UnsafeCell<Printk>);
unsafe impl Sync for GlobalPrintk {}

struct Printk {
    office: u64,
    start: u64,
    end: u64,
}

impl Printk {
    pub const fn empty() -> Self {
        Self {
            office: 0,
            start: 0,
            end: 0,
        }
    }
    fn init(&mut self) {
        if self.start != 0 && self.end != 0 {
            return;
        }
        self.start = unsafe { _printk_buf_start as u64 };
        self.end = unsafe { _printk_buf_end as u64 };
        self.office = self.start;
    }

    fn write_bytes(&mut self, bytes: &[u8]) {
        if self.start == 0 || self.end <= self.start {
            return;
        }
        let mut ptr = self.office;
        for &b in bytes {
            unsafe {
                core::ptr::write_volatile(ptr as *mut u8, b);
            }
            ptr += 1;
            if ptr >= self.end {
                ptr = self.start;
            }
        }
        self.office = ptr;
    }

    pub fn write(&mut self, msg: &str) {
        self.write_bytes(msg.as_bytes());
    }
}

impl core::fmt::Write for Printk {
    fn write_str(&mut self, s: &str) -> core::fmt::Result {
        self.write_bytes(s.as_bytes());
        Ok(())
    }
}

pub unsafe fn init_printk() {
    if unsafe { PRINTK_INIT } {
        return;
    }
    let p = unsafe { &mut *PRINTK.0.get() };
    p.init();
    unsafe { PRINTK_INIT = true }
}

pub fn printk(msg: &str) {
    if !unsafe { PRINTK_INIT } {
        unsafe { init_printk() }
    }
    let p = unsafe { &mut *PRINTK.0.get() };
    p.write(msg);
}

/// 格式化打印函数
pub fn printk_fmt(args: core::fmt::Arguments) {
    if !unsafe { PRINTK_INIT } {
        unsafe { init_printk() }
    }
    // 直接把格式化输出写入预留的 printk 环形缓冲区(不依赖栈临时缓冲)
    let p = unsafe { &mut *PRINTK.0.get() };
    let buf_start = p.start;
    let buf_end = p.end;
    // 记录写入前位置
    let start_off = p.office;
    let _ = fmt::write(p, args);
    // 写入后位置
    let end_off = p.office;

    // 如果环形缓冲区未初始化或无数据,直接返回
    if buf_start == 0 || buf_end <= buf_start || start_off == end_off {
        return;
    }

    // 从环形缓冲区读出刚写入的数据并发送到 UART(遇 '\n' 先发 '\r')
    let mut addr = start_off;
    while addr != end_off {
        let b = unsafe { core::ptr::read_volatile(addr as *const u8) };
        if b == b'\n' {
            ttys::putc(b'\r');
        }
        ttys::putc(b);
        addr += 1;
        if addr >= buf_end {
            addr = buf_start;
        }
    }
}

#[macro_export]
macro_rules! printk {
    ($($arg:tt)*) => {
        $crate::early::Printk::printk_fmt(core::format_args!($($arg)*))
    };
}

六、初始化

#[unsafe(no_mangle)]
pub extern "C" fn init(fdt_addr: u64) -> ! {
    // 初始化内核堆
    unsafe {
        early::Printk::init_printk();
        early::ttys::init_TTYS();
        early::PMM::init_PMM(fdt_addr);
    }
}

然后我们有了“完整”的内存控制能力,但是很显然这和内存隔离还差得远,我们只能分配物理内存页,还不能进行虚拟内存映射,更别说内存隔离了。

七、启动MMU

这一步就是传说中的MMU,只要放入页表,就能启动MMU了?
对的,理论上是这样的,我们只需要建立页表,然后开启MMU就行了。但是桥豆麻袋,页表怎么建立?建立成什么样子?MMU怎么开启?这些都是问题。

先说一个现实问题,页表本质上是一颗多叉树,理论上你是可以直接为每个页分配一个页表项的,但是这样子的话,页表会非常大,非常浪费内存(最差情况下,你要手动分配512512512),而且效率也不高。并且且还要考虑页表的分配问题,页表本身也是需要内存的。 同时为了让自己别太坐牢,要考虑使用结构体,但是结构体本身也是需要内存的。

还要一个问题,我们肯定想写这样的代码:

    // usable regions
    muu_buf.push(PageNode {
        pa: _kernel_usable_regions_start as u64,
        pa_size: _kernel_usable_regions_end as u64 - _kernel_usable_regions_start as u64,
        va: KERNEL_BASE + _kernel_usable_regions_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // reserved regions
    muu_buf.push(PageNode {
        pa: _kernel_reserved_regions_start as u64,
        pa_size: _kernel_reserved_regions_end as u64 - _kernel_reserved_regions_start as u64,
        va: KERNEL_BASE + _kernel_reserved_regions_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // pmm bitmap
    muu_buf.push(PageNode {
        pa: _kernel_pmm_bitmap_start as u64,
        pa_size: _kernel_pmm_bitmap_end as u64 - _kernel_pmm_bitmap_start as u64,
        va: KERNEL_BASE + _kernel_pmm_bitmap_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // printk buffer
    muu_buf.push(PageNode {
        pa: _printk_buf_start as u64,
        pa_size: _printk_buf_end as u64 - _printk_buf_start as u64,
        va: KERNEL_BASE + _printk_buf_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // mmu_tables
    muu_buf.push(PageNode {
        pa: _kernel_mmu_tables_start as u64,
        pa_size: _kernel_mmu_tables_end as u64 - _kernel_mmu_tables_start as u64,
        va: KERNEL_BASE + _kernel_mmu_tables_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

而不是挨个页表项去设置,那可太地狱了。

八、准备一些工具

思考来我们其实准备两种工具:

  1. 页表管理器(Page Table Manager):用于创建和管理页表结构,
  2. Vec类似物:用于人类友好的存储页表项。

Vec类似物比较容易,本质上就是一个动态数组,我们只需要预留一块内存,然后通过手动管理长度和容量,就可以实现一个类似Vec的功能。但是每次只能扩展4K,而且内存物理不连续,本质上可以用单链表解决

use crate::early::PMM;
use core::marker::PhantomData;
use core::ptr::NonNull;

// 每页大小
const PAGE_SIZE: usize = 4096;
// 预留 64 字节给 header + 对齐缓冲(内核里这是常见做法)
const MAX_ELEM_SIZE: usize = PAGE_SIZE - 64;

#[repr(C)]
pub struct PageHeader<T>
where
    T: 'static,
{
    pub next: Option<NonNull<PageHeader<T>>>, // 指向下一页
    pub used: usize,                          // 已使用元素数量
    pub capacity: usize,                      // 当前页容量(以元素数量计)
    _marker: PhantomData<T>,                  // 泛型标记
}

pub struct PVec<T>
where
    T: 'static,
{
    head: Option<NonNull<PageHeader<T>>>, // 指向第一页
    tail: Option<NonNull<PageHeader<T>>>, // 指向最后一页
    len: usize,                           // 总元素数量
}

impl<T> PVec<T>
where
    T: 'static,
{
    pub const fn new() -> Self {
        Self {
            head: None,
            tail: None,
            len: 0,
        }
    }

    /// 向向量末尾添加元素
    pub fn push(&mut self, value: T) {
        // 如果没有页,分配新页
        if self.head.is_none() {
            unsafe {
                // 分配新页
                let page_addr = PMM::alloc_page().expect("Failed to allocate page");
                let page_ptr = page_addr as *mut PageHeader<T>;

                page_ptr.write(PageHeader {
                    next: None,
                    used: 0,
                    capacity: MAX_ELEM_SIZE / core::mem::size_of::<T>(),
                    _marker: PhantomData,
                });

                // 使用 NonNull::new_unchecked 包装裸指针
                let non_null_ptr: NonNull<PageHeader<T>> = NonNull::new_unchecked(page_ptr);

                // NonNull console Copy trait,所以可以赋值给两个字段
                self.head = Some(non_null_ptr);
                self.tail = Some(non_null_ptr);
            }
        }
        // 查看尾页是否有足够空间
        unsafe {
            let tail_ptr = self.tail.unwrap().as_mut();
            if tail_ptr.used >= tail_ptr.capacity {
                // 尾页已满,分配新页
                let page_addr = PMM::alloc_page().expect("Failed to allocate page");
                let page_ptr = page_addr as *mut PageHeader<T>;

                page_ptr.write(PageHeader {
                    next: None,
                    used: 0,
                    capacity: MAX_ELEM_SIZE / core::mem::size_of::<T>(),
                    _marker: PhantomData,
                });

                let non_null_ptr: NonNull<PageHeader<T>> = NonNull::new_unchecked(page_ptr);

                // 更新尾页的 next 指针
                tail_ptr.next = Some(non_null_ptr);
                // 更新尾页指针
                self.tail = Some(non_null_ptr);
            }

            // 在尾页插入元素
            let tail_ptr = self.tail.unwrap().as_mut();
            let elem_ptr = (tail_ptr as *mut PageHeader<T> as *mut u8)
                .add(core::mem::size_of::<PageHeader<T>>())
                .add(tail_ptr.used * core::mem::size_of::<T>())
                as *mut T;

            elem_ptr.write(value);
            tail_ptr.used += 1;
            self.len += 1;
        }
    }

    pub fn len(&self) -> usize {
        self.len
    }

    pub fn last(&self) -> Option<&T> {
        if self.len == 0 {
            return None;
        }
        unsafe {
            let tail_ptr = self.tail.unwrap().as_ref();
            let elem_ptr = (tail_ptr as *const PageHeader<T> as *const u8)
                .add(core::mem::size_of::<PageHeader<T>>())
                .add((tail_ptr.used - 1) * core::mem::size_of::<T>())
                as *const T;
            Some(&*elem_ptr)
        }
    }

    /// 清空向量,释放所有页
    pub fn clear(&mut self) {
        // 释放所有页
        let mut current = self.head;
        while let Some(page_ptr) = current {
            unsafe {
                let page_ref = page_ptr.as_ref();
                current = page_ref.next;
                PMM::free_page(page_ptr.as_ptr() as u64);
            }
        }
        self.head = None;
        self.tail = None;
        self.len = 0;
    }

    /// 弹出最后一个元素
    /// 如果向量为空,返回 None
    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }
        unsafe {
            let tail_ptr = self.tail.unwrap().as_mut();
            let elem_ptr = (tail_ptr as *mut PageHeader<T> as *mut u8)
                .add(core::mem::size_of::<PageHeader<T>>())
                .add((tail_ptr.used - 1) * core::mem::size_of::<T>())
                as *mut T;
            let value = elem_ptr.read();
            tail_ptr.used -= 1;
            self.len -= 1;
            Some(value)
        }
    }

    /// 获取指定索引的元素的引用
    pub fn at(&mut self, idx: usize) -> Option<&T> {
        // 没有元素或越界
        if self.len == 0 {
            return None;
        }

        let cap_per_page = MAX_ELEM_SIZE / core::mem::size_of::<T>();

        // 找到理论上放在第几页
        let page_count = idx / cap_per_page;

        // 修复: 使用 div_ceil 替代手动向上取整计算
        if page_count >= self.len.div_ceil(cap_per_page) {
            // 越界
            return None;
        }
        let mut current = self.head;
        let mut page_idx = 0;
        while let Some(page_ptr) = current {
            if page_idx == page_count {
                // 找到对应页
                unsafe {
                    let page_ref = page_ptr.as_ref();
                    let elem_idx = idx % cap_per_page;
                    if elem_idx >= page_ref.used {
                        return None;
                    }
                    let elem_ptr = (page_ref as *const PageHeader<T> as *const u8)
                        .add(core::mem::size_of::<PageHeader<T>>())
                        .add(elem_idx * core::mem::size_of::<T>())
                        as *const T;
                    return Some(&*elem_ptr);
                }
            } else {
                // 继续找下一页
                current = unsafe { page_ptr.as_ref().next };
                page_idx += 1;
            }
        }
        // 没有找到对应页(越界)
        None
    }

    pub fn is_empty(&self) -> bool {
        self.len == 0
    }

    pub fn get_mut(&mut self, index: usize) -> Option<&mut T> {
        if index >= self.len {
            return None;
        }
        let mut current = self.head;
        let mut idx = index;
        while let Some(page_ptr) = current {
            unsafe {
                let page_ref = page_ptr.as_ref();
                if idx < page_ref.used {
                    let elem_ptr = (page_ref as *const PageHeader<T> as *const u8)
                        .add(core::mem::size_of::<PageHeader<T>>())
                        .add(idx * core::mem::size_of::<T>())
                        as *mut T;
                    return Some(&mut *elem_ptr);
                } else {
                    idx -= page_ref.used;
                    current = page_ref.next;
                }
            }
        }
        None
    }

    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }
        let mut current = self.head;
        let mut idx = index;
        while let Some(page_ptr) = current {
            unsafe {
                let page_ref = page_ptr.as_ref();
                if idx < page_ref.used {
                    let elem_ptr = (page_ref as *const PageHeader<T> as *const u8)
                        .add(core::mem::size_of::<PageHeader<T>>())
                        .add(idx * core::mem::size_of::<T>())
                        as *const T;
                    return Some(&*elem_ptr);
                } else {
                    idx -= page_ref.used;
                    current = page_ref.next;
                }
            }
        }
        None
    }

    pub fn iter(&self) -> Iter<'_, T> {
        Iter {
            current_page: self.head,
            page_offset: 0,
            _marker: PhantomData,
        }
    }

    pub fn iter_mut(&mut self) -> IterMut<'_, T> {
        IterMut {
            current_page: self.head,
            page_offset: 0,
            _marker: PhantomData,
        }
    }
}

pub struct Iter<'a, T>
where
    T: 'static,
{
    current_page: Option<NonNull<PageHeader<T>>>,
    page_offset: usize,
    _marker: PhantomData<&'a T>,
}

impl<'a, T> Iterator for Iter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            if let Some(page_ptr) = self.current_page {
                let page_ref = page_ptr.as_ref();
                if self.page_offset < page_ref.used {
                    // 计算当前元素地址
                    let elem_ptr = (page_ref as *const PageHeader<T> as *const u8)
                        .add(core::mem::size_of::<PageHeader<T>>())
                        .add(self.page_offset * core::mem::size_of::<T>())
                        as *const T;
                    self.page_offset += 1;
                    Some(&*elem_ptr)
                } else {
                    // 当前页遍历完毕,移动到下一页
                    self.current_page = page_ref.next;
                    self.page_offset = 0;
                    self.next()
                }
            } else {
                None
            }
        }
    }
}

pub struct IterMut<'a, T>
where
    T: 'static,
{
    current_page: Option<NonNull<PageHeader<T>>>,
    page_offset: usize,
    _marker: PhantomData<&'a mut T>,
}

impl<'a, T> Iterator for IterMut<'a, T> {
    type Item = &'a mut T;

    fn next(&mut self) -> Option<Self::Item> {
        unsafe {
            if let Some(page_ptr) = self.current_page {
                let page_ref = page_ptr.as_ref(); // 用于读取 next 和 used
                if self.page_offset < page_ref.used {
                    // 计算当前元素可变指针
                    let elem_ptr = (page_ptr.as_ptr() as *mut u8)
                        .add(core::mem::size_of::<PageHeader<T>>())
                        .add(self.page_offset * core::mem::size_of::<T>())
                        as *mut T;
                    self.page_offset += 1;
                    Some(&mut *elem_ptr)
                } else {
                    // 移动到下一页
                    self.current_page = page_ref.next;
                    self.page_offset = 0;
                    self.next()
                }
            } else {
                None
            }
        }
    }
}

impl<'a, T> IntoIterator for &'a PVec<T>
where
    T: 'static,
{
    type Item = &'a T;
    type IntoIter = Iter<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter()
    }
}

impl<'a, T> IntoIterator for &'a mut PVec<T>
where
    T: 'static,
{
    type Item = &'a mut T;
    type IntoIter = IterMut<'a, T>;

    fn into_iter(self) -> Self::IntoIter {
        self.iter_mut()
    }
}

impl<T> Drop for PVec<T>
where
    T: 'static,
{
    fn drop(&mut self) {}
}

但是到了tbale就开始坐牢了,页表需要一个4K页,但是我们管理页表也需要一些额外内存,也就是现在没办法让一页带上下一页的元信息,但是如果分配2个4K页就太浪费了,而且两个页物理上不一定连续,所以我们需要一个办法来管理页表的分配。

解决办法是:让页表本身就表示了下一页的位置,比如说我们分配一个页表,然后让这个页表的最后一个项指向下一个页表的位置,这样子就能把页表串起来了。类似多叉树,但是这里叉本身就是树的内容。

九、理解页表

页表说白了就干一件事:给你个va(虚拟地址),把它转换为pa(物理地址),这就是页表的作用。那页表是怎么做到的呢?答案是多级页表。

页表通过本身存储的数据,就能把虚拟地址转换为物理地址,详细来说:

  1. 页表中存储了一个数字
  2. 取头部的几个bit作为索引,找到第一级页表项
  3. 取接下来的几个bit作为索引,找到第二级页表项
  4. 依此类推,直到最后一级页表项,此时最后一集页表加上偏移就是物理地址

打个比方:
想象一下我们是如何测量长度的:

  1. 拿一把米尺,测量出米的数量(一级页表),写到头,比方说 10.
  2. 拿一把分尺,测量出厘米的数量(二级页表),写到头,比方说 10.2
  3. 拿一把厘米尺,测量出毫米的数量(三级页表),写到头,比方说 10.23
  4. 最后在厘米尺上读出最小刻度毫米(偏移),比如说 10.234
    最终我们就得到了一个长度 10.234 米,这个过程就类似于多级页表的地址转换过程。

反过来,当你拿到了一个虚拟地址时,你可以通过页表的多级索引,逐级找到对应的物理地址。
还是这个例子,比方说你拿到了一个虚拟地址 0x12345678,你可以这样做:

  1. 取虚拟地址的高位部分作为一级页表的索引,比如 0x12
  2. 使用这个索引去一级页表中查找对应的页表项,得到二级页表的地址
  3. 取虚拟地址的中间部分作为二级页表的索引,比如 0x34
  4. 使用这个索引去二级页表中查找对应的页表项,得到三级页表的地址
  5. 取虚拟地址的低位部分作为三级页表的索引,比如 0x56
  6. 使用这个索引去三级页表中查找对应的页表项,得到物理页的地址
  7. 最后加上虚拟地址的偏移部分,比如 0x78,得到最终的物理地址
    通过这种方式,页表能够高效地将虚拟地址映射到物理

还是以尺子作为例子

  1. 你拿到一个虚拟地址,就像你拿到一个长度值 (20.325)
  2. 你先用米尺测量,找到对应的米数(一级页表) 20
  3. 然后用分尺测量,找到对应的厘米数(二级页表) 3
  4. 然后用厘米尺测量,找到对应的毫米数(三级页表) 2
  5. 最后加上偏移,得到最终的物理地址 (20 + 5) = 25 cm
    通过这种类比,你可以更直观地理解页表是如何工作的,以及它们如何将虚拟地址转换为物理地址的过程。
    你就把虚拟地址20.325当成20.325米,然后通过多级尺子(页表)逐级测量,最终得到物理地址25 cm。

当然这个例子中我们默认了恒等映射,即20.325米最终映射到25cm,这只是为了说明页表的工作原理,实际中映射关系可以是任意的。比方说20.3米可能映射到三级页表,3对应是40,偏移是6,这取决于页表中存储的映射关系。结果就是:46cm

所以我的解决方案是:让管理器作为一个附加结构体,专门负责页表的分配和管理。页表本身只负责存储映射关系,而管理器负责分配新的页表页,并将它们链接起来形成多级页表结构。

//! 早期页表映射管理(适用于 AArch64 Stage-1,4KiB 粒度)
//! 支持 4KiB page、2MiB block、1GiB block 的混合映射

use core::cmp::min;

use crate::early::pvec::PVec;
use crate::early::PMM;
use crate::printk;

/// 物理地址类型
pub type PhysAddr = u64;

/// 页表项标志类型
pub type PageFlags = u64;

/// AArch64 Stage-1 常用位定义(EL1 kernel mapping)
pub const ENTRY_VALID: u64 = 1 << 0;
pub const ENTRY_TABLE: u64 = 1 << 1;
pub const ENTRY_BLOCK_LOW: u64 = 1 << 0;
pub const ENTRY_PAGE: u64 = 1 << 1;

pub const ENTRY_AF: u64 = 1 << 10;
pub const ENTRY_SH_ISH: u64 = 0b11 << 8;
pub const ENTRY_AP_RW_EL1: u64 = 0b00 << 6;
pub const ENTRY_PXN: u64 = 1 << 53;
pub const ENTRY_UXN: u64 = 1 << 54;
pub const ENTRY_ATTR_NORMAL_WB: u64 = 0b000 << 2;

/// 常用组合标志
pub const TABLE_FLAGS: u64 = ENTRY_VALID | ENTRY_TABLE;

// 默认属性(仅用于参考,实际使用 node.flags)
pub const ATTR_FLAGS: u64 =
    ENTRY_AF | ENTRY_AP_RW_EL1 | ENTRY_SH_ISH | ENTRY_ATTR_NORMAL_WB | ENTRY_PXN | ENTRY_UXN;

pub const PAGE_FLAGS: u64 = ENTRY_VALID | ENTRY_PAGE | ATTR_FLAGS;
pub const BLOCK_FLAGS: u64 = ENTRY_VALID | ATTR_FLAGS;

/// 物理地址掩码(4KiB 对齐)
pub const PHYS_ADDR_MASK: u64 = 0x0000_FFFF_FFFF_F000;

pub const PAGE_SIZE: u64 = 0x1000;
pub const BLOCK_2M: u64 = 0x20_0000;
pub const BLOCK_1G: u64 = 0x4000_0000;

/// 页表节点(必须 4KiB 对齐)
#[repr(C, align(4096))]
pub struct PTreeNode {
    pub entries: [u64; 512],
}

/// 映射描述符(早期使用)
#[derive(Clone, Copy)]
pub struct PageNode {
    pub va: u64,
    pub pa: u64,
    pub pa_size: u64,
    pub flags: u64, // 映射属性
}

impl PageNode {
    pub const fn empty() -> Self {
        Self {
            va: 0,
            pa: 0,
            pa_size: 0,
            flags: 0,
        }
    }

    pub const fn new(va: u64, pa: u64, pa_size: u64, flags: u64) -> Self {
        Self {
            va,
            pa,
            pa_size,
            flags,
        }
    }
}

/// 早期页表管理器(只持有 L0 物理地址)
pub struct PTreeMap {
    pub root_page: PhysAddr,
}

impl PTreeMap {
    /// 从数组构建页表
    pub fn from_pvec(&mut self, pvec: &PVec<PageNode>) {
        if self.is_none() {
            panic!("PTreeMap::from_pvec: no root table");
        }

        for &node in pvec.iter() {
            if node.pa_size == 0 || (node.va | node.pa) & 0xFFF != 0 {
                printk!(
                    "Invalid map: va={:#x} pa={:#x} size={:#x}\n",
                    node.va,
                    node.pa,
                    node.pa_size
                );
                continue;
            }

            map_range(
                0,
                self.root_page,
                node.va,
                node.va,
                node.pa,
                node.pa_size,
                BLOCK_1G,
                node.flags,
            );
        }
    }
}

/// 递归映射函数
/// level: 0=L0, 1=L1, 2=L2, 3=L3
fn map_range(
    level: u64,
    table_pa: u64,
    orig_va: u64,
    curr_va: u64,
    orig_pa: u64,
    remaining: u64,
    max_granule: u64,
    flags: u64,
) {
    let table = unsafe { &mut *(table_pa as *mut PTreeNode) };
    let mut va = curr_va;
    let end = orig_va + remaining;

    // Shift: L0=39, L1=30, L2=21, L3=12
    let shift = 39 - level * 9;
    let granule = 1u64 << shift;

    while va < end {
        let idx = (va >> shift) & 0x1FF;

        let block_va_start = va & !(granule - 1);
        let offset_in_block = va - block_va_start;
        let map_size = min(granule - offset_in_block, end - va);

        let pa_this = orig_pa + (va - orig_va);

        let va_aligned = offset_in_block == 0;
        let pa_aligned = (pa_this & (granule - 1)) == 0;
        let full_block = map_size == granule;

        // Block mapping allowed at Level 1 (1G) and Level 2 (2M).
        // Standard AArch64 typically doesn't use L0 blocks (512G). L3 is Page.
        let can_block = va_aligned
            && pa_aligned
            && full_block
            && granule <= max_granule
            && level > 0 // Disallow L0 blocks
            && level < 3; // Disallow L3 blocks (must be Page)

        if can_block {
            // Block descriptor (L1/L2): Valid=1, Type=0 (bit 1 = 0)
            // Ensure bit 1 is cleared from flags
            let block_flags = flags & !ENTRY_TABLE;
            table.entries[idx as usize] = (pa_this & PHYS_ADDR_MASK) | block_flags;
        } else if level == 3 {
            // Page descriptor (L3): Valid=1, Type=1 (bit 1 = 1)
            // Ensure bit 1 is set
            let page_flags = flags | ENTRY_PAGE;
            table.entries[idx as usize] = (pa_this & PHYS_ADDR_MASK) | page_flags;
        } else {
            let entry = table.entries[idx as usize];
            let next_pa = if (entry & ENTRY_VALID) != 0 {
                if (entry & ENTRY_TABLE) != 0 {
                    entry & PHYS_ADDR_MASK
                } else {
                    // 如果已经是 Block 映射,跳过此范围
                    va += map_size;
                    continue;
                }
            } else {
                let new_pa = unsafe { PMM::alloc_page().expect("alloc table page failed") };
                unsafe {
                    core::ptr::write_bytes(new_pa as *mut u8, 0, PAGE_SIZE as usize);
                }
                table.entries[idx as usize] = (new_pa & PHYS_ADDR_MASK) | TABLE_FLAGS;
                new_pa
            };
            map_range(
                level + 1,
                next_pa,
                va,
                va,
                pa_this,
                map_size,
                max_granule,
                flags,
            );
        }

        va += map_size;
    }
}

这里其实坐牢了很久,主要是页表表面上看是一个找零问题:

  1. 用大页表吃掉大块内存
  2. 用小页表吃掉小块内存

但是实际情况是:大页表不一定能用,因为地址可能没对齐,所以只能用小页表,而小页表又很浪费内存,所以需要一个折中的办法,就是尽可能用大页表,然后剩下的部分再用小页表。

也就是用人话说:带对齐的贪心找零问题

  1. 计算当前级别的页表粒度(4K/2M/1G) (固定桶大小)
  2. 检查当前虚拟地址和物理地址是否对齐该粒度 (这个桶是否能够真好装下)
  3. 如果对齐且剩余大小足够大,使用该粒度进行映射 (正好装下)
  4. 否则,递归进入下一级页表,继续尝试更小粒度映射 (用多个小桶装,本桶记录小桶位置而不是它的位置)

然后我们就能建立页表了,需要注意页表标识位的问题。

十、准备切换页表

我们已经有了页表管理器,但是我们要设计系统布局,我们希望:

  1. 内核空间:高地址空间
  2. 用户空间:低地址空间
  3. 能够有一个方法让内核便捷的访问物理内存

所以我们使用半高映射,也就是把内核映射到高地址空间,同时把物理内存也映射到高地址空间的一部分,这样子内核就能通过高地址空间访问物理内存了。

/// MMU 管理模块
/// 保存当前使用的页表信息以及页表项相关操作
use crate::early::ptreemap::{PTreeMap, PTreeNode, PageNode};
use crate::early::pvec::PVec;
use crate::early::PMM;
use crate::kernel::ktreemap::KTreeMap;
use crate::printk;
use crate::vmm_init;
use core::arch::asm;
///MMU 管理模块
///保存当前使用的页表信息以及页表项相关操作
use core::cell::UnsafeCell;
/// 当前使用的MMU规则
pub static GLOBAL_MMU: GlobalMMU = GlobalMMU(UnsafeCell::new(MMUManager::empty()));
/// MMU 是否已初始化
pub(crate) static mut MMU_INITIALIZED: bool = false;
/// MMU 是否正在运行
static mut MMU_RUNNING: bool = false;

pub struct GlobalMMU(pub(crate) UnsafeCell<MMUManager>);
unsafe impl Sync for GlobalMMU {}

unsafe extern "C" {
    fn _kernel_virt_start();
    fn _kernel_text_start();
    fn _kernel_text_end();

    fn _kernel_rodata_start();
    fn _kernel_rodata_end();

    fn _kernel_data_start();
    fn _kernel_data_end();

    fn _kernel_bss_start();
    fn _kernel_bss_end();

    fn _kernel_stack_start();
    fn _kernel_stack_end();

    fn _kernel_usable_regions_start();
    fn _kernel_usable_regions_end();

    fn _kernel_reserved_regions_start();
    fn _kernel_reserved_regions_end();

    fn _kernel_pmm_bitmap_start();
    fn _kernel_pmm_bitmap_end();

    fn _printk_buf_start();
    fn _printk_buf_end();

    fn _kernel_mmu_tables_start();
    fn _kernel_mmu_tables_end();

    fn _kernel_virt_end();
}

/// MMU 管理器,只保存根页表的物理地址
pub struct MMUManager {
    root_table: u64,
}

impl MMUManager {
    /// 创建一个空的 MMUManager
    pub const fn empty() -> Self {
        MMUManager { root_table: 0 }
    }
    /// 初始化 MMU 管理器,设置根页表地址
    pub fn init(&mut self) {
        self.root_table = _kernel_mmu_tables_start as u64;
    }
    /// 设置根页表地址
    pub fn set_root_table(&mut self, root_table: u64) {
        self.root_table = root_table;
    }
}

// ARM64 页表项(PTE)标志位定义(基于 ARMv8-A 架构,4KB 页面)

/// 有效位(Valid bit)——必须为 1 才表示该条目有效
pub const PTE_VALID: u64 = 1 << 0;

/// 表项类型:L0-L2 中 1 = Table,0 = Block;L3 中 1 = Page,0 = Invalid
pub const PTE_TABLE: u64 = 1 << 1;
pub const PTE_PAGE: u64 = 1 << 1;

/// AF (Access Flag) —— 访问位,硬件不会自动置位,需软件处理缺页异常后设置
pub const PTE_AF: u64 = 1 << 10;

/// SH[9:8] —— Shareability field
pub const PTE_SH_NON_SHAREABLE: u64 = 0 << 8;
pub const PTE_SH_OUTER_SHAREABLE: u64 = 2 << 8;
pub const PTE_SH_INNER_SHAREABLE: u64 = 3 << 8;

/// AP[7:6] —— Access Permissions
pub const PTE_AP_RO: u64 = 1 << 7; // Read-only(若未设,则为 read-write)
pub const PTE_AP_RW: u64 = 0 << 7;
pub const PTE_AP_EL0: u64 = 1 << 6; // EL0 可访问(用户态)
pub const PTE_AP_EL1: u64 = 0 << 6; // 仅 EL1+ 可访问(内核态)

/// NS (Non-secure bit) —— 安全扩展相关,通常在非安全世界设为 1
pub const PTE_NS: u64 = 1 << 5;

/// ATTRINDX[4:2] —— Memory attribute index (MAIR_EL1 中的索引)
pub const PTE_ATTR_MASK: u64 = 7 << 2;
pub const PTE_ATTR_NORMAL: u64 = 0 << 2; // 通常对应 MAIR_EL1[0]
pub const PTE_ATTR_DEVICE_nGnRnE: u64 = 1 << 2;
pub const PTE_ATTR_DEVICE_nGnRE: u64 = 2 << 2;
pub const PTE_ATTR_DEVICE_GRE: u64 = 3 << 2;
// 具体含义取决于 MAIR_EL1 的配置

/// UXN / PXN —— Unprivileged Execute Never / Privileged Execute Never
pub const PTE_UXN: u64 = 1 << 54; // EL0 不可执行
pub const PTE_PXN: u64 = 1 << 53; // EL1 不可执行

/// Contiguous hint —— 连续页提示(性能优化)
pub const PTE_CONT: u64 = 1 << 52;

/// DBM (Dirty Bit Management) —— 若支持,硬件可自动管理 dirty 状态
pub const PTE_DBM: u64 = 1 << 51;

/// nG (not Global) —— 若置位,TLB entry 不是全局的(与 ASID 相关)
pub const PTE_nG: u64 = 1 << 11;

/// 高地址位(对于 48-bit 虚拟地址,高位需符号扩展;52-bit 地址则使用更多高位)
/// 但页表项中的物理地址字段从 bit 12 开始(4KB 对齐)

// 常用组合宏(方便使用)
// 注意:这里默认是 4KB 页面的 L3 页表项,因此需要设置 PTE_PAGE (bit 1)

pub const KERNEL_R: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL1 | PTE_AP_RO;
pub const KERNEL_RW: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL1 | PTE_AP_RW;
pub const KERNEL_RX: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL1 | PTE_AP_RO | PTE_UXN;
pub const KERNEL_RWX: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL1 | PTE_AP_RW | PTE_UXN;

pub const USER_R: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL0 | PTE_UXN | PTE_AP_RO;
pub const USER_RW: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL0 | PTE_UXN | PTE_AP_RW;
pub const USER_RX: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL0 | PTE_AP_RO;
pub const USER_RWX: u64 =
    PTE_VALID | PTE_PAGE | PTE_AF | PTE_SH_INNER_SHAREABLE | PTE_AP_EL0 | PTE_AP_RW;

pub const KERNEL_DEVICE_RW: u64 = PTE_VALID
    | PTE_PAGE
    | PTE_AF
    | PTE_SH_INNER_SHAREABLE
    | PTE_AP_EL1
    | PTE_ATTR_DEVICE_nGnRnE
    | PTE_AP_RW;
pub const KERNEL_DEVICE_R: u64 = PTE_VALID
    | PTE_PAGE
    | PTE_AF
    | PTE_SH_INNER_SHAREABLE
    | PTE_AP_EL1
    | PTE_ATTR_DEVICE_nGnRnE
    | PTE_AP_RO;
pub const KERNEL_DEVICE_W: u64 = PTE_VALID
    | PTE_PAGE
    | PTE_AF
    | PTE_SH_INNER_SHAREABLE
    | PTE_AP_EL1
    | PTE_ATTR_DEVICE_nGnRnE
    | PTE_AP_RW;
pub const USER_DEVICE_RW: u64 = PTE_VALID
    | PTE_PAGE
    | PTE_AF
    | PTE_SH_INNER_SHAREABLE
    | PTE_AP_EL0
    | PTE_ATTR_DEVICE_nGnRnE
    | PTE_AP_RW;

pub const VIEW_BASE: u64 = 0xffff_8000_0000_0000;
pub const KERNEL_BASE: u64 = 0xffff_8080_0000_0000;
pub const KERNEL_HEAP: u64 = 0xffff_0040_0000_0000; // 4TB
pub const USER_BASE: u64 = 0x0;

#[inline(always)]
fn higher_half_addr(addr: u64) -> u64 {
    let virt_start = unsafe { _kernel_virt_start as u64 };
    debug_assert!(
        addr >= virt_start,
        "attempted to lift address below kernel start"
    );
    KERNEL_BASE + (addr - virt_start)
}

/// 初始化 MMU
pub fn init_MMU() {
    let mut muu_buf: PVec<PageNode> = PVec::new();

    // 内存视图
    let rom_beas = unsafe { PMM::get_beas().expect("MMU get rom_beas error") };
    let pages = unsafe { PMM::get_page_size().expect("MMU get pages error") };
    muu_buf.push(PageNode {
        pa: 0x0,
        pa_size: unsafe { PMM::get_beas().expect("error no_get_beas") } + 512 * 1024 * 1024,
        va: VIEW_BASE,
        flags: KERNEL_RW,
    });

    // 恒等映射所有用到的空间
    muu_buf.push(PageNode {
        pa: 0x0,
        pa_size: unsafe { PMM::get_beas().expect("error no_get_beas") } + 512 * 1024 * 1024,
        va: 0x0,
        flags: KERNEL_RWX,
    });

    // 内核各段映射
    // text
    muu_buf.push(PageNode {
        pa: _kernel_text_start as u64,
        pa_size: _kernel_text_end as u64 - _kernel_text_start as u64,
        va: KERNEL_BASE + _kernel_text_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RX,
    });

    // rodata
    muu_buf.push(PageNode {
        pa: _kernel_rodata_start as u64,
        pa_size: _kernel_rodata_end as u64 - _kernel_rodata_start as u64,
        va: KERNEL_BASE + _kernel_rodata_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_R,
    });

    // data
    muu_buf.push(PageNode {
        pa: _kernel_data_start as u64,
        pa_size: _kernel_data_end as u64 - _kernel_data_start as u64,
        va: KERNEL_BASE + _kernel_data_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // bss
    muu_buf.push(PageNode {
        pa: _kernel_bss_start as u64,
        pa_size: _kernel_bss_end as u64 - _kernel_bss_start as u64,
        va: KERNEL_BASE + _kernel_bss_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // stack
    muu_buf.push(PageNode {
        pa: _kernel_stack_start as u64,
        pa_size: _kernel_stack_end as u64 - _kernel_stack_start as u64,
        va: KERNEL_BASE + _kernel_stack_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // usable regions
    muu_buf.push(PageNode {
        pa: _kernel_usable_regions_start as u64,
        pa_size: _kernel_usable_regions_end as u64 - _kernel_usable_regions_start as u64,
        va: KERNEL_BASE + _kernel_usable_regions_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // reserved regions
    muu_buf.push(PageNode {
        pa: _kernel_reserved_regions_start as u64,
        pa_size: _kernel_reserved_regions_end as u64 - _kernel_reserved_regions_start as u64,
        va: KERNEL_BASE + _kernel_reserved_regions_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // pmm bitmap
    muu_buf.push(PageNode {
        pa: _kernel_pmm_bitmap_start as u64,
        pa_size: _kernel_pmm_bitmap_end as u64 - _kernel_pmm_bitmap_start as u64,
        va: KERNEL_BASE + _kernel_pmm_bitmap_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // printk buffer
    muu_buf.push(PageNode {
        pa: _printk_buf_start as u64,
        pa_size: _printk_buf_end as u64 - _printk_buf_start as u64,
        va: KERNEL_BASE + _printk_buf_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    // mmu_tables
    muu_buf.push(PageNode {
        pa: _kernel_mmu_tables_start as u64,
        pa_size: _kernel_mmu_tables_end as u64 - _kernel_mmu_tables_start as u64,
        va: KERNEL_BASE + _kernel_mmu_tables_start as u64 - _kernel_virt_start as u64,
        flags: KERNEL_RW,
    });

    let mut ptreemap = PTreeMap::empty();
    ptreemap.set_root(_kernel_mmu_tables_start as u64);
    ptreemap.from_pvec(&muu_buf);
    if ptreemap.is_none() {
        panic!("MMU init_MMU failed!");
    }
    crate::printk!("MMU initialized successfully.\n");
    // ptreemap.dump();
    unsafe {
        MMU_INITIALIZED = true;
    }
    init_higher_half();
}

// 设置寄存器,启用 MMU
pub fn init_higher_half() {
    printk!("Jumping to higher-half address space...\n");
    let root_table = _kernel_mmu_tables_start as u64;
    let target_pc = higher_half_addr(init_kernel_address_space as u64);
    set_now_table_addr(root_table + VIEW_BASE);
    printk!(
        "MMU root table at {:#x}, jumping to {:#x}\n",
        root_table,
        target_pc
    );
    unsafe {
        asm!(
            // 1. 设置 MAIR_EL1 (Memory Attribute Indirection Register)
            // Attr0 = 0xFF (Normal Memory, Outer WB, Inner WB)
            // Attr1 = 0x00 (Device-nGnRnE)
            // Attr2 = 0x04 (Device-nGnRE)
            // Attr3 = 0x0C (Device-GRE)
            "ldr {tmp}, =0x000000000C0400FF",
            "msr mair_el1, {tmp}",

            // 2. 设置 TCR_EL1 (Translation Control Register)
            // T0SZ=16 (48-bit), T1SZ=16 (48-bit), TG0=4KB, TG1=4KB
            // IPS=48-bit, SH=Inner Shareable, ORGN/IRGN=WB
            "ldr {tmp}, =0x5B5103510",
            "msr tcr_el1, {tmp}",

            // 3. 设置 TTBR0/1_EL1 (Translation Table Base Registers)
            "msr ttbr0_el1, {ttbr}",
            "msr ttbr1_el1, {ttbr}",
            "isb",

            // 4. 启用 MMU (SCTLR_EL1)
            "mrs {tmp}, sctlr_el1",
            "orr {tmp}, {tmp}, #1",       // M=1 Enable MMU
            "orr {tmp}, {tmp}, #(1<<2)",  // C=1 Enable D-Cache
            "orr {tmp}, {tmp}, #(1<<12)", // I=1 Enable I-Cache
            "msr sctlr_el1, {tmp}",
            "isb",

            // 5. 跳转到高地址
            "br {target}",

            ttbr = in(reg) root_table,
            target = in(reg) target_pc,
            tmp = out(reg) _,
        );
        core::hint::unreachable_unchecked();
    }
}

#[unsafe(no_mangle)]
extern "C" fn init_kernel_address_space() {
    // 现在已经在 higher-half 地址空间运行
    printk!("MMU is now running in higher-half address space.\n");
    let target = higher_half_addr(vmm_init as u64);
    printk!("try goto {:#x}\n", target);
    unsafe {
        asm!("br {target}", target = in(reg) target);
    }
}

pub fn switch_mmu_table(pa_table: u64, old_table_clean: bool) {
    printk!(
        "Switching MMU table {:#x} to {:#x}\n",
        get_now_table_addr(),
        pa_table
    );
    unsafe {
        asm!(
            // 切换 TTBR1_EL1 到新的页表地址
            "msr ttbr1_el1, {ttbr}",
            "isb",
            ttbr = in(reg) pa_table,
        );
    }
    printk!("Switched MMU table to {:#x}\n", pa_table);
    // 释放旧表
    if old_table_clean {
        set_new_clean_old(pa_table);
    }
}

/// 获取当前的页表位置
pub fn get_now_table_addr() -> u64 {
    unsafe {
        let ptr = GLOBAL_MMU.0.get();
        (*ptr).root_table
    }
}

/// 设置当前页表位置
pub fn set_now_table_addr(addr: u64) {
    unsafe {
        let ptr = GLOBAL_MMU.0.get();
        (*ptr).root_table = addr - VIEW_BASE;
    }
}

/// 设置新的页表,并释放旧的页表
pub fn set_new_clean_old(addr: u64) {
    unsafe {
        let ptr = GLOBAL_MMU.0.get();
        let old = (*ptr).root_table as *mut KTreeMap;
        (*old).free();
        (*ptr).root_table = addr;
    }
}

/// 虚拟地址转换为物理地址
#[inline(always)]
pub fn va_to_pa(virt_addr: u64) -> u64 {
    virt_addr - VIEW_BASE
}

/// 物理地址转换为虚拟地址
#[inline(always)]
pub fn pa_to_va(phys_addr: u64) -> u64 {
    phys_addr + VIEW_BASE
}

这里其实是我们第一张页表:

  1. 把物理内存映射到高地址空间的一部分
  2. 把内核各段映射到对应的高地址空间位置
  3. 把物理内存映射到低地址空间(恒等映射)
  4. 把本身做恒等映射

第四点很重要,因为在调试的时候,没办法保证是切换页表错了,还是页表本身错了,还是高地址映射错了。需要尊从一个原则:代码运行在哪里,哪里就要在页表切换前后保持不变

十一、真正进入MMU


// 设置寄存器,启用 MMU
pub fn init_higher_half() {
    printk!("Jumping to higher-half address space...\n");
    let root_table = _kernel_mmu_tables_start as u64;
    let target_pc = higher_half_addr(init_kernel_address_space as u64);
    set_now_table_addr(root_table + VIEW_BASE);
    printk!(
        "MMU root table at {:#x}, jumping to {:#x}\n",
        root_table,
        target_pc
    );
    unsafe {
        asm!(
            // 1. 设置 MAIR_EL1 (Memory Attribute Indirection Register)
            // Attr0 = 0xFF (Normal Memory, Outer WB, Inner WB)
            // Attr1 = 0x00 (Device-nGnRnE)
            // Attr2 = 0x04 (Device-nGnRE)
            // Attr3 = 0x0C (Device-GRE)
            "ldr {tmp}, =0x000000000C0400FF",
            "msr mair_el1, {tmp}",

            // 2. 设置 TCR_EL1 (Translation Control Register)
            // T0SZ=16 (48-bit), T1SZ=16 (48-bit), TG0=4KB, TG1=4KB
            // IPS=48-bit, SH=Inner Shareable, ORGN/IRGN=WB
            "ldr {tmp}, =0x5B5103510",
            "msr tcr_el1, {tmp}",

            // 3. 设置 TTBR0/1_EL1 (Translation Table Base Registers)
            "msr ttbr0_el1, {ttbr}",
            "msr ttbr1_el1, {ttbr}",
            "isb",

            // 4. 启用 MMU (SCTLR_EL1)
            "mrs {tmp}, sctlr_el1",
            "orr {tmp}, {tmp}, #1",       // M=1 Enable MMU
            "orr {tmp}, {tmp}, #(1<<2)",  // C=1 Enable D-Cache
            "orr {tmp}, {tmp}, #(1<<12)", // I=1 Enable I-Cache
            "msr sctlr_el1, {tmp}",
            "isb",

            // 5. 跳转到高地址
            "br {target}",

            ttbr = in(reg) root_table,
            target = in(reg) target_pc,
            tmp = out(reg) _,
        );
        core::hint::unreachable_unchecked();
    }
}

这样就进入了 MMU 模式,接下来就可以使用虚拟地址进行内存访问了。但是在此之前,我们仍然要处理一下遗留的问题,比如说缓存一致性、TLB 刷新等,这些都是 MMU 启用后需要注意的事项。

  1. 我们先要求PMM封存此时的物理内存使用情况作为不可分配释放的内存区域,即抛弃对老物理内存的管理,承认为内核保留
  2. 我们要更换新页表,不再映射内核在低地址空间的恒等
  3. 我们要更新工具,用新工具走VMM而不是PMM进行内存分配
use core::sync::atomic::Ordering;

use crate::{
    early::PMM,
    kernel::{ktreemap::KTreeMap, MMU},
    printk,
};

unsafe extern "C" {
    fn _kernel_virt_start();
    fn _kernel_text_start();
    fn _kernel_text_end();

    fn _kernel_rodata_start();
    fn _kernel_rodata_end();

    fn _kernel_data_start();
    fn _kernel_data_end();

    fn _kernel_bss_start();
    fn _kernel_bss_end();

    fn _kernel_stack_start();
    fn _kernel_stack_end();

    fn _kernel_usable_regions_start();
    fn _kernel_usable_regions_end();

    fn _kernel_reserved_regions_start();
    fn _kernel_reserved_regions_end();

    fn _kernel_pmm_bitmap_start();
    fn _kernel_pmm_bitmap_end();

    fn _printk_buf_start();
    fn _printk_buf_end();

    fn _kernel_mmu_tables_start();
    fn _kernel_mmu_tables_end();

    fn _kernel_virt_end();
}

/// 初始化 Pmap 模块
/// 必须在切换到页表启动后调用
pub fn init_Pmap() {
    printk!("init_Pmap\n");
    printk!("set mmu_buf\n");
    use crate::early::ptreemap::PTreeMap;
    use crate::early::ptreemap::PageNode;
    use crate::early::pvec::PVec;
    use crate::early::PMM::*;
    use crate::kernel::MMU::*;

    let frame_allocator_ptr = FRAME_ALLOCATOR.load(Ordering::SeqCst);
    printk!("Frame allocator ptr: {:#x}\n", frame_allocator_ptr as u64);
    let raw_addr = frame_allocator_ptr as u64;
    printk!("Global PMM at physical address: {:#x}\n", raw_addr);

    let virt_addr = (raw_addr + VIEW_BASE) as *mut GlobalPMM;
    printk!("Global PMM at virtual address: {:#x}\n", virt_addr as u64);
    FRAME_ALLOCATOR.store(virt_addr, Ordering::SeqCst);

    let mut muu_buf: PVec<PageNode> = PVec::new();
    let mut table_map = PTreeMap::empty();
    table_map.set_root(unsafe { PMM::alloc_page().expect("init_Pmam Error") });

    // 内存视图
    let rom_beas = unsafe { PMM::get_beas().expect("MMU get rom_beas error") };
    let pages = unsafe { PMM::get_page_size().expect("MMU get pages error") };
    muu_buf.push(PageNode {
        pa: rom_beas as u64,
        pa_size: 512 * 1024 * 1024, // 512MB
        va: VIEW_BASE,
        flags: KERNEL_RW,
    });

    // 恒等映射0x0900_0000 ~ 0x1000_0000 ttys 设备树等
    muu_buf.push(PageNode {
        pa: 0x0900_0000,
        pa_size: 0x0700_0000,
        va: 0x0900_0000,
        flags: KERNEL_DEVICE_RW,
    });

    // 内核各段映射
    // text
    muu_buf.push(PageNode {
        pa: _kernel_text_start as u64 - KERNEL_BASE,
        pa_size: _kernel_text_end as u64 - _kernel_text_start as u64,
        va: _kernel_text_start as u64,
        flags: KERNEL_RX,
    });

    // rodata
    muu_buf.push(PageNode {
        pa: _kernel_rodata_start as u64 - KERNEL_BASE,
        pa_size: _kernel_rodata_end as u64 - _kernel_rodata_start as u64,
        va: _kernel_rodata_start as u64,
        flags: KERNEL_R,
    });

    // data
    muu_buf.push(PageNode {
        pa: _kernel_data_start as u64 - KERNEL_BASE,
        pa_size: _kernel_data_end as u64 - _kernel_data_start as u64,
        va: _kernel_data_start as u64,
        flags: KERNEL_RW,
    });

    // bss
    muu_buf.push(PageNode {
        pa: _kernel_bss_start as u64 - KERNEL_BASE,
        pa_size: _kernel_bss_end as u64 - _kernel_bss_start as u64,
        va: _kernel_bss_start as u64,
        flags: KERNEL_RW,
    });

    // stack
    muu_buf.push(PageNode {
        pa: _kernel_stack_start as u64 - KERNEL_BASE,
        pa_size: _kernel_stack_end as u64 - _kernel_stack_start as u64,
        va: _kernel_stack_start as u64,
        flags: KERNEL_RW,
    });

    // usable regions
    muu_buf.push(PageNode {
        pa: _kernel_usable_regions_start as u64 - KERNEL_BASE,
        pa_size: _kernel_usable_regions_end as u64 - _kernel_usable_regions_start as u64,
        va: _kernel_usable_regions_start as u64,
        flags: KERNEL_RW,
    });

    // reserved regions
    muu_buf.push(PageNode {
        pa: _kernel_reserved_regions_start as u64 - KERNEL_BASE,
        pa_size: _kernel_reserved_regions_end as u64 - _kernel_reserved_regions_start as u64,
        va: _kernel_reserved_regions_start as u64,
        flags: KERNEL_RW,
    });

    // pmm bitmap
    muu_buf.push(PageNode {
        pa: _kernel_pmm_bitmap_start as u64 - KERNEL_BASE,
        pa_size: _kernel_pmm_bitmap_end as u64 - _kernel_pmm_bitmap_start as u64,
        va: _kernel_pmm_bitmap_start as u64,
        flags: KERNEL_RW,
    });

    // printk buffer
    muu_buf.push(PageNode {
        pa: _printk_buf_start as u64 - KERNEL_BASE,
        pa_size: _printk_buf_end as u64 - _printk_buf_start as u64,
        va: _printk_buf_start as u64,
        flags: KERNEL_RW,
    });

    // mmu_tables
    muu_buf.push(PageNode {
        pa: table_map.root_page,
        pa_size: _kernel_mmu_tables_end as u64 - _kernel_mmu_tables_start as u64,
        va: _kernel_mmu_tables_start as u64,
        flags: KERNEL_R,
    });

    // kernel heap
    muu_buf.push(PageNode {
        pa: unsafe { PMM::alloc_page().unwrap() },
        pa_size: 0x1000,
        va: KERNEL_HEAP,
        flags: KERNEL_RW,
    });

    printk!("VMM build table_map\n");
    table_map.from_pvec(&muu_buf);
    // table_map.dump();

    // 切换栈指针到虚拟地址空间
    unsafe {
        let sp = _kernel_stack_start as u64;
        core::arch::asm!("mov sp, {}", in(reg) sp);
        printk!("sp => {:#x}\n", sp);
    }

    // 设置PMM进入全局模式
    unsafe { PMM::set_global_mode() };
    let addr = table_map.root_page;
    printk!("Switched to new page table: {:#x}\n", addr);
    // 切换页表
    MMU::switch_mmu_table(addr, false);
    printk!("init_Pmap done\n");
    printk!("Welcome to VMM world!\n");
    todo!()
}

/// 直接在内存视图分配一页物理内存,返回虚拟地址
pub fn alloc_page() -> Option<u64> {
    Some(unsafe { PMM::alloc_page().unwrap() } + MMU::VIEW_BASE)
}

/// 直接回收一份
pub fn free_page(va: u64) {
    unsafe { PMM::free_page(va - MMU::VIEW_BASE) };
}

/// 要求在某个虚拟地址扩展
/// 会切换页表,不应该频繁调用
pub fn vmm_at(va: u64, size: u64, flag: u64) {
    let mut table = KTreeMap::from_addr(_kernel_mmu_tables_start as u64);
    for i in 0..(size / 0x1000) {
        table.edit(
            va + 0x1000 * i,
            unsafe { PMM::alloc_page().unwrap() },
            0x1000,
            flag,
        );
    }
    unsafe { table.mmu_switch() };
}

/// 要求回收地址空间
/// 会切换页表,不应该频繁调用
pub fn unvmm_at(pa: u64, size: u64) {
    let mut table = KTreeMap::from_addr(_kernel_mmu_tables_start as u64);
    table.remort(pa, size);
    unsafe { table.mmu_switch() };
}

接下来,除了VMM,不应该有如何东西接触到物理地址,也不应该接触到PMM和页表。

十二、MMU后日谈 之 VMM

然后更新工具ktreemap.rs,实际上就是把之前的工具PMM改为VMM即可,然后更新一些特性,比方说自动从某个地址自动获取页表,自动更换页表并释放旧页表等。

然后写一个kmalloc, 本质上就是从VMM分配内存,然后在页表中建立映射关系,内存不足时候,请求VMM扩展并自动分配内存。本体对外提供kmallockfree接口(这里作者并没有实现一个高度可可用的,只是简单实现,就不贴出来了)同时提供小内存分配页表和大内存分配页表,分别用于小内存分配和大内存分配。

之后如果愿意,实现各种异常跳转处理、同步异常、页表缺失异常等。并逐步提供用户态加载切换(简单来说就是:映射对应用户地址,拷贝过去,异常退出,切换页表),异常调用(调用到处理代码,切换页表,内核),堆分配器。。。。

这些其实已经没有新东西了,顶多是之前东西的排练组合。

现在再来看看这张图:

image
现在我们从上往下解析:

  1. 内核空间
    准确来说分为两段:
  • 0xffff_8000_0000_0000 ~ 0xffff_8080_0000_0000 : 视图 + 空洞
  • 0xffff_8080_0000_0000 ~ 0xffff_80ff_ffff_ffff : 内核空间+内核代码
  1. 用户空间
  • 0x0000_0000_0040_0000 ~ 0x0000_7fff_ffff_ffff : 用户空间
  1. 恒等映射
  • 0x0000_0000_0001_0000 ~ 0x0000_0000_0040_0000 : 恒等映射物理内存(包括io设备,系统调用段等)
  1. 故意不映射段
  • 0x0000_0000_0000_0000 ~ 0x0000_0000_0001_0000 : 故意不映射,防止 NULL 指针访问

然后用户空间本质上是套娃,用户空间的页表映射到用户空间地址,而内核空间的页表映射到内核空间地址。理论上你爱怎么映射都行,只要不冲突就行。

那么共享库呢?共享库本质上是把同一份代码映射到不同的用户空间地址即可。因为代码是只读的,所以可以多用户共享。

内核实际上并没有运行在任何逻辑位置,只是通过页表被映射到了某个位置而已。内核代码可以运行在0地址空间,也可以运行在高地址空间,只要页表映射正确即可,甚至不映射,直到异常发生再映射也行。

恭喜你发明了现代系统中最重要的组件之一:虚拟内存管理器(VMM)。其中NX和页表等硬件保护是现代操作系统安全的基石,没有之一,然后再通过软件层的权限管理(用户态/内核态)等机制,构建起一个相对安全的运行环境。

现在才真正实现了内存管理的分离,内核不再直接操作物理内存,而是通过虚拟内存进行操作,这样就可以实现更复杂的内存管理策略,比如说内存保护、内存共享、内存映射文件等高级功能。

十三、 后日谈

那么,为什么libc可以被调试,但是一旦飘到内核立刻被kill呢,因为实际上libc只是一个比较特殊的用户态程序,封装了上层系统调用接口而已,并没有直接操作硬件资源的权限,所以在用户态运行是安全的。一旦进入内核态,libc的某些操作可能会触发内核的保护机制,导致程序被终止。

同时,操作系统理论上可以直接不映射内核,虽然说页表设计0xffff是内核,但是实际上内核并不需要映射到任何地址空间,只有在发生异常或者系统调用时,才会临时映射内核代码和数据到当前的地址空间中进行处理。这样可以进一步提高系统的安全性,防止恶意程序通过漏洞访问内核空间。并且映射到哪里都是内核决定的,内核大可以只映射只读异常处理,然后映射自身到某个地址执行完毕后再卸载掉。

这样你能够接触到的就是异常处理和用户态的libc,但是异常处理就是简单的门结构+跳转,不会有太多危险。并且触发异常处理就已经把你从用户态踢出去了,根本没有机会继续搞事情,同时切换页表卸载用户态映射,让两边互不可见。

现在的kernel_pwn 打驱动的本质上还是利用内核态的漏洞进行攻击,而不是直接攻击内核本身。因为内核本身有严格的权限和保护机制,直接攻击内核难度较大。而驱动程序通常运行在内核态,拥有较高的权限,如果驱动程序存在漏洞,攻击者可以利用这些漏洞来提升权限,进而攻击内核。

哪怕看着没有越界保护,但是实际上你只能访问到被严格设计的某些特定函数中,而内核空间地址本身并不一定映射到任何物理内存,除非内核允许你访问某些特定的内存区域(比如说IO设备寄存器等)。所以攻击者即使能够通过漏洞访问内核空间地址,也不一定能够访问到有用的数据或者代码。

同时用页表除了可以分配逻辑连续的内存,还带来了更强的内存保护能力。通过页表,可以为不同的内存区域设置不同的访问权限(读、写、执行),从而防止非法访问和代码注入攻击。

十四、总结

这么长的文章终于写完了,感谢你的耐心阅读。讲真的页表制作那些代码折磨了好久才调试好,本来还想学iot安全的,莫名其妙就写到了操作系统内核开发。

(。・ω・。)ノ

posted @ 2026-02-04 11:44  归海言诺  阅读(69)  评论(0)    收藏  举报