通過記憶體布局完成局部變數訪問解析

來源:互聯網
上載者:User

首先看一段代碼:

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

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.