从单片机到操作系统学习笔记
前言
之前学习了二进制,最近打算向单片机方向进军,于是学着学着莫名其妙学到了系统安全上,顺手写了个操作系统,现在系统学到了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
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"]
总之就是一些单片机的特有依赖
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");
}
memory.x:内存布局文件
这个文件定义了单片机的内存布局,告诉链接器如何分配内存:
MEMORY
{
FLASH : ORIGIN = 0x08000000, LENGTH = 64K
RAM : ORIGIN = 0x20000000, LENGTH = 20K
}
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();
}
}
-
svd/STM32F103.svd:设备描述文件
这个文件是单片机的设备描述文件,包含寄存器和外设的信息,可以从这里下载。 -
.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"
.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
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部分定义了FLASH和RAM两个区域,分别对应单片机的闪存和SRAM。
上面的MEMORY比较好理解,大概就是:把这些东西烧录到对应位置即可。
下面的SECTIONS部分定义了各个段的布局,但是暂时不用细究,后面会讲到。
如果你对二进制比较熟悉,可以点开看看:
点击查看详细内容
本质上,ELF文件是由多个段(section)组成的,每个段包含不同类型的数据或代码。链接脚本通过定义这些段的布局,告诉链接器如何将它们放置在内存中。
经典的比方说.data、.text、.bss等段,分别存放已初始化的数据、代码和未初始化的数据。
但其实这些名字只是约定俗成的,链接脚本可以定义任意名称的段,并指定它们的属性和位置。比方说我们在打二进制攻防和二进制补丁时候,会直接增加一个段.fox_code,用来存放我们自己的代码。这里的.user_elf和.user_data段就是类似的概念。
至于里面乱七八糟的LOADADDR、ADDR、PROVIDE等指令,都是链接脚本的语法,用来定义符号和地址。具体可以参考GNU链接器脚本文档。
不用管,因为我们在下一章就要手写他们了。
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
- 链接 → 可执行 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 调试或烧录
- 烧录
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 0x80或syscall来实现的,而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 // 异常返回
在上面的代码中,我们定义了两个特权操作:
load_user_elf:加载用户代码和数据到指定内存区域svc_set_r0_value:将寄存器r0设置为特定值0x12345678
在用户代码中,我们可以通过执行svc #1来调用load_user_elf,通过执行svc #2来调用svc_set_r0_value。
同样的,要进入用户态,我们需要伪造一个调用栈,并切换到进程栈指针(PSP)和非特权态(control寄存器设置为3),然后通过异常返回(EXC_RETURN)跳转到用户代码执行。
这里解答一下可能会蒙的问题:
-
什么是特权态和非特权态?
从cpu视角下,本质上就是允不允许访问某些特定寄存器或执行某些特权指令。特权态可以访问所有资源,而非特权态则受到限制。 -
什么是进程栈指针(PSP)和主栈指针(MSP)?
MSP是系统默认使用的栈指针,通常用于内核态和中断处理。而PSP则是为用户态设计的栈指针,允许用户代码使用独立的栈空间。 -
为什么要伪造调用栈而不是直接跳转?
因为你直接跳转过去不还是特权吗,跳了个寂寞(笑)。本质上要通过cpu定义的异常返回机制(EXC_RETURN)来切换到用户态,这个机制会自动恢复寄存器状态,包括程序计数器(PC)和栈指针(PSP)。那么栈哪里来呢,当然是伪造一个放在用户栈空间里咯。(第一次进入用户态,后面用真的即可),返回地址就填用户程序入口地址,异常返回时候自然执行用户程序。
题外话:
其实进入内核态也是类似的,cpu自动保存上下文,但是你在自己栈上其实可以伪造一个上下文,然后通过异常返回跳转到内核态执行特权代码,达到利用目的。恭喜你发明了内核pwn的基本原理。
你可能会想,为什么理论上内核态代码不能被用户态代码利用呢?答案是:设计不当。只要设计不当,内核态代码就有可能被用户态代码利用。
还有例外情况:有没有想过ELF标准内存模型(忘了的我贴个图)为什么不能直接跳转到内核代码执行呢,打过pwn的都知道:直接跳到内核代码执行会直接被kill掉,但是从现在这个角度来看,理论上来说都在一个内存,只要我不利用某些特权代码,我就可以执行吧。。。当然,在当前的设计下是可能的,但是下一章我们会看到,现代操作系统通过内存隔离等机制来防止这种情况发生。

回到正题:
调用特权异常后,要经过分流,根据调用的栈类型和调用号分流,或者换一个更熟悉的说法:“门结构”进行分类跳转。
恭喜你解锁了成就:特权异常!
理论上你可以把剩下的异常全部写个遍,然后写一个简单的内存分配器,加载用户代码就是最早期的操作系统了。ψ(`∇´)ψ
二、从单片机到操作系统
在上一章我们写了特权异常,但是有一个问题我们没有解决一个问题:如果用户可以直接访问到内核代码,尽管调用不了某些寄存器,但是依然可以通过特权异常利用内核代码,而且可以直接刷写内存(非特权),以及读写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调试。
在这里我选择了armv8(aarch64)架构,因为它支持64位,功能更强大,也更接近现代处理器的架构,而且它的页表结构也比较简单,适合我们学习。
三、QEMU?启动!
这里需要科普一下qume的模拟,它分为两种模式:
- 用户模式模拟(User Mode Emulation):这种模式下,QEMU只模拟用户空间的应用程序,不涉及内核态代码。适合运行单个应用程序。
- 系统模式模拟(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
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库。
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");
}
}
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;
}
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
.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
.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 可执行文件路径
.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,在系统最开始没有串口输出时候会卡住
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
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文件是干什么的:
-
把段放到该放的地方,比方说
.kernel_text就是把所有的代码段放到这里(比方说.text.boot,*.text,.text.*),然后对齐到0x1000(4KB)边界。在某些情况下预留空间. += 0x1000;意思是预留1页(4KB)空间。 -
定义一些符号,比如
_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_handler,rust_exception_current_spx_sync函数。
同时我们使用
#![no_std]
#![no_main]
来禁用标准库和main
同时我们发现了一些特殊的函数
#[unsafe(no_mangle)]
pub extern "C" fn
#[no_mangle]:告诉编译器不要对函数名进行重命名,这样我们在汇编中就可以通过函数名直接调用它。extern "C":指定函数使用C语言的调用约定,确保函数在汇编和Rust之间正确调用。unsafe:表示这个函数包含不安全代码,需要调用者保证安全性。
好了,主要的准备工作都完成了。
四、从内存开始(PMM)
理论上我们刚刚的操作和单片机上没有任何区别,顶多复杂了一点点,但是我们并没有实现内存隔离。要实现内存隔离,首先我们要知道内存有多少、在哪里、有没有使用。
这个时候就有长得帅的小伙伴跳出来甩出来一张图:

这我知道啊,可用的空间不就是总内存 - 内核占用 - 已分配 - 不可用吗?
的确,但是问题是:
- 总内存多少?
- 内核占用多少?
- 已分配多少?
- 不可用多少?
对的,就是四个都不知道,哪怕通过汇编符号获取内核占用的地址范围,也不知道总内存是多少,更别说已分配和不可用了。而且,给出的这张内存分布图真的正确吗,我就问一个问题好了,正常的用户程序一般运行在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,
);
}
答案是:
- 我们先解决总内存的问题,通过解析设备树获取所有的内存区域,然后把这些区域存储到预留的内存区域中。这一块比较小,我们假设最多64个内存区域(够用了),然后通过遍历设备树中的
/memory节点,获取所有的内存区域,存储到预留的内存区域中。 - 然后保留保留内存区域,这一块也比较小,我们同样假设最多64个保留内存区域,然后通过遍历设备树中的
/reserved-memory节点,获取所有的保留内存区域,存储到预留的内存区域中。 - 最后我们计算总内存大小和基地址,然后初始化物理内存管理器,使用bitset一个bit存储一页(0x1000 = 4KB),标记已使用的内存区域。
通过这种方式,我们就可以动态地获取内存区域的数量,而不需要预定义一个固定大小的数组。在这里我避免bitset过大,直接假设512MB内存。
对的,解决问题的方法就是这么简单粗暴:我就假设512MB内存,够用就行,如果有多的内存,我启动到一定阶段后再次去获取内存区域,然后动态扩展内存管理器。
总之一通操作下来,我们可以管理512MB内存了。然后我们写一个简单的alloc_page、free_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,
});
而不是挨个页表项去设置,那可太地狱了。
八、准备一些工具
思考来我们其实准备两种工具:
- 页表管理器(Page Table Manager):用于创建和管理页表结构,
- 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(物理地址),这就是页表的作用。那页表是怎么做到的呢?答案是多级页表。
页表通过本身存储的数据,就能把虚拟地址转换为物理地址,详细来说:
- 页表中存储了一个数字
- 取头部的几个bit作为索引,找到第一级页表项
- 取接下来的几个bit作为索引,找到第二级页表项
- 依此类推,直到最后一级页表项,此时最后一集页表加上偏移就是物理地址
打个比方:
想象一下我们是如何测量长度的:
- 拿一把米尺,测量出米的数量(一级页表),写到头,比方说 10.
- 拿一把分尺,测量出厘米的数量(二级页表),写到头,比方说 10.2
- 拿一把厘米尺,测量出毫米的数量(三级页表),写到头,比方说 10.23
- 最后在厘米尺上读出最小刻度毫米(偏移),比如说 10.234
最终我们就得到了一个长度 10.234 米,这个过程就类似于多级页表的地址转换过程。
反过来,当你拿到了一个虚拟地址时,你可以通过页表的多级索引,逐级找到对应的物理地址。
还是这个例子,比方说你拿到了一个虚拟地址 0x12345678,你可以这样做:
- 取虚拟地址的高位部分作为一级页表的索引,比如 0x12
- 使用这个索引去一级页表中查找对应的页表项,得到二级页表的地址
- 取虚拟地址的中间部分作为二级页表的索引,比如 0x34
- 使用这个索引去二级页表中查找对应的页表项,得到三级页表的地址
- 取虚拟地址的低位部分作为三级页表的索引,比如 0x56
- 使用这个索引去三级页表中查找对应的页表项,得到物理页的地址
- 最后加上虚拟地址的偏移部分,比如 0x78,得到最终的物理地址
通过这种方式,页表能够高效地将虚拟地址映射到物理
还是以尺子作为例子
- 你拿到一个虚拟地址,就像你拿到一个长度值 (20.325)
- 你先用米尺测量,找到对应的米数(一级页表) 20
- 然后用分尺测量,找到对应的厘米数(二级页表) 3
- 然后用厘米尺测量,找到对应的毫米数(三级页表) 2
- 最后加上偏移,得到最终的物理地址 (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;
}
}
这里其实坐牢了很久,主要是页表表面上看是一个找零问题:
- 用大页表吃掉大块内存
- 用小页表吃掉小块内存
但是实际情况是:大页表不一定能用,因为地址可能没对齐,所以只能用小页表,而小页表又很浪费内存,所以需要一个折中的办法,就是尽可能用大页表,然后剩下的部分再用小页表。
也就是用人话说:带对齐的贪心找零问题
- 计算当前级别的页表粒度(4K/2M/1G) (固定桶大小)
- 检查当前虚拟地址和物理地址是否对齐该粒度 (这个桶是否能够真好装下)
- 如果对齐且剩余大小足够大,使用该粒度进行映射 (正好装下)
- 否则,递归进入下一级页表,继续尝试更小粒度映射 (用多个小桶装,本桶记录小桶位置而不是它的位置)
然后我们就能建立页表了,需要注意页表标识位的问题。
十、准备切换页表
我们已经有了页表管理器,但是我们要设计系统布局,我们希望:
- 内核空间:高地址空间
- 用户空间:低地址空间
- 能够有一个方法让内核便捷的访问物理内存
所以我们使用半高映射,也就是把内核映射到高地址空间,同时把物理内存也映射到高地址空间的一部分,这样子内核就能通过高地址空间访问物理内存了。
/// 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
}
这里其实是我们第一张页表:
- 把物理内存映射到高地址空间的一部分
- 把内核各段映射到对应的高地址空间位置
- 把物理内存映射到低地址空间(恒等映射)
- 把本身做恒等映射
第四点很重要,因为在调试的时候,没办法保证是切换页表错了,还是页表本身错了,还是高地址映射错了。需要尊从一个原则:代码运行在哪里,哪里就要在页表切换前后保持不变
十一、真正进入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 启用后需要注意的事项。
- 我们先要求PMM封存此时的物理内存使用情况作为不可分配释放的内存区域,即抛弃对老物理内存的管理,承认为内核保留
- 我们要更换新页表,不再映射内核在低地址空间的恒等
- 我们要更新工具,用新工具走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扩展并自动分配内存。本体对外提供kmalloc和kfree接口(这里作者并没有实现一个高度可可用的,只是简单实现,就不贴出来了)同时提供小内存分配页表和大内存分配页表,分别用于小内存分配和大内存分配。
之后如果愿意,实现各种异常跳转处理、同步异常、页表缺失异常等。并逐步提供用户态加载切换(简单来说就是:映射对应用户地址,拷贝过去,异常退出,切换页表),异常调用(调用到处理代码,切换页表,内核),堆分配器。。。。
这些其实已经没有新东西了,顶多是之前东西的排练组合。
现在再来看看这张图:

现在我们从上往下解析:
- 内核空间
准确来说分为两段:
- 0xffff_8000_0000_0000 ~ 0xffff_8080_0000_0000 : 视图 + 空洞
- 0xffff_8080_0000_0000 ~ 0xffff_80ff_ffff_ffff : 内核空间+内核代码
- 用户空间
- 0x0000_0000_0040_0000 ~ 0x0000_7fff_ffff_ffff : 用户空间
- 恒等映射
- 0x0000_0000_0001_0000 ~ 0x0000_0000_0040_0000 : 恒等映射物理内存(包括io设备,系统调用段等)
- 故意不映射段
- 0x0000_0000_0000_0000 ~ 0x0000_0000_0001_0000 : 故意不映射,防止 NULL 指针访问
然后用户空间本质上是套娃,用户空间的页表映射到用户空间地址,而内核空间的页表映射到内核空间地址。理论上你爱怎么映射都行,只要不冲突就行。
那么共享库呢?共享库本质上是把同一份代码映射到不同的用户空间地址即可。因为代码是只读的,所以可以多用户共享。
内核实际上并没有运行在任何逻辑位置,只是通过页表被映射到了某个位置而已。内核代码可以运行在0地址空间,也可以运行在高地址空间,只要页表映射正确即可,甚至不映射,直到异常发生再映射也行。
恭喜你发明了现代系统中最重要的组件之一:虚拟内存管理器(VMM)。其中NX和页表等硬件保护是现代操作系统安全的基石,没有之一,然后再通过软件层的权限管理(用户态/内核态)等机制,构建起一个相对安全的运行环境。
现在才真正实现了内存管理的分离,内核不再直接操作物理内存,而是通过虚拟内存进行操作,这样就可以实现更复杂的内存管理策略,比如说内存保护、内存共享、内存映射文件等高级功能。
十三、 后日谈
那么,为什么libc可以被调试,但是一旦飘到内核立刻被kill呢,因为实际上libc只是一个比较特殊的用户态程序,封装了上层系统调用接口而已,并没有直接操作硬件资源的权限,所以在用户态运行是安全的。一旦进入内核态,libc的某些操作可能会触发内核的保护机制,导致程序被终止。
同时,操作系统理论上可以直接不映射内核,虽然说页表设计0xffff是内核,但是实际上内核并不需要映射到任何地址空间,只有在发生异常或者系统调用时,才会临时映射内核代码和数据到当前的地址空间中进行处理。这样可以进一步提高系统的安全性,防止恶意程序通过漏洞访问内核空间。并且映射到哪里都是内核决定的,内核大可以只映射只读异常处理,然后映射自身到某个地址执行完毕后再卸载掉。
这样你能够接触到的就是异常处理和用户态的libc,但是异常处理就是简单的门结构+跳转,不会有太多危险。并且触发异常处理就已经把你从用户态踢出去了,根本没有机会继续搞事情,同时切换页表卸载用户态映射,让两边互不可见。
现在的kernel_pwn 打驱动的本质上还是利用内核态的漏洞进行攻击,而不是直接攻击内核本身。因为内核本身有严格的权限和保护机制,直接攻击内核难度较大。而驱动程序通常运行在内核态,拥有较高的权限,如果驱动程序存在漏洞,攻击者可以利用这些漏洞来提升权限,进而攻击内核。
哪怕看着没有越界保护,但是实际上你只能访问到被严格设计的某些特定函数中,而内核空间地址本身并不一定映射到任何物理内存,除非内核允许你访问某些特定的内存区域(比如说IO设备寄存器等)。所以攻击者即使能够通过漏洞访问内核空间地址,也不一定能够访问到有用的数据或者代码。
同时用页表除了可以分配逻辑连续的内存,还带来了更强的内存保护能力。通过页表,可以为不同的内存区域设置不同的访问权限(读、写、执行),从而防止非法访问和代码注入攻击。
十四、总结
这么长的文章终于写完了,感谢你的耐心阅读。讲真的页表制作那些代码折磨了好久才调试好,本来还想学iot安全的,莫名其妙就写到了操作系统内核开发。
(。・ω・。)ノ

浙公网安备 33010602011771号