在 ARM64 机器上调进程,很多人第一次看 /proc/<pid>/maps 都会愣一下:这地址怎么和 x86_64 上长得完全不一样?我之前排查一个服务崩溃,gdb 进去 backtrace 全是问号,折腾了半天,最后把地址空间布局捋清楚才定位到问题。这篇文章就把 ARM64 进程虚拟地址空间这件事讲透,顺便把 amd64 和 arm64 那些经常让人纠结的区别也一起说清楚。做服务端、嵌入式、Android 逆向,或者单纯被“进程又崩了”折磨的同学,都可以看看。
1. 先搞懂:虚拟地址空间到底是什么,ARM64 特殊在哪
1.1 一个进程看到的“内存”是假的
CPU 并不会直接访问物理内存地址,而是通过 MMU 做地址翻译。操作系统为每个进程维护一张页表,进程代码里出现的地址全部是虚拟地址,经过页表转换成物理地址才能真的读到数据。这样做有两个本质好处:一是进程之间天然隔离,A 进程访问不到 B 进程的数据,更改不了 B 的内存,操作系统也不用担心应用乱写;二是程序加载、链接时不用关心物理内存到底够不够、碎片化严不严重,反正页表会帮你映射到任意物理页。
这个“进程能看到的虚拟地址范围”就是进程虚拟地址空间。它不是一个连续的大块内存,而是由操作系统按页粒度、用 VMA(虚拟内存区域)的方式一块块拼起来的。从地址 0 开始,到当前硬件和内核支持的地址上限结束,里面有可执行文件映射、堆、栈、动态库、共享内存,以及内核偷偷塞进来的一些特殊映射。研究进程虚拟地址空间布局,说白了就是搞清楚这些区域分别位于哪段地址、为什么在那里、以及排查问题时怎么利用这些信息。
1.2 ARM64 的地址位宽:不是所有 64 位都能用
很多人有个误区:64 位系统就能用 64 位地址。真实情况是,连服务器厂商也不敢把 64 位地址全部开放。ARM64 架构当前主流配置是 48 位虚拟地址,部分内核和硬件组合支持 52 位,但绝大多数发行版默认还是 48 位。为什么不用满 64 位?因为页表会爆炸。虚拟地址每多 1 位,需要映射的地址空间就翻一倍,页表的层级、条目数量、TLB 开销全都跟着涨,性能和硬件成本都撑不住。48 位已经能覆盖 256TB 空间,对几乎所有应用都够用了。
所以 ARM64 下虚拟地址被分成两半:低半部分 0x0000000000000000 到 0x0000ffffffffffff 是用户空间;高半部分从 0xffff000000000000 往上属于内核空间。硬件层面,ARM64 用 TTBR0 和 TTBR1 两个页表基址寄存器来区分:访问低地址用 TTBR0 指向的用户页表,访问高地址用 TTBR1 指向的内核页表。这样的好处是,用户态进程跑在 EL0 异常级别时,天然只能访问用户页表覆盖的低半区地址,一旦尝试访问内核地址,MMU 直接抛异常,不需要额外软件检查。
这也就解开了一个常见困惑:为什么 ARM64 用户进程的栈地址、mmap 地址看起来总是 0x0000ffff... 开头,和 x86_64 的 0x00007f... 完全不一样。其实它们都处在各自用户空间的最高位附近,只是 x86_64 和 ARM64 在地址划分规则上不同。记住关键区分:用户地址的高 16 位是 0x0000,内核地址的高 16 位是 0xffff,中间差了一位,不要看到 ffff 就以为是内核。
1.3 用户态和内核态的边界是怎么定的
内核空间地址有个很直观的特征:高 16 位全为 1,也就是 0xffff...。在 48 位 VA 配置下,内核镜像通常位于 0xffff000000000000 到 0xffff800000000000 之间,具体位置看内核编译配置。用户进程哪怕拿到了一个内核地址,也无法通过它访问任何数据,因为用户态页表根本没有内核区域的映射,MMU 会拒绝访问权限。
这里补充一个容易混淆的点:用户空间 256TB 的上限 0x0000ffffffffffff,地址尾部也全是 f。比如栈顶可能在 0x0000fffffffff000 附近。有些同学拿 gdb 看栈地址,发现一长串 f 就以为是内核栈,其实不是,那仍然是用户栈,只是处在用户空间的最高段。区分标准就是看高 16 位:0x0000 开头是用户态,0xffff 开头是内核态。这点在跨平台调试时一定要建立条件反射。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 手把手拆解 ARM64 用户进程的地址空间布局
2.1 一份真实的 maps 输出长什么样
直接看实际数据最直观。在 Linux 上,/proc/<pid>/maps 会按地址从低到高列出进程所有虚拟内存区域。下面是一份 ARM64 上运行 nginx worker 进程时抓到的典型输出,格式有压缩:
code复制55f0b3a00000-55f0b3c00000 r-xp 00000000 fd:01 123456 /usr/sbin/nginx
55f0b3c00000-55f0b3d00000 r--p 00020000 fd:01 123456 /usr/sbin/nginx
55f0b3d00000-55f0b3e00000 rw-p 00030000 fd:01 123456 /usr/sbin/nginx
55f0b3e00000-55f0b4020000 rw-p 00000000 00:00 0 [heap]
7f3d5c000000-7f3d5c010000 r-xp 00000000 fd:01 789101 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
7f3d5c010000-7f3d5c020000 r--p 00010000 fd:01 789101 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
7f3d5c020000-7f3d5c030000 rw-p 00020000 fd:01 789101 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
0000fffff7ff0000-0000fffff7ff10
