首先看一段代碼:
void a(){int arr[10];int i;for(i=0;i<10;i++){arr[i]=i; // 初始化數組arr[]}}void b(){int arr2[10]; int i;for(i=0;i<10;i++){printf("%d", arr2[i]); //未初始化arr2[],直接輸出它的值}// 結果是輸出0123456789}int _tmain(int argc, _TCHAR* argv[]){a();b();}
學過C語言的童鞋都知道,這段代碼是有問題的,函數a()中對arr[]的初始化,隨著函數a()的執行完畢,棧上的局部變數被釋放,變數arr[]便“不存在”了,函數b()未初始化數組arr2[]的值,便使用它,其值是隨機的。在VS2005和VS2010運行上述代碼,輸出結果是
竟然是數組arr[]的值,晴天霹靂啊,難道“a()函數內的局部變數,竟在b()函數內得到了訪問”,當然這種說法是錯誤的,我們決定深入的研究下這個問題,話說“彙編一出,真相大白”,就一起來分析一下。
下面為函數a()的彙編代碼,學過彙編的人,應該知道CPU內建棧機制的實現,即SS:IP,push,pop操作對應相應SS:IP的改變。
在下面代碼裡,ebp,esp是函數a()內的棧的棧底地址和棧頂地址。
void a()
{
00401030 push ebp //將ebp壓棧
00401031 mov ebp,esp //將棧頂的值賦給棧底指標,即初始化該棧,(彙編含義為mov 寄存器esp的值至ebp)
00401033 sub esp,2Ch //將棧頂指標減少2Ch,即分配出一片記憶體為函數a()內的變數使用
//2Ch對應的10進位是44,這44個位元組,每個int 4位元組,arr[10]佔40個位元組,i佔4個位元組
int arr[10];
int i;
for(i=0;i<10;i++)
00401036 mov dword ptr [i],0
0040103D jmp a+18h (401048h)
0040103F mov eax,dword ptr [i]
00401042 add eax,1
00401045 mov dword ptr [i],eax
00401048 cmp dword ptr [i],0Ah
0040104C jge a+2Ah (40105Ah)
{
arr[i]=i; // 初始化數組arr[]
0040104E mov ecx,dword ptr [i]
00401051 mov edx,dword ptr [i]
00401054 mov dword ptr arr[ecx*4],edx
00401058 jmp a+0Fh (40103Fh)
}
}
0040105A mov esp,ebp //銷毀函數a()的棧
0040105C pop ebp //恢複之前入棧的ebp的值
0040105D ret
其實,函數a()執行後的,記憶體布局為
| 0x0012FF68 |
10 |
//變數i的值 |
| 0x0012FF64 |
9 |
|
| 0x0012FF60 |
8 |
|
| 0x0012FF5C |
7 |
|
| 0x0012FF58 |
6 |
|
| 0x0012FF56 |
5 |
|
| 0x0012FF52 |
4 |
|
| 0x0012FF48 |
3 |
|
| 0x0012FF44 |
2 |
|
| 0x0012FF40 |
1 |
|
| 0x0012FF3C |
0 |
//數組arr[] |
其中ebp為0x0012FF68,esp為0x0012FF3C(這些記憶體位址分配根據每台PC的情況不同而不同,以上地址為我的PC的記憶體位址),函數執行到最後
0040105A mov esp,ebp //銷毀函數a()的棧
將指向棧底的ebp的值付給esp(棧頂的值),這個棧為空白了,裡面沒變數了,從函數的角度說,所有的局部變數都已經“不存在”了,我們不應該用函數的局部變數做傳回值,但是我們看到了
0040105C pop ebp //恢複之前入棧的ebp的值
0040105D ret
恢複了ebp的值,然後ret(彙編指令與call相對應,控制CS:IP的值),但我們應該注意到, 這段記憶體空間即0x0012FF68至0x0012FF3C並未被改寫或隨機化(實際上不存在隨機化記憶體這種說法),然後我們看下函數b()的代碼:
void b()
{
00401060 push ebp
00401061 mov ebp,esp
00401063 sub esp,2Ch
int arr2[10];
int i;
for(i=0;i<10;i++)
00401066 mov dword ptr [i],0
0040106D jmp b+18h (401078h)
0040106F mov eax,dword ptr [i]
00401072 add eax,1
00401075 mov dword ptr [i],eax
00401078 cmp dword ptr [i],0Ah
0040107C jge b+35h (401095h)
{
printf("%d", arr2[i]); //未初始化arr2[],直接輸出它的值
0040107E mov ecx,dword ptr [i]
00401081 mov edx,dword ptr arr2[ecx*4]
00401085 push edx
00401086 push offset ___xt_z+120h (41DB5Ch)
0040108B call printf (4010D1h)
00401090 add esp,8
00401093 jmp b+0Fh (40106Fh)
}
// 結果是輸出0123456789
}
其中ebp還是為0x0012FF68,esp還是為0x0012FF3C,也就是函數b()的局部變數棧的記憶體布局,同函數a()的(已經釋放了,又被函數b()拿來使用),完全一樣,所以從結果上看好像是函數b()輸出了函數a()的值。
任何改變函數a()或者函數b()的記憶體布局的做法,都會影響到以上代碼的結果,比如只在函數a()內多定義數組或變數,或者只在函數b()內多定義數組或變數(int x;int arr1[10]),,都會改變函數b()的輸出,也可以思考下,為什麼main()定義的變數為什麼沒改變輸出結果,只是改變了ebp和esp的值。
總之,上面的代碼利用記憶體布局,完成了不可能完成的訪問,但具體編程不應該使用這種方法,1.太顛覆常規概念;2.跟編譯器有很大關係,以上代碼在GCC上的某些版本輸出便是隨機數,所以這還是一種“錯誤做法”。
參考資料http://topic.csdn.net/u/20110324/19/b2c6a156-7b7e-41d1-933d-34f34b240034.html