4. 进程内存布局:代码段、数据段、堆、栈、mmap区域的布局与起始地址

在 Linux 系统中,每个用户态进程都拥有独立的虚拟地址空间。理解这个空间的布局——从低地址到高地址,代码段、数据段、堆、内存映射区、栈的排列方式——是掌握内存管理的第一步。本章将深入剖析进程内存布局的静态结构与动态行为,并给出各区域的典型起始地址。

4.1 经典 32 位进程内存布局

在 32 位系统(用户空间 3GB,内核空间 1GB)中,进程的虚拟地址空间从 0x00000000 到 0xBFFFFFFF(3GB)。各区域的典型排列如下:

关键概念: 布局顺序由低到高依次为:代码段 → 数据段 → 堆 → 内存映射区(mmap) → 栈 → 内核空间。堆向上增长,栈向下增长,两者相向而行,中间由 mmap 区域隔开。
避坑指南: 不要混淆“堆向上增长”与“栈向下增长”的方向。堆的 brk 指针向高地址移动,栈的 sp 指针向低地址移动。两者之间的空隙是进程可用的虚拟地址空间。

4.1.1 各区域起始地址与大小

区域名称 典型起始地址(32位) 典型大小 增长方向 说明
代码段(.text) 0x08048000 由可执行文件决定 固定 只读,包含机器指令
数据段(.data/.bss) 0x08049000 附近 由可执行文件决定 固定 已初始化/未初始化全局变量
堆(Heap) 0x0804A000 附近 动态增长 向上(高地址) 通过 brk/sbrk 系统调用管理
mmap 区域 0x40000000 附近 动态增长 双向 文件映射、共享内存、动态库
栈(Stack) 0xBFFFFFFF 向下 默认 8MB(可调) 向下(低地址) 局部变量、函数调用帧
内核空间 0xC0000000 1GB — 用户态不可直接访问

4.1.2 进程内存布局全景 SVG 图

32位进程内存布局全景图 32位进程虚拟地址空间布局 地址 0x00000000 0xFFFFFFFF 用户空间 (3GB) 代码段 0x08048000 数据段 0x08049000 堆 (向上增长) 0x0804A000 brk→ mmap (动态库/共享内存) 0x40000000 栈 (向下增长) 0xBFFFFFFF ←sp 内核空间 0xC0000000 布局要点 • 代码段只读,防止程序被修改 • 数据段包含 .data 和 .bss • 堆和栈相向增长,避免冲突 • mmap 区域用于加载动态库 • 内核空间用户态不可访问 • 各区域起始地址因 ASLR 而异

4.2 64 位进程内存布局

在 64 位系统(如 x86_64)中,虚拟地址空间巨大(理论 2^48 位),用户空间通常为 47 位(128TB)。布局与 32 位类似,但地址范围完全不同:

区域名称 典型起始地址(64位) 说明
代码段(.text) 0x400000(4MB) 默认加载地址,可被 PIE 随机化
数据段(.data/.bss) 0x600000 附近 紧随代码段之后
堆(Heap) 0x601000 附近 通过 brk 管理,向上增长
mmap 区域 0x7f0000000000 附近 动态库、共享内存、匿名映射
栈(Stack) 0x7ffffffde000 附近 向下增长,默认 8MB
内核空间 0xffff800000000000 用户态不可访问
注意: 64 位布局中,代码段起始地址 0x400000 是 ELF 默认加载基址。如果启用了 PIE(Position Independent Executable),该地址会被 ASLR 随机化,通常落在 0x55xxxxxxxxxx 范围内。同样,栈和 mmap 区域的起始地址也会因 ASLR 而大幅变化。

4.3 各区域详细解析

4.3.1 代码段(.text)

代码段是只读的,包含程序的机器指令。其关键属性:

  • 权限: r-x(读 + 执行,不可写)
  • 起始地址: 由 ELF 头中的 e_entry 和程序头表决定
  • 大小: 编译时确定,运行时不变
  • 保护机制: 写保护防止代码注入攻击

4.3.2 数据段(.data / .bss)

数据段分为两部分:

子段 内容 权限 文件占用
.data 已初始化的全局变量和静态变量 rw- 占用磁盘空间
.bss 未初始化的全局变量和静态变量 rw- 不占用磁盘空间(仅记录大小)

4.3.3 堆(Heap)

堆是动态内存分配的主要区域,由 malloc/free 管理。其底层通过 brk 系统调用调整堆顶指针:

// 查看当前堆顶(brk 指针)
#include <unistd.h>
#include <stdio.h>

int main() {
    void *curr_brk = sbrk(0);
    printf("当前堆顶: %p\n", curr_brk);
    
    // 分配 1MB 堆空间
    void *new_brk = sbrk(1024 * 1024);
    printf("新堆顶: %p\n", sbrk(0));
    
    // 释放
    sbrk(-1024 * 1024);
    return 0;
}
实战经验: 现代 malloc 实现(如 glibc 的 ptmalloc)对于大块内存(超过 128KB)会直接使用 mmap 分配,而不是扩展堆。因此,堆区域主要用于小对象分配,大对象分配在 mmap 区域。

4.3.4 mmap 区域

mmap 区域是 Linux 进程中最灵活的区域,用于:

  • 动态库加载: ld.so 将共享库映射到此区域
  • 大块内存分配: malloc 对大请求使用 mmap
  • 文件映射: mmap 系统调用直接映射文件
  • 共享内存: 进程间通信的 POSIX 共享内存
  • 匿名映射: 用于 malloc 和 calloc 的大块分配

4.3.5 栈(Stack)

栈是函数调用的核心数据结构,每个函数调用对应一个栈帧:

// 查看栈地址示例
#include <stdio.h>

void func() {
    int local_var = 42;
    printf("栈变量地址: %p\n", &local_var);
}

int main() {
    int main_var = 10;
    printf("main 栈变量地址: %p\n", &main_var);
    func();
    return 0;
}

栈的关键特性:

  • 自动管理: 函数进入时分配栈帧,返回时自动释放
  • LIFO 结构: 后进先出,与函数调用嵌套匹配
  • 固定大小: 默认 8MB(可通过 ulimit -s 调整)
  • 溢出风险: 无限递归或超大局部变量会导致栈溢出(SIGSEGV)

4.4 通过 /proc/pid/maps 查看实际布局

Linux 提供了 /proc/[pid]/maps 文件,可以实时查看进程的内存布局。以下是一个实际进程的 maps 输出示例:

# cat /proc/1234/maps
00400000-00401000 r-xp 00000000 08:01 1234567    /usr/bin/simple_prog
00600000-00601000 r--p 00000000 08:01 1234567    /usr/bin/simple_prog
00601000-00602000 rw-p 00001000 08:01 1234567    /usr/bin/simple_prog
00602000-00623000 rw-p 00000000 00:00 0          [heap]
7f0000000000-7f0000020000 r-xp 00000000 08:01 7654321    /lib/x86_64-linux-gnu/libc.so.6
7f0000020000-7f00001f0000 ---p 00020000 08:01 7654321    /lib/x86_64-linux-gnu/libc.so.6
7f00001f0000-7f00001f1000 r--p 00020000 08:01 7654321    /lib/x86_64-linux-gnu/libc.so.6
7f00001f1000-7f00001f4000 rw-p 00021000 08:01 7654321    /lib/x86_64-linux-gnu/libc.so.6
7f00001f4000-7f00001f7000 rw-p 00000000 00:00 0
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0          [stack]
解读方法: 每行格式为 起始地址-结束地址 权限 偏移 设备号 inode 路径。权限字段 r=读,w=写,x=执行,p=私有,s=共享。通过观察地址范围,可以精确判断各区域的位置和大小。

4.5 ASLR 对布局的影响

地址空间布局随机化(ASLR)是现代操作系统的安全机制,它会随机化以下区域的起始地址:

区域 ASLR 影响 随机化范围
代码段 仅 PIE 可执行文件受影响 通常 28 位随机化
堆 堆起始地址随机化 通常 13 位随机化
mmap 区域 动态库、匿名映射地址随机化 通常 28 位随机化
栈 栈顶地址随机化 通常 22 位随机化
调试提示: 在调试或性能分析时,可以通过 echo 0 > /proc/sys/kernel/randomize_va_space 临时关闭 ASLR(需要 root 权限)。生产环境务必保持 ASLR 开启(值为 2)。

4.6 进程内存布局的 SVG 架构图

进程内存布局架构图 进程内存布局架构图 内核空间 (Kernel Space) 用户空间 (User Space) 栈 (Stack) 局部变量、函数调用帧 向下增长 mmap 区域 动态库、共享内存、大块分配 堆 (Heap) 动态内存分配 (malloc/free) 向上增长 数据段 (.data / .bss) 代码段 (.text) 布局关键点 1. 代码段只读可执行 2. 数据段可读写 3. 堆向上增长,栈向下增长 4. mmap 区域位于堆和栈之间 5. 内核空间用户态不可访问 6. ASLR 随机化各区域基址 7. 通过 /proc/pid/maps 查看 8. 64位地址范围远大于32位

4.7 实战:使用 pmap 工具分析进程布局

pmap 命令是分析进程内存布局的利器,它封装了 /proc/pid/maps 的读取:

# 查看当前 shell 进程的内存布局
$ pmap $$
1234:   bash
0000555555554000    192K r-xp bash          # 代码段
0000555555584000      4K r--p bash          # 只读数据
0000555555585000     28K rw-p bash          # 数据段
000055555558c000     20K rw-p   [anon]      # 匿名映射(堆)
00007ffff7dce000   1944K r-xp libc-2.31.so  # libc 代码
00007ffff7fae000   2048K ----- libc-2.31.so # 保护页
00007ffff81ae000     16K r--p libc-2.31.so  # libc 只读数据
00007ffff81b2000      8K rw-p libc-2.31.so  # libc 数据
00007ffff81b4000     52K rw-p   [anon]      # libc 匿名映射
00007ffff81c3000      8K rw-p   [anon]      # 线程栈等
00007ffffffde000    132K rw-p   [stack]     # 主栈
total 4080K
实战技巧: 使用 pmap -x [pid] 可以查看更详细的信息,包括 RSS(驻留内存大小)和 Dirty(脏页大小)。这对于分析内存泄漏和内存占用非常有用。

4.8 本章小结

进程内存布局是 Linux 内存管理的基石。通过本章的学习,你应该掌握:

  • 32 位和 64 位系统中各区域的典型起始地址和排列顺序
  • 代码段、数据段、堆、mmap 区域、栈的功能和权限
  • 堆向上增长、栈向下增长的动态行为
  • ASLR 对布局的影响和调试方法
  • 使用 /proc/pid/maps 和 pmap 工具分析实际进程