1. 概念
MMU
现代操作系统普遍采用虚拟内存管理(Virtual Memory Management)机制,这需要处理器中的MMU(Memory Management Unit,内存管理单元)提供支持。
PA
如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为PA(Physical Address,以下简称PA),如下图所示。
VA
如果处理器启用了MMU,CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将VA映射成PA,如下图所示。
如果是32位处理器,则内地址总线是32位的,与CPU执行单元相连(图中只是示意性地画了4条地址线),而经过MMU转换之后的外地址总线则不一定是32位的。也就是说,虚拟地址空间和物理地址空间是独立的,32位处理器的虚拟地址空间是4GB,而物理地址空间既可以大于也可以小于4GB。
2.内存分页

- CPU 一旦开启MMU,MMU是个硬件。CPU就只知道虚拟地址了。如果地址是32位,0x12345670 。
- 假设MMU的管理是把每一页的内存分成4K,那么其中的670是页内偏移,作为d;0x12345 是页号,作为p。通过虚拟地址去查对应的物理地址,用0x12345去查一张页表,页表(Page table)本身在内存。
- 硬件里有寄存器,记录页表的基地址,每次进程切换时,寄存器就会更新一次,因为每个进程的页表不同。
- CPU一旦访问虚拟地址,通过页表查到页表项,页表项记录对应的物理地址。
总结:一旦开启MMU,CPU只能看到虚拟地址,MMU才能看到物理地址。
虚拟地址是指针,物理地址是个整数。内存中的一切均通过虚拟地址来访问。
说明: 去内存里读取页表会比较慢,CPU里有个高速单元TLB ,它是页表的高速缓存。CPU就不需要在内存里读页表,直接在tlb中读取,从虚拟地址到物理地址的映射。如果TLB中读取不到,才回到内存里读取页表映射,并且在tlb中命中。
虚拟地址:0x12345 670 --> 1M
物理地址:1M+670 MMU去访问这个物理地址。
内存的映射以页为单位。
Pagefault,是CPU提供的功能。两种情况会出现Pagefault。
- 一是,CPU通过虚拟地址没有查到对应的物理地址。
- 二是,MMU没有访问物理地址的权限。
2.1 页表(Page table)记录的页权限
核心原理:MMU + CPU 异常级别 = 内存保护
物理内存本身无权限概念 ------ 它只是字节阵列。
权限控制完全由 MMU + CPU 模式协同实现。每个MMU中的页表项,除了有虚拟地址到物理地址的映射之外,还可以标注这个页的 RWX权限 和 kernel和user模式权限(用户空间,内核空间读取地址的权限),它们是内存管理两个的非常重要的权限。
- 一是,这一页地址的RWX权限 ,标记这4k地址的权限。一般用来做保护。
- 二是,MMU的页表项中,还可以标注这一页的地址:可以在内核态访问,还是只能在用户态访问。用户一般映射到0~3G,只有当CPU陷入到内核模式,才可以访问3G以上地址。
关键机制:
CPU 运行在不同 Exception Level(EL)
- EL0:用户态(非特权)
- EL1:内核态(特权)
- EL2:Hypervisor
- EL3:Secure Monitor
MMU 页表项(PTE)包含权限位
- 控制 谁可以访问(EL0 vs EL1+)
- 控制 如何访问(Read/Write/Execute)
2.2 ARMv8 页表项(PTE)中的权限字段详解
以 64 位页表项(L3 PTE)为例:

AP 字段组合

注意:
AP=00表示 EL0 无访问权限(不是"只读"!)- 要让用户程序访问某页,必须设为
01或11
执行权限(NX bit)
- UXN=1 → 用户态不可执行(防 shellcode)
- PXN=1 → 内核态不可执行(防 ROP 攻击)
3.用户态 → 内核态切换(系统调用)
场景:用户程序调用 write(fd, buf, size)
用户代码:
write(1, "hello", 5); // 运行在 EL0
-
触发 SVC 指令(Supervisor Call):
1svc #0 // 软中断,陷入 EL1 -
CPU 自动:
- 切换到 EL1
- 保存返回地址到
ELR_EL1 - 跳转到 异常向量表 (
VBAR_EL1 + 0x400)
-
内核处理:
- 验证参数(
buf是否在用户空间?) - 通过
copy_from_user()安全拷贝数据 - 执行写操作(此时可访问所有内存)
- 验证参数(
-
返回用户态:
eret // 从 EL1 返回 EL0,恢复 PC 和 PSTATE
关键点:
- 用户传入的指针(如
buf)必须指向用户空间(EL0 可访问区域)- 内核通过 权限检查 + 地址范围检查 防止越权访问
4.权限如何配置
限由 操作系统在建立页表时设置 ,最终写入 页表项(PTE)。
Linux 内核中的关键函数:
页表项权限宏(arch/arm64/include/asm/pgtable.h)
// 用户可读写
#define PAGE_COPY __pgprot(_PAGE_DEFAULT | PTE_RDONLY | PTE_USER)
#define PAGE_SHARED __pgprot(_PAGE_DEFAULT | PTE_USER)
// 内核只读
#define PAGE_KERNEL_RO __pgprot(_PAGE_DEFAULT | PTE_RDONLY)
// 用户可执行(代码段)
9#define PAGE_EXEC __pgprot(_PAGE_DEFAULT | PTE_USER | PTE_UXN_CLEAR)
其中:
PTE_USER→ 设置 AP[2:1] = 11(EL0 RW, EL1 RW)PTE_RDONLY→ 设置 AP[2:1] = 01(EL0 R, EL1 RW)PTE_UXN_CLEAR→ UXN=0(允许用户执行)
创建 VMA 时指定权限(mmap)
// mmap(PROT_READ | PROT_EXEC, MAP_PRIVATE, fd, 0);
// 内核会设置 PTE: AP=01 (R), UXN=0 (可执行)
内核直接映射(如 ioremap)
void __iomem *ioremap(phys_addr_t phys, size_t size);
// 返回的虚拟地址:AP=00(仅 EL1 可访问),UXN=1(不可执行)
5. 实际例子:为什么用户不能访问内核内存?
场景:用户程序尝试读取内核地址
#include <stdio.h>
int main() {
volatile char *p = (char*)0xFFFF000000000000; // 内核地址
printf("%c\n", *p); // 尝试读
return 0;
}
发生什么?
- CPU 在 EL0 执行
ldr x0, [x1] - MMU 查页表,发现该页的 AP=00(EL0 无权限)
- 触发 Data Abort 异常
- 内核收到 SIGSEGV 信号
- 进程被杀死:
Segmentation fault
这就是 内存保护 的体现!

