3.20 内存布局
一个进程的虚拟地址空间被划分为多个段(segment),每个段有不同的用途、权限和增长方向。
x86-64 Linux 典型内存布局
高地址 0x7FFFFFFFFFFF (用户空间顶)
┌──────────────────────────────┐
│ 内核空间 │ ← 用户态不可访问
│ (操作系统驻留) │
├──────────────────────────────┤ 0x7FFFFFFFFFFF
│ │
│栈 │ ←向下增长(高地址$→$低地址)
│ (stack) │
│ ↓ │
│ │
│ (空闲区域) │
│ │
│ ↑ │
│堆 │ ←向上增长(低地址$→$高地址)
│ (heap) │
├──────────────────────────────┤
│ 共享库 │ ← 映射 .so 文件
│ (.so 文件映射) │
├──────────────────────────────┤
│ .bss (未初始化数据段) │ ← 读/写
├──────────────────────────────┤
│ .data (初始化数据段) │ ← 读/写
├──────────────────────────────┤
│ .rodata (只读数据段) │ ← 只读
├──────────────────────────────┤
│ .text (代码段) │ ← 只读/执行
└──────────────────────────────┘ 0x400000 (典型起始地址)
低地址
各段详解
代码段(.text)
- 内容:编译后的机器指令
- 权限:读 + 执行(不可写,防止代码被修改)
- 大小:编译时确定,不增长
- 特点:通常是只读的,多个进程可以共享同一份代码
# 查看 .text 段
objdump -d program | less只读数据段(.rodata)
- 内容:字符串常量、switch 跳转表、全局常量
- 权限:只读
- 大小:编译时确定
// 以下数据存放在 .rodata
const char *msg = "Hello"; // 字符串常量 "Hello" 在 .rodata
const int table[] = {1,2,3}; // const 数组在 .rodata数据段(.data)
- 内容:已初始化的全局变量和静态变量
- 权限:读 + 写
- 大小:编译时确定
// 以下变量存放在 .data
int global_var = 42; // 已初始化的全局变量
static long file_static = 10; // 已初始化的静态变量
char buf[100] = {0}; // 已初始化的全局数组BSS 段(.bss)
- 内容:未初始化的全局变量和静态变量
- 权限:读 + 写
- 大小:编译时只记录需要的大小,不占用可执行文件空间
- 特点:程序加载时由内核初始化为 0
// 以下变量在 .bss
int uninit_global; //未初始化的全局变量 $→ .bss$
$static long static_zero = 0; //$初始化为 $0 →$编译器优化到 .bss
int big_array[10000]; // 大数组,不在可执行文件中占空间.bss vs .data 的区别:如果你有
int arr[10000] = {1,2,3},这 40000 字节都要存储在 .data 段中(会增大可执行文件)。而int arr[10000](未初始化)只需要记录”需要 40000 字节”的信息,可执行文件不包含这些字节。
堆(Heap)
- 内容:动态分配的内存(
malloc/free、new/delete) - 权限:读 + 写
- 增长方向:向上(从低地址向高地址增长)
- 管理:由
brk/sbrk系统调用或mmap管理
int *p = malloc(100); // 从堆上分配 100 字节
// p 指向堆区域的某个位置
free(p); // 释放回堆堆管理的两个关键数据结构:
- malloc 维护空闲列表(free list)
- 碎片化(fragmentation)是堆管理的主要挑战
栈(Stack)
- 内容:函数调用的栈帧()
- 权限:读 + 写
- 增长方向:向下(从高地址向低地址增长)
- 大小:通常 8 MB(Linux 默认 ulimit -s)
# 查看栈大小限制
ulimit -s # Linux,通常输出 8192 (KB)每个函数调用创建一个新的栈帧,栈帧包含:
- 返回地址
- 保存的寄存器
- 局部变量
- 函数参数(超过 6 个的部分)
共享库区域
- 内容:动态链接库(.so 文件)的代码和数据
- 权限:取决于段(代码段只读执行,数据段读写)
- 特点:通过
mmap系统调用映射到进程地址空间
# 查看进程的内存映射
cat /proc/self/maps # 在 Linux 上查看当前进程内存布局的 C 程序验证
#include <stdio.h>
#include <stdlib.h>
int uninit_global; // .bss
int init_global = 42; // .data
const int const_global = 100; // .rodata 或 .text
void func() {} // .text
int main() {
static int static_var = 5; // .data
int local_var = 10; // 栈
int *heap_var = malloc(4); // 堆
printf("code (.text): %p\n", func);
printf("string (.rodata): %p\n", "hello");
printf("init (.data): %p\n", &init_global);
printf("uninit (.bss): %p\n", &uninit_global);
printf("heap: %p\n", heap_var);
printf("stack: %p\n", &local_var);
free(heap_var);
return 0;
}典型输出(地址从低到高):
code (.text): 0x401132
string (.rodata): 0x402004
init (.data): 0x404018
uninit (.bss): 0x40401c
heap: 0x1c9b010
stack: 0x7ffdf8e4a3a4
地址空间布局随机化(ASLR)
现代操作系统对内存布局进行随机化以提高安全性:
# Linux 上查看 ASLR 设置
cat /proc/sys/kernel/randomize_va_space
# 0 = 关闭, 1 = 部分随机, 2 = 完全随机ASLR 随机化的区域:
- 栈的起始地址
- 堆的起始地址
- 共享库的加载地址
- (代码段地址在 x86-64 上通常固定)
相关笔记
-
- 栈帧的详细结构
-
- 栈溢出攻击与防御
-
- 数据段中的对齐
-
- 可执行文件中的段
-
- 虚拟内存管理
链接到
- 上一个知识点:3.19 数据对齐
- 下一个知识点:3.21 缓冲区溢出