一、C语言中的程序地址空间
1.分区
一个运行中的 C 程序,其地址空间按功能划分为 7 个核心区域,从低地址到高地址依次排列,每个区域有明确的读写权限和用途:
| 区域名称 | 地址范围(示意) | 核心功能 | 关键特性 |
|---|---|---|---|
| 代码段(Text) | 低地址 | 存储程序的二进制可执行指令(main、函数等) |
只读、可共享(多进程复用) |
| 数据段(Data) | 代码段后 | 存储已初始化的全局变量、静态变量(如int a = 10;) |
可读可写、程序运行全程存在 |
| 未初始化数据段(BSS) | 数据段后 | 存储未初始化的全局变量、静态变量(如int b;) |
程序启动时自动初始化为 0、占用虚拟地址不占磁盘空间 |
| 堆(Heap) | BSS 段后 | 动态内存分配区域(malloc/calloc/realloc申请的内存) |
向上增长(地址升高)、手动管理 |
| 内存映射区 | 堆与栈之间 | 映射共享库、动态库(如libc.so)、文件等 |
灵活分配、按需映射 |
| 栈(Stack) | 高地址 | 存储局部变量、函数参数、返回地址、寄存器上下文 | 向下增长(地址降低)、自动回收 |
| 环境 / 参数区 | 栈顶(更高地址) | 存储命令行参数(argv)、环境表(envp/environ) |
2.核心区详解
代码段(Text)
存放编译后的机器指令,比如你写的
printf()、for循环对应的二进制代码。权限为只读(防止程序意外修改自身指令),多个进程执行同一程序时,代码段可共享物理内存,节省资源。
数据段 + BSS 段
两者合称 "静态存储区",区别仅在于是否初始化。
BSS 段的优势:编译时仅记录变量名和大小,不占用可执行文件的磁盘空间,程序启动时由操作系统分配内存并置 0。
堆(Heap)
程序员手动管理的动态内存,通过
malloc申请、free释放。堆的增长方向是 "向上"(从低地址到高地址),大小受限于系统剩余物理内存。
常见问题:内存泄漏(忘记
free)、野指针(free后未置 NULL)、双重释放(重复free)。
栈(Stack)
自动管理的临时内存,函数调用时分配栈帧(存储局部变量、参数、返回地址),函数返回时栈帧自动销毁。
栈的增长方向是 "向下"(从高地址到低地址),默认大小有限(Linux 下约 8MB),超出则触发 "栈溢出"(Stack Overflow)。
环境 / 参数区
存放
main函数的argv(命令行参数)和envp(环境变量),本质是两个以NULL结尾的字符指针数组。环境表(
envp)的每个指针指向KEY=VALUE格式的字符串(如PATH=/usr/bin),与栈紧邻但地址更高。
3.验证地址空间布局
cpp
#include <stdio.h>
#include <stdlib.h>
// 全局变量(数据段/BSS段)
int global_init = 10; // 数据段
int global_uninit; // BSS段
static int static_init = 20; // 数据段
static int static_uninit; // BSS段
void print_addr() {
// 局部变量(栈)
int local_var = 30;
// 动态内存(堆)
int *heap_var = malloc(sizeof(int));
printf("=== 各区域地址(从低到高)===\n");
printf("代码段(函数地址):%p\n", print_addr);
printf("数据段(初始化全局变量):%p\n", &global_init);
printf("数据段(初始化静态变量):%p\n", &static_init);
printf("BSS段(未初始化全局变量):%p\n", &global_uninit);
printf("BSS段(未初始化静态变量):%p\n", &static_uninit);
printf("堆(malloc分配):%p\n", heap_var);
printf("栈(局部变量):%p\n", &local_var);
printf("环境表(environ):%p\n", environ); // 需声明extern char **environ;
free(heap_var);
}
int main(int argc, char *argv[], char *envp[]) {
print_addr();
return 0;
}
输出结果:
bash
=== 各区域地址(从低到高)===
代码段(函数地址):0x55f8b76a8660
数据段(初始化全局变量):0x55f8b78ac010
数据段(初始化静态变量):0x55f8b78ac014
BSS段(未初始化全局变量):0x55f8b78ac020
BSS段(未初始化静态变量):0x55f8b78ac024
堆(malloc分配):0x55f8b94092a0
栈(局部变量):0x7ffd7b8e7abc
环境表(environ):0x7ffd7b8e95d8
从输出可清晰看到:代码段 < 数据段 / BSS 段 < 堆 < 栈 < 环境表,符合经典布局顺序。
二、进程地址空间
但实际上,程序地址空间并不是真实的物理内存,而是"虚拟"内存
我们用一段代码验证:
cpp
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 0;
}
else if(id == 0)
{
g_val = 100;
printf("child[%d]:%d:%p\n", getpid(), g_val, &g_val);
}
else
{
printf("parent[%d]:%d:%p\n", getpid(), g_val, &g_val);
}
return 0;
}
输出:
bash
child[2996]:100:0x80497d8
parent[2995]:0:0x80497d8
我们发现,父子进程输出地址是一样的,但是变量的内容不一样:
内容变量不一样,所以父子进程输出地址的变量绝对不是同一个变量
但地址值是一样的,说明绝对不是物理地址
在Linux环境中,这种地址叫做虚拟地址
OS将虚拟地址转化为物理地址,具体关系如下所示:

这张图描绘了父进程通过
fork()创建子进程后的内存映射变化:1.初始状态(fork () 刚完成时)
子进程会复制父进程的
task_struct和mm_struct,但页表和物理内存是共享的。此时父子进程的虚拟地址(如
g_val)指向同一个物理内存页,页表被标记为只读。2.子进程尝试写入(触发写时复制)
当子进程对共享的物理页(如
g_val所在的已初始化数据区)执行写入操作时,CPU 会检测到 "写只读页" 的异常。内核会为该物理页创建一个副本,更新子进程的页表,使其指向新的物理页。
之后子进程的写入操作就只会作用于这个新副本,不会影响父进程的原数据。