15分钟带你了解虚拟内存

前言这篇文章主要是想尽量直观的介绍虚拟内存的知识,而虚拟内存的知识不管作为在校学生的基础知识,面试的问题以及计算机程序本身性能的优化都有着重要的意义。而起意写这篇文章主要还是因为在python,人工智能的大浪潮下,我发现好多人对这方面真的无限趋近于不知道。我不是说懂这些基础知识比懂人工智能水平就是高,但是作为一个软件工程师,我觉得相对于调库调参,我们更应该有更牢靠的基础知识。不然很容易陷入,高深的...

15分钟带你了解虚拟内存

前言

这篇文章主要是想尽量直观的介绍虚拟内存的知识,而虚拟内存的知识不管作为在校学生的基础知识,面试的问题以及计算机程序本身性能的优化都有着重要的意义。而起意写这篇文章主要还是因为在python,人工智能的大浪潮下,我发现好多人对这方面真的无限趋近于不知道。我不是说懂这些基础知识比懂人工智能水平就是高,但是作为一个软件工程师,我觉得相对于调库调参,我们更应该有更牢靠的基础知识。不然很容易陷入,高深的数学不会,基础的知识也不知道的尴尬境地。毕竟从事算法核心的,没有多少人,而作为工程师,我始终觉得我们的使命是如何把这些天赋异禀,脑袋发达的人的想法,构思,算法变成真正可用的东西。而在我从业不算长的年限中遇过的人来看,这绝对不是一种很简单的能力。

阅读本文,需要有基本的c语言和python语言知识,如果提到虚拟内存,脑海中就有虚拟内存分布图的大概样子,那就完美适配这篇文章了。我希望通过这篇文章可以帮助你可以通过推理的方法回答出虚拟内存的各种问题,可以知道这个东西是如何真正和程序结合起来的。

文章大体分为三个部分,

第一部分,介绍虚拟内存的基本知识

第二部分,会直观的展示虚拟内存和我们的程序代码到底是怎么联系起来的

第三部分,我会演示如何改掉虚拟内存的内容,和修改这些内容到底意味着什么,吹的大一点,如何hack一个程序

本文所有的代码都很简单,只有c语言代码和python代码,并且我都跑过,如果你使用以下的环境,应该代码都能跑起来看到结果:

  • 一台Linux发行版的机器,我用的,一个树莓pi
  • Python 3
  • gcc 5.4.0

什么是虚拟内存

如果你是一个程序员,至少你肯定听过内存这个词,虽然你可能真的不知道内存是什么,但是确实在现代程序语言的包装下,你依然可以写出各种程序。如果你真的不知道,那么我觉得还是应该去学习下内存的知识的以及计算机程序是如何被执行起来的。而什么叫虚拟,我至今记得我大学操作系统老师上虚拟内存这一节的时候引用的解释,我拙劣的翻译成中文大概就是:

真实就是这个东西存在并且感受到,虚拟就是这个东西存在但是你感觉不到。

虚拟内存就是这么一类东西,它确实存在,而你却不能在程序中感受到他。为什么要有虚拟内存,原因有很多,比如操作系统分配内存的时候,很难保证一个程序用的内存地址一定是连续的。比如内存是一个全局的东西而且只有一个,而程序有无数个,直接操作内存出问题的概率大,管理也不方便等等。于是虚拟内存的概念就给计算机程序的编写者,编译器等等都提供了一段独立,连续的“内存”空间。而实际上,这段内存不是真是存在的,其地址空间可以比真实的地址空间还要大,通过各种换出换入技术,让程序以为自己运行在一段连续的地址空间上。虚拟内存的概念的伟大之处在于给计算机科学的各种概念设计提供了一种思路,隔离,虚拟,直到现在,docker,各种虚拟化技术不能不说和虚拟内存的概念没有关系。

而提到虚拟内存那么无论在什么样关于操作系统的教科书里一定有这么一张图:

我当时在学习的时候老师会跟我们说这个虚拟内存由哪些部分组成,为了文章看起来比较整体,让我再简单的说明下,对于一个运行的程序,到底有哪些部分组成:

首先虚拟内存的寻址地址是由机器和操作系统决定,比如你是一个32bit的操作系统,那么寻址空间就是4GB,换句话说你的程序可以跑在一个0到0xffff ffff的“盒子”里,而如果你是64位的操作系统,那么这个寻址空间就会更大,意味着,你有更大的“盒子”,可以有更多的可能。

而图中的低地址就是0x0,假设是32位操作系统,那么高地址就是0xffff ffff。那么,就让我们按照人类的认知习惯,从低往高看看每一层都“住”着些什么。

最下面是text段,这里放着程序的执行的代码等等,如果你用objdump这样的程序打开一个程序,最前面你能看到应该是你的代码转化而成的汇编语言。

往上就是已初始化数据段和未初始化数据段,这里存放着全局变量,而这些都会被exec去执行,他们不仅有不同的名称,还有不同的权限,在后面的展示中,你可以直观的看到这些。

而再往上是堆段,也就是面试中经常会被问的,malloc,new出来的内存是存放在哪里的,没错,就是这里。而他的上面是另一个面试问题的来源,局部变量,参数都存在哪里。

住在顶楼的是命令行参数,环境变量等等。

而这些都是理论书本上写的,类似于告诉你两点之间有且只有一条直线一样。到底两点之间是不是真的只能画一条直线,最好的办法应该是自己画一画,以真实去验证理论。所以,到底一个程序在内存中真的是这样吗,或者说我们的程序代码到底和这样一个概念有什么关系,下面的章节就让你看看“虚拟”是如何可以被真实的展示的。

/proc/{pid}/maps

在这一节的最开始,我不得不特别简单的介绍linux下的proc文件夹,其实正确的应该叫他文件系统。而这也是为什么要使用Linux作为代码运行环境的原因,Windows上要看到一个程序的虚拟内存不是不可以,但是要去使用一些第三方工具,唯有Linux,在不需要任何工具的情况就能直观的给你展示所有的内容。而Proc文件系统就是这样一个入口。

如果你在Linux的命令行中输入ls /proc/,你会发现好多内容,其中有很多以数字为名字的文件夹。这些数字对应的就是一个一个的进程,而这些数字就是进程的pid,此时你可以更进一步,随便选一个数字大一点的文件夹,看看里面到底有什么。在我的电脑上,我选了7199这个数字,使用ls /proc/7199。你会看到更多的文件和文件夹,而且这些文件的名字都很有意思,比如cpuset,比如mem,比如cmdline等等。没错,这些文件里存储的就是该进程相关的信息,比如命令行,比如环境变量等等。而LINUX中一切都是文件的思想也在这里得到了体现。proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息。而和我们这个主题相关的文件就是/proc/pid/maps和/proc/pid/mem。一个显示了改进程虚拟内存的分布,一个就是真正的虚拟内存的文件表现了。作为好奇的人类,你可以随便找一个pid文件夹看看maps文件里的内容,而mem由于特殊设置是无法被直接读取查看的。或者,你可以跟着这篇文章后面的代码,查看自己的程序的maps文件。

我编写了一个很简单小程序叫做showVM,这个程序会是下一章的主角。在我运行showVM文件后,使用下面的命令找到这个程序的id:

ps aux | grep showVM

在我的机器上,这一次运行分配的ID是20772,接下来就是让人充满啊!哈!感的时刻了。既然找到了id,根据最前面介绍的proc文件系统知识,首先使用 cat /proc/20855/maps查看下这个进程的虚拟内存分布图:

maps文件是一个非常值得细细研究的文件,这就是一个虚拟内存最好的示意图。和上面的有一些些不同,貌似这个虚拟内存地址似乎不是从0x0开始到0xffff ffff结束,和我上面说的32位操作系统寻址空间有点差别。而这个由于和本文所想介绍的主题不是那么的联系紧密,而太多的细节容易让人偏离主题,所以这个有兴趣的话可以就是那句俗话,自己去搜索搜索。

废话不再多扯了,就从一眼最熟悉的两个词开始,stack和heap。maps文件的第一列是地址,所以从这个文件中可以最直接的验证的就是heap是存在于低地址段,而stack位于高地址段。还有一个就是这两个段的权限都是可读可写,这样保证了这两段是可以被程序读写的。

这个时候再回到上面的示意图中,可以看到图中所绘,stack的更高地址存储的是命令行参数,而heap更低地址是代码段和数据段。而这里,我想从更低的地址开始说起,因为即使你从来没接触过aps文件,你会发现最后一列是文件的名称,最低地址放着的是我们自己的程序代码文件。这不足为奇,一个程序总要把自己的可执行部分放在虚拟内存中,这样CPU才能找到并且执行,这里比较有意思的是这里貌似有三个重复的,但是仔细看,你会发现这三个部分的权限是不同的,而示意图中heap之下也正好有三个部分,看起来正好是对应了示意图的三个部分。但是这个想法是不准确的,可以看到这三个部分:

第一个部分是可读可执行权限,这里存放的是代码。

第二个部分只有读权限,这个部分涉及另外一类称之为RELRO的技术,简答来说这个技术在gcc,linux中采用可以减少非法篡改着修改可写区域的机会,不是简单的一节两节可以说清楚的。考虑到这个和了解熟悉虚拟内存分布的关系不大,如果没有兴趣,完全可以暂时忽略这个部分。

第三个部分是可读可写的部分,这里存放的呢就是各种数据,和上面的示意图可能有点不一样,这里包括已经初始化的和未被初始化的数据。

说完heap更低的地址,下面再看看另一个部分,stack更高的地址。这里有很多缩写名词,而这些名词又涉及到更多的细节,主要是内核态和用户态的相关知识,这个部分就很深入而且不是很少的篇幅就能叙述清除的,在这里只需要知道,在Linux虚拟地址空间映射中,最高的1GB是kernel space的映射,具体有什么作用呢?可以完成比如用户态,内核态数据交换,在这里映射一些内核态的函数,加快调用内核态函数时的速度等等。这1GB的地址的内容,用户态的程序是不可以读不可以写的。

对应着示意图,似乎maps文件多了一个部分,就是中间的一串.so文件。当然,只要你稍微有点Linux的知识,你会知道这些都是Linux的库文件,也就是可执行程序。那么虚拟内存里面为什么要放这么多库文件呢?很明显的一点,就是这些库文件肯定是我们的程序需要调用的文件,这一部分叫做内存映射文件,最大的好处就是可以提高程序的运行速度。

说了这么多,对应着示意图,Linux虚拟内存地址更准确的示意图应该是这样的:

回归代码

作为程序员,我们的世界里最直接面对的就是代码了。如果书上描写的一切不能用代码证明,感觉总是缺少点什么,而这一节主要就是用真实的代码证明maps文件里面的各个区域。而和内存交互,最直接想到的应该就是使用c语言,而证明maps文件的各个部分最简单的方法就是打印出各个部分的地址然后和maps文件一一对应。

 1 /************************************************************************* 2  > File Name: showVM.c 3  > Author:  4  > Mail:  5  > Created Time: Wed 03 Jul 2019 01:24:28 PM CST 6  ************************************************************************/ 7  8 #include <stdio.h> 9 #include <string.h>10 #include <stdlib.h>11 #include <unistd.h>12 13 14 int add(int a, int b){15  return a b;16 }17 18 int del(int a, int b){19  return a-b;20 }21 22 int (*fPointer)(int a, int b);23 int global = 0;24 int global_uninitialized;25 26 int main(int argc,char *argv[])27 {28  int var = 0;29  char *chOnHeap = "test";30  //chOnHeap = (char*)malloc(8);31  int *nOnHeap = (int*)malloc(sizeof(int)*1);32  *nOnHeap = 200;33 34  fPointer = add;35  while(1)36  {37sleep(1);38printf("-------------------------------------------------------------------------------\n");39printf("global address = %p\n",(void*)&global);40printf("global uninitialized address = %p\n",(void*)&global_uninitialized);41printf("var value = %d, address = %p\n",var,(void*)&var);42printf(
源文地址:https://www.guoxiongfei.cn/cntech/21544.html