?
(最近感覺自己對java好無知啊!以下是轉自網絡上的文章,以供自己學習...........)
JAVA
文件編譯執行與虛擬機(JVM)介紹
Java
虛擬機(JVM)是可運行Java代碼的假想計算機。只要根據JVM規格描述將解釋器移植到特定的計算機上,就能保證經過編譯的任何Java代碼能夠在該系統上運行。本文首先簡要介紹從Java文件的編譯到最終執行的過程,隨后對JVM規格描述作一說明。
一.Java源文件的編譯、下載、解釋和執行
Java應用程序的開發周期包括編譯、下載、解釋和執行幾個部分。Java編譯程序將Java源程序翻譯為JVM可執行代碼?字節碼。這一編譯過程同C/C++的編譯有些不同。當C編譯器編譯生成一個對象的代碼時,該代碼是為在某一特定硬件平臺運行而產生的。因此,在編譯過程中,編譯程序通過查表將所有對符號的引用轉換為特定的內存偏移量,以保證程序運行。Java編譯器卻不將對變量和方法的引用編譯為數值引用,也不確定程序執行過程中的內存布局,而是將這些符號引用信息保留在字節碼中,由解釋器在運行過程中創立內存布局,然后再通過查表來確定一個方法所在的地址。這樣就有效的保證了Java的可移植性和安全性。
運行JVM字節碼的工作是由解釋器來完成的。解釋執行過程分三部進行:代碼的裝入、代碼的校驗和代碼的執行。裝入代碼的工作由"類裝載器"(class loader)完成。類裝載器負責裝入運行一個程序需要的所有代碼,這也包括程序代碼中的類所繼承的類和被其調用的類。當類裝載器裝入一個類時,該類被放在自己的名字空間中。除了通過符號引用自己名字空間以外的類,類之間沒有其他辦法可以影響其他類。在本臺計算機上的所有類都在同一地址空間內,而所有從外部引進的類,都有一個自己獨立的名字空間。這使得本地類通過共享相同的名字空間獲得較高的運行效率,同時又保證它們與從外部引進的類不會相互影響。當裝入了運行程序需要的所有類后,解釋器便可確定整個可執行程序的內存布局。解釋器為符號引用同特定的地址空間建立對應關系及查詢表。通過在這一階段確定代碼的內存布局,Java很好地解決了由超類改變而使子類崩潰的問題,同時也防止了代碼對地址的非法訪問。
隨后,被裝入的代碼由字節碼校驗器進行檢查。校驗器可發現操作數棧溢出,非法數據類型轉化等多種錯誤。通過校驗后,代碼便開始執行了。
Java字節碼的執行有兩種方式:
1.即時編譯方式:解釋器先將字節碼編譯成機器碼,然后再執行該機器碼。
2.解釋執行方式:解釋器通過每次解釋并執行一小段代碼來完成Java字節碼程序的所有操作。
通常采用的是第二種方法。由于JVM規格描述具有足夠的靈活性,這使得將字節碼翻譯為機器代碼的工作
具有較高的效率。對于那些對運行速度要求較高的應用程序,解釋器可將Java字節碼即時編譯為機器碼,從而很好地保證了Java代碼的可移植性和高性能。
二.JVM規格描述
JVM的設計目標是提供一個基于抽象規格描述的計算機模型,為解釋程序開發人員提很好的靈活性,同時也確保Java代碼可在符合該規范的任何系統上運行。JVM對其實現的某些方面給出了具體的定義,特別是對Java可執行代碼,即字節碼(Bytecode)的格式給出了明確的規格。這一規格包括操作碼和操作數的語法和數值、標識符的數值表示方式、以及Java類文件中的Java對象、常量緩沖池在JVM的存儲映象。這些定義為JVM解釋器開發人員提供了所需的信息和開發環境。Java的設計者希望給開發人員以隨心所欲使用Java的自由。
JVM定義了控制Java代碼解釋執行和具體實現的五種規格,它們是:
JVM指令系統
JVM寄存器
JVM棧結構
JVM碎片回收堆
JVM存儲區
2.1JVM指令系統
JVM指令系統同其他計算機的指令系統極其相似。Java指令也是由操作碼和操作數兩部分組成。操作碼為8位二進制數,操作數進緊隨在操作碼的后面,其長度根據需要而不同。操作碼用于指定一條指令操作的性質(在這里我們采用匯編符號的形式進行說明),如iload表示從存儲器中裝入一個整數,anewarray表示為一個新數組分配空間,iand表示兩個整數的"與",ret用于流程控制,表示從對某一方法的調用中返回。當長度大于8位時,操作數被分為兩個以上字節存放。JVM采用了"big endian"的編碼方式來處理這種情況,即高位bits存放在低字節中。這同 Motorola及其他的RISC CPU采用的編碼方式是一致的,而與Intel采用的"little endian "的編碼方式即低位bits存放在低位字節的方法不同。
Java指令系統是以Java語言的實現為目的設計的,其中包含了用于調用方法和監視多先程系統的指令。Java的8位操作碼的長度使得JVM最多有256種指令,目前已使用了160多種操作碼。
2.2JVM指令系統
所有的CPU均包含用于保存系統狀態和處理器所需信息的寄存器組。如果虛擬機定義較多的寄存器,便可以從中得到更多的信息而不必對棧或內存進行訪問,這有利于提高運行速度。然而,如果虛擬機中的寄存器比實際CPU的寄存器多,在實現虛擬機時就會占用處理器大量的時間來用常規存儲器模擬寄存器,這反而會降低虛擬機的效率。針對這種情況,JVM只設置了4個最為常用的寄存器。它們是:
pc程序計數器
optop操作數棧頂指針
frame當前執行環境指針
vars指向當前執行環境中第一個局部變量的指針
所有寄存器均為32位。pc用于記錄程序的執行。optop,frame和vars用于記錄指向Java棧區的指針。
2.3JVM棧結構
作為基于棧結構的計算機,Java棧是JVM存儲信息的主要方法。當JVM得到一個Java字節碼應用程序后,便為該代碼中一個類的每一個方法創建一個棧框架,以保存該方法的狀態信息。每個棧框架包括以下三類信息:
局部變量
執行環境
操作數棧
局部變量用于存儲一個類的方法中所用到的局部變量。vars寄存器指向該變量表中的第一個局部變量。
執行環境用于保存解釋器對Java字節碼進行解釋過程中所需的信息。它們是:上次調用的方法、局部變量指針和操作數棧的棧頂和棧底指針。執行環境是一個執行一個方法的控制中心。例如:如果解釋器要執行iadd(整數加法),首先要從frame寄存器中找到當前執行環境,而后便從執行環境中找到操作數棧,從棧頂彈出兩個整數進行加法運算,最后將結果壓入棧頂。
操作數棧用于存儲運算所需操作數及運算的結果。
2.4JVM碎片回收堆
Java類的實例所需的存儲空間是在堆上分配的。解釋器具體承擔為類實例分配空間的工作。解釋器在為一個實例分配完存儲空間后,便開始記錄對該實例所占用的內存區域的使用。一旦對象使用完畢,便將其回收到堆中。
在Java語言中,除了new語句外沒有其他方法為一對象申請和釋放內存。對內存進行釋放和回收的工作是由Java運行系統承擔的。這允許Java運行系統的設計者自己決定碎片回收的方法。在SUN公司開發的Java解釋器和Hot Java環境中,碎片回收用后臺線程的方式來執行。這不但為運行系統提供了良好的性能,而且使程序設計人員擺脫了自己控制內存使用的風險。
2.5JVM存儲區
JVM有兩類存儲區:常量緩沖池和方法區。常量緩沖池用于存儲類名稱、方法和字段名稱以及串常量。方法區則用于存儲Java方法的字節碼。對于這兩種存儲區域具體實現方式在JVM規格中沒有明確規定。這使得Java應用程序的存儲布局必須在運行過程中確定,依賴于具體平臺的實現方式。
JVM是為Java字節碼定義的一種獨立于具體平臺的規格描述,是Java平臺獨立性的基礎。目前的JVM還存在一些限制和不足,有待于進一步的完善,但無論如何,JVM的思想是成功的。
對比分析:如果把Java原程序想象成我們的C++原程序,Java原程序編譯后生成的字節碼就相當于C++原程序編譯后的80x86的機器碼(二進制程序文件),JVM虛擬機相當于80x86計算機系統,Java解釋器相當于80x86CPU。在80x86CPU上運行的是機器碼,在Java解釋器上運行的是Java字節碼。
Java解釋器相當于運行Java字節碼的“CPU”,但該“CPU”不是通過硬件實現的,而是用軟件實現的。Java解釋器實際上就是特定的平臺下的一個應用程序。只要實現了特定平臺下的解釋器程序,Java字節碼就能通過解釋器程序在該平臺下運行,這是Java跨平臺的根本。當前,并不是在所有的平臺下都有相應Java解釋器程序,這也是Java并不能在所有的平臺下都能運行的原因,它只能在已實現了Java解釋器程序的平臺下運行。
堆和棧的區別
?
非本人作也!因非常經典,所以收歸旗下,與眾人閱之!原作者不祥!
堆和棧的區別
一、預備知識—程序的內存分配
一個由c/C++編譯的程序占用的內存分為以下幾個部分
1、棧區(stack)—?由編譯器自動分配釋放?,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。
2、堆區(heap)?—?一般由程序員分配釋放,?若程序員不釋放,程序結束時可能由OS回收?。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。
3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,?未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。?-?程序結束后有系統釋放?
4、文字常量區—常量字符串就是放在這里的。?程序結束后由系統釋放
5、程序代碼區—存放函數體的二進制代碼。
二、例子程序?
這是一個前輩寫的,非常詳細?
//main.cpp?
int?a?=?0;?全局初始化區?
char?*p1;?全局未初始化區?
main()?
{?
int?b;?棧?
char?s[]?=?"abc";?棧?
char?*p2;?棧?
char?*p3?=?"123456";?123456\0在常量區,p3在棧上。?
static?int?c?=0;?全局(靜態)初始化區?
p1?=?(char?*)malloc(10);?
p2?=?(char?*)malloc(20);?
分配得來得10和20字節的區域就在堆區。?
strcpy(p1,?"123456");?123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。?
}?
?
二、堆和棧的理論知識?
2.1申請方式?
stack:?
由系統自動分配。?例如,聲明在函數中一個局部變量?int?b;?系統自動在棧中為b開辟空間?
heap:?
需要程序員自己申請,并指明大小,在c中malloc函數?
如p1?=?(char?*)malloc(10);?
在C++中用new運算符?
如p2?=?(char?*)malloc(10);?
但是注意p1、p2本身是在棧中的。?
2.2?
申請后系統的響應?
棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。?
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,?
會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。?
2.3
申請大小的限制?
棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。?
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。?
2.4
申請效率的比較:?
棧由系統自動分配,速度較快。但程序員是無法控制的。?
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.?
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。?
2.5
堆和棧中的存儲內容?
棧:?在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。?
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。?
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。?
2.6
存取效率的比較?
char?s1[]?=?"aaaaaaaaaaaaaaa";?
char?*s2?=?"bbbbbbbbbbbbbbbbb";?
aaaaaaaaaaa
是在運行時刻賦值的;?
而bbbbbbbbbbb是在編譯時就確定的;?
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。?
比如:?
#include?
void?main()?
{?
char?a?=?1;?
char?c[]?=?"1234567890";?
char?*p?="1234567890";?
a?=?c[1];?
a?=?p[1];?
return;?
}?
對應的匯編代碼?
10:?a?=?c[1];?
00401067?8A?4D?F1?mov?cl,byte?ptr?[ebp-0Fh]?
0040106A?88?4D?FC?mov?byte?ptr?[ebp-4],cl?
11:?a?=?p[1];?
0040106D?8B?55?EC?mov?edx,dword?ptr?[ebp-14h]?
00401070?8A?42?01?mov?al,byte?ptr?[edx+1]?
00401073?88?45?FC?mov?byte?ptr?[ebp-4],al?
第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,在根據edx讀取字符,顯然慢了。?
2.7
小結:?
堆和棧的區別可以用如下的比喻來看出:?
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。?
使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。?
windows
進程中的內存結構
在閱讀本文之前,如果你連堆棧是什么多不知道的話,請先閱讀文章后面的基礎知識。?
接觸過編程的人都知道,高級語言都能通過變量名來訪問內存中的數據。那么這些變量在內存中是如何存放的呢?程序又是如何使用這些變量的呢?下面就會對此進行深入的討論。下文中的C語言代碼如沒有特別聲明,默認都使用VC編譯的release版。?
首先,來了解一下?C?語言的變量是如何在內存分部的。C?語言有全局變量(Global)、本地變量(Local),靜態變量(Static)、寄存器變量(Regeister)。每種變量都有不同的分配方式。先來看下面這段代碼:?
#include??
int?g1=0,?g2=0,?g3=0;?
int?main()?
{?
static?int?s1=0,?s2=0,?s3=0;?
int?v1=0,?v2=0,?v3=0;?
//
打印出各個變量的內存地址?
printf("0x%08x\n",&v1);?//
打印各本地變量的內存地址?
printf("0x%08x\n",&v2);?
printf("0x%08x\n\n",&v3);?
printf("0x%08x\n",&g1);?//打印各全局變量的內存地址?
printf("0x%08x\n",&g2);?
printf("0x%08x\n\n",&g3);?
printf("0x%08x\n",&s1);?//打印各靜態變量的內存地址?
printf("0x%08x\n",&s2);?
printf("0x%08x\n\n",&s3);?
return?0;?
}?
編譯后的執行結果是:?
0x0012ff78?
0x0012ff7c?
0x0012ff80?
0x004068d0?
0x004068d4?
0x004068d8?
0x004068dc?
0x004068e0?
0x004068e4?
輸出的結果就是變量的內存地址。其中v1,v2,v3是本地變量,g1,g2,g3是全局變量,s1,s2,s3是靜態變量。你可以看到這些變量在內存是連續分布的,但是本地變量和全局變量分配的內存地址差了十萬八千里,而全局變量和靜態變量分配的內存是連續的。這是因為本地變量和全局/靜態變量是分配在不同類型的內存區域中的結果。對于一個進程的內存空間而言,可以在邏輯上分成3個部份:代碼區,靜態數據區和動態數據區。動態數據區一般就是“堆棧”。“棧(stack)”和“堆(heap)”是兩種不同的動態數據區,棧是一種線性結構,堆是一種鏈式結構。進程的每個線程都有私有的“棧”,所以每個線程雖然代碼一樣,但本地變量的數據都是互不干擾。一個堆棧可以通過“基地址”和“棧頂”地址來描述。全局變量和靜態變量分配在靜態數據區,本地變量分配在動態數據區,即堆棧中。程序通過堆棧的基地址和偏移量來訪問本地變量。?
├———————┤
低端內存區域?
│?……?│?
├———————┤?
│?動態數據區?│?
├———————┤?
│?……?│?
├———————┤?
│?代碼區?│?
├———————┤?
│?靜態數據區?│?
├———————┤?
│?……?│?
├———————┤高端內存區域?
堆棧是一個先進后出的數據結構,棧頂地址總是小于等于棧的基地址。我們可以先了解一下函數調用的過程,以便對堆棧在程序中的作用有更深入的了解。不同的語言有不同的函數調用規定,這些因素有參數的壓入規則和堆棧的平衡。windows?API的調用規則和ANSI?C的函數調用規則是不一樣的,前者由被調函數調整堆棧,后者由調用者調整堆棧。兩者通過“__stdcall”和“__cdecl”前綴區分。先看下面這段代碼:?
#include??
void?__stdcall?func(int?param1,int?param2,int?param3)?
{?
int?var1=param1;?
int?var2=param2;?
int?var3=param3;?
printf("0x%08x\n",?m1);?//
打印出各個變量的內存地址?
printf("0x%08x\n",?m2);?
printf("0x%08x\n\n",?m3);?
printf("0x%08x\n",&var1);?
printf("0x%08x\n",&var2);?
printf("0x%08x\n\n",&var3);?
return;?
}?
int?main()?
{?
func(1,2,3);?
return?0;?
}?
編譯后的執行結果是:?
0x0012ff78?
0x0012ff7c?
0x0012ff80?
0x0012ff68?
0x0012ff6c?
0x0012ff70?
├———————┤<—
函數執行時的棧頂(ESP)、低端內存區域?
│?……?│?
├———————┤?
│?var?1?│?
├———————┤?
│?var?2?│?
├———————┤?
│?var?3?│?
├———————┤?
│?RET?│?
├———————┤<—“__cdecl”函數返回后的棧頂(ESP)?
│?parameter?1?│?
├———————┤?
│?parameter?2?│?
├———————┤?
│?parameter?3?│?
├———————┤<—“__stdcall”函數返回后的棧頂(ESP)?
│?……?│?
├———————┤<—棧底(基地址?EBP)、高端內存區域?
上圖就是函數調用過程中堆棧的樣子了。首先,三個參數以從又到左的次序壓入堆棧,先壓“param3”,再壓“param2”,最后壓入“param1”;然后壓入函數的返回地址(RET),接著跳轉到函數地址接著執行(這里要補充一點,介紹UNIX下的緩沖溢出原理的文章中都提到在壓入RET后,繼續壓入當前EBP,然后用當前ESP代替EBP。然而,有一篇介紹windows下函數調用的文章中說,在windows下的函數調用也有這一步驟,但根據我的實際調試,并未發現這一步,這還可以從param3和var1之間只有4字節的間隙這點看出來);第三步,將棧頂(ESP)減去一個數,為本地變量分配內存空間,上例中是減去12字節(ESP=ESP-3*4,每個int變量占用4個字節);接著就初始化本地變量的內存空間。由于“__stdcall”調用由被調函數調整堆棧,所以在函數返回前要恢復堆棧,先回收本地變量占用的內存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前壓入參數占用的內存(ESP=ESP+3*4),繼續執行調用者的代碼。參見下列匯編代碼:?
;--------------func?
函數的匯編代碼-------------------?
:00401000?83EC0C?sub?esp,?0000000C?//
創建本地變量的內存空間?
:00401003?8B442410?mov?eax,?dword?ptr?[esp+10]?
:00401007?8B4C2414?mov?ecx,?dword?ptr?[esp+14]?
:0040100B?8B542418?mov?edx,?dword?ptr?[esp+18]?
:0040100F?89442400?mov?dword?ptr?[esp],?eax?
:00401013?8D442410?lea?eax,?dword?ptr?[esp+10]?
:00401017?894C2404?mov?dword?ptr?[esp+04],?ecx?
……………………
(省略若干代碼)?
:00401075?83C43C?add?esp,?0000003C?;
恢復堆棧,回收本地變量的內存空間?
:00401078?C3?ret?000C?;函數返回,恢復參數占用的內存空間?
;如果是“__cdecl”的話,這里是“ret”,堆棧將由調用者恢復?
;-------------------
函數結束-------------------------?
;--------------
主程序調用func函數的代碼--------------?
:00401080?6A03?push?00000003?//
壓入參數param3?
:00401082?6A02?push?00000002?//壓入參數param2?
:00401084?6A01?push?00000001?//壓入參數param1?
:00401086?E875FFFFFF?call?00401000?//調用func函數?
;如果是“__cdecl”的話,將在這里恢復堆棧,“add?esp,?0000000C”?
聰明的讀者看到這里,差不多就明白緩沖溢出的原理了。先來看下面的代碼:?
#include??
#include??
void?__stdcall?func()?
{?
char?lpBuff[8]="\0";?
strcat(lpBuff,"AAAAAAAAAAA");?
return;?
}?
int?main()?
{?
func();?
return?0;?
}?
編譯后執行一下回怎么樣?哈,“"0x00414141"指令引用的"0x00000000"內存。該內存不能為"read"。”,“非法操作”嘍!"41"就是"A"的16進制的ASCII碼了,那明顯就是strcat這句出的問題了。"lpBuff"的大小只有8字節,算進結尾的\0,那strcat最多只能寫入7個"A",但程序實際寫入了11個"A"外加1個\0。再來看看上面那幅圖,多出來的4個字節正好覆蓋了RET的所在的內存空間,導致函數返回到一個錯誤的內存地址,執行了錯誤的指令。如果能精心構造這個字符串,使它分成三部分,前一部份僅僅是填充的無意義數據以達到溢出的目的,接著是一個覆蓋RET的數據,緊接著是一段shellcode,那只要著個RET地址能指向這段shellcode的第一個指令,那函數返回時就能執行shellcode了。但是軟件的不同版本和不同的運行環境都可能影響這段shellcode在內存中的位置,那么要構造這個RET是十分困難的。一般都在RET和shellcode之間填充大量的NOP指令,使得exploit有更強的通用性。?
├———————┤<—
低端內存區域?
│?……?│?
├———————┤<—由exploit填入數據的開始?
│?│?
│?buffer?│<—填入無用的數據?
│?│?
├———————┤?
│?RET?│<—指向shellcode,或NOP指令的范圍?
├———————┤?
│?NOP?│?
│?……?│<—填入的NOP指令,是RET可指向的范圍?
│?NOP?│?
├———————┤?
│?│?
│?shellcode?│?
│?│?
├———————┤<—由exploit填入數據的結束?
│?……?│?
├———————┤<—高端內存區域?
windows
下的動態數據除了可存放在棧中,還可以存放在堆中。了解C++的朋友都知道,C++可以使用new關鍵字來動態分配內存。來看下面的C++代碼:?
#include??
#include??
#include??
void?func()?
{?
char?*buffer=new?char[128];?
char?bufflocal[128];?
static?char?buffstatic[128];?
printf("0x%08x\n",buffer);?//
打印堆中變量的內存地址?
printf("0x%08x\n",bufflocal);?//打印本地變量的內存地址?
printf("0x%08x\n",buffstatic);?//打印靜態變量的內存地址?
}?
void?main()?
{?
func();?
return;?
}?
程序執行結果為:?
0x004107d0?
0x0012ff04?
0x004068c0?
可以發現用new關鍵字分配的內存即不在棧中,也不在靜態數據區。VC編譯器是通過windows下的“堆(heap)”來實現new關鍵字的內存動態分配。在講“堆”之前,先來了解一下和“堆”有關的幾個API函數:?
HeapAlloc?
在堆中申請內存空間?
HeapCreate?創建一個新的堆對象?
HeapDestroy?銷毀一個堆對象?
HeapFree?釋放申請的內存?
HeapWalk?枚舉堆對象的所有內存塊?
GetProcessHeap?取得進程的默認堆對象?
GetProcessHeaps?取得進程所有的堆對象?
LocalAlloc?
GlobalAlloc?
當進程初始化時,系統會自動為進程創建一個默認堆,這個堆默認所占內存的大小為1M。堆對象由系統進行管理,它在內存中以鏈式結構存在。通過下面的代碼可以通過堆動態申請內存空間:?
HANDLE?hHeap=GetProcessHeap();?
char?*buff=HeapAlloc(hHeap,0,8);?
其中hHeap是堆對象的句柄,buff是指向申請的內存空間的地址。那這個hHeap究竟是什么呢?它的值有什么意義嗎?看看下面這段代碼吧:?
#pragma?comment(linker,"/entry:main")?//
定義程序的入口?
#include??
_CRTIMP?int?(__cdecl?*printf)(const?char?*,?...);?//
定義STL函數printf?
/*---------------------------------------------------------------------------?
寫到這里,我們順便來復習一下前面所講的知識:?
(*注)printf函數是C語言的標準函數庫中函數,VC的標準函數庫由msvcrt.dll模塊實現。?
由函數定義可見,printf的參數個數是可變的,函數內部無法預先知道調用者壓入的參數個數,函數只能通過分析第一個參數字符串的格式來獲得壓入參數的信息,由于這里參數的個數是動態的,所以必須由調用者來平衡堆棧,這里便使用了__cdecl調用規則。BTW,Windows系統的API函數基本上是__stdcall調用形式,只有一個API例外,那就是wsprintf,它使用__cdecl調用規則,同printf函數一樣,這是由于它的參數個數是可變的緣故。?
---------------------------------------------------------------------------*/?
void?main()?
{?
HANDLE?hHeap=GetProcessHeap();?
char?*buff=HeapAlloc(hHeap,0,0x10);?
char?*buff2=HeapAlloc(hHeap,0,0x10);?
HMODULE?hMsvcrt=LoadLibrary("msvcrt.dll");?
printf=(void?*)GetProcAddress(hMsvcrt,"printf");?
printf("0x%08x\n",hHeap);?
printf("0x%08x\n",buff);?
printf("0x%08x\n\n",buff2);?
}?
執行結果為:?
0x00130000?
0x00133100?
0x00133118?
hHeap
的值怎么和那個buff的值那么接近呢?其實hHeap這個句柄就是指向HEAP首部的地址。在進程的用戶區存著一個叫PEB(進程環境塊)的結構,這個結構中存放著一些有關進程的重要信息,其中在PEB首地址偏移0x18處存放的ProcessHeap就是進程默認堆的地址,而偏移0x90處存放了指向進程所有堆的地址列表的指針。windows有很多API都使用進程的默認堆來存放動態數據,如windows?2000下的所有ANSI版本的函數都是在默認堆中申請內存來轉換ANSI字符串到Unicode字符串的。對一個堆的訪問是順序進行的,同一時刻只能有一個線程訪問堆中的數據,當多個線程同時有訪問要求時,只能排隊等待,這樣便造成程序執行效率下降。?
最后來說說內存中的數據對齊。所位數據對齊,是指數據所在的內存地址必須是該數據長度的整數倍,DWORD數據的內存起始地址能被4除盡,WORD數據的內存起始地址能被2除盡,x86?CPU能直接訪問對齊的數據,當他試圖訪問一個未對齊的數據時,會在內部進行一系列的調整,這些調整對于程序來說是透明的,但是會降低運行速度,所以編譯器在編譯程序時會盡量保證數據對齊。同樣一段代碼,我們來看看用VC、Dev-C++和lcc三個不同編譯器編譯出來的程序的執行結果:?
#include??
int?main()?
{?
int?a;?
char?b;?
int?c;?
printf("0x%08x\n",&a);?
printf("0x%08x\n",&b);?
printf("0x%08x\n",&c);?
return?0;?
}?
這是用VC編譯后的執行結果:?
0x0012ff7c?
0x0012ff7b?
0x0012ff80?
變量在內存中的順序:b(1字節)-a(4字節)-c(4字節)。?
這是用Dev-C++編譯后的執行結果:?
0x0022ff7c?
0x0022ff7b?
0x0022ff74?
變量在內存中的順序:c(4字節)-中間相隔3字節-b(占1字節)-a(4字節)。?
這是用lcc編譯后的執行結果:?
0x0012ff6c?
0x0012ff6b?
0x0012ff64?
變量在內存中的順序:同上。?
三個編譯器都做到了數據對齊,但是后兩個編譯器顯然沒VC“聰明”,讓一個char占了4字節,浪費內存哦。?
基礎知識:?
堆棧是一種簡單的數據結構,是一種只允許在其一端進行插入或刪除的線性表。允許插入或刪除操作的一端稱為棧頂,另一端稱為棧底,對堆棧的插入和刪除操作被稱為入棧和出棧。有一組CPU指令可以實現對進程的內存實現堆棧訪問。其中,POP指令實現出棧操作,PUSH指令實現入棧操作。CPU的ESP寄存器存放當前線程的棧頂指針,EBP寄存器中保存當前線程的棧底指針。CPU的EIP寄存器存放下一個CPU指令存放的內存地址,當CPU執行完當前的指令后,從EIP寄存器中讀取下一條指令的內存地址,然后繼續執行。?
參考:《Windows下的HEAP溢出及其利用》by:?isno?
《windows核心編程》by:?Jeffrey?Richter?
摘要:?討論常見的堆性能問題以及如何防范它們。(共?9?頁)
前言
您是否是動態分配的?C/C++?對象忠實且幸運的用戶?您是否在模塊間的往返通信中頻繁地使用了“自動化”?您的程序是否因堆分配而運行起來很慢?不僅僅您遇到這樣的問題。幾乎所有項目遲早都會遇到堆問題。大家都想說,“我的代碼真正好,只是堆太慢”。那只是部分正確。更深入理解堆及其用法、以及會發生什么問題,是很有用的。
什么是堆?
(如果您已經知道什么是堆,可以跳到“什么是常見的堆性能問題?”部分)
在程序中,使用堆來動態分配和釋放對象。在下列情況下,調用堆操作:?
事先不知道程序所需對象的數量和大小。
對象太大而不適合堆棧分配程序。
堆使用了在運行時分配給代碼和堆棧的內存之外的部分內存。下圖給出了堆分配程序的不同層。
nload="javascript:if(this.width>screen.width-333)this.width=screen.width-333" border=0 dypop="按此在新窗口瀏覽圖片">
GlobalAlloc/GlobalFree
:Microsoft?Win32?堆調用,這些調用直接與每個進程的默認堆進行對話。
LocalAlloc/LocalFree
:Win32?堆調用(為了與?Microsoft?Windows?NT?兼容),這些調用直接與每個進程的默認堆進行對話。
COM?
的?IMalloc?分配程序(或?CoTaskMemAlloc?/?CoTaskMemFree):函數使用每個進程的默認堆。自動化程序使用“組件對象模型?(COM)”的分配程序,而申請的程序使用每個進程堆。
C/C++?
運行時?(CRT)?分配程序:提供了?malloc()?和?free()?以及?new?和?delete?操作符。如?Microsoft?Visual?Basic?和?Java?等語言也提供了新的操作符并使用垃圾收集來代替堆。CRT?創建自己的私有堆,駐留在?Win32?堆的頂部。
Windows?NT?
中,Win32?堆是?Windows?NT?運行時分配程序周圍的薄層。所有?API?轉發它們的請求給?NTDLL。
Windows?NT?
運行時分配程序提供?Windows?NT?內的核心堆分配程序。它由具有?128?個大小從?8?到?1,024?字節的空閑列表的前端分配程序組成。后端分配程序使用虛擬內存來保留和提交頁。
在圖表的底部是“虛擬內存分配程序”,操作系統使用它來保留和提交頁。所有分配程序使用虛擬內存進行數據的存取。
分配和釋放塊不就那么簡單嗎?為何花費這么長時間?
堆實現的注意事項
傳統上,操作系統和運行時庫是與堆的實現共存的。在一個進程的開始,操作系統創建一個默認堆,叫做“進程堆”。如果沒有其他堆可使用,則塊的分配使用“進程堆”。語言運行時也能在進程內創建單獨的堆。(例如,C?運行時創建它自己的堆。)除這些專用的堆外,應用程序或許多已載入的動態鏈接庫?(DLL)?之一可以創建和使用單獨的堆。Win32?提供一整套?API?來創建和使用私有堆。有關堆函數(英文)的詳盡指導,請參見?MSDN。
當應用程序或?DLL?創建私有堆時,這些堆存在于進程空間,并且在進程內是可訪問的。從給定堆分配的數據將在同一個堆上釋放。(不能從一個堆分配而在另一個堆釋放。)
在所有虛擬內存系統中,堆駐留在操作系統的“虛擬內存管理器”的頂部。語言運行時堆也駐留在虛擬內存頂部。某些情況下,這些堆是操作系統堆中的層,而語言運行時堆則通過大塊的分配來執行自己的內存管理。不使用操作系統堆,而使用虛擬內存函數更利于堆的分配和塊的使用。
典型的堆實現由前、后端分配程序組成。前端分配程序維持固定大小塊的空閑列表。對于一次分配調用,堆嘗試從前端列表找到一個自由塊。如果失敗,堆被迫從后端(保留和提交虛擬內存)分配一個大塊來滿足請求。通用的實現有每塊分配的開銷,這將耗費執行周期,也減少了可使用的存儲空間。
Knowledge?Base?
文章?Q10758,“用?calloc()?和?malloc()?管理內存”?(搜索文章編號),?包含了有關這些主題的更多背景知識。另外,有關堆實現和設計的詳細討論也可在下列著作中找到:“Dynamic?Storage?Allocation:?A?Survey?and?Critical?Review”,作者?Paul?R.?Wilson、Mark?S.?Johnstone、Michael?Neely?和?David?Boles;“International?Workshop?on?Memory?Management”,?作者?Kinross,?Scotland,?UK,?1995?年?9?月(http://www.cs.utexas.edu/users/oops/papers.html)(英文)。
Windows?NT?
的實現(Windows?NT?版本?4.0?和更新版本)?使用了?127?個大小從?8?到?1,024?字節的?8?字節對齊塊空閑列表和一個“大塊”列表。“大塊”列表(空閑列表[0])?保存大于?1,024?字節的塊。空閑列表容納了用雙向鏈表鏈接在一起的對象。默認情況下,“進程堆”執行收集操作。(收集是將相鄰空閑塊合并成一個大塊的操作。)收集耗費了額外的周期,但減少了堆塊的內部碎片。
單一全局鎖保護堆,防止多線程式的使用。(請參見“Server?Performance?and?Scalability?Killers”中的第一個注意事項,?George?Reilly?所著,在?“MSDN?Online?Web?Workshop”上(站點:http://msdn.microsoft.com/workshop/server/iis/tencom.asp(英文)。)單一全局鎖本質上是用來保護堆數據結構,防止跨多線程的隨機存取。若堆操作太頻繁,單一全局鎖會對性能有不利的影響。
什么是常見的堆性能問題?
以下是您使用堆時會遇到的最常見問題:?
分配操作造成的速度減慢。光分配就耗費很長時間。最可能導致運行速度減慢原因是空閑列表沒有塊,所以運行時分配程序代碼會耗費周期尋找較大的空閑塊,或從后端分配程序分配新塊。
釋放操作造成的速度減慢。釋放操作耗費較多周期,主要是啟用了收集操作。收集期間,每個釋放操作“查找”它的相鄰塊,取出它們并構造成較大塊,然后再把此較大塊插入空閑列表。在查找期間,內存可能會隨機碰到,從而導致高速緩存不能命中,性能降低。
堆競爭造成的速度減慢。當兩個或多個線程同時訪問數據,而且一個線程繼續進行之前必須等待另一個線程完成時就發生競爭。競爭總是導致麻煩;這也是目前多處理器系統遇到的最大問題。當大量使用內存塊的應用程序或?DLL?以多線程方式運行(或運行于多處理器系統上)時將導致速度減慢。單一鎖定的使用—常用的解決方案—意味著使用堆的所有操作是序列化的。當等待鎖定時序列化會引起線程切換上下文。可以想象交叉路口閃爍的紅燈處走走停停導致的速度減慢。?
競爭通常會導致線程和進程的上下文切換。上下文切換的開銷是很大的,但開銷更大的是數據從處理器高速緩存中丟失,以及后來線程復活時的數據重建。
堆破壞造成的速度減慢。造成堆破壞的原因是應用程序對堆塊的不正確使用。通常情形包括釋放已釋放的堆塊或使用已釋放的堆塊,以及塊的越界重寫等明顯問題。(破壞不在本文討論范圍之內。有關內存重寫和泄漏等其他細節,請參見?Microsoft?Visual?C++(R)?調試文檔?。)
頻繁的分配和重分配造成的速度減慢。這是使用腳本語言時非常普遍的現象。如字符串被反復分配,隨重分配增長和釋放。不要這樣做,如果可能,盡量分配大字符串和使用緩沖區。另一種方法就是盡量少用連接操作。
競爭是在分配和釋放操作中導致速度減慢的問題。理想情況下,希望使用沒有競爭和快速分配/釋放的堆。可惜,現在還沒有這樣的通用堆,也許將來會有。
在所有的服務器系統中(如?IIS、MSProxy、DatabaseStacks、網絡服務器、?Exchange?和其他),?堆鎖定實在是個大瓶頸。處理器數越多,競爭就越會惡化。
盡量減少堆的使用
現在您明白使用堆時存在的問題了,難道您不想擁有能解決這些問題的超級魔棒嗎?我可希望有。但沒有魔法能使堆運行加快—因此不要期望在產品出貨之前的最后一星期能夠大為改觀。如果提前規劃堆策略,情況將會大大好轉。調整使用堆的方法,減少對堆的操作是提高性能的良方。
如何減少使用堆操作?通過利用數據結構內的位置可減少堆操作的次數。請考慮下列實例:
struct?ObjectA?{
???//?objectA?
的數據?
}
struct?ObjectB?{
???//?objectB?
的數據?
}
//?
同時使用?objectA?和?objectB
//
//?
使用指針?
//
struct?ObjectB?{
???struct?ObjectA?*?pObjA;
???//?objectB?的數據?
}
//
//?
使用嵌入
//
struct?ObjectB?{
???struct?ObjectA?pObjA;
???//?objectB?的數據?
}
//
//?
集合?–?在另一對象內使用?objectA?和?objectB
//
struct?ObjectX?{
???struct?ObjectA??objA;
???struct?ObjectB??objB;
}
避免使用指針關聯兩個數據結構。如果使用指針關聯兩個數據結構,前面實例中的對象?A?和?B?將被分別分配和釋放。這會增加額外開銷—我們要避免這種做法。
把帶指針的子對象嵌入父對象。當對象中有指針時,則意味著對象中有動態元素(百分之八十)和沒有引用的新位置。嵌入增加了位置從而減少了進一步分配/釋放的需求。這將提高應用程序的性能。
合并小對象形成大對象(聚合)。聚合減少分配和釋放的塊的數量。如果有幾個開發者,各自開發設計的不同部分,則最終會有許多小對象需要合并。集成的挑戰就是要找到正確的聚合邊界。
內聯緩沖區能夠滿足百分之八十的需要(aka?80-20?規則)。個別情況下,需要內存緩沖區來保存字符串/二進制數據,但事先不知道總字節數。估計并內聯一個大小能滿足百分之八十需要的緩沖區。對剩余的百分之二十,可以分配一個新的緩沖區和指向這個緩沖區的指針。這樣,就減少分配和釋放調用并增加數據的位置空間,從根本上提高代碼的性能。
在塊中分配對象(塊化)。塊化是以組的方式一次分配多個對象的方法。如果對列表的項連續跟蹤,例如對一個?{名稱,值}?對的列表,有兩種選擇:選擇一是為每一個“名稱-值”對分配一個節點;選擇二是分配一個能容納(如五個)“名稱-值”對的結構。例如,一般情況下,如果存儲四對,就可減少節點的數量,如果需要額外的空間數量,則使用附加的鏈表指針。?
塊化是友好的處理器高速緩存,特別是對于?L1-高速緩存,因為它提供了增加的位置?—不用說對于塊分配,很多數據塊會在同一個虛擬頁中。
正確使用?_amblksiz。C?運行時?(CRT)?有它的自定義前端分配程序,該分配程序從后端(Win32?堆)分配大小為?_amblksiz?的塊。將?_amblksiz?設置為較高的值能潛在地減少對后端的調用次數。這只對廣泛使用?CRT?的程序適用。
使用上述技術將獲得的好處會因對象類型、大小及工作量而有所不同。但總能在性能和可升縮性方面有所收獲。另一方面,代碼會有點特殊,但如果經過深思熟慮,代碼還是很容易管理的。
其他提高性能的技術
下面是一些提高速度的技術:?
使用?Windows?NT5?堆?
由于幾個同事的努力和辛勤工作,1998?年初?Microsoft?Windows(R)?2000?中有了幾個重大改進:
改進了堆代碼內的鎖定。堆代碼對每堆一個鎖。全局鎖保護堆數據結構,防止多線程式的使用。但不幸的是,在高通信量的情況下,堆仍受困于全局鎖,導致高競爭和低性能。Windows?2000?中,鎖內代碼的臨界區將競爭的可能性減到最小,從而提高了可伸縮性。
使用?“Lookaside”列表。堆數據結構對塊的所有空閑項使用了大小在?8?到?1,024?字節(以?8-字節遞增)的快速高速緩存。快速高速緩存最初保護在全局鎖內。現在,使用?lookaside?列表來訪問這些快速高速緩存空閑列表。這些列表不要求鎖定,而是使用?64?位的互鎖操作,因此提高了性能。
內部數據結構算法也得到改進。
這些改進避免了對分配高速緩存的需求,但不排除其他的優化。使用?Windows?NT5?堆評估您的代碼;它對小于?1,024?字節?(1?KB)?的塊(來自前端分配程序的塊)是最佳的。GlobalAlloc()?和?LocalAlloc()?建立在同一堆上,是存取每個進程堆的通用機制。如果希望獲得高的局部性能,則使用?Heap(R)?API?來存取每個進程堆,或為分配操作創建自己的堆。如果需要對大塊操作,也可以直接使用?VirtualAlloc()?/?VirtualFree()?操作。
上述改進已在?Windows?2000?beta?2?和?Windows?NT?4.0?SP4?中使用。改進后,堆鎖的競爭率顯著降低。這使所有?Win32?堆的直接用戶受益。CRT?堆建立于?Win32?堆的頂部,但它使用自己的小塊堆,因而不能從?Windows?NT?改進中受益。(Visual?C++?版本?6.0?也有改進的堆分配程序。)
使用分配高速緩存?
分配高速緩存允許高速緩存分配的塊,以便將來重用。這能夠減少對進程堆(或全局堆)的分配/釋放調用的次數,也允許最大限度的重用曾經分配的塊。另外,分配高速緩存允許收集統計信息,以便較好地理解對象在較高層次上的使用。
典型地,自定義堆分配程序在進程堆的頂部實現。自定義堆分配程序與系統堆的行為很相似。主要的差別是它在進程堆的頂部為分配的對象提供高速緩存。高速緩存設計成一套固定大小(如?32?字節、64?字節、128?字節等)。這一個很好的策略,但這種自定義堆分配程序丟失與分配和釋放的對象相關的“語義信息”。?
與自定義堆分配程序相反,“分配高速緩存”作為每類分配高速緩存來實現。除能夠提供自定義堆分配程序的所有好處之外,它們還能夠保留大量語義信息。每個分配高速緩存處理程序與一個目標二進制對象關聯。它能夠使用一套參數進行初始化,這些參數表示并發級別、對象大小和保持在空閑列表中的元素的數量等。分配高速緩存處理程序對象維持自己的私有空閑實體池(不超過指定的閥值)并使用私有保護鎖。合在一起,分配高速緩存和私有鎖減少了與主系統堆的通信量,因而提供了增加的并發、最大限度的重用和較高的可伸縮性。
需要使用清理程序來定期檢查所有分配高速緩存處理程序的活動情況并回收未用的資源。如果發現沒有活動,將釋放分配對象的池,從而提高性能。
可以審核每個分配/釋放活動。第一級信息包括對象、分配和釋放調用的總數。通過查看它們的統計信息可以得出各個對象之間的語義關系。利用以上介紹的許多技術之一,這種關系可以用來減少內存分配。
分配高速緩存也起到了調試助手的作用,幫助您跟蹤沒有完全清除的對象數量。通過查看動態堆棧返回蹤跡和除沒有清除的對象之外的簽名,甚至能夠找到確切的失敗的調用者。
MP?
堆?
MP?堆是對多處理器友好的分布式分配的程序包,在?Win32?SDK(Windows?NT?4.0?和更新版本)中可以得到。最初由?JVert?實現,此處堆抽象建立在?Win32?堆程序包的頂部。MP?堆創建多個?Win32?堆,并試圖將分配調用分布到不同堆,以減少在所有單一鎖上的競爭。
本程序包是好的步驟?—一種改進的?MP-友好的自定義堆分配程序。但是,它不提供語義信息和缺乏統計功能。通常將?MP?堆作為?SDK?庫來使用。如果使用這個?SDK?創建可重用組件,您將大大受益。但是,如果在每個?DLL?中建立這個?SDK?庫,將增加工作設置。
重新思考算法和數據結構?
要在多處理器機器上伸縮,則算法、實現、數據結構和硬件必須動態伸縮。請看最經常分配和釋放的數據結構。試問,“我能用不同的數據結構完成此工作嗎?”例如,如果在應用程序初始化時加載了只讀項的列表,這個列表不必是線性鏈接的列表。如果是動態分配的數組就非常好。動態分配的數組將減少內存中的堆塊和碎片,從而增強性能。
減少需要的小對象的數量減少堆分配程序的負載。例如,我們在服務器的關鍵處理路徑上使用五個不同的對象,每個對象單獨分配和釋放。一起高速緩存這些對象,把堆調用從五個減少到一個,顯著減少了堆的負載,特別當每秒鐘處理?1,000?個以上的請求時。
如果大量使用“Automation”結構,請考慮從主線代碼中刪除“Automation?BSTR”,或至少避免重復的?BSTR?操作。(BSTR?連接導致過多的重分配和分配/釋放操作。)
摘要
對所有平臺往往都存在堆實現,因此有巨大的開銷。每個單獨代碼都有特定的要求,但設計能采用本文討論的基本理論來減少堆之間的相互作用。?
評價您的代碼中堆的使用。
改進您的代碼,以使用較少的堆調用:分析關鍵路徑和固定數據結構。
在實現自定義的包裝程序之前使用量化堆調用成本的方法。
如果對性能不滿意,請要求?OS?組改進堆。更多這類請求意味著對改進堆的更多關注。
要求?C?運行時組針對?OS?所提供的堆制作小巧的分配包裝程序。隨著?OS?堆的改進,C?運行時堆調用的成本將減小。
操作系統(Windows?NT?家族)正在不斷改進堆。請隨時關注和利用這些改進。
Murali?Krishnan?是?Internet?Information?Server?(IIS)?組的首席軟件設計工程師。從?1.0?版本開始他就設計?IIS,并成功發行了?1.0?版本到?4.0?版本。Murali?組織并領導?IIS?性能組三年?(1995-1998),?從一開始就影響?IIS?性能。他擁有威斯康星州?Madison?大學的?M.S.和印度?Anna?大學的?B.S.。工作之外,他喜歡閱讀、打排球和家庭烹飪。
http://yanricheng.javaeye.com/blog/131344