资讯详情

你打印的地址是假的:虚拟内存到底替你做了什么

📅 2026/10/27 20:50:50 | 华诺云谱 👁 阅读
你打印的地址是假的:虚拟内存到底替你做了什么
先猜输出。这段程序 fork 出父子两个进程都打印全局变量x的地址和它的值#includestdio.h#includeunistd.h#includesys/wait.hintx42;intmain(void){pid_tpidfork();if(pid0){x100;printf(child: %p - %d\n,x,x);}else{wait(NULL);printf(parent: %p - %d\n,x,x);}return0;}先猜父子打印的地址是否相同、值是否相同再猜同一份程序单独跑两次打印的地址会不会变。三个问题的答案分别是地址相同、值不同一个 100 一个 42、两次运行的地址不一样。三个不正常全出自同一只手虚拟内存。篇 15 结尾说过Cortex-A 上 Linux 驱动拿到的地址是坐标的坐标这一篇进那层坐标的加工厂看0x7ffd3a2b1c94这类地址从 CPU 手里到内存条之前到底被谁经手。一、VA 到 PA一条翻译流水线先说结论%p打出来的地址是虚拟地址VA, Virtual Address不是物理地址PA。CPU 执行一条访存指令时送进地址总线的并不是这个值它要先过一道翻译。流水线的起点是 CPU 里的 MMUMemory Management Unit。每条访存指令拿到 VA 后MMU 把它切成两半高位若干位当虚拟页号VPN低位若干位当页内偏移offsetx86-64 与 arm64 常用 4KB 页即低 12 位。VPN 拿去查页表page table——那是内存里的一张映射表逐级查下去末端表项给出对应的物理页号PFNPFN 与 offset 一拼物理地址才算成形这才被送上地址总线。x86-64 的页表是四级结构虚拟地址 48 位有效位切成 4 段各 9 位做四级索引每级表项 8 字节恰好一页 4KB 装 512 项四级各取一页、末级拼出物理页号。arm64 同样多级常规 48 位扩展配置 52 位中间级还有块描述符允许一次映射大块L1 块对应 1GB、L2 块对应 2MB末级只能映射 4KB 页。这套多级设计同时省了内存没有进程会把 48 位地址空间用满用不到的中间级页表根本不用存在。查表不是没有代价的所以 MMU 里还有一块缓存TLBTranslation Lookaside Buffer。刚查过的翻译结果缓存在 TLB 里下一次访存命中即走不必再翻四级表。TLB 的失效不由编译器和用户程序管理只能在内核层显式处理切换进程或修改页表后由内核刷新 TLBx86 写 CR3、arm64 发 TLBI 指令也是后面mmap、munmap语义的隐藏成本。图VA 到 PA 的翻译流水线。高 36 位逐级查表拿物理页号低 12 位页内偏移原样拼接TLB 缓存翻译结果。二、每个进程一张表隔离从哪来页表不止一张每个进程一张。同一个虚拟地址0x400000在 A 进程的页表里指向物理帧 100在 B 进程的页表里可以指向物理帧 2050——地址数值相同落在两块完全不同的物理内存上。父子进程打印同一个x读到两个值机制就在这fork给子进程复制了一张新页表父子从此各走各的映射。上下文切换的真相也随之显形CPU 只有一套 MMU当前进程的页表是寄存器里存的一个基址x86-64 是CR3arm64 是TTBR0。进程切换时内核换掉这个寄存器指向的页表基址下一瞬间同一个虚拟地址就指向了另一处物理内存。进程隔离不是硬件给每个进程发了独立的内存条而是同一块内存条上每张表各映射一份互不相干的视图。图同一地址两个世界。父子进程的页表把同一个虚拟页映射到不同物理帧子进程先写触发 COW 拿自己的副本。这套视图还顺带解决了桌面系统最头疼的碎片与安全问题每个进程都拥有从 0 开始排布的大片地址空间代码段、数据段、堆、栈各就各位分配器在连续虚拟地址上怎么排都行物理页的碎片由内核在后台收拾进程想摸别人的内存页表里没有那条映射摸到的只有异常。三、这张表让出的三个魔法页表一旦成为地址的名义中枢许多桌面编程想当然的能力就浮出了水面。魔法一按需调页demand paging。程序启动或malloc大片内存时内核并不急着分配物理页只在页表里放一个暂不存在的标记等程序第一次触碰该页CPU 触发缺页异常内核才补上一个填零的物理页。malloc(1G)瞬间返回、只碰了几个字节的程序几乎不耗物理内存“malloc 成功不等于内存到手的机制层就在这。补的页为什么是零页匿名映射的语义承诺新内存内容为 0”这也把未初始化变量读成 0 的现实原因解释了一半另一半是 .bss 不占文件留给篇 23。魔法二写时复制COW。fork那一刻内核不复制物理内存只把父子两边的页表项都标成只读、指向同一批物理页。谁先写谁触发缺页异常内核给谁单独复制一份。开场的父子同地址不同值正是子进程先写x、触发 COW 拿了自己的副本。fork 一个上百进程的大程序从复制整个地址空间降为复制页表加按需分页UNIX 的分裂能力有一半是这张表给的。魔法三mmap。把文件、设备或匿名内存接进页表映射文件后按页读入、只读用过的部分映射设备后用户态可以像访问数组一样访问硬件缓冲区映射匿名内存就是malloc大块时的底层通道。文件 I/O 从读进缓冲区变成地址空间里多了几页这条路径的细节留给系列后续篇目。四、C 程序视角的三处印证虚拟内存不只是操作系统课的术语回看这个系列讲过的几个为什么机制层全部收敛到这。印证一同一程序两次运行地址不一样。因为地址空间布局随机化ASLR每次启动内核把栈、堆、库的基址在随机偏移上排布0x7ffd3a2b1c94里的高位每次都不同。早年缓冲区溢出攻击靠猜固定地址下毒ASLR 让地址本身成了秘密。印证二悬空指针崩与不崩的统一解释。篇 8 拆过free之后读旧值多数不崩换个大块必崩。机制层一句话小块 free 进分配器的空闲链那块虚拟页的映射还在读旧值不过是从一个已易主的合法页里取数不崩大块走mmap分配free即munmap页表项被摘掉虚拟地址变成没人认领的野地址再访问 MMU 查表失败直接段错误。崩与不崩是页表项在不在的问题不是玄学。印证三空指针为什么几乎总是崩而不是读到 0。内核故意不映射地址 0 附近那一页空指针解引用在页表里查不到任何有效映射异常是最快的反馈。这个题目值得单独一篇下一篇《段错误的根因为什么解引用空指针通常会崩》专门拆从信号、规范位一直拆到裸机边界。写在最后%p打出的是虚拟地址CPU 访存前由 MMU 查页表翻译成物理地址VPN 查表、offset 原样拼接4KB 页下低 12 位不参与翻译。x86-64 四级页表、arm64 多级页表用不到的中间级不必存在TLB 缓存翻译结果进程/页表变更后需刷新。每进程一张页表同一虚拟地址在不同进程里映射到不同物理帧进程隔离是视图隔离不是内存条隔离上下文切换的本质是换页表基址寄存器CR3/TTBR0。页表让出三个魔法按需调页malloc 大块不耗物理页、写时复制fork 不复制内存、mmap把一切接进地址空间。悬空指针崩与不崩是页表项在不在的问题brk 小块的映射还在、mmap 大块已摘表ASLR 让地址本身成了秘密。内存与底层系列才刚开头。下一篇回到那个让每个初学者都摔过一跤的问题《段错误的根因为什么解引用空指针通常会崩》把 SIGSEGV、规范地址与裸机边界一次拆完。同系列文章会陆续发布关注后即可收到更新。回复「交流」我很乐意分享我的学习经验和工作经验直接来聊。感谢阅读。转载请注明出处与作者。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑