
一、程序地址空间程序地址空间是程序运行成为进程之后进程自身视角下所能访问的全部地址范围。这片地址空间被操作系统划分成多个功能区域包括代码段.text、初始化数据段.data、未初始化数据段.bss、只读常量段.rodata、堆heap、栈stack以及共享库映射区域等。这些区域分别存放着程序指令、常量、全局变量、动态分配的内存、函数调用栈等运行时数据共同支撑起一个进程从启动到结束的完整生命周期。下面通过一段 C 程序逐一验证各个区域的地址分布与特性#include stdio.h #include unistd.h #include stdlib.h int g_unval; // 未初始化全局变量 → BSS 段 int g_val 100; // 已初始化全局变量 → 数据段 (.data) int main(int argc, char *argv[], char *env[]) { const char *str helloworld; // 字符串字面量 → 只读常量段 (.rodata) static int test 10; // 静态局部变量 → 数据段 (.data) // 动态申请堆内存 char *heap_mem (char*)malloc(10); char *heap_mem1 (char*)malloc(10); char *heap_mem2 (char*)malloc(10); char *heap_mem3 (char*)malloc(10); printf(code addr: %p\n, main); // 代码段地址 printf(init global addr: %p\n, g_val); // 数据段地址 printf(uninit global addr: %p\n, g_unval); // BSS 段地址 printf(test static addr: %p\n, test); // 静态变量地址 printf(heap addr: %p\n, heap_mem); // 堆内存地址 printf(heap addr: %p\n, heap_mem1); printf(heap addr: %p\n, heap_mem2); printf(heap addr: %p\n, heap_mem3); printf(stack addr: %p\n, heap_mem); // 指针变量本身在栈上 printf(stack addr: %p\n, heap_mem1); printf(stack addr: %p\n, heap_mem2); printf(stack addr: %p\n, heap_mem3); printf(read only string addr: %p\n, str); // 只读常量段地址 return 0; }运行上述程序后结合输出结果可以依次验证各区域1.main函数地址验证代码段 (.text)。代码段存放可执行指令地址位置固定权限为只读可执行程序运行期间不可修改。2.g_val已初始化全局变量验证数据段 (.data)。数据段存放初始化过的全局变量和静态变量程序加载时从可执行文件中拷贝到内存。3.g_unval未初始化全局变量验证BSS 段。BSS 段存放未初始化全局变量程序加载时由操作系统统一清零因此不占用可执行文件空间。4.teststatic 静态局部变量静态变量不属于栈存放于数据段 (.data)生命周期随整个进程而不是函数局部栈函数返回后依然存在。5.heap_mem / heap_mem1...malloc 返回值验证堆 (heap)。堆由 malloc 动态申请地址从低往高增长多次 malloc 得到连续但有间隙的地址间隙是内存对齐、堆管理元数据。注意heap_mem本身是指针变量存放在栈上heap_mem的值才是堆内存地址。6.heap_mem、heap_mem1...取指针变量本身的地址验证栈 (stack)。局部变量包括指针变量都在栈上栈地址向低地址方向增长与堆的增长方向相反。7.str的值字符串字面量helloworld地址验证只读常量段 (.rodata)。该区域存放字符串常量等只读数据属于只读区域任何修改都会导致段错误。二、虚拟地址空间2.1 虚拟地址#include stdio.h #include unistd.h #include sys/types.h int num 100;//全局变量 int main(int argc, char *argv[], char *env[]) { pid_t id fork(); if(id 0) { while(1) { printf(我是子进程pid%dppid%dnum%daddress%p\n,getpid(), getppid(), num, num); num;//将num自增 sleep(1); } } else if(id 0) { while(1) { printf(我是父进程pid%dppid%dnum%daddress%p\n,getpid(), getppid(), num, num); sleep(1); } } return 0; }当我们运行上述代码观察结果时会发现一个奇特现象子进程内的 num 随着循环不断自增而父进程中的 num 始终保持原值。同一个变量却同时存在两个不同的值这本身就令人费解更让人疑惑的是对比父子进程中这个变量的地址二者居然是完全相同的这看起来似乎违背常理。而这种现象并不是系统或者编译产生的错误因为我们通过获取到的地址本质都是虚拟地址。创建进程时PCB 中的mm_struct会管理一份属于该进程的独立虚拟地址空间。当调用 fork 创建子进程时子进程的虚拟地址空间会基于父进程的虚拟地址空间进行拷贝这正是进程独立性的体现也解释了父子进程中 num 拥有相同虚拟地址却能保存不同数值的原因。那虚拟地址和真实物理地址之间又是如何建立联系的虚拟地址空间依靠页表完成地址转换把虚拟地址映射到物理内存地址。fork 拷贝子进程时会对父进程的页表执行浅拷贝父子进程此时共享同一份物理内存。一旦子进程试图修改变量 num就会触发写时复制操作系统会在物理内存中新开辟一块空间存放修改后的数据再把子进程中该变量对应的虚拟地址重新映射到这块新的物理内存。2.2 什么是虚拟地址空间通过上面 fork 创建子进程的现象我们就可以引出虚拟地址空间的概念。虚拟地址空间是操作系统借助 MMU 硬件为每一个进程抽象出来的一套独立连续的地址编号集合由进程 PCB 中的mm_struct结构体进行管理。它并不是真实存在的物理内存只是进程视角下看到的地址范围。进程在代码中使用取到的所有地址全部都是这类虚拟地址。操作系统通过页表建立虚拟地址到物理内存地址之间的映射关系。不同进程各自拥有独立的虚拟地址空间哪怕两个进程内的虚拟地址完全相同经过页表转换后也可以指向不同的物理内存以此实现进程之间的内存隔离。形象的理解操作系统大富翁系统中的各个进程私生子。大富翁向每位私生子许诺可以拥有一份遗产物理内存但这份许诺只是给每个私生子单独画下的一张大饼虚拟地址空间。由于私生子数量众多大富翁需要为每个人的大饼单独建立档案来管理mm_struct记录这份虚拟地址空间的布局、页表等信息。虚拟地址空间的区域划分在虚拟地址程序地址空间中包含多个不同功能的区域因此mm_struct需要对这些区域进行划分划分的方式就是记录每一块区域的起始地址与结束地址。struct mm_struct { /*...*/ struct vm_area_struct *mmap; /* 指向虚拟区间(VMA)链表 */ struct rb_root mm_rb; /* red_black树 */ unsigned long task_size; /*具有该结构体的进程的虚拟地址空间的⼤⼩*/ /*...*/ // 代码段、数据段、堆栈段、参数段及环境段的起始和结束地址。 unsigned long start_code, end_code, start_data, end_data; unsigned long start_brk, brk, start_stack; unsigned long arg_start, arg_end, env_start, env_end; /*...*/ }2.3 为什么需要虚拟地址空间1.控制进程的行为拦截进程的非法访问以此保护物理内存避免一个进程破坏其他进程或者内核的数据。2.依托虚拟地址空间和页表可以让进程的内存布局从物理上的无序碎片变为进程视角下有序连续的地址空间。3.将进程管理和内存管理进行解耦合让操作系统可以更灵活地管理内存资源。三、程序地址空间和虚拟地址空间的区别与联系3.1 联系程序地址空间本质就是单个进程所拥有的虚拟地址空间。操作系统为每个进程创建独立的虚拟地址空间这个分配给进程、划分出代码段、数据段、堆、栈等区域的地址范围就是程序地址空间。我们在代码中用获取到的地址都属于这片虚拟地址。mm_struct结构体管理着程序地址空间的各个区域依靠页表完成虚拟地址到物理内存的映射fork 创建子进程拷贝的也是父进程的程序地址空间。3.2 区别虚拟地址空间是操作系统结合 MMU 硬件提供的通用内存抽象概念是一套地址映射模型而程序地址空间是这个模型作用在某一个运行进程上的实例。虚拟地址空间描述的是地址映射这套机制本身程序地址空间侧重描述一个进程内部的地址分段布局代码段、BSS 段、堆、栈等。简单来说虚拟地址空间是通用概念程序地址空间是进程实例。四、小结本文围绕程序地址空间与虚拟地址空间展开讲解核心要点如下1.程序地址空间是进程视角下可访问的全部地址范围被划分为代码段、数据段、BSS 段、堆、栈以及只读常量段等区域存放程序指令、常量、全局变量、动态分配内存和函数调用栈等运行时数据。2.虚拟地址通过 fork 创建子进程时父子进程变量地址相同但值不同的现象说明代码中通过取到的地址本质都是虚拟地址由 PCB 中的mm_struct管理。3.地址转换虚拟地址依靠页表映射到物理内存fork 时页表浅拷贝使父子进程共享物理内存子进程修改变量时触发写时复制重新映射到新开辟的物理内存。4.虚拟地址空间是操作系统借助 MMU 为每个进程抽象出的独立连续地址编号集合由mm_struct记录各区域起始与结束地址实现进程间内存隔离。5.为什么需要一是拦截非法访问保护物理内存二是把物理无序碎片变为进程视角下有序连续的地址空间三是将进程管理与内存管理解耦合提升灵活性。6.区别与联系程序地址空间本质是单个进程所拥有的虚拟地址空间实例虚拟地址空间是通用地址映射模型程序地址空间侧重描述进程内部的地址分段布局。