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/freenew/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 上通常固定)

相关笔记

    • 栈帧的详细结构
    • 栈溢出攻击与防御
    • 数据段中的对齐
    • 可执行文件中的段
    • 虚拟内存管理

链接到