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 图
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 架构图
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工具分析实际进程