内存管理【3】

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以上地址。

关键机制:

  1. CPU 运行在不同 Exception Level(EL)

    • EL0:用户态(非特权)
    • EL1:内核态(特权)
    • EL2:Hypervisor
    • EL3:Secure Monitor
  2. MMU 页表项(PTE)包含权限位

    • 控制 谁可以访问(EL0 vs EL1+)
    • 控制 如何访问(Read/Write/Execute)

2.2 ARMv8 页表项(PTE)中的权限字段详解

以 64 位页表项(L3 PTE)为例:

AP 字段组合

注意

  • AP=00 表示 EL0 无访问权限(不是"只读"!)
  • 要让用户程序访问某页,必须设为 0111

执行权限(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;
}

发生什么?

  1. CPU 在 EL0 执行 ldr x0, [x1]
  2. MMU 查页表,发现该页的 AP=00(EL0 无权限)
  3. 触发 Data Abort 异常
  4. 内核收到 SIGSEGV 信号
  5. 进程被杀死:Segmentation fault

这就是 内存保护 的体现!

相关推荐
i橡皮擦2 小时前
使用gamedig 查询恐龙岛TheIsle游戏服务器
运维·服务器·游戏·steam·恐龙岛·the isle
pcc_is_world2 小时前
Nginx HTTPS服务器搭建与认证配置
服务器·nginx·https
shizhan_cloud2 小时前
Shell 变量进阶知识
linux·运维
火山灿火山2 小时前
Qt常用控件(一)
服务器·qt
liulilittle3 小时前
C++ 并发双阶段队列设计原理与实现
linux·开发语言·c++·windows·算法·线程·并发
YFLICKERH3 小时前
【Linux系统】磁盘文件系统
linux
森G3 小时前
五、Linux字符设备驱动
linux·arm开发·c++·ubuntu
生信大表哥3 小时前
Claude Code / Gemini CLI / Codex CLI 安装大全(Linux 服务器版)
linux·python·ai·r语言·数信院生信服务器
繁星蓝雨3 小时前
我与C++的故事(杂谈)
开发语言·c++