一、ARM64 架构地址空间的「黄金分割」
ARM64(ARMv8-A)采用 48 位虚拟地址(Linux 默认配置),总空间为 256TB,分为高低两个 128TB 区域:
1. 地址空间整体布局
虚拟地址空间(48位,256TB)
┌───────────────────────────────────────┐
│ 用户空间(低128TB) │
│ 0x000000000000 ~ 0x000FFFFFFFFF │ (低地址段,用户进程使用)
├───────────────────────────────────────┤
│ 保留隔离区 │
│ 0x010000000000 ~ 0xFFFFEFFFFFFFFFFF │ (未使用,防止地址越界)
├───────────────────────────────────────┤
│ 内核空间(高128TB) │
│ 0xFFFF00000000 ~ 0xFFFFFFFFFFFFFFFF │ (高地址段,内核专属空间)
└───────────────────────────────────────┘
2. 核心划分依据
- 分界线:
0xFFFF00000000
(由内核配置CONFIG_ARM64_PAGE_OFFSET
决定) - 用户空间:低 128TB,地址最高位为
0b00...
(前 16 位为 0) - 内核空间:高 128TB,地址最高位为
0b111111...
(前 16 位为全 1)
二、图形化对比:用户空间 vs 内核空间
1. 地址范围与用途对比图
虚拟地址空间(ARM64, 48位)
低地址───────────────────────────────────────高地址
┌───────────────────────────────────────┐
│ 用户空间(128TB) │
│ ╔══════════════════════════════════╗ │
│ ║ 进程A的代码/数据/堆/栈 ║ │
│ ║ 进程B的代码/数据/堆/栈 ║ │
│ ║ ... ║ │
│ ╚══════════════════════════════════╝ │
│ │
├───────────────────────────────────────┤
│ 内核空间(128TB) │
│ ╔══════════════════════════════════╗ │
│ ║ 内核代码(.text/.data) ║ │
│ ║ 物理内存映射(memmap) ║ │
│ ║ 设备寄存器映射(ioremap) ║ │
│ ║ 动态分配内存(vmalloc) ║ │
│ ║ 全局内核数据(如task_struct) ║ │
│ ╚══════════════════════════════════╝ │
└───────────────────────────────────────┘
2. 关键差异点对比表
特征 | 用户空间地址 | 内核空间地址 |
---|---|---|
地址范围 | 0x000000000000 ~ 0x000FFFFFFFFF | 0xFFFF00000000 ~ 0xFFFFFFFFFFFFFFFF |
最高位标志 | 前 16 位为0b00... (低地址段) | 前 16 位为0b111111... (高地址段) |
访问权限 | 用户态受限(仅本进程可访问) | 内核态全权限(所有进程共享) |
页表机制 | 每个进程独立页表(地址隔离) | 全局页表(所有进程共享同一映射) |
特权标志 | 页表项设置PXN=1 (用户态禁止执行) | 页表项设置PXN=0 (内核态允许执行) |
典型场景 | 存储用户程序、堆、栈、动态库 | 存储内核代码、设备驱动、物理内存映射 |
三、技术实现:ARM64 如何隔离两个空间?
1. 页表项中的「权限开关」
ARM64 通过 页表项(PTE)标志位 控制访问权限,核心标志:
PXN
(Permission Execute Never):- 用户空间页表项:
PXN=1
→ 用户态下禁止执行该页的代码(防恶意代码注入)。 - 内核空间页表项:
PXN=0
→ 内核态下允许执行(如内核函数调用)。
- 用户空间页表项:
UXN
(User Execute Never):- 用户空间数据页:
UXN=1
→ 用户态下禁止执行数据页(防栈溢出攻击)。
- 用户空间数据页:
graph TD
A[用户态程序访问地址] --> B{地址 < 0xFFFF00000000 ?}
B -->|是| C[查询用户进程页表]
C --> D{页表项PXN=1 ?}
D -->|是| E[允许访问(用户空间数据/代码)]
D -->|否| F[触发页错误(用户态禁止执行)]
B -->|否| G[触发页错误(用户态禁止访问内核空间)]H[内核态程序访问地址] --> I{地址 >= 0xFFFF00000000 ?}
I -->|是| J[查询全局内核页表]
J --> K[允许访问(内核态无PXN限制)]
I -->|否| L[查询用户进程页表(需通过copy_from_user)]
3. 实战代码:判断地址所属空间
// ARM64内核空间起始地址(Linux默认)
#define KERNEL_SPACE_START 0xFFFF00000000ULLbool is_kernel_address(unsigned long addr) {// 检查地址是否位于高128TB(内核空间)return (addr & 0xFF0000000000ULL) == 0xFF0000000000ULL;
}// 示例:
unsigned long user_addr = 0x12345678ABCDULL; // 低地址,用户空间
unsigned long kernel_addr = 0xFFFF12345678ABCDULL; // 高地址,内核空间printf("user_addr属于%s空间\n", is_kernel_address(user_addr) ? "内核" : "用户");
// 输出:user_addr属于用户空间
四、64 位 vs 32 位 ARM 地址空间对比
特征 | ARM64(48 位) | ARM32(32 位,3:1 划分) |
---|---|---|
总空间 | 256TB | 4GB |
用户空间 | 低 128TB(0x000... ~ 0x00F...) | 低 3GB(0x00000000 ~ 0xBFFFFFFF) |
内核空间 | 高 128TB(0xFFF... ~ 0xFFFF...) | 高 1GB(0xC0000000 ~ 0xFFFFFFFF) |
地址标志 | 最高 16 位全 1(内核)/ 全 0(用户) | 最高 2 位为 11(内核,如 0xC0000000) |
隔离方式 | 物理地址标签 + PXN/UXN 硬件保护 | 软件页表权限 + PAGE_OFFSET 划分 |
五、典型场景:进程切换时的地址空间变化
当用户态进程通过 syscall
进入内核时:
- 地址切换:
- 用户态栈地址(如
0x0000...
)→ 内核态栈地址(如0xFFFF...
)。
- 用户态栈地址(如
- 权限升级:
- CPU 从 EL0(用户态) 切换到 EL1(内核态),解除 PXN/UXN 限制。
- 页表共享:
- 用户空间页表继续使用,但内核空间通过全局页表访问高地址段。
总结:3 步快速区分 ARM64 地址空间
- 看最高位:前 16 位是否为
0b111111...
(内核空间)或0b00...
(用户空间)。 - 查访问权限:用户态程序能否直接访问(能→用户空间,否则→内核空间)。
- 看上下文:代码是否运行在内核态(如中断处理、系统调用),若是则为内核空间地址。
通过这种「地址范围 + 硬件权限 + 执行环境」的三重判断,就能准确区分 ARM64 架构下的用户空间与内核空间地址。