前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >内存地址中藏着的学问

内存地址中藏着的学问

作者头像
KINGYT
发布2020-09-28 15:35:26
1.3K0
发布2020-09-28 15:35:26
举报

0x01 虚拟地址

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

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

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

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

代码语言:javascript
复制
$ 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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-09-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档