Linux 程序地址空间深度解析:虚拟地址背后的真相

Linux 程序地址空间深度解析:虚拟地址背后的真相
在这里插入图片描述

🔥草莓熊Lotso:个人主页
❄️个人专栏: 《C++知识分享》《Linux 入门到实践:零基础也能懂》
✨生活是默默的坚持,毅力是永久的享受!


🎬 博主简介:

在这里插入图片描述

文章目录


前言:

在 C/C++ 开发中,我们经常会打印变量或函数的地址,但你有没有想过:这些地址真的是物理内存地址吗?为什么父子进程中同一个变量的地址相同,内容却能各自独立?其实,我们看到的所有地址都是虚拟地址,而 Linux 的 “程序地址空间”(准确说是进程地址空间)正是这一切的核心。本文从地址空间布局、虚拟地址与物理地址的映射、内核数据结构三个维度,拆解 Linux 程序地址空间的底层逻辑,帮你搞懂 “为什么虚拟地址能隔离进程”“为什么 malloc 不是真的分配物理内存” 等关键问题。

一. 先看现象:打破你对 “地址” 的认知!

先通过一个简单的代码实验,感受虚拟地址的 “诡异” 之处(之前也提到过):

#include<stdio.h>#include<unistd.h>#include<stdlib.h>int g_val =0;// 全局变量intmain(){ pid_t id =fork();// 创建子进程if(id <0){perror("fork failed");return1;}elseif(id ==0){// 子进程 g_val =100;// 子进程修改全局变量printf("子进程[PID:%d]:g_val=%d,地址=%p\n",getpid(), g_val,&g_val);}else{// 父进程sleep(3);// 等待子进程修改完成printf("父进程[PID:%d]:g_val=%d,地址=%p\n",getpid(), g_val,&g_val);}sleep(1);return0;}

编译运行结果

子进程[PID:12345]:g_val=100,地址=0x80497e8 父进程[PID:12344]:g_val=0,地址=0x80497e8 

关键现象:父子进程中g_val的地址完全相同,但值却不一样!

在这里插入图片描述

结论:这个地址绝对不是物理内存地址 —— 物理地址相同的变量不可能存储不同内容。Linux 中我们看到的所有地址,都是 虚拟地址,物理地址由操作系统统一管理,用户完全无法直接访问。OS 必须负责将 虚拟地址 转化成 物理地址


二. 进程地址空间布局:内存的 “逻辑分区”

进程地址空间是操作系统为每个进程分配的 “逻辑内存范围” ,它让每个进程都以为自己独占一块连续的内存,实际物理内存可能是离散的,甚至尚未分配。其经典布局(32位Linux)如下(从高地址到低地址)

在这里插入图片描述

2.1 地址空间分布详情

分区核心作用特点
内核空间(1G)运行内核代码、管理硬件资源(如进程调度、内存分配)用户进程不可直接访问,仅内核可操作
命令行参数与环境变量存储 argv(命令行参数)和 env(环境变量)高地址起始,向下生长
栈(Stack)存储局部变量、函数调用栈帧向下生长(地址从高到低分配),自动分配/释放
共享区(mmap)映射共享库、文件、匿名共享内存进程间可共享数据
堆(Heap)动态内存分配(malloc / new向上生长(地址从低到高分配),需手动申请/释放
未初始化数据区(BSS)存储未初始化的全局变量、静态变量程序启动时初始化为 0
初始化数据区(Data)存储已初始化的全局变量、静态变量占用磁盘空间,加载时直接映射到内存
代码区(Text)存储程序指令(二进制代码)只读属性,防止意外修改

2.2 代码验证地址空间布局

通过打印不同区域的地址,验证上述布局:

#include<stdio.h>#include<stdlib.h>// 初始化全局变量(Data区)int g_unval;// 未初始化全局变量(BSS区)int g_val =100;intmain(int argc,char*argv[],char*env[]){constchar*str ="helloworld";// *str = 'H' // 错误// 代码区(main函数地址)printf("code addr: %p\n", main);// 数据区printf("init global addr: %p\n",&g_val);printf("uninit global addr: %p\n",&g_unval);// 静态变量(Data区)staticint test =10;// 如果是 &heap_mem 就在栈区,因为其本身就是个变量。// 如果是 heap_mem 就在堆区。char*heap_mem =(char*)malloc(10);char*heap_mem1 =(char*)malloc(10);char*heap_mem2 =(char*)malloc(10);char*heap_mem3 =(char*)malloc(10);// 堆区(堆向上生长,heap_mem2 > heap_mem1)printf("heap addr: %p\n", heap_mem);//heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem1);//heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem2);//heap_mem(0), &heap_mem(1)printf("heap addr: %p\n", heap_mem3);//heap_mem(0), &heap_mem(1)// 静态数据区printf("test static addr: %p\n",&test);//heap_mem(0), &heap_mem(1)// 栈区(栈向下生长)printf("stack addr: %p\n",&heap_mem);//heap_mem(0), &heap_mem(1)printf("stack addr: %p\n",&heap_mem1);//heap_mem(0), &heap_mem(1)printf("stack addr: %p\n",&heap_mem2);//heap_mem(0), &heap_mem(1)printf("stack addr: %p\n",&heap_mem3);//heap_mem(0), &heap_mem(1)// 只读字符串printf("read only string addr: %p\n", str);// 命令行参数与环境变量for(int i =0;i < argc; i++){printf("argv[%d]: %p\n", i, argv[i]);}for(int i =0; env[i]; i++){printf("env[%d]: %p\n", i, env[i]);}return0;}

运行结果符合布局顺序(地址又高到低)
环境变量 > 命令行参数 > 栈区 > 堆区 > 数据区 > 代码区

在这里插入图片描述

三. 虚拟地址与物理地址:映射的核心逻辑

进程地址空间的核心是 “虚拟地址”,它与物理地址通过 “页表 + MMU” 实现映射,这是进程隔离、内存高效利用的关键。

3.1 核心概念

  • 虚拟地址(VA:进程看到的地址,仅在进程内部有效,不同进程的虚拟地址可以重复;
  • 物理地址(PA:真实硬件内存的地址,全局唯一,仅 OS 可以直接访问;
  • 页表:内核为每个进程维护的 “地址映射表” ,记录虚拟地址到物理地址的队对应关系;
  • MMU(内存管理单元):CPU硬件组件,负责将虚拟地址通过页表转换为物理地址。
在这里插入图片描述
📌 注意:上面的图就足矣说明一个问题,同一个变量,地址相同,其实就是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。
补充:利用两个图片示例,过渡一下

如何理解区域划分? – 38线的例子

在这里插入图片描述

如何理解虚拟地址空间?-- 大富翁画饼的例子

在这里插入图片描述

映射的一般流程:

  • 进程执行时,CPU 收到的是虚拟地址;
  • MMU 根据当前进程的页表,将虚拟地址转换为物理地址;
  • CPU 通过物理地址访问真实的物理内存
  • 若虚拟地址未映射物理地址(如 malloc 后还没写入数组),会触发 "缺页异常",内核为其分配物理内存并更新页表(这一步暂时只需要知道就行,后面还会再讲的)

3.2 父子进程地址映射的秘密

我们再来回顾一下开篇的实验,父子进程 g_val 虚拟地址相同但内容不同的原因如下:

  • fork 创建子进程时,会复制父进程的页表(浅拷贝),因此虚拟地址映射关系初始完全相同。
  • 当子进程修改 g_val 时,触发 “写时拷贝” – 内核为子进程分配新的物理内存,修改子进程页表中 g_val 的映射关系,父进程的映射不变;
  • 最终,父子进程的相同虚拟地址,映射到不同的物理地址,因此内容数据独立。

四. 内核数据结构:地址空间的 “管理者”

Linux 内核通过三个核心结构体管理进程地址空间,确保每个进程的地址空间独立且有序。

4.1 mm_struct(内存描述符)

每个进程的 task_struct(PCB)中都有一个 mm_struct 指针,它是进程地址空间的 “总描述符” ,记录地址空间的整体信息:

structmm_struct{structvm_area_struct*mmap;// 指向虚拟内存区域链表structrb_root mm_rb;// 虚拟内存区域红黑树(快速查找)unsignedlong task_size;// 具有该结构体的进程的虚拟地址空间的大小unsignedlong start_code, end_code;// 代码区起始/结束地址unsignedlong start_data, end_data;// 数据区起始/结束地址unsignedlong start_brk, brk;// 堆区起始/当前结束地址unsignedlong start_stack;// 栈区起始地址};
  • 核心作用:描述地址空间的整体布局,组织虚拟内存区域;
  • 每个进程有且仅有一个 mm_struct ,确保地址空间独立。
在这里插入图片描述

先来看看由 task_struct 到 mm_struct ,进程的地址空间的分布情况

在这里插入图片描述


既然每一个进程都会有自己独立的 mm_struct ,操作系统肯定是要将这么多进程的 mm_struct 组织起来的!虚拟地址空间的组织方式有两种:

  1. 当虚拟区较少时采取单链表,由 mmap 指针指向这个链表;
  2. 当虚拟区间多时采取红黑树进行管理,由 mm_rb 指向这颗树。

Linux 内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域(VMA),由于每个进程不同质的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个 vm_area_struct 结构来分表表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是 vm_area_struct 结构来连接各个 VMA,方便进程快访问,同时也解决了比如栈区中间有段释放了那它剩下的两段区域该怎么管理的问题。

4.2 vm_area_struct(虚拟内存区域)

地址空间的每个分区(如代码区、堆区、栈区)都是一个 vm_area_struct,它描述单个虚拟内存区域的属性:

structvm_area_struct{unsignedlong vm_start;// 区域起始虚拟地址unsignedlong vm_end;// 区域结束虚拟地址structvm_area_struct*vm_next;// 下一个虚拟区域unsignedlong vm_flags;// 区域属性(标志位,如只读、可写、可执行)structmm_struct*vm_mm;// 关联的mm_struct};

所以我们可以对之前那个图再进程更细致的描述,如下图所示:

在这里插入图片描述


在这里插入图片描述
  • 例如:代码区对应一个 vm_flags 为 “只读 + 可执行” 的 vm_area_struct
  • 内核通过链表(mmap)或红黑树(mm_rb)管理多个 vm_area_struct,快速查找指定虚拟地址所属区域。

数据结构关系

task_struct(进程控制块) ↓ mm_struct(内存描述符) ↓ vm_area_struct(代码区)、vm_area_struct(堆区)、vm_area_struct(栈区)...(链表/红黑树组织) ↓ 页表(虚拟地址→物理地址映射) ↓ MMU(硬件地址转换) ↓ 物理内存 
在这里插入图片描述

五. 为什么需要虚拟地址空间?

虚拟地址空间不是多余的,它解决了直接使用物理地址的三大痛点:

在这里插入图片描述

1. 进程隔离与安全

  • 每个进程的虚拟地址空间独立,无法直接访问其他进程的虚拟地址,更无法直接操作物理内存;
  • 内核通过页表控制访问权限(如代码区只读),防止进程恶意修改指令或系统内存,提升安全性。

2. 地址连续与物理离散

  • 进程看到的虚拟地址是连续的,便于程序编写(如数组访问);
  • 实际物理地址可以是离散的,内核通过页表将离散的物理内存 “拼接” 成连续的虚拟地址,提高物理内存利用率。

3. 延迟分配与高效利用

  • mallocnew时,内核仅在虚拟地址空间中预留空间,不分配物理内存;
  • 当进程首次写入数据时,触发 “缺页异常”,内核才分配物理内存并建立映射,避免物理内存浪费。

4. 地址无关性

  • 程序编译时无需关心实际物理地址,仅需使用虚拟地址;
  • 内核可将程序加载到任意虚拟地址,通过页表映射到合适的物理地址,提高程序的可移植性。

✅️ 补充

在这里插入图片描述


📝 图示理解:

在这里插入图片描述
❌️ 常见误区总结:“程序地址空间”=“物理内存”:错误!程序地址空间是逻辑概念,物理内存是硬件资源,两者通过页表映射关联;malloc 成功 = 物理内存已分配:错误!malloc 仅分配虚拟地址,物理内存是延迟分配的,首次写入才会真正分配;虚拟地址相同 = 物理地址相同:错误!不同进程的相同虚拟地址,会通过各自的页表映射到不同物理地址,实现进程隔离;栈和堆的生长方向固定:32 位 Linux 中栈向下、堆向上生长,但不是绝对的,具体由内核和编译器决定。

结尾:

🍓 我是草莓熊 Lotso!若这篇技术干货帮你打通了学习中的卡点: 👀 【关注】跟我一起深耕技术领域,从基础到进阶,见证每一次成长 ❤️ 【点赞】让优质内容被更多人看见,让知识传递更有力量 ⭐ 【收藏】把核心知识点、实战技巧存好,需要时直接查、随时用 💬 【评论】分享你的经验或疑问(比如曾踩过的技术坑?),一起交流避坑 🗳️ 【投票】用你的选择助力社区内容方向,告诉大家哪个技术点最该重点拆解 技术之路难免有困惑,但同行的人会让前进更有方向~愿我们都能在自己专注的领域里,一步步靠近心中的技术目标! 

结语:Linux 程序地址空间的核心是 “虚拟地址 + 页表映射”,它让进程以为自己独占内存,同时实现了安全隔离、高效利用物理内存的目标。理解这一机制,能帮你更好地排查内存泄漏、进程崩溃等问题(如野指针本质是访问了无效的虚拟地址)。本文覆盖了地址空间布局、虚拟与物理地址映射、内核数据结构三大核心,结合实验和代码帮你巩固理解。如果需要深入学习 “缺页异常处理”“写时拷贝实现”“大页内存” 等进阶内容,可以进一步扩展。

✨把这些内容吃透超牛的!放松下吧✨ʕ˘ᴥ˘ʔづきらど

Read more

Flutter 三方库 flutter_curve25519 的鸿蒙化适配指南 - 实现高性能 X25519 密钥交换、端到端加密与椭圆曲线加密实战

Flutter 三方库 flutter_curve25519 的鸿蒙化适配指南 - 实现高性能 X25519 密钥交换、端到端加密与椭圆曲线加密实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 flutter_curve25519 的鸿蒙化适配指南 - 实现高性能 X25519 密钥交换、端到端加密与椭圆曲线加密实战 前言 在 Flutter for OpenHarmony 的安全协议开发中,椭圆曲线密码学(ECC)是构建端到端加密(E2EE)的基础。flutter_curve25519 是 Curve25519 算法的高性能实现。它能够快速生成公私钥对并进行安全密钥协商(X25519)。本文将指导大家如何在鸿蒙端利用该库构建金融级的安全通信底座。 一、原理解析 / 概念介绍 1.1 基础原理 Curve25519 是一种目前公认最快速、最高效且抗定时攻击的椭圆曲线。flutter_curve25519 将复杂的数学运算通过二进制优化,提供了简洁的 API。 graph LR

By Ne0inhk
Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获

Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 super_log 的鸿蒙化适配指南 - 实现极具视觉冲击力的彩色终端日志、支持动态过滤与全局异常捕获 前言 在进行 Flutter for OpenHarmony 的日常开发调试时,面对控制台里密密麻麻、死板单调的白色日志,开发者很容易在大海捞针般的排错过程中产生疲劳。super_log 是一个专注于日志可视化体验的增强库。它通过丰富的配色方案和清晰的结构化打印,让鸿蒙控制台里的每条日志都具备“辨识度”。本文将介绍如何在鸿蒙端利用 super_log 让你的代码“自白”得更加生动。 一、原理解析 / 概念介绍 1.1 基础原理 super_log 基于终端的 ANSI 颜色转义序列。它通过解析日志级别,并在输出字符串中自动嵌入特定的颜色代码。同时,它还内置了美观的边框修饰符(Box

By Ne0inhk
鸿蒙APP开发从入门到精通:性能优化与Next原生合规

鸿蒙APP开发从入门到精通:性能优化与Next原生合规

《鸿蒙APP开发从入门到精通》第11篇:性能优化与Next原生合规 🏎️✅ 内容承接与核心价值 这是《鸿蒙APP开发从入门到精通》的第11篇——性能优化与Next原生合规篇,承接第10篇的「AI原生与用户增长」,100%复用项目架构,为后续第12篇的电商购物车全栈项目最终上线铺垫性能优化与Next原生合规的核心技术。 学习目标: * 掌握鸿蒙APP性能优化的定义与架构; * 实现启动优化、渲染优化、网络优化等性能优化功能; * 理解Next原生合规的原理与实现方式; * 开发代码规范、权限合规、数据合规等合规功能; * 优化性能与合规的用户体验(响应速度、内存占用、电池消耗)。 学习重点: * 鸿蒙APP性能优化的开发流程; * 性能优化的分类与使用场景; * 启动优化、渲染优化、网络优化的实现; * Next原生合规的设计与实现。 一、 性能优化基础 🎯 1.1 性能优化定义 性能优化是指对应用进行优化,提高应用的响应速度、降低内存占用、减少电池消耗等,主要包括以下方面: * 启动优化:优化应用的启动时间; * 渲染优化:优化应用的界

By Ne0inhk
Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter for OpenHarmony:git 纯 Dart 实现的 Git 操作库(在应用内实现版本控制) 深度解析与鸿蒙适配指南 前言 Git 通常作为命令行工具存在。但在某些特殊场景下,你可能需要在 App 内部直接操作 Git 仓库,例如: * 开发一个手机端的 Git 客户端 App。 * 使用 Git 作为笔记应用(如 Obsidian)的同步后端。 * 在应用内拉取远程配置或 CMS 内容。 git 是一个纯 Dart 实现的 Git 核心库(类似于 Java 的 JGit)。它负责直接读写

By Ne0inhk