這篇文章主要是想盡可能直觀的介紹虛擬內存的知識,而虛擬內存的知識無論做爲在校學生的基礎知識,面試的問題以及計算機程序自己性能的優化都有着重要的意義。而起意寫這篇文章主要仍是由於在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段裏的數據。一個簡單的操做帶來的是一個安全性不小的提高,擾動實際上是特別美妙的事情,隨機性才讓咱們的世界變得如此豐富多彩。
這篇文章也在個人公衆號同步發表,個人這個公衆號嘛,佛系更新,固然,本質上是想到一個話題不容易(懶的好藉口),歡迎關注哦: