内存布局总览

程序的内存空间主要由三大区域构成:代码段(文本段)静态数据区域动态数据区域

代码段和静态数据区

磁盘占用

.text段、.rodata段、.data段,这三部分存储实际二进制数据,是可执行文件大小的主要来源

.bss段本身、堆和栈不占用可执行文件的磁盘大小

代码段

代码段又称文本段.text ,用于存放源代码编译链接后生成的CPU指令,即机器码

特性

  1. 只读 + 可执行,确保CPU能安全的读取和执行程序
  2. 内容编译时确定,运行时不变,不存储任何数据(仅存指令)
  3. 在磁盘文件中完整存储,程序运行时拷贝到内存低地址区域

CPU 运行时通过读取内存 .text 段的指令,完成程序的所有逻辑(函数调用、循环、运算等)。

静态数据区

静态数据区分为三段 .bss段:专门存储未初始化&初始化为0全局变量静态变量 .data段:储存初始化不为0全局变量静态变量 .rodata段:存储常量字符串

.bss

.bss段特性

  1. 编译期确定大小、运行时固定不变
  2. 不占用可执行文件的磁盘空间(核心)
  3. 加载时批量清零的连续内存块

为什么说.bss段不占用磁盘空间:

可执行文件中只记录 BSS 段的「起始地址、结束地址、大小」 程序加载时,操作系统直接将这段内存区域「批量清零」(硬件层面效率极高),既节省磁盘空间,又保证变量初始值为零

记录.bss段大小的 “描述信息” 确实占用磁盘,但这部分空间极小(通常是几个字节),且属于可执行文件的 “辅助结构”(如段表),而非.bss段本身占用磁盘空间

.data段 、.rodata

.data段 、.rodata段 特性

  1. 编译期确定大小、运行时固定不变
  2. 程序加载时,从可执行文件读取到内存

.rodata只读,任何对其数据进行修改的行为都会导致程序崩溃

char* p = "hello";  // 字符串 "hello" 存储在 .rodata(只读),指针 p 是全局变量,存储在 .data(可读写)
p = "world";        // 合法:修改的是指针 p 的值(.data 段的内容),不是字符串本身
// p[0] = 'H';       // 非法:尝试修改 .rodata 段的字符串,触发段错误

动态数据区

动态数据区域,又称「动态内存区域」是程序内存布局中运行时才分配、大小可动态伸缩的内存区域,核心用于存储「编译期无法确定大小 / 生命周期」的数据

栈和堆

栈和堆的主要区别在于:管理方式 栈是由编译器自动管理的内存区块 堆是由程序员手动管理的内存区块

栈的占用:

栈(Stack)的占用完全由「函数调用和局部数据定义」触发:只要执行函数调用,或在函数内定义非静态局部数据(变量、数组、结构体等),就会占用栈内存

函数调用:栈的核心占用场景(函数栈帧的创建)

每次调用函数时,编译器会自动在栈上创建一个「函数栈帧」(Stack Frame),栈帧会占用栈内存,包含 3 类关键数据:

  • 函数参数:调用函数时传递的参数(如 func(10, 'a') 中的 10 和 'a');
  • 返回地址:函数执行完后,需要回到的调用者代码位置(确保程序流程正确);
  • 栈帧信息:维护栈帧的指针(如 ebp 寄存器值),用于函数返回时恢复上一层栈帧。

定义局部变量(非静态):直接占用栈内存

函数内定义的「非静态局部变量」(包括普通变量、数组、结构体实例等),都会在当前函数的栈帧中占用栈内存,生命周期与函数调用一致(函数返回后释放)。

寄存器溢出数据:编译器自动的栈占用

当函数内使用的寄存器数量超过 CPU 可用通用寄存器时,编译器会将部分临时数据「溢出」到栈上存储(称为「Spill」),这是编译器的优化行为,无需程序员干预,但会占用少量栈内存。

通用寄存器

通用寄存器是 CPU 内部的高速存储单元(速度比内存快几百倍),用于临时存放函数执行时的 变量、计算中间结果、函数参数 等 —— 编译器会优先把频繁使用的数据放在这里,避免频繁访问内存(栈 / 堆),提升程序效率

堆的使用:

堆内存的占用完全由 “主动申请” 触发

编译期未知大小的数据:必须用堆

如果数据的大小的是 “运行时才能确定” 的(比如由用户输入、文件内容、网络数据决定),栈无法分配(栈需要编译期确定大小),只能用堆。

#include <stdlib.h>
#include <stdio.h>
 
int main() {
    int n;
    printf("请输入数组长度:");
    scanf("%d", &n);  // 运行时才知道n的值(编译期无法确定)
    
    // 申请n个int的堆内存(占用 4*n 字节堆空间)
    int* arr = (int*)malloc(n * sizeof(int));
    if (arr == NULL) return -1;  // 堆申请可能失败,必须检查
    
    arr[0] = 100;  // 堆内存可读写,修改数据内容
    free(arr);     // 手动释放,避免内存泄漏
    arr = NULL;
    return 0;
}

大体积数据:优先用堆(避免栈溢出)

栈有默认大小上限(如 Linux 8MB、Windows 1MB),如果数据体积超过栈的剩余空间,会触发 “栈溢出”,此时必须用堆。

// 大型缓冲区
// 错误示例:10MB 局部数组(栈放不下,会栈溢出)
void bad_func() {
    char buf[1024 * 1024 * 10];  // 10MB 栈内存,超出默认栈大小
}
 
// 正确示例:10MB 堆内存(堆上限远高于栈,安全)
void good_func() {
    char* buf = (char*)malloc(1024 * 1024 * 10);  // 占用 10MB 堆内存
    if (buf == NULL) return;
    free(buf);  // 手动释放
    buf = NULL;
}

跨函数共享 / 长期存活的数据:必须用堆

栈内存的生命周期与函数绑定(函数返回后自动释放),如果需要在一个函数中创建数据,让其他函数(甚至全局)访问,且数据在创建函数返回后仍需存在,必须用堆。

// 函数A创建数据,函数B需要访问(数据需长期存活)
int* create_data(int init_val) {
    // 用堆申请内存(函数返回后,堆内存不会释放)
    int* data = (int*)malloc(sizeof(int));
    if (data != NULL) {
        *data = init_val;  // 初始化数据
    }
    return data;  // 返回堆内存地址给调用者
}
 
void use_data(int* data) {
    printf("数据值:%d\n", *data);  // 访问堆内存中的数据
}
 
int main() {
    int* ptr = create_data(200);  // 堆内存被占用
    use_data(ptr);                // 跨函数访问堆数据
    free(ptr);                    // 不再使用时释放
    ptr = NULL;
    return 0;
}

 动态伸缩的数据结构:依赖堆

需要动态添加 / 删除元素的数据结构(如链表、哈希表、动态数组),其底层必须用堆 —— 因为大小会随运行时操作变化,栈无法动态调整。

ASLR地址空间布局随机化

ASLR(Address Space Layout Randomization):地址空间布局随机化

ASLR 是操作系统提供的一种 内存安全防护机制,核心目的是通过「随机化程序内存布局的关键地址」,让攻击者无法预测栈、堆、共享库等内存区域的起始地址,从而抵御依赖固定地址的漏洞利用(如缓冲区溢出、Return-to-libc 攻击)。

不改变内存布局结构,仅添加随机偏移量