1. 程式人生 > >堆疊平衡:估計這是最詳細的講解堆疊平衡的了 vc++6.0

堆疊平衡:估計這是最詳細的講解堆疊平衡的了 vc++6.0

#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同志 幫我做義務審校,並提出修改建議
本文源於公司內部交流 要涉及到溢位,因此有了這篇堆疊平衡的文章