堆疊平衡:估計這是最詳細的講解堆疊平衡的了 vc++6.0
阿新 • • 發佈:2019-01-11
本文源於公司內部交流 要涉及到溢位,因此有了這篇堆疊平衡的文章#include <stdio.h> #include <stdlib.h> #include <string.h> #include <windows.h> int ShowEsp(int* arg1,int* arg2); /* 引言 各種面試寶典上都會說 又說棧在程序空間的高地址部分,向下擴充套件; 堆在程序空間的低地址部分,堆向上擴充套件 來驗證一下是否正如所說這些變數在記憶體中如何分佈? */ int main() { //0) int i=0xABCDABCD; //int* j = (int*)malloc(sizeof(int)); //malloc為什麼被我注掉了? //memcpy(j,&i,sizeof(int)); int* k = (int*)VirtualAlloc(NULL,sizeof(int),MEM_COMMIT,PAGE_READWRITE); int* ptr = NULL; ptr = (int*)&i; //ptr = j; /* malloc 是crt執行庫實現 系統為crt庫在程序靠近2G的高地址處保留大塊堆記憶體 用連結串列管理 呼叫malloc時,單獨從此處抽取分配給呼叫者 */ ptr = k; //1) 忽略malloc這另類 看看實際系統定義諾幹棧變數後,檢視他們在記憶體中的分佈 int arg1=0x40302010; int arg2=0x20; int* ptr1=NULL; int* ptr2=NULL; char str[] = {"1234567"}; printf("arg1:%x\narg2:%x\n",&arg1,&arg2); printf("ptr1:%x\nptr2:%x\n",&ptr1,&ptr2); printf("str:%x\n",str); //結論:變數的地址是連續並且向下擴充套件 //編譯器通過sub esp,immd來開闢棧空間 //再來看下變數地址存放的內容 arg1 char* arg1Ptr = (char*)&arg1; printf("arg1Ptr[0]:%02x\narg1Ptr[1]:%02x\narg1Ptr[2]:%02x\narg1Ptr[3]:%02x\n",*arg1Ptr,*(arg1Ptr+1),*(arg1Ptr+2),*(arg1Ptr+3)); //程式沒問題吧,感覺在用arg1Ptr取arg1各個位元組的內容,看下arg1在記憶體中的分佈 //......................................................................... //intel cpu小端機 (題外話 網路程式設計時 ip/port轉換即為大小端位元組轉換) 資料在記憶體中按高高低低分佈 高位元組 在高位 低位元組在低位 //2) 知道了變數在記憶體中的分佈 再看下如何存取變數,alt-8 // arg1 = 0x80706050; arg2 = arg1; arg2 = 0x20; //程式編譯後 用[ebp-N]相對偏移取變數 why? //intel CPU用esp指向當前函式棧頂,變數又儲存在棧中,理所當然的可以用esp取變數。 //但是變數在不斷擴充esp在不斷減小 ------ //假設 現在要取arg1變數值 可能會編譯成 mov eax,[esp+0x40],意思是arg1離棧頂esp相差0x40個位元組 //如果程式又新定義一個棧變數,棧頂向下移動,即esp=esp-4 此時 esp離arg1的距離為0x44 //如果再次取arg1的值 會編譯成 mov eax,[esp+0x44] 這樣編譯起來太麻煩了 //a) intel CPU用ebp做當前函式幀[不是強制約定 習慣],也就是棧底,某些面試寶典會寫 //函式棧底不改變,說的就是ebp再當前函式中不改變。棧變數編譯後記憶體位置固定下來,現在ebp又是固定不變的準繩 //這樣無論程式怎樣擴充套件棧變數,ebp到各個變數之間的距離都不會改變 //3) 面試寶典還說 c++引數入棧順序是 從右往左壓棧 全部入完後 壓入函式返回地址 //既然 大家對堆疊達成共識才看到這了 再來看下呼叫函式,繼續反彙編 觀察堆疊變化 ShowEsp(&arg1,&arg2); //a)入棧操作 (esp暫存器的變化) 用的是push 每次push後esp減4 壓入返回地址後 jmp到ShowEsp //程式jmp到ShowEsp 這裡也跟到esp //5) 堆疊平衡的收尾 /* 來看下函式返回後當前棧頂還剩啥 指令執行順序-|esp變化------|儲存ebp後變數相對於新棧幀ebp的距離------- push &arg2 |1次esp=esp-4 | ebp+0x0C push &arg1 |2次esp=esp-4 | ebp+0x08 */ /*函式引數已經顯得不重要了可以忽略不計,再說main函式中依然可以通過[ebp-N]的形式訪問這些變數 但是目前堆疊還沒有恢復到函式呼叫前的樣子 還倒欠main函式8個位元組,於是編譯器採用了一種簡單粗暴 卻有行之有效的辦法 add esp,0x08 於是堆疊平衡 還有一個問題,函式範圍值在哪?eax中 eax 4位元組 不管返回什麼都沒問題 */ exit(0); } int ShowEsp(int* arg1,int* arg2) { //進入到ShowEsp後 先是取形參,然後賦值給區域性變數 //區域性變數訪問 已經不是這裡的重點 此處重點看下如何取形參 int op1,op2,res; //atl-8 op1 = *arg1; op2 = *arg2; /* 訪問arg1 arg2 被翻譯成mov eax,[ebp+8],[ebp+0x0c] 之前訪問變數是用[ebp-N]的形式 現在怎麼變成使用[ebp+N]的形式? 解釋這個 還是得看反彙編的結果 反彙編的前幾句如下 push ebp mov ebp,esp sub esp,4Ch 上面2-a)處寫到過 intel CPU用ebp做當前函式幀,剛才指令流是在main函式中,因此ebp是基於main函式的 現在進入到ShowEsp函式中,要形成新的函式幀,因此先用push ebp把前一個函式的函式幀儲存起來,然後 mov ebp,esp把當前的棧頂esp賦值給ebp形成新的函式幀 最後的sub esp,4Ch 為本函式建立棧空間 如果把之前main函式中的函式呼叫和引數入棧 以及此處生成新的函式幀一系列動作聯合起來 並檢視esp在此期間的 變化: 指令執行順序-|esp變化------|儲存ebp後變數相對於新棧幀ebp的距離------- push &arg2 |1次esp=esp-4 | ebp+0x0C push &arg1 |2次esp=esp-4 | ebp+0x08 call ShowEsp |3次esp=esp-4 | ebp+0x04 push ebp |4次esp=esp-4 | ebp+0x00 這應該能解釋函式取形參用mov eax,[ebp+0x08]等形式 */ res = op1+op2; //4)堆疊平衡的下半段 /* 還是拿某寶典說事,說c++是_stdcall c是_cdcel call 區別是函式執行結束 一個由被呼叫的函式恢復堆疊 另一個是由呼叫者恢復堆疊 這程式碼是_cdcel call 由呼叫者恢復堆疊 來看下這函式怎麼恢復堆疊 繼續返回編 */ return res; /* 程式結尾處看到 mov esp ebp pop ebp ret 8 還記得函式入口處的? push ebp mov ebp esp 都說是堆疊操作了,所有的操作要呼應對吧,執行了 mov esp ebp pop ebp 這兩句之後,函式堆疊恢復到發生呼叫ShowEsp的情景,雖然ShowEsp分配的棧變數還存在,但 已經處在esp指向的範圍之外,換句話說,此時再新建棧變數,以前棧上的變數就會覆蓋。當然 很少有人這麼做。這也是有時候函式返回了,還能得到函式內部變數的原因。注意,所謂的寶典 會說,函式返回會自動清棧變數,反正c的程式碼,我沒看到這種語句 最後的ret語句會使程式返回到函式呼叫的地方,這個地方由誰指定? 還記得3-a)處呼叫ShowEsp時,把下一條指令壓入堆疊?就是那個時候指定函式返回地址,翻閱intel手冊 說發生ret時,返回到當前棧頂指向的值 來看下當前棧頂還剩啥 指令執行順序-|esp變化------|儲存ebp後變數相對於新棧幀ebp的距離------- push &arg2 |1次esp=esp-4 | ebp+0x0C push &arg1 |2次esp=esp-4 | ebp+0x08 call ShowEsp |3次esp=esp-4 | ebp+0x04 棧頂還剩call ShowEsp時壓入的返回地址,ret執行後 相當於pop eax, jmp eax 彈出一個棧值。 於是我們乘坐這個傳送門返回到main函式中 [題外話]如果修改這個返回地址會得到意想不到的結果,這是後面要講的緩衝區溢位 */ } //文章結尾感謝同事yj同志 幫我做義務審校,並提出修改建議