内存地址中藏着的学问

0x01 虚拟地址

作为一个技术人员,不管你日常用的是什么语言,你都应该或多或少的听过c语言。而如果你了解c,那你一定知道它有个,有时可以让你天马行空,有时又可以让你郁郁寡欢的数据类型,是的,它就是指针。

指针本质上和其他的数据类型一样,存放的都是一个数值,只不过指针的这个数值表示的是内存地址,而非具体数据。

但你知道吗,这个地址可不是真实的物理内存地址,而是一个假的地址,我们称之为虚拟地址。

不信?那你看看这段代码的输出,你觉得你的机器会有这么大的内存吗?

$ cat main.c
#include <stdio.h>
int main(int argc, char **argv) {
  printf("%p\n", &argc);
  printf("%p\n", &argv);
  return 0;
}
$ gcc main.c && ./a.out
0x7ffefd057a8c
0x7ffefd057a80

0x02 虚拟地址到物理地址的转换

既然我们是用内存来存取数据,最终肯定是要用到它的物理地址的,那虚拟地址是如何转换成物理地址的呢?

这个其实是由硬件和操作系统一起来完成的。

当我们在存取某个内存变量时,其对应到的汇编代码其实就是mov指令,当cpu在执行类似指令时,如果遇到内存地址,则会根据一定的规则,自动将该虚拟的内存地址,转换成真实的物理地址,这在硬件层面是自动完成的。

这个过程我们称之为paging(其实转换过程更复杂,还包含segmentation等阶段)。

在软件层面,或者说是操作系统层面,我们要为这种paging机制提供一种数据结构,叫做 hierarchical paging structures,用来定义虚拟地址到物理地址的映射规则,cpu在paging的过程中,会使用到我们提供的这个数据结构,进而转换出我们期望的物理地址。

hierarchical paging structures的格式及使用方式,是由硬件定义的,具体描述可以参考Intel或AMD的官方文档。

操作系统按照这种格式,为每个进程定义一个自己的hierarchical paging structures,所以每个进程都有自己独享的虚拟地址空间,在进程运行的过程中,操作系统会把各个进程的物理内存使用情况,记录在hierarchical paging structures里。

总之,虚拟地址到物理地址的转换,就是根据操作系统提供的映射规则,由cpu在执行指令时自动完成的。

0x03 四种paging模式

计算机发展到现在(x86体系),paging模式已经有了四种,分别是32-bit paging、PAE paging、4-level paging、5-level paging。我们可以将其做粗略划分,前两种模式是应用于32位平台,后两种模式是应用于64位平台。

因为现在32位的机器已经很少了,所以前两种模式我们就不再介绍,而后两种模式的实现机制基本上也是相同的,只是各自支持的虚拟地址空间范围和物理地址空间范围不同。

对于64位的平台来说,4-level paging支持256TiB的虚拟地址空间,以及64TiB的物理地址空间,5-level paging支持128PiB的虚拟地址空间,以及4PiB的物理地址空间。

由上可见,4-level paging支持的内存空间对我们来说已经足够用了,现实生活中很少会用到5-level paging,5-level paging只是对未来的一个预期及扩展。

所以下面我们主要讨论4-level paging。

0x04 4-level paging

为了使以下分析便于理解,我花了很长长长的时间画了下图:

我先来简单介绍下,最上面的64位地址表示的是要被转换的虚拟地址,中间最左边的是cr3寄存器,用于存放PML4 table的物理地址,接着的四个矩形就是组成hierarchical paging structures的四层结构,最右面的绿色区域描述的就是最终物理地址的计算规则,该图的最下面是hierarchical paging structures的四层结构里,每个table的各个entry编码规则。

当操作系统创建一个进程时,也会为该进程创建属于它的hierarchical paging structures,所谓的hierarchical paging structures,就是由一系列的4KiB大小的内存page组成的一个树形结构,对于4-level paging来说,这个树形结构有4层,分别对应到上图中的 page map level 4 table、page directory pointer table、page directory、page table。

每层的每个table中都有512个8字节大小的entry,每个entry又指向下一层中的一个table,这样层层关联,就形成了一个4层树形结构。

hierarchical paging structures的第一层只会有一个table,其他层可有多个。

当操作系统的调度程序将该进程设置为运行状态时,会把它的hierarchical paging structures的第一层的table,对于4-level paging来说就是PML4 table,的物理地址放到cr3寄存器中,这样当cpu在执行程序代码并遇到虚拟地址时,就会自动执行以下步骤,将虚拟地址转换成物理地址。

1. 根据cr3寄存器中的PML4 table的物理地址,找到PML4 table,该table是由512个8字节大小的entry组成的4KiB大小的page,我们也可以将其理解成是一个长度为512的64位整形的数组。

2. 将虚拟地址中的47:39位(从0开始,以下如果没有特殊说明,用于表示bit位置的数字都是从0开始)组成的数字作为PML4 table的索引,并找到其对应的entry(简写为PML4E),该entry中存放着下一层的page directory pointer table的物理地址。

3. 根据PML4E中的物理地址,我们找到第二层中的一个page directory pointer table,该table也是一个由512个8字节大小的entry组成的4KiB page,或者说是长度为512的64位整形的数组。

4. 接着用虚拟地址中的38:30位作为索引,定位到page directory pointer table中的一个entry。

5. 依次往复,我们最终通过虚拟地址中的20:12位,定位到了第四层的一个page table的entry,简写为 PTE。

6. 在PTE中,我们拿出其中51:12位,作为最终物理地址的51:12位,然后从虚拟地址中拿出剩下11:0位,作为最终物理地址的11:0位,这样我们就得到了一个总长度为52位的物理地址,cpu会拿着这个物理地址去到对应的内存中存取数据。

这就是4-level paging的虚拟地址转换过程,5-level paging的转换过程和4-level paging的基本类似,区别就是在cr3寄存器和PML4 table之间多加了一层PML5 table,然后虚拟地址中,用56:48这9个bits做为该table的索引。

0x05 4-level paging中的物理地址

由上图可见,4-level paging在做虚拟地址到物理地址的转换过程中,不管是cr3,还是各个entry,存放的都是下一层级中的一个table的真实的物理地址,且包括最终的物理地址,长度都是52位,而并不是虚拟地址中使用的48位。

那为什么是52位呢?

其实确切来说应该是最大52位,不同硬件平台可以自己选择支持多少位,这个规范可以从AMD官方文档

以及Intel的官方文档

看到。

再参考linux内核文档的 5level-paging (在文章最后的参考资料中有具体网址),我们可以确切得知,4-level paging的有效虚拟地址是48位,有效物理地址是46位,5-level paging的有效虚拟地址是57位,有效物理地址是52位,这个在上面的4-level paging和5-level paging的虚拟和物理地址空间范围的讨论中也有提到过。

0x06 4-level paging中的虚拟地址

再来看4-level paging中的虚拟地址,由上面分析可知,其有效虚拟地址只有48位(47:0),那63:48的地址位存放的是什么呢?

由AMD文档

及Intel文档

可知,虚拟地址的63:48位存放的是47位的sign extension,也就是说,如果虚拟地址的47位是0,则63:48位必须是0,如果47位是1,则63:48位必须是1,这个格式被称为 canonical address form。

0x07 我们能用虚拟地址干些什么

既然我们知道了虚拟地址的编码格式,那我们可以用它来干些什么呢?

回到文章最开始的地方,我们知道c语言中,地址被存到了指针类型的变量里,我们通过对指针进行操作,间接的对其指向的内存进行了操作。

既然我们知道了,指针中存放的地址的高16位 (63:48) 有canonical address form 这种规则,那我们就可以利用这种规则,在这16位中存放一些我们自己的数据,比如该指针对应的数据类型,当我们需要使用该指针时,我们再根据指针地址中的47位,将整个地址恢复成其原来的canonical address form。

很好,但别急,还有更好的。

其实虚拟地址中的47位我们也可以使用,也就是说,虚拟地址中的63:47都可以用来存储我们自己的数据。

为什么呢?

我们知道,每个进程都有自己单独的虚拟地址空间,对于64位地址来说,就是从0x0000 0000 0000 0000到0xffff ffff ffff ffff的地址范围。

我们还知道,在该地址空间内,不仅有我们的用户程序,还有内核代码(是的,内核代码也映射到了用户进程的虚拟地址空间)。

根据上面定义的虚拟地址的canonical address form,内核将虚拟地址空间进一步划分:

其把虚拟地址的47位为0的地址空间划给了用户代码,而47位为1的地址空间划给了内核代码。

而我们写的程序肯定是在用户空间,所以,我们程序中能用到的虚拟地址的63:47位肯定为0。

这就是为什么我们可以使用虚拟地址中63:47这高17位的原因。

其实该技巧已经在很多 JIT compiler 中被使用到了。

0x08 这种技巧不会和将来的 5-level paging冲突吗

我们上文讲过,5-level paging是会使用虚拟地址的56:48位作为PML5 table的索引,那如果我们像上面描述的那样,在程序中使用虚拟地址的高17位来存储我们的数据,那5-level paging来临时,我们的程序岂不是都不可用了?

这个大可放心,写内核的大神们早已经帮我们想好了兼容方式

简单来说就是默认情况下,内核不会分配47位及其以上的虚拟地址空间给用户,除非用户指定要求,完美。

0x09 虚拟地址的意义

聊了这么多,那虚拟地址存在的意义是什么呢?为什么不直接使用物理地址呢?

好处非常多,我简单说几个吧。

比如进程间的内存隔离,因为你访问的任何虚拟地址都是你自己进程的地址,这样即使有恶意进程,也不会破坏其他进程的数据。

比如物理内存的按需分配,你要操作系统给你分配内存,其实它是只分配了虚拟地址空间,真正的物理内存分配是要等到你使用时才会触发。

比如共享相同的内核代码,以及共享库代码,这样这些共用的代码就只占用一份内存,他们会以映射到进程虚拟地址空间的方式,供用户进程使用。

0x0a 结束语

计算机世界的知识就是这么庞大与繁杂,一个小小的地址就能牵扯出这么多的学问。

很多人在面对这份繁杂时就已望而却步,很多人投入其中但因为频频受挫也中途退出,但还有一部分人,他们依旧努力坚持并苦中做乐,坚信他们肯定能等来黑夜的黎明。

谁说做技术做学问就不是星辰大海了,我们选择了这条路并扬帆启航,虽然孤独,虽然黑暗,但有信念相陪,有星辰作伴,我们一定可以看到黎明,到达彼岸。

0x0b 参考资料

Intel® 64 and IA-32 Architectures Software Developer’s Manual

AMD64 Architecture Programmer’s Manual

https://lwn.net/Articles/106177/

https://lwn.net/Articles/717293/

https://www.kernel.org/doc/html/latest/x86/x86_64/5level-paging.html

本文分享自微信公众号 - Linux内核及JVM底层相关技术研究(ytcode),作者:wangyuntao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-09-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 计算机中内存地址计算问题

    在软件设计师考试中经常会出现这种题目 例如(2013年下半年的软件设计师考试题目)

    用户5166556
  • Swift3中的Array内存地址和关联对象的问题

    xferris
  • python中如何查看指定内存地址的内容

    python中一般并不需要查看内存内容,但作为从C/C++过来的人,有的时候还是想看看内存,有时是为了验证内容是否与预期一致,有时是为了探究下内存布局。

    李拜六不开鑫
  • python中查看变量内存地址的方法

    本文实例讲述了python中查看变量内存地址的方法。分享给大家供大家参考。具体实现方法如下:

    py3study
  • 【python爬虫】爬取知乎收藏夹内所有问题名称地址保存至Mysql

    转载请注明源地址,代码在Github中(可能会更新):https://github.com/qqxx6661/python/

    蛮三刀酱
  • Linux从头学02:x86中内存【段寻址】方式的来龙去脉

    饭是一口一口的吃,计算机也是一步一步的发展,例如下面这张英特尔公司的 CPU 型号历史:

    IOT物联网小镇
  • macOS下利用dSYM文件将crash文件中的内存地址转换为可读符号

    一、使用流程     Windows下的程序运行崩溃时,往往可以利用pdb文件快速解析出程序崩溃的具体位置,甚至可以对应到源代码的具体行数。macOS下的sym...

    24K纯开源
  • 揭开暗网服务的神秘面纱(上)

    各位Freebuf的同学们大家好,我将会在这一系列的文章中跟大家讨论有关匿名系统安全方面内容,包括暗网的运行机制以及其中的各种匿名服务。以下是我在2016年Ha...

    FB客服
  • 我是如何面试别人List相关知识的

    ?先来点鸡汤 前几年易中天可谓非常的火,接受过很多采访。他的情况比较特殊,在武汉读高中时期,恰逢“知识青年上山下乡”活动,就到新疆去了。 在新疆生产建设兵团...

    Java团长
  • 【面试】我是如何面试别人List相关知识的,深度有点长文

    前几年易中天可谓非常的火,接受过很多采访。他的情况比较特殊,在武汉读高中时期,恰逢“知识青年上山下乡”活动,就到新疆去了。

    帅地
  • Tor的恶意应用

    Tor本来是为用户提供匿名上网保护用户隐私的工具,但是对于一些用户来说,他们可以利用Tor的隐蔽性进行黑客攻击或非法交易活动。总结Tor的恶意应用主要表现在以下...

    FB客服
  • Spring Boot 2.0(六):使用 Docker 部署 Spring Boot 开源软件云收藏

    云收藏项目已经开源2年多了,作为当初刚开始学习 Spring Boot 的练手项目,使用了很多当时很新的技术,现在看来其实很多新技术是没有必要使用的,但做为学习...

    纯洁的微笑
  • 详细讲解:零知识证明 之 zk-SNARK 开篇

    zk-SNARK 全称是“Zero-Knowledge Succinct Non-Interactive Argument ofKnowledge”,中文是“零...

    林冠宏-指尖下的幽灵
  • 急死!CPU被挖矿了,却找不到哪个进程!

    最近有朋友在群里反馈,自己服务器的CPU一直处于高占用状态,但用top、ps等命令却一直找不到是哪个进程在占用,怀疑中了挖矿病毒,急的团团转。

    轩辕之风
  • 渗透技巧 | 查找网站后台方法总结整理

    链接:https://pan.baidu.com/s/1y3vEMEkQQiErs5LeujWZ-A 提取码:3e1b

    HACK学习
  • 暗网Tor路由器用户被坑,最新研究: 原来比特币交易可以泄露他们的身份……

    比特币并不是一个完全匿名的支付网络。然而,实际上,有很多人(即使是最注重隐私的人)似乎都忘记了这一点。

    区块链大本营
  • C语言自定义函数如何返回数组(上)?

    最近看到一些同学问题,有提到说:如何在一个函数中返回数组呢? 能否直接在自定义 函数中,写成char *类型返回值,直接返回呢?,代码如下: ? 直接返回str...

    编程范 源代码公司
  • 挖洞经验之代理不当日进内网

    大家好,我是STCX,应M姐姐之邀写一篇文章为咱们公众号做点贡献,那我就扯一下之前挖一个漏洞的经验。 ---- 正向代理和反向代理是forward/revers...

    ChaMd5安全团队
  • 追踪影响数百万用户的Android广告软件开发人员

    ESET研究人员在Google Play上发现了活跃一年的广告软件运营商。所涉及的应用程序已安装了800万次,背后的运营商使用了一些技巧来隐藏。

    FB客服

扫码关注云+社区

领取腾讯云代金券