这篇文章主要是想尽可能直观的介绍虚拟内存的知识,而虚拟内存的知识无论做为在校学生的基础知识,面试的问题以及计算机程序自己性能的优化都有着重要的意义。而起意写这篇文章主要仍是由于在python,人工智能的大浪潮下,我发现好多人对这方面真的无限趋近于不知道。我不是说懂这些基础知识比懂人工智能水平就是高,可是做为一个软件工程师,我以为相对于调库调参,咱们更应该有更牢靠的基础知识。否则很容易陷入,高深的数学不会,基础的知识也不知道的尴尬境地。毕竟从事算法核心的,没有多少人,而做为工程师,我始终以为咱们的使命是如何把这些天赋异禀,脑壳发达的人的想法,构思,算法变成真正可用的东西。而在我从业不算长的年限中遇过的人来看,这绝对不是一种很简单的能力。python
阅读本文,须要有基本的c语言和python语言知识,若是提到虚拟内存,脑海中就有虚拟内存分布图的大概样子,那就完美适配这篇文章了。我但愿经过这篇文章能够帮助你能够经过推理的方法回答出虚拟内存的各类问题,能够知道这个东西是如何真正和程序结合起来的。linux
文章大致分为三个部分,程序员
第一部分,介绍虚拟内存的基本知识面试
第二部分,会直观的展现虚拟内存和咱们的程序代码究竟是怎么联系起来的算法
第三部分,我会演示如何改掉虚拟内存的内容,和修改这些内容到底意味着什么,吹的大一点,如何hack一个程序docker
本文全部的代码都很简单,只有c语言代码和python代码,而且我都跑过,若是你使用如下的环境,应该代码都能跑起来看到结果:小程序
若是你是一个程序员,至少你确定听过内存这个词,虽然你可能真的不知道内存是什么,可是确实在现代程序语言的包装下,你依然能够写出各类程序。若是你真的不知道,那么我以为仍是应该去学习下内存的知识的以及计算机程序是如何被执行起来的。而什么叫虚拟,我至今记得我大学操做系统老师上虚拟内存这一节的时候引用的解释,我拙劣的翻译成中文大概就是:数组
真实就是这个东西存在而且感觉到,虚拟就是这个东西存在可是你感受不到。安全
虚拟内存就是这么一类东西,它确实存在,而你却不能在程序中感觉到他。为何要有虚拟内存,缘由有不少,好比操做系统分配内存的时候,很难保证一个程序用的内存地址必定是连续的。好比内存是一个全局的东西并且只有一个,而程序有无数个,直接操做内存出问题的几率大,管理也不方便等等。因而虚拟内存的概念就给计算机程序的编写者,编译器等等都提供了一段独立,连续的“内存”空间。而实际上,这段内存不是真是存在的,其地址空间能够比真实的地址空间还要大,经过各类换出换入技术,让程序觉得本身运行在一段连续的地址空间上。虚拟内存的概念的伟大之处在于给计算机科学的各类概念设计提供了一种思路,隔离,虚拟,直到如今,docker,各类虚拟化技术不能不说和虚拟内存的概念没有关系。app
而提到虚拟内存那么不管在什么样关于操做系统的教科书里必定有这么一张图:
我当时在学习的时候老师会跟咱们说这个虚拟内存由哪些部分组成,为了文章看起来比较总体,让我再简单的说明下,对于一个运行的程序,到底有哪些部分组成:
首先虚拟内存的寻址地址是由机器和操做系统决定,好比你是一个32bit的操做系统,那么寻址空间就是4GB,换句话说你的程序能够跑在一个0到0xffff ffff的“盒子”里,而若是你是64位的操做系统,那么这个寻址空间就会更大,意味着,你有更大的“盒子”,能够有更多的可能。
而图中的低地址就是0x0,假设是32位操做系统,那么高地址就是0xffff ffff。那么,就让咱们按照人类的认知习惯,从低往高看看每一层都“住”着些什么。
最下面是text段,这里放着程序的执行的代码等等,若是你用objdump这样的程序打开一个程序,最前面你能看到应该是你的代码转化而成的汇编语言。
往上就是已初始化数据段和未初始化数据段,这里存放着全局变量,而这些都会被exec去执行,他们不只有不一样的名称,还有不一样的权限,在后面的展现中,你能够直观的看到这些。
而再往上是堆段,也就是面试中常常会被问的,malloc,new出来的内存是存放在哪里的,没错,就是这里。而他的上面是另外一个面试问题的来源,局部变量,参数都存在哪里。
住在顶楼的是命令行参数,环境变量等等。
而这些都是理论书本上写的,相似于告诉你两点之间有且只有一条直线同样。到底两点之间是否是真的只能画一条直线,最好的办法应该是本身画一画,以真实去验证理论。因此,到底一个程序在内存中真的是这样吗,或者说咱们的程序代码到底和这样一个概念有什么关系,下面的章节就让你看看“虚拟”是如何能够被真实的展现的。
在这一节的最开始,我不得不特别简单的介绍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 { 37 sleep(1); 38 printf("-------------------------------------------------------------------------------\n"); 39 printf("global address = %p\n",(void*)&global); 40 printf("global uninitialized address = %p\n",(void*)&global_uninitialized); 41 printf("var value = %d, address = %p\n",var,(void*)&var); 42 printf("chOnHeap value = %s, pointer address = %p, pointed address = %p\n",chOnHeap,(void*)&chOnHeap,chOnHeap); 43 printf("nOnHeap value = %d, pointer address = %p, pointed address = %p\n",*nOnHeap,(void*)&nOnHeap,nOnHeap); 44 45 printf("main address = %p\n",(void*)&main); 46 for(int i = 0; i < argc; i++){ 47 printf("argument address = %p\n",(void*)&argv[i]); 48 } 49 printf("add address = %p\n", (void *)&add); 50 printf("del address = %p\n", (void *)&del); 51 printf("function pointer address = %p, pointed address = %p ,value = %d\n",(void *)&fPointer,fPointer,(*fPointer)(10,20)); 52 53 printf("--------------------------------------------------------------------------------\n"); 54 } 55 56 free(nOnHeap); 57 //free(chOnHeap); 58 return 1; 59 }
而后使用如下命令编译这个文件:
gcc -Wall -Wextra -Werror showVM.c -o showVM
下面就是运行showVM,获得输出以下,准确的说应该是一次输出以下:
对应着上一节的maps文件,咱们就能够开始咱们的代码验证之旅了。
首先,对于global变量,无论是已初始化的或者是未初始化的,都是位于0x21000-0x22000这个段中的,对应上面的maps文件,能够看到不管是初始化的数据或者未初始化数据都是放在上面所说的heap之下的第三部分,可写可读区域的。
接下来就是最多见的局部变量的位置,在无数的关于c语言的书中,都会相似这样的描写: c语言中,一个变量是在栈上分配(存储)的。这里能够看到这个变量var的地址是0x7e8441d8,位于0x7e824000-0x7e845000之间,而且能够看到是更接近于7e845000,彷佛能够印证栈都是从高地址向低地址增加的。不过,只有一个变量的话,有可能正好这个变量就坐落于这个区域。没有关系,咱们能够用声明更多的变量看看栈究竟是怎样生长的。
在接下里的两行,打印的是两个指针的地址,而指针自己是一个变量,因此能够看到他们的地址都是在栈上。若是结合上面一个变量的地址来看,正好每个都是前一个的地址减去4,而这和32位机器上指针的大小一致。能够看到,在虚拟内存中,栈是由高地址往低地址生长的。
仍是这两行,根据c语言书里面关于变量分配的另一句话,“指针数据都是存储(分配)在堆上的”,彷佛从这个输出中看有点出入。对于这两个指针,指向整数的那个指针,所指向的整数确实是分配在堆上的,由于地址0x1fce018确实坐落于0x1fce000-0x1fef000之间,并且从这个位置来看,堆彷佛是从低地址往高地址分配的。而指向字符串的那个指针所指的地址明显不是在栈上,而是在0x10000-0x11000这个区域之间。这不是堆的区域,而是可执行文件存放的区域,从下一行main函数的地址更加能够证实这一点。为何会这样呢?由于c语言把这种字面量(string literal)都放在所谓的“文字常量区”,这里的数据会在程序结束后由程序本身释放,因此即便对于这个指针不进行free也不会形成内存泄露。因此,对于这道常见的面试题,“指针指向的值都分配在哪里?”,若是你的回答能够说起文字常量区,那么必定是更有加分的。
那么,若是再多想一步,如何让指向字符串的指针所指的值也分配在堆上呢?办法有不少,好比malloc以后用strncpy,有兴趣能够试试,你会发现,这个时候指向的地址就是在堆上了。不过,千万别忘了这样的以后指针须要被free,否则就会有内存泄漏。另外,其实还有一个颇有意思的行为,这个行为凸显出了编译器的机智。若是在这个文件中再定义一个指针,指向的值仍是“test”,那么这两个指针指向的地址会是同样的,有兴趣只要稍微在上面的代码中加一点内容就能够验证。这种聪明的行为最直接的好处就是能够节省空间,不少这种细小的行为,至少我以为真的是颇有意思的。
讲完了指针以及main函数的地址,在示意图中说还有一部分位置是留给命令行参数的。因而,我也作了小小的验证,能够看到,虽然我这个程序执行只有一个命令行参数,也就是程序名,可是不妨碍看看这个参数究竟是在哪一个区域中。能够看到其地址是在前面分配的栈空间的更高地址,344明显大于1d4,因此说,和示意图中说的同样,命令行参数是位于栈空间之上的。
剩下来我想展现的是函数的地址,所谓调用函数,其实就是执行某一个地址的代码。因此,能够看到,函数地址是位于可执行区域的,和main的地址在一个区域,maps文件里也代表了这个区域具备的是可读可执行权限。
另一个,既然函数是地址,那么按照c语言的规范,就可使用一个指针指向这个地址,而体如今代码之中,就是函数指针。最后一行,打印了指向add函数的函数指针的地址,由于这个指针是全局定义的,因此指针自己的地址是位于全局的数据去,和globa数据同样。而指向的地址,就是add函数的地址,固然,执行的也就是add函数。
好了,如今咱们使用程序自己打印出程序中不一样变量的地址,而且咱们知道了,maps 文件能够显示整个虚拟内存地址的分布。而正如上面提到的,还有一个和虚拟内存相关的文件,mem,这个文件就是一个程序虚拟内存的映射。而做为一个文件,就有可能有读写的权限,而下一节,就是让你看看如何hack掉一个正在运行的程序的行为(虚拟内存数据)。
这一节,我想作的是,改掉一个正在运行的程序的函数指针指向的地址,这样会让一个函数的结果改变,或者说执行本身想要的函数。在一些用心良苦,技术高超的侵入者里,就这一个行为就彻底有可能控制你整个电脑。固然,在我这里,我程序自己就知道函数的地址,因此,只要你理解上面所说的,看起来有点太过于玩具。而真正的黑客,会用精心构造好的代码修改掉虚拟内存中任何一个能够有写权限的地方,从而达到随心所欲的目的。
就像前面所说的,既然我知道一个指针的地址,并且又知道修改后函数应该指向的地址,那么就很简单了,读出这个文件,在这里就是mem文件了,将文件写指针指向这个位置,修改之,大功告成。而完成这个操做,能够选择任一语言,只要有文件操做的接口,而我,选择的是python。
1 #!/usr/bin/env python3 2 # coding=utf-8 3 import sys 4 pid = int(sys.argv[1]) 5 address = int(sys.argv[2],16) 6 byte_arr = [] 7 for num in range(3,len(sys.argv)): 8 byte_arr.append(int(sys.argv[num],16)) 9 10 mem_filename = "/proc/{}/mem".format(pid) 11 print("[*] mem: {}".format(mem_filename)) 12 13 try: 14 mem_file = open(mem_filename, 'rb+') 15 except IOError as e: 16 print("[ERROR] Can not open file {}:".format(mem_filename)) 17 print(" I/O error({}): {}".format(e.errno, e.strerror)) 18 exit(1) 19 20 mem_file.seek(address) 21 mem_file.write(bytearray(byte_arr)) 22 23 mem_file.close()
在执行这个程序时,可能须要使用sudo来提高权限执行。这个python程序很简单,也没啥错误提示,处理的,由于我只是想展现下基本的原理。这个脚本接受的参数依次为pid,你想改变的地址的16进制字符串,好比我想改变的那个函数指针在文件内的偏移就是他的地址 21040,想替换的终极数据,一个byte数组。这里有一点讲究,就是你须要知道一些大端,小端机器的知识,这个并不难,搜索引擎2分钟就能够告诉你答案。我想把这个函数指针指向的地址改为减法函数的地址,看起来应该改为0x10504,也就是传入01,05,04。可是若是你传入这个数据,会发现运行着的showVM程序马上就崩溃了。而若是你认真学习了关于大端小端的知识,你会发现这里应该传入的实际上是04 05 01 00。这个缘由,就留给热爱探索的人吧。
好了,要想看到神奇的事情发生,只须要作两步,第一步,运行showVM,第二步,根据你的输出向这个python文件传入对应的参数,由于我又从新运行了下showVM,因此,下面执行的截图和上面会略有不一样:
准备好,奇迹发生的时刻:
你能够看到,正在运行的程序,获得的结果变了,原本是10+20=30,如今变成了10-20=-10了。函数指针的地址也变了,确实指向了del。就这一套小把戏,理论上你能够改这个输出中的任意地址,可是实际上,有些你是改不了的,由于权限问题。
是否是很神奇?你还能够想一想到其余有意思的实验,好比修改掉一个运行程序的字符串。方法也并不复杂,从maps文件里找到heap段的范围,在这个范围里搜索须要的字符串。有可能搜不到,由于按照上面说的,字面量字符串可能不是存储在heap区域的,而他所存储的区域你是没法修改的。这里假设在heap中搜到你所须要的字符串,那么剩下的就是找到这个位置,修改其中的内容,你会发现和上面一摸同样的效果。
最后我想说的是,若是观察maps文件更仔细一点,你会发现当你执行同一个程序,开头的三个段地址是不会改变的,可是heap开始的地址貌似并非固定的,为何要这么作?这里涉及到虚拟内存实现中的一个常见技术,这里会有一个随机gap,目的是增长安全性。由于前三段是固定的,而heap又是如此重要,由于你彻底能够改变heap中的内容来改变一个指针指向的内容。因此一段随机的偏移可让侵入者不那么容易的找到heap段里的数据。一个简单的操做带来的是一个安全性不小的提高,扰动实际上是特别美妙的事情,随机性才让咱们的世界变得如此丰富多彩。
这篇文章也在个人公众号同步发表,个人这个公众号嘛,佛系更新,固然,本质上是想到一个话题不容易(懒的好借口),欢迎关注哦: