Skip to content

进程虚拟地址空间区域划分 ​

我们在启动一个进程的时候, 操作系统会进行资源分配. 接下来以 C++ 作为载体稍微对这块内容进行说明罢.

环境说明 ​

编译环境说明 ——

Environment: Ubuntu 26.04 LTS / aarch64 / Ubuntu clang version 21.1.8 (6ubuntu1) / C++23

Build: clang++ -std=c++23 -O0 -g -Wall -Wextra

引入 ​

众所周知, 一个程序它的不同“内容”会被分在分配的内存空间中的不同部分, 下面简单地对其进行说明 ——

C++
#include <iostream>
#include <unistd.h>

int globalValue = 10;

void foo() { }

int main()
{
    int localValue = 20;
    int* dynamicValue = new int(30);

    std::cout << "PID     : " << getpid() << '\n';

    std::cout << "code    : "
              << reinterpret_cast<void*>(&foo)
              << '\n';

    std::cout << "global  : "
              << static_cast<void*>(&globalValue)
              << '\n';

    std::cout << "local   : "
              << static_cast<void*>(&localValue)
              << '\n';

    std::cout << "heap    : "
              << static_cast<void*>(dynamicValue)
              << '\n';

    std::cin.get();

    delete dynamicValue;
}

运行结果 ——

PID     : 26876
code    : 0xb24103720b68
global  : 0xb24103740070
local   : 0xffffe3129cd8
heap    : 0xb24135241020

我们可以看出一个特点, code 和 global 存储的位置比较近, 然后 local 在一个区域, heap 又是另一块区域.

text 和 data ​

我们的 foo 会被翻译成机器指令, 一般是可执行但不可写(如果一块既可以执行又可以外部写入的话, 想必十分甚至九分的不安全), 而 globalValue 存储的区域应当是可读可写的. 我们会发现此处系统需要的权限已经出现了分歧. 不妨在程序持续运行的时候, 进行一个验证, 查看下 virtual memory mapping

bash
cat /proc/<PID>/maps

我们可以对照一下, 得到 code 和 global 所出的区域分别是

b24103720000-b24103721000 r-xp 00000000 ...
b24103740000-b24103741000 rw-p 00010000 ...

额外先说一下权限

r = readable
w = writable
x = executable
p = private

综上所述, 我们可以得出程序内存中进行区域划分的一个意义 —— 表达不同内容的访问权限.

实际上这两个位置就是常说的 .data 数据段 和 .text 代码段. ——

简单来说, .text 通常用于保存机器码, 而 .data 通常保存已经经过初始化, 可读可写的静态数据.(不过类似 const int a = 10; 这种可能会被存储到 .rodata 之类的地方, 后续有机会拓展)

用汇编可能会更容易识别.

bash
clang++ -std=c++23 -O0 -fno-pie -S main.cpp -o main.s

然后查看生成的汇编文件

asm
.text
.globl  _Z3foov
.p2align    2
.type   _Z3foov,@function

_Z3foov:
    ret

说明 foo 方法在 .text

asm
.type   globalValue,@object
.data
.globl  globalValue
.p2align    2, 0x0

globalValue:
    .word   10
	.size   globalValue, 4

说明 globalValue 在 .data 区域, 并且被初始化成 10.

stack ​

int localValue = 20; 此变量被我们声明在一个函数体内, 我们都知道它会在进入函数体内的时候被创建, 然后在函数退出的时候自动销毁. 不过此处我们需要知道, 变量的释放确乎是与其在函数体内的声明顺序有关(也是先声明的后被释放), 不过对象析构和栈空间的回收是两件事情. 需要分开看.

它位于的位置也非常容易证明 ——

bash
cat /proc/26876/maps | grep stack

得到

ffffe310b000-ffffe312c000 rw-p 00000000 00:00 0            [stack]

结合上文, 我们可以判断 localValue 变量存储的位置落于 stack 区.

我们查看汇编, 看看它的具体行为是什么 ——

在进入 main 的时候

asm
sub     sp, sp, #80

stp     x29, x30, [sp, #64]
add     x29, sp, #64

先借助 sub 来给当前 main 预留 80 字节的空间作为栈帧,

  • 在 sp + 64 写入 x29 的值, 存储进入 main 之前调用者的 Frame Pointer, 也就是记录 “从何而来” —— 后续在 frame chain / stack unwinding 的时候就很方便.
  • 在 sp + 64 + 8 写入 x30 的值, 作为返回地址, Link Register

这也比较巧妙 —— 借助 80 = 64 + 16 = 64 + 8 + 8 所以实际给局部变量等的空间是 64 字节, 而上头为什么是 sp + 64 也非常好理解了, 这样让旧的 x29 和 x30 的值刚好存在高地址, 靠近原先 sp 的位置, 这样之后的变量也非常好的往低地址来进行存储

接着借助 add x29, sp, #64, 让 x29 成为当前 main 函数的 Frame Pointer ——

而且有意思的, 我们会发现是使用 sub 做减法操作来指向存储空间

mov     w8, #20
stur    w8, [x29, #-8]

此处可以看到, 在当前的区域中, 我们先把 20 存储 w8, 接着再把寄存器 w8 里的 32 位值,存到内存地址 x29 - 8 的位置, 翻译成“人话” —— 在当前的栈帧中, 把相对 frame pointer 向低地址偏移 8 个字节的位置下写入 w8 的数据. 所以可以看出, 实际上局部变量只是栈区中的一个 offset 而已, 非常的高效.

然后释放的时候也非常方便快速 —— 我们直接移动栈顶指针

asm
add sp, sp, #80

让这一块不再属于当前栈帧即可, 并不需要真的抹除什么数据, 这也是我们常说栈区开辟/释放非常高效的底层原因.

heap ​

上文说到栈区域中的变量和当前栈帧的生命周期绑定, 所以我们常说的堆区是否可以看成是 —— 存储无法和某一次函数调用绑定生命周期的变量的地方呢? (感觉可以是一种理解, 不过对于大型对象等的旧理解也要保留)

在 int* dynamicValue = new int(30); 中, 我们需要注意两个地方 —— 一个是 dynamicValue 本身是一个栈区变量, 生命周期和当前的栈帧绑定. 但是后面的 new int 是一块动态申请的空间, 生命周期不和当前栈帧绑定, 需要手动调用 delete 进行释放.

我们继续查看一下汇编 ——

先进行空间的申请

asm
mov x0, #4
bl  _Znwm

这样, x0 就类似于一个指向 heap 区域上某块内存的地址.

然后进行数据的写入

asm
mov w8, #30
str w8, [x0]

最后再把这个地址返回给栈帧上的局部变量

asm
stur x0, [x29, #-16]

那么此时其实我们可以再得出另一个结论 —— 进程虚拟地址空间区域划分可以用于不同的生命周期管理

综上 ​

其实各个段自身的行为都非常的有趣, 比如栈区从高地址向低地址, 函数调用是怎么实现的? 接着为什么堆区是从低地址到高地址写数据? 这是谁规定的? 这是一定的吗? 空间还会被划分成其他块吗? 等等, 害很多可以深究的地方, 都埋上钩子.

此篇只是空间区域划分的基础引入 —— 对于不同语义的数据, 它的访问权限 / 生命周期不同, 则可能需要不同的存储和管理方式, 因此会在进程中虚拟地址空间的不同区域进行划分.

(以及上述很多行为仅仅是因为没有经过编译器优化等的, 所以在实际执行的时候可能还不太一样)

Released under the MIT License.