内存布局总览
程序的内存空间主要由三大区域构成:代码段(文本段)、静态数据区域和动态数据区域
代码段和静态数据区
磁盘占用
.text段、.rodata段、.data段,这三部分存储实际二进制数据,是可执行文件大小的主要来源
.bss段本身、堆和栈不占用可执行文件的磁盘大小
代码段
代码段又称文本段.text ,用于存放源代码编译链接后生成的CPU指令,即机器码
特性
- 只读 + 可执行,确保CPU能安全的读取和执行程序
- 内容编译时确定,运行时不变,不存储任何数据(仅存指令)
- 在磁盘文件中完整存储,程序运行时拷贝到内存低地址区域
CPU 运行时通过读取内存 .text 段的指令,完成程序的所有逻辑(函数调用、循环、运算等)。
静态数据区
静态数据区分为三段
.bss段:专门存储未初始化&初始化为0的全局变量和静态变量
.data段:储存初始化不为0的全局变量和静态变量
.rodata段:存储常量和字符串
.bss段
.bss段特性
- 编译期确定大小、运行时固定不变
- 不占用可执行文件的磁盘空间(核心)
- 加载时批量清零的连续内存块
为什么说.bss段不占用磁盘空间:
可执行文件中只记录 BSS 段的「起始地址、结束地址、大小」 程序加载时,操作系统直接将这段内存区域「批量清零」(硬件层面效率极高),既节省磁盘空间,又保证变量初始值为零
记录.bss段大小的 “描述信息” 确实占用磁盘,但这部分空间极小(通常是几个字节),且属于可执行文件的 “辅助结构”(如段表),而非.bss段本身占用磁盘空间
.data段 、.rodata段
.data段 、.rodata段 特性
- 编译期确定大小、运行时固定不变
- 程序加载时,从可执行文件读取到内存
.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 攻击)。
不改变内存布局结构,仅添加随机偏移量