1.3 可执行文件与进程内存布局


1.3 可执行文件与进程内存布局

本节摘要:程序运行时,操作系统把可执行文件的各个"节"映射进一块虚拟地址空间,配合内核分配的栈与运行时库管理的堆,构成进程内存全景。本节画出这张地图并逐区讲解,再给出"任意一个变量落在哪"的判定方法——这是后续指针、动态内存、递归各章的公共地基。

一张必须刻进脑子的地图

图 1 进程虚拟内存布局透视

图 1 进程虚拟内存布局透视

从下往上是:低地址页(封死,防止空指针乱闯)、代码段、数据段、BSS、堆(向上长)、共享库区、栈(向下长)。堆与栈相向而行,中间是待分配的空档——64 位地址空间巨大,实际几乎不会撞上,但栈本身的尺寸上限是有限的。

一个程序把变量放满全图

用一段程序把五类变量各就各位:

#include <stdio.h> #include <stdlib.h> int g_init = 42; /* data 段 */ int g_zero; /* bss 段,加载时为 0 */ static int g_hidden; /* bss 段,私有符号 */ int main(void) { int local = 7; /* 栈 */ static int s_local = 3; /* data 段,不随函数返回销毁 */ int *p = malloc(100 * sizeof(int)); /* p 在栈上,100 个 int 在堆上 */ char *msg = "hello"; /* p 在栈上,字符串本体在只读区 */ printf("g_init=%p g_zero=%p\n", (void*)&g_init, (void*)&g_zero); printf("local=%p s_local=%p\n", (void*)&local, (void*)&s_local); printf("heap=%p msg=%p main=%p\n", (void*)p, (void*)msg, (void*)main); free(p); return local * 0 + g_init * 0 + g_hidden * 0 + s_local * 0; }

一次典型运行输出(地址每次启动不同,但相对关系稳定):

g_init=0x404030 g_zero=0x404038 local=0x7ffd3a2b1c44 s_local=0x404034 heap=0x1a2b3c4d010 msg=0x402004 main=0x401136

读地址就能看出地图:0x4040xx 一片是 data/bss;0x7ffd... 是栈(靠近栈区高地址);0x1a2b... 是堆;0x4011xx 是代码段里的 main地址是分片聚居的——这是判定"变量住在哪"最直接的经验法则。

另外两个要点藏在这段代码里:

  • static 修饰局部变量改变的是位置与生命周期(搬到 data/bss,进程期间常驻),不是可见性(函数外依旧不可见)。
  • 字符串字面量在只读区,msg[0]='H' 会直接段错误。它不是"存在指针里的字符串",指针只是一个 8 字节的地址盒子。

虚拟内存:为什么每个进程都觉得自己独占机器

上面所有地址都是虚拟地址。每个进程看到的是自己一整套地址空间,由 CPU 的内存管理单元(MMU)按页(通常 4KB)翻译成物理地址。好处直接体现在 C 程序员的生活里:

  1. 隔离:进程 A 的野指针写不进进程 B 的内存,越界访问自己地图外的地址触发段错误;
  2. 按需调页malloc 一大块内存只登记了地址范围,真正写入那页时才分配物理页——所以"malloc 成功"不代表物理内存已经到手;
  3. 共享:同一份 libc 代码在物理内存只有一份,映射进所有进程。

段错误(segfault)的本质也就清楚了:访问的虚拟地址在当前进程的映射表里没有合法条目,MMU 报页错误,内核转成 SIGSEGV 信号,默认行为是终止进程。"段错误"不是 C 语言的概念,是操作系统与硬件协作的产物。

布局判定速查

你写下的东西 落在哪 谁分配 何时失效
局部变量(含参数副本) 编译器生成的代码 函数返回
局部 static 变量 data 或 bss 加载器 进程结束
全局变量 / 全局 static data 或 bss 加载器 进程结束
malloc 家族返回的内存 运行时分配器 free 时
字符串字面量 只读区 text 加载器 进程结束
函数与代码 text 加载器 进程结束
局部数组 栈(整块连续) 编译器生成的代码 函数返回

⚠️ 常见坑:返回局部变量的地址。函数返回后栈帧作废,那块内存随时被下一次调用覆写——拿到地址的调用方读到的可能是任何东西,运气差时还能"碰巧正确",毒害更深。返回堆内存或 static 缓冲区才安全,前者要记得 free。

💡 关键直觉:看到变量先问三个问题——住在哪个区、谁负责分配、什么时候死。三个都答得出,指针类 bug 的数量会断崖式下降。

本节要点回顾

  • 进程地址空间是分片地图:低地址只读代码区、data、bss、向上长的堆、共享库区、向下长的栈。
  • 地址聚居有规律:打印地址看前缀片段,就能反推变量住在哪个区。
  • static 局部变量搬进 data/bss:生命周期与全局相同,可见性仍是函数内。
  • 虚拟内存提供隔离与按需调页:段错误是访问无映射条目的虚拟地址被 MMU 拦截。
  • 一切指针问题的前提是知道指向的对象住在哪、何时失效:这张地图是指针章的入场券。

至此第 1 章完结。下一章我们把镜头从"程序整体"缩到"一个数据":变量在字节层面长什么样,整数为什么用补码,浮点数的 8 个字节里藏着什么。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U