找回密碼
 立即注冊(cè)

QQ登錄

只需一步,快速開始

搜索
查看: 3137|回復(fù): 0
收起左側(cè)

一個(gè)有趣的變量關(guān)聯(lián)問題

[復(fù)制鏈接]
ID:71922 發(fā)表于 2015-1-11 00:12 | 顯示全部樓層 |閱讀模式
(環(huán)境是vc6Debug方式下)
#include<stdio.h>
void test()
{
int t;
scanf("%d",&t);
在這里加入代碼
}
main()
{
int m;
test();
printf("m=%d",m);
}

要在test函數(shù)中加入代碼,影響main函數(shù)里面的變量,很明顯我們要加的代碼是一個(gè)賦值語句,語句的右值是我們輸入的t的值,而左值,則是test函數(shù)里t變量的地址加上main函數(shù)里m變量的地址相對(duì)于t變量的偏移的值。

那么問題的實(shí)質(zhì),就轉(zhuǎn)變?yōu)樽兞縨的地址相對(duì)于變量t地址的偏移了。
這個(gè)偏移如何求?

基礎(chǔ)知識(shí):
讓我們跳出這個(gè)題,看看我們手頭掌握的知識(shí)。
函數(shù)調(diào)用需要用到堆棧,當(dāng)程序調(diào)用函數(shù)時(shí),C將函數(shù)調(diào)用后面的指令地址(稱為返回地址)壓入堆棧。然后,C將函數(shù)的參數(shù)從右至左依次壓入堆棧。最后,如果函數(shù)聲明了局部變量,C將堆?臻g分配給函數(shù)以存儲(chǔ)變量的值。
當(dāng)函數(shù)結(jié)束的時(shí)候,C釋放存儲(chǔ)局部變量和參數(shù)的堆?臻g。然后,C根據(jù)返回地址判斷下一步要執(zhí)行的指令。C從堆棧中移走返回值并將地址放入IP寄存器中。
函數(shù)調(diào)用的堆棧示意圖如下圖所示:




需要說明的是,什么是堆棧呢?我們都知道,變量、常量在內(nèi)存中的分配大致分成四個(gè)區(qū)域,分別是堆棧、堆、常數(shù)存儲(chǔ)區(qū)以及全局、靜態(tài)存儲(chǔ)區(qū)。分配在堆棧中的變量具有的最主要的特點(diǎn)就是當(dāng)這個(gè)變量不再需要的時(shí)候,編譯器會(huì)自動(dòng)回收,函數(shù)內(nèi)的局部變量的存儲(chǔ)單元都可以在棧上分配。
實(shí)際上,堆棧是系統(tǒng)內(nèi)存中的一塊區(qū)域,這個(gè)區(qū)域在操作系統(tǒng)初始化時(shí)得到分配。

探究本題:
由堆棧的知識(shí),我們知道了,test函數(shù)里的t,以及main函數(shù)里的m都是分配在堆棧中的,因此他們之間的地址偏移量,也就取決于函數(shù)調(diào)用時(shí)堆棧是如何操作的。
我們通過匯編代碼來仔細(xì)分析下堆棧的操作方式,調(diào)試壞境是VC6.0,在windows操作系統(tǒng)中,堆棧的生長方向是由高到低的。

32:
33:
34: main()
35: {
00401070 push ebp
00401071 mov ebp,esp ①
00401073 sub esp,44h ②
00401076 push ebx
00401077 push esi
00401078 push edi
00401079 lea edi,[ebp-44h] ③
0040107C mov ecx,11h
00401081 mov eax,0CCCCCCCCh
00401086 rep stos dword ptr [edi] ④
36: int m;
37: test();
00401088 call @ILT+0(test) (00401005) ⑤
38: printf("m=%d",m);
0040108D mov eax,dword ptr [ebp-4]
00401090 push eax
00401091 push offset string "m=%d" (0042201c)
00401096 call printf (00401160)
0040109B add esp,8 ⑥
39: }
0040109E pop edi
0040109F pop esi
004010A0 pop ebx
004010A1 add esp,44h
004010A4 cmp ebp,esp
004010A6 call __chkesp (00401120)
004010AB mov esp,ebp
004010AD pop ebp
004010AE ret ⑦
--- No source file ----

下圖是到步驟⑤時(shí)的堆棧情況,實(shí)際上函數(shù)main()和test()一樣,也可以看做是函數(shù)調(diào)用的情況,只不過函數(shù)main是由mainCRTStartup這個(gè)默認(rèn)函數(shù)調(diào)用的,而函數(shù)test是由函數(shù)main調(diào)用的,兩者在調(diào)用上的匯編代碼都是一樣的。




好了,堆棧的全貌我們看到了,現(xiàn)在就要真正的開工了。我們結(jié)合匯編代碼來看看到底函數(shù)調(diào)用時(shí),堆棧是如何操作的。
當(dāng)一切都還沒有發(fā)生卻即將發(fā)生的時(shí)候,是一個(gè)令人雞凍的時(shí)刻,是時(shí)候該向過去告別了,因?yàn)槲覀兗磳⑦M(jìn)入一個(gè)新的函數(shù),一段新的旅程。但是別急,磨刀不誤砍柴功,有些事情必須先做準(zhǔn)備,那么就從這里開始準(zhǔn)備吧。
此時(shí),棧頂esp的值是0x12ff84,這個(gè)地址上存的值是0x4012c9,我們?cè)偃タ纯?x4012c9位置的指令。很好,在這個(gè)指令上一行我們看到了一條醒目的指令:call @ILT+5(main),一切都明白了,0x4012c9就是函數(shù)的返回地址。旅行雖然令人興奮,但是我們必須把出口的地址記下來,畢竟,旅行只是生活的調(diào)節(jié),我們終將還得繼續(xù)自己的生活。

① push ebp
mov ebp, esp
這段代碼很容易,就是把棧底寄存器ebp入棧。ebp主要用于給出堆棧中數(shù)據(jù)區(qū)基址的偏移,從而方便的實(shí)現(xiàn)直接存取堆棧中的數(shù)據(jù)。
mov ebp, esp 就是把當(dāng)前esp的值賦給ebp。實(shí)際上,就是給ebp重新賦了值,該值就是這個(gè)main()函數(shù)在堆棧中的基址,以后main()函數(shù)中分配的局部變量等的地址都會(huì)通過ebp算出偏移,來進(jìn)行讀寫運(yùn)算。
我們看到0x12ff80地址上存的是0x12ffc0,這是調(diào)用main()函數(shù)的函數(shù)在堆棧中的基址,我們?cè)谶@里把這個(gè)值保存下來,以便退出main()函數(shù)時(shí)能夠恢復(fù)ebp。

② sub esp, 44h
在存儲(chǔ)了ebp,并且重新對(duì)ebp賦值了后,代碼將esp減去44h,這個(gè)區(qū)域是預(yù)留給函數(shù)的局部變量的。在該函數(shù)中定義的局部變量,將依次序在這塊區(qū)域中得到分配。
有人說了,萬一這個(gè)44h的區(qū)域不夠怎么辦,實(shí)際上這個(gè)44h是可以變化的,編譯器會(huì)根據(jù)實(shí)際情況調(diào)整這個(gè)數(shù)值。我測試了一下,如果定義了1個(gè)變量,就會(huì)分配44h的空間,定義了2個(gè)變量,就會(huì)分配48h的空間……依次類推。

③ push ebx
push esi
push edi
lea edi,[ebp-44h]
分配了44h空間后,ebx, esi, edi的值將依次入棧。為什么要先分配44h的空間,再push這三個(gè)寄存器,而不是一口氣把ebp, ebx, esi, edi這四個(gè)東東全部都?jí)哼M(jìn)去,再分配局部變量的空間呢。
我想是因?yàn)檫@個(gè)空間是分配給局部變量的,而所有局部變量的地址都是通過ebp加上偏移量算出來的,因此讓這個(gè)空間和ebp緊挨能夠提高運(yùn)算的效率。
ebx寄存器是基地址寄存器,是四個(gè)數(shù)據(jù)寄存器中唯一可作為存儲(chǔ)器指針使用的寄存器。esi寄存器是源變址寄存器,edi則是目的變址寄存器。這兩個(gè)寄存器的典型用法就是進(jìn)行字符串操作時(shí),esi作為源指針,edi作為目的指針。
lea edi, [ebp-44h] 這一句使得edi的值由0x00000000變成了(0x12ff80 – 44h = )0x12ff3c,這個(gè)地址是44h的最后一個(gè)地址,至于為什么要賦成這個(gè)值,我們馬上就會(huì)知道了。

④ mov ecx,11h
mov eax,0CCCCCCCCh
rep stos dword ptr [edi]
我們?cè)冖谥蟹峙涞目臻g似乎還沒有初始化,現(xiàn)在我們就著手來對(duì)這段區(qū)域進(jìn)行初始化。在③中已經(jīng)把edi的值賦給了這塊空間的一端,而ecx這個(gè)計(jì)數(shù)寄存器里存儲(chǔ)的是循環(huán)次數(shù),一共要循環(huán)11h次,哦,這個(gè)11h就是44h/4得到的。很顯然,eax里存儲(chǔ)的是初值羅,這段話的意思就是,從0x12ff3c這個(gè)地址開始,以后的11h個(gè)字的值初始化為0xcccccccc。
程序走到這里,基本上函數(shù)調(diào)用的步驟已經(jīng)全部完成了,我們總結(jié)一下,看看堆棧里到底發(fā)生了什么。
壓入了4個(gè)寄存器的值,分別是ebp,ebx,esi,edi的值。分配了一個(gè)大小為44h的空間,這個(gè)空間的大小根據(jù)函數(shù)的具體情況,由編譯器自行決定。對(duì)這個(gè)大小為44h的空間的每一個(gè)字初始化為0xcccccccc。
OK,我們來算算到目前為止,我們用了堆棧多少空間:0x12ff84 – 0x12ff30 = 54h。

⑤ call @ILT+0(test) (00401005)
做完函數(shù)調(diào)用的工作,就要開始真正的函數(shù)本題的工作了。我們看到int m;這一句C語言并沒有轉(zhuǎn)化成任何的匯編語言,實(shí)際上,這只是一個(gè)聲明的語句,告訴編譯器這里有一個(gè)變量,叫做m。那么,什么時(shí)候分配這個(gè)m的空間呢,我們繼續(xù)往下看,就會(huì)看到一句mov eax,dword ptr [ebp-4],對(duì)頭,就在這里,地址是ebp-4,正是44h的最開始的地址。
test()這句話匯編為call @ILT+0(test) (00401005),那么我們又要開始一段新的旅程了。

OK, 我們現(xiàn)在來到了test函數(shù),也離我們需要解答的地方越來越近了。其實(shí)我們走到了這里,聰明的你,是不是已經(jīng)知道了答案了呢?
還是和之前一樣,我們從匯編看起。

2: void test()
3: {
0040DA10 push ebp
0040DA11 mov ebp,esp ⑴
0040DA13 sub esp,44h ⑵
0040DA16 push ebx
0040DA17 push esi
0040DA18 push edi
0040DA19 lea edi,[ebp-44h] ⑶
0040DA1C mov ecx,11h
0040DA21 mov eax,0CCCCCCCCh
0040DA26 rep stos dword ptr [edi] ⑷
4: int t;
5: scanf("%d",&t);
0040DA28 lea eax,[ebp-4]
0040DA2B push eax
0040DA2C push offset string "%d" (00422fd8)
0040DA31 call scanf (004010c0)
0040DA36 add esp,8

7: } ⑸
0040DA3F pop edi
0040DA40 pop esi
0040DA41 pop ebx
0040DA42 add esp,44h
0040DA45 cmp ebp,esp
0040DA47 call __chkesp (00401120)
0040DA4C mov esp,ebp
0040DA4E pop ebp
0040DA4F ret
--- No source file ---

堆棧如下圖所示:





函數(shù)的調(diào)用和main()函數(shù)一樣,而如前所述,局部變量t分配到44h空間的第一個(gè)字,也就是0x12ff24這個(gè)地址。而局部變量m則分配到main()函數(shù)44h空間的第一個(gè)字,也就是0x12ff7c中。
OK,我們至此找到了變量t和變量m的偏移,0x12ff7c – 0x12ff24 = 58h,因此我們問題的答案就顯而易見了:
*((&t) + 0x58 / 4) = t;

發(fā)散思維:
題目已經(jīng)解決了,但是我們注意到了,test()函數(shù)沒有參數(shù),顯然,我們也很想知道,如果這個(gè)函數(shù)有參數(shù),堆棧將如何工作。
那么,把題目里的程序改一下子吧:

#include<stdio.h>
void test(int x, char *pStr, double y)
{
int t;
scanf("%d",&t);
*((&t) + 0x58/4) = t;
}

void main()
{
int m;
test(1, "Hello World!", 3.14);
printf("m=%d",m);
}
給test()函數(shù)加上了3個(gè)參數(shù),分別是int、char *以及double型的,編譯環(huán)境依然是Windows操作系統(tǒng),VC++6.0。

我們來看看關(guān)鍵部分的匯編代碼:
14: test(1, "Hello World!", 3.14);
00401088 push 40091EB8h
0040108D push 51EB851Fh ①
00401092 push offset string "Hello World!" (00426028) ②
00401097 push 1 ③
00401099 call @ILT+0(test) (00401005) ④
0040109E add esp,10h ⑤
涉及到這一段代碼的堆棧如下圖:





①②③分別把三個(gè)參數(shù)壓入棧中,順序是從右向左。
先壓入函數(shù)的參數(shù),再壓入返回地址,與基礎(chǔ)知識(shí)里講的正好相反,可見壓入的順序是與編譯器有關(guān)的。

總結(jié):
在windows操作系統(tǒng),VC Debug環(huán)境下:
1, 函數(shù)的局部變量是分配在棧中的,這個(gè)區(qū)域是將ebp壓入棧后分配的一塊區(qū)域。這也是為什么分配在棧上的局部變量在函數(shù)結(jié)束后會(huì)自動(dòng)收回的原因,因?yàn)楹瘮?shù)結(jié)束后會(huì)退棧,因此分配給該函數(shù)局部變量的那塊區(qū)域也就退棧掉了,自然也就自動(dòng)收回了。
2, 函數(shù)調(diào)用時(shí),會(huì)自動(dòng)將ebp, ebx, esi和edi壓入棧,函數(shù)的局部變量是通過ebp加上偏移來進(jìn)行訪問的。
3, 函數(shù)的調(diào)用是先將參數(shù)由右向左壓入棧,再壓入下一條指令的地址作為返回地址。

回復(fù)

使用道具 舉報(bào)

本版積分規(guī)則

小黑屋|51黑電子論壇 |51黑電子論壇6群 QQ 管理員QQ:125739409;技術(shù)交流QQ群281945664

Powered by 單片機(jī)教程網(wǎng)

快速回復(fù) 返回頂部 返回列表