一、虚拟地址空间划分
ARM32 系统采用 4GB 虚拟地址空间,分为用户空间和内核空间:
用户空间:0x00000000 ~ 0xBFFFFFFF(3GB),供用户进程使用。
内核空间:0xC0000000 ~ 0xFFFFFFFF(1GB),由内核独占。
划分原因:
ARMv7 架构通过 CPU 运行模式(如用户态 usr 和内核态 svc)隔离访问权限。用户进程通过系统调用进入内核态后,才能访问内核空间。

二、内核空间详细布局
内核空间按功能分为以下区域(具体地址范围因配置而异):
1. 线性映射区(Low Memory)
范围:0xC0000000 ~ 0xC0000000 + high_memory(默认 high_memory = 760MB)。
作用:直接映射物理内存的低端区域(物理地址 0 ~ high_memory),虚拟地址与物理地址通过 固定偏移量(PAGE_OFFSET = 0xC0000000)转换。
特点:
物理地址与虚拟地址一一对应:虚拟地址 = 物理地址 + PAGE_OFFSET。
用于内核代码段(.text)、数据段(.data)、未初始化数据段(.bss)等核心内存。
示例:若物理内存为 1GB,则 high_memory 设为 760MB,剩余 264MB 通过高端内存机制管理。
2. vmalloc 动态映射区
范围:线性映射区结束地址至 0xEF800000(约 240MB ~ 936MB)。
作用:分配虚拟连续但物理离散的内存(如 DMA 缓冲区、动态模块加载)。
特点:
物理页通过 vmalloc() 动态分配,访问需遍历页表,性能低于线性映射区。
典型用例:大块非连续内存需求或物理内存碎片化场景。
3. Fixmap 固定映射区
范围:0xFFF00000 ~ 0xFFFE0000(896KB)。
作用:预留给固定用途的虚拟地址(如设备寄存器映射、临时页表操作)。
特点:
虚拟地址固定,物理地址在运行时动态绑定。
常用于早期启动阶段(MMU 启用前)或外设寄存器访问。
4. 模块加载区(Modules)
范围:0xBF000000 ~ 0xBFE00000(14MB)。
作用:加载内核模块(.ko 文件)的代码和数据。
5. pkmap 持久映射区
范围:0xBFE00000 ~ 0xC0000000(2MB)。
作用:临时映射高端内存页,供内核长期使用(如用户空间页表项)。
6. 异常向量表
地址:0xFFFF0000。
作用:存储 CPU 异常处理函数入口地址(如中断、缺页异常)。
三、典型内存布局示例
某 ARM32 平台启动日志显示的内存布局如下:
Virtual kernel memory layout: vector : 0xffff0000 - 0xffff1000 (4 kB) fixmap : 0xffc00000 - 0xfff00000 (3072 kB) vmalloc : 0xf0800000 - 0xff800000 (240 MB) lowmem : 0xc0000000 - 0xf0000000 (768 MB) pkmap : 0xbfe00000 - 0xc0000000 (2 MB) modules : 0xbf000000 - 0xbfe00000 (14 MB)
Lowmem:768MB,直接映射物理内存低端区域。
vmalloc:240MB,覆盖大部分动态分配需求。
Fixmap:3MB,用于特殊固定映射场景。
四、高端内存(High Memory)管理
当物理内存超过 high_memory(如 1GB 物理内存时,high_memory = 760MB),剩余物理内存需通过 高端内存机制 动态映射:
动态映射方式:
使用 kmap() 或临时映射窗口(如 pkmap)访问高端内存页。
映射后需及时释放,避免占用有限的内核虚拟地址空间。
典型场景:
用户进程的页表项(Page Table Entries)映射。
内核临时访问大块物理内存(如文件系统缓存)。
五、关键机制与优化
线性映射与性能:
线性映射区通过直接地址转换(加减 PAGE_OFFSET)实现快速访问,适用于内核核心数据结构。
连续映射标志(PTE_CONT):
若虚拟地址、物理地址和长度按 64KB 对齐,启用连续映射标志,合并多个 PTE 项以减少 TLB 未命中。
Fixmap 与早期启动:
在 MMU 启用前,通过 Fixmap 窗口临时映射物理内存,完成页表初始化(参考 head.S 启动代码)。
六、实际应用场景
驱动开发:外设寄存器需通过 ioremap() 映射到内核空间,访问时需注意地址对齐和缓存策略。
内存调试:通过 /proc/iomem 或内核日志检查内存布局是否符合预期。
性能调优:减少 vmalloc 使用频率,优先使用线性映射区提升访问效率。
七、ARM32 用户虚拟地址空间划分
用户进程的 3GB 虚拟地址空间(0x00000000 ~ 0xBFFFFFFF)按功能分为以下区域(以 Linux 内核默认配置为例):
1. 代码段(Text Segment)
起始地址:0x00008000(默认 ELF 入口地址)。
作用:存储可执行代码(如程序指令、只读数据)。
特点:只读权限,不可动态修改。
2. 数据段(Data Segment)
范围:代码段结束地址至堆起始地址。
作用:存储全局变量、静态变量(已初始化的 .data 段和未初始化的 .bss 段)。
特点:可读写,.bss 段初始化为零。
3. 堆(Heap)
起始地址:数据段结束地址(动态增长)。
作用:动态分配内存(如 malloc() 或 new)。
增长方向:低地址 → 高地址。
特点:由 brk/sbrk 系统调用扩展堆顶指针。
4. 内存映射区(Memory Mapping Region)
布局策略:
传统布局:起始地址 TASK_UNMAPPED_BASE(通常为 0x40000000),向高地址增长。
新布局:起始地址紧邻栈底(栈基址 - 随机偏移),向低地址增长。
作用:
映射动态库(如 libc.so)、文件映射(mmap)、共享内存。
默认启用地址随机化(ASLR),起始地址附加随机偏移。
5. 栈(Stack)
起始地址:STACK_TOP = 0xBFFFFFFF(默认栈顶)。
作用:存储局部变量、函数调用上下文。
增长方向:高地址 → 低地址。
特点:默认启用栈随机化(栈基址减去随机偏移)。
6. 环境变量与参数区
位置:栈底下方(如 0xBFFFFFFF - 随机偏移)。
作用:存储命令行参数(argv)、环境变量(envp)。
八、案例分析:运行一个 C 程序的虚拟地址布局
以执行 /bin/hello 程序为例,通过 pmap 或 /proc/[pid]/maps 查看其虚拟地址布局:
地址范围 权限 用途 00008000-00009000 r-xp /bin/hello(代码段) 0000a000-0000b000 rw-p .data(数据段) 00020000-00040000 rw-p [heap](堆) B6E00000-B6F00000 rw-p 匿名映射(动态内存分配) B6F00000-B6F21000 r-xp /lib/libc.so.6(动态库代码) B6F21000-B6F23000 rw-p /lib/libc.so.6(动态库数据) B6F23000-B6F25000 rw-p 匿名映射(动态库 BSS) B6FFE000-B7000000 rw-p [stack](栈)
1.关键特征解析
代码段与数据段:
代码段(0x00008000)仅包含可执行指令。
数据段(0x0000a000)存储全局变量和静态变量。
堆与动态内存:
堆(0x00020000)通过 malloc() 动态扩展。
匿名映射区(B6E00000)可能用于大块内存分配(如 mmap(MAP_ANONYMOUS))。
动态库加载:
libc.so 的代码和数据段映射到内存映射区,体现共享库的加载机制。
栈与随机化:
栈区(B6FFE000)因 ASLR 随机偏移,起始地址低于 0xBFFFFFFF。
3.示例程序:简单C程序的内存布局
// hello.c
#include <stdio.h>
#include <stdlib.h>
int global_init = 10; // 数据段(.data)
const int global_ro = 20; // 只读数据段(.rodata)
int global_uninit; // BSS段
int main() {
int local_stack = 30; // 栈
int *heap_var = malloc(sizeof(int)); // 堆
*heap_var = 40;
printf("Text: %p\n", main);
printf("Data: %p\n", &global_init);
printf("ROData: %p\n", &global_ro);
printf("BSS: %p\n", &global_uninit);
printf("Heap: %p\n", heap_var);
printf("Stack: %p\n", &local_stack);
while(1); // 保持进程运行以便查看 /proc/<pid>/maps
return 0;
}
1.编译与运行
arm-linux-gnueabi-gcc -static test.c -o test # 静态编译(简化地址布局)
./test &
cat /proc/$(pidof test)/maps # 查看内存映射
2. 输出结果示例
00008000-0000a000 r-xp 00000000 00:01 1234 /test # 代码段
0000a000-0000b000 rw-p 00002000 00:01 1234 /test # 数据段(.data)
0000b000-0000c000 rw-p 00000000 00:00 0 [anon] # BSS段
00010000-00400000 rw-p 00000000 00:00 0 [heap] # 堆
40000000-40001000 r-xp 00000000 00:01 5678 /lib/libc.so.6 # 动态库代码
40010000-40020000 rw-p 00000000 00:01 5678 /lib/libc.so.6 # 动态库数据
bef00000-bf000000 rw-p 00000000 00:00 0 [stack] # 栈
3. 地址解释
代码段:0x00008000(起始地址由可执行文件格式决定)。
数据段:0x0000a000(包含 .data 和 .rodata,静态编译后可能合并)。
堆:0x00010000 ~ 0x00400000(动态扩展)。
动态库:0x40000000 映射 libc.so 的代码和数据。
栈:0xbef00000 ~ 0xbf000000(接近用户空间顶部)。
十、布局机制与内核交互
1. 地址空间创建
ELF 加载:load_elf_binary() 初始化代码段、数据段,并设置堆和栈的初始地址。
内存映射区策略:arch_pick_mmap_layout() 选择传统或新布局(默认新布局)。
2. 随机化机制
栈随机化:STACK_TOP 减去随机偏移(通过 arch_randomize_brk() 生成)。
内存映射区随机化:起始地址附加或减去随机值(通过 mmap_base 计算)。
3. 动态扩展
堆扩展:调用 brk() 调整堆顶指针。
内存映射:mmap() 动态分配匿名页或文件映射。
十一、与 ARM64 的对比
特性 ARM32 ARM64
用户空间范围 0x00000000 ~ 0xBFFFFFFF (3GB) 0x0000000000000000 ~ 0x0000FFFFFFFFFFFF (256TB)
内核空间范围 0xC0000000 ~ 0xFFFFFFFF (1GB) 0xFFFF000000000000 ~ 0xFFFFFFFFFFFFFFFF (256TB)
高端内存 存在(物理内存 > 760MB) 不存在(48 位寻址足够覆盖)
内存映射布局 传统或新布局(默认新布局) 统一线性映射
十二、调试与验证工具
查看虚拟地址布局:
cat /proc/[pid]/maps # 显示进程内存映射
pmap [pid] # 格式化输出内存区域
ASLR 控制:
echo 0 > /proc/sys/kernel/randomize_va_space # 关闭随机化
echo 2 > /proc/sys/kernel/randomize_va_space # 启用完全随机化
总结
ARM32 应用程序的虚拟地址空间通过 代码段、数据段、堆、栈、内存映射区 的模块化划分,兼顾静态分配与动态扩展需求。内核通过 布局策略选择(传统/新布局)和 地址随机化 机制,平衡性能与安全性。实际调试中,结合 /proc/[pid]/maps 和动态分析工具,可直观验证内存布局的合理性。