Skip to content

运行时内存映像与地址空间

2026 大纲 一(三)5 程序运行时的内存映像与地址空间

程序装进内存之后,长什么样

上一节的装入程序把可执行文件搬进了内存。搬完之后,这块内存里是怎么摆的

不是把文件原样拷一份进去。可执行文件里存的是"静态的描述",内存里要的是"能跑的现场"—— 两者的差别不只是位置:文件里几个 KB 的东西,跑起来可能占好几 MB; 文件里根本不存在的堆和栈,跑起来才出现;而每个进程还都能看到一整套内核

要把这些说清,先得分开两个一直被混着用的词:地址空间有多大内存有多大。 它们由完全不同的东西决定,而正是它们的不相等,逼出了后面整章的虚拟内存。

这一节讲三件事:一个进程能编出多少个地址、这些地址被切成了哪几段每一段里装的东西从哪来以及 CPU 送出的地址是怎么变成真正的内存单元的。 最后一件事里藏着一处顺序问题——越界检查在地址变换之前——它是本节唯一会算错的地方。

一、地址空间

概念定义谁产生、给谁看
逻辑地址(相对地址、虚拟地址)程序地址空间中相对某个基准(通常为 0)连续编号的地址CPU 执行指令时产生,程序里能看到
物理地址(绝对地址)CPU 执行指令和处理数据时面向的实际主存地址送上地址总线、直接选中内存单元
逻辑地址空间一个进程全部逻辑地址的集合每个进程一份,互不相干
物理地址空间全部物理地址的集合,大小取决于实际安装的主存容量全机唯一

地址空间有多大是一个纯粹的计数问题:n 位地址能编出 2n 个互不相同的编号,每个编号对应一个字节。64 位地址理论上是 16 EB,实际处理器只实现其中一部分(如 48 位 = 256 TB),因为把全部位数都实现出来对页表层数的代价太高。

2n 这个范围被切成两半:用户空间(低地址一大段,用户态与内核态都能访问,放本进程的代码、数据、堆、栈、共享库)与内核空间(高地址一段,只有内核态能访问,放内核代码、内核数据结构、各进程的 PCB 与内核栈)。32 位 Linux 的经典划分是低 3 GB + 高 1 GB。

内核空间出现在每一个进程顶部不是浪费,而是省掉一次地址空间切换:进程随时可能因中断、异常、系统调用进入内核 ⇒ 进入后要立刻访问内核代码与数据 ⇒ 内核若不在当前地址空间里就得先切换整套页表并刷新 TLB ⇒ 而中断和系统调用极其频繁,这个代价无法接受。

二、进程的内存映像

高地址 ┌──────────────────────┐
       │      内核空间          │ ← 各进程共享同一份物理内存;用户态访问即触发保护异常
       ├──────────────────────┤
       │      栈 Stack         │ ← 向低地址增长:函数栈帧(局部变量/参数/返回地址)
       │         ↓             │
       ├──────────────────────┤
       │  共享库 / 内存映射区    │ ← 动态链接库(.so/.dll)、mmap 映射的文件与匿名内存
       ├──────────────────────┤
       │         ↑             │
       │      堆 Heap          │ ← 向高地址增长:malloc/new 动态申请
       ├──────────────────────┤
       │      BSS 段           │ ← 未初始化的全局/静态变量;文件里不存内容,装入时清零
       ├──────────────────────┤
       │      数据段 Data      │ ← 已初始化的全局/静态变量;内容来自可执行文件
       ├──────────────────────┤
       │      代码段 Text      │ ← 机器指令,只读、可共享
低地址 └──────────────────────┘

中间那段常被画成"空闲区域",其实它并不空——动态链接库和 mmap 映射的内容就落在这里,这也解释了"为什么两个进程各自 malloc 得到的地址会差很远"以及内存映射文件映到了哪儿。

区域判据:内容从哪来在可执行文件中占空间吗读写权限大小变化
代码段编译产生的机器指令(原样存着)只读、可执行固定
数据段有初值的全局/静态变量(初值要存下来)可读写固定
BSS 段无初值的全局/静态变量不占(只记长度)可读写固定
运行时 malloc/new 申请不占(运行时才有)可读写向高地址增长
共享库/映射区装入或运行时映射进来不占(只记依赖了哪些库)按段而定映射时变动
函数调用自动产生不占可读写向低地址增长

BSS 这条最能体现"文件布局 ≠ 内存布局":int b[1000000]; 在文件里只留一句"BSS 段长 4 000 000 B",跑起来却实打实占满 4 MB——一个几 KB 的可执行文件占用几 MB 内存就是这么来的。

栈帧里存了什么、堆和栈除增长方向外还差在哪(想弄清递归为什么耗栈、栈溢出为什么能改写返回地址、堆为什么有碎片时展开)

栈不是一堆散乱的局部变量,它以栈帧(stack frame)为单位组织——每发生一次函数调用就压入一个栈帧,返回时整帧弹出。一个栈帧自高地址向低地址通常含:

栈帧内容作用谁写进去的
实参传给被调用函数的参数调用者
返回地址被调用函数执行完要跳回哪调用指令(call)自动压栈
上一帧的帧指针用于返回时恢复调用者的栈帧被调用函数入口处压栈
被调用者保存的寄存器保证返回后调用者看到的寄存器不变被调用函数
局部变量本次调用的私有数据被调用函数

由此可以立刻推出三件事:① 递归为什么会耗尽栈——每层递归一个栈帧,深度乘以帧长就是栈的消耗;② 局部变量为什么函数一返回就失效——整帧被弹出,那块空间随时会被下一次调用覆盖;③ 栈溢出为什么可能改写返回地址——返回地址就压在局部变量的相邻位置,写越界的数组会覆盖到它。

比较判据
谁分配、谁回收编译器生成的代码自动完成程序员显式申请与释放看"释放这件事写不写在源码里"
分配动作移动一次栈指针在空闲块链表里查找合适的块前者 O(1),后者要搜索 → 这就是快慢差别的来源
增长方向高 → 低低 → 高相向增长,共享中间空闲区
生命期随函数调用结束到显式释放为止
碎片无(严格后进先出)有(释放顺序任意)后进先出 ⇒ 不可能在中间挖洞

堆栈相向增长不是巧合:把两个大小都无法预知的区域放在同一段空闲空间的两端,谁需要谁往中间长,比给两者各划一个固定上限更省空间——只有当两者之和撑满中间空闲区时才真正耗尽。

三、地址变换与内存保护

逻辑地址转成物理地址的过程叫地址重定位(地址映射、地址变换)。按转换时机分两种:

方式时机硬件支持能否移动程序
静态地址重定位装入时一次性改写无需硬件
动态地址重定位运行时每次访存必需(重定位寄存器 + 界地址寄存器 / MMU 与页表)是,且便于共享

现代系统一律用动态重定位,承担这件事的部件是 MMU(内存管理单元)——它位于 CPU 与地址总线之间,CPU 送出的每一个逻辑地址都必须先过 MMU。这件事只能做在硬件里:交给软件,每条访存指令都要额外跑一段程序,速度无法接受。

图上两个寄存器 / 后面的名字是同一条逻辑在分页系统下的对应物,不要按字面直接替换:分页把逻辑地址拆成页号 P + 页内偏移 W,比较的两端随之降到"页"这一级。判据本身没变——先用"这个进程合法到哪"这个上界挡一道,通过了才做变换,变的只是上界的计量单位(连续分配按字节计,分页按表项个数计)。分页/分段各自的地址划分与检查次数见内存管理基本概念;异常的分类见中断和异常的处理

内存保护有两条路,最容易搞反的就是"检查的是转换前还是转换后的地址":

机制检查的是哪个地址怎么判用在哪
上下限(界限)寄存器物理地址转换后的物理地址是否落在 [下限, 上限] 内早期连续分配
基址 + 界地址寄存器逻辑地址逻辑地址是否 < 界地址值,通过后才加基址动态重定位、分区式管理
一次合法访问、一次越界访问、一次换出重装的地址算法(想在具体数字上确认"先检查后变换"这个顺序时展开)

某系统采用动态重定位,某进程的重定位寄存器值为 0x00200000,界地址寄存器值为 0x00010000

(1)访问逻辑地址 0x00008000 先做越界检查,不要急着做加法:0x00008000=32768<65536=0x00010000合法。合法才做变换:PA=0x00200000+0x00008000=0x00208000(十进制 2097152+32768=2129920)。

(2)访问逻辑地址 0x00012000 7372865536越界,硬件不再进行地址变换,产生地址越界异常并陷入内核,OS 终止该进程。注意此处不存在"算出来的物理地址"——顺手算出 0x00212000 是错的,硬件在比较这一步就已中止了变换。这正是把检查放在变换之前的价值。

(3)该进程被换出后重新装入到物理地址 0x00500000 新物理地址 =0x00500000+0x00008000=0x00508000界地址寄存器不用改——它描述的是逻辑地址空间有多长,是程序自身的属性,与被放在物理内存的哪里毫无关系。

考点速记

  1. 逻辑地址空间大小 =2地址位数,物理地址空间大小取决于实际装了多少主存,两者无关。32 位机装 2 GB 内存,每个进程的逻辑地址空间仍是 4 GB——这个不相等正是虚拟内存存在的理由。
  2. 判断一个地址属于哪类只有一条:需要经过 MMU 才能用的就是逻辑地址,MMU 输出的那一端才是物理地址。用户程序全程只接触逻辑地址。
  3. 内存映像自低到高是代码段 → 数据段 → BSS 段 → 堆(向高增长)→ 共享库/映射区 → 栈(向低增长)→ 内核空间PCB 在内核区,用户看不到也改不了。
  4. 系统里只有一份内核:各进程顶部那段虚拟地址映射到同一份物理内存,为的是进内核时不必切页表、不必刷 TLB;安全由页表项的特权级位保证。
  5. BSS 段在文件里只记长度、在内存里占满全长——一个几 KB 的可执行文件占用几 MB 内存就是这么来的。
  6. 越界检查先于地址变换,且查的是逻辑地址。顺序不能反:先加基址再检查,得到的物理地址已经可能指向别人。分页下判据不变,只是降到页一级——比的是页号 < 页表长度(以表项个数计,不是字节数)。
  7. 两套保护机制查的不是同一个地址:上下限寄存器查转换后的物理地址基址 + 界地址寄存器查转换前的逻辑地址。后者更优——非法地址在做加法前就被挡住,且程序移动时只需改基址。
  8. 越界产生的是异常(程序性中断事件),处理原则是终止进程;同一步被发现的缺页却可恢复,调页后重新执行原指令。

这一节在真题里被考过的形式

这一节没有属于自己标签的真题,所以本页下方没有练习区。 但它不是不考——恰恰相反,它出过一整道 8 分大题,只是那道题的标签挂在了进程那一侧。

  • 给一张虚拟地址空间布局图和一段 C 程序,逐个问各对象位于哪个区域(os-2025-46,标签挂在 process-thread-concepts 下,可在进程基本概念页面的练习区找到)。这道题把本节第三条速记完整考了一遍:PCB 在操作系统内核区(它是 OS 的私有数据,用户进程不能直接访问);main() 的代码在只读代码段;全局指针变量 ptr 在读写数据段(有初值或无初值的全局变量都归数据段一侧,不在栈上);局部变量 length 若不在寄存器则在用户栈;而 ptr 指向的那串字符在堆malloc(100) 分配的)。⚠️ 最容易错的是把 ptrptr 指向的内容混为一谈——指针变量本身和它指向的空间在两个不同的区域,这正是这道题设计的分辨点。
  • 除此之外,本节的结论是大量其他题目的前置认知,只是不单独设问:判断线程共享哪些区域(栈私有、堆与数据段共享)、判断进程切换要换掉哪些东西、以及内存管理整章的地址变换计算,用的都是这一节的地址空间模型。

复习优先级按"给对象定位区域"这一种问法来准备就够。把内存映像那张图从低到高背熟, 再把"指针变量 vs 指针指向的内容""全局变量 vs 局部变量""PCB 在内核区"这三对分辨点记牢, 2025-46 那类题就能拿满。第六、七条那两个顺序问题(先检查后变换、查的是转换前还是转换后的地址) 在内存管理章会被反复使用,这里先建立印象即可。

易错:把指针变量本身和它指向的内容放在同一个区域。全局指针 ptr 在数据段,malloc 出来的空间在堆。

易错:认为 PCB 在进程的用户空间里。它是 OS 的私有数据,在内核区。

易错:把逻辑地址空间大小和物理内存容量混为一谈。前者由地址位数定,后者由实际装了多少内存定。

易错:把越界检查放在地址变换之后。必须先检查再变换,否则算出的物理地址可能已指向别的进程。

易错:分页时拿页号和"页表字节数"比。界限以表项个数计。

易错:认为 BSS 段在可执行文件里也占同样大的空间。它只记一个长度,装入时才实打实开辟并清零。

易错:把地址越界当成可恢复的故障。它属程序性中断事件,处理原则是终止进程;缺页才是重新执行原指令。

教材出处
  • 孙钟秀、费翔林《操作系统教程》(第 6 版)3.1.1 存储管理的主要模式(p82~p83):逻辑地址(相对地址)"总是相对某个基准(通常为 0)开始连续编号",物理地址(绝对地址)是"CPU 执行指令和处理数据时面向的实际主存地址",且"物理地址空间……其大小取决于实际安装的主存容量"。
  • 同书 3.1.2 存储管理的功能(p84~p85):地址转换(重定位)分静态地址重定位与动态地址重定位,动态地址重定位"具有允许程序的主存移动、便于程序共享、主存利用率高等优点";存储保护一节给出"CPU 检查是否允许访问,如果不允许则产生地址保护异常,由操作系统进行相应处理"。
  • 同书 2.2.2 中断源(p46):程序性中断事件的处理原则中,"非法指令、用户态使用特权指令、地址越界、非法存取等指令异常:终止进程";"虚拟地址异常:发现了不在实际主存的地址,需要调整主存后重新执行指令"。
  • 汤小丹《计算机操作系统》1.4.2 存储器管理功能(p18):内存保护"一种比较简单的内存保护机制是设置两个界限寄存器,分别用于存放正在执行程序的上界和下界。在程序运行时,系统须对每条指令所要访问的地址进行检查,如果发生越界,便发出越界中断请求,以停止该程序的执行"。

相关知识

程序的链接与装入操作系统结构内存管理基本概念