亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > C++ > 正文

淺談C/C++堆棧指引——C/C++堆棧很強大

2019-11-08 02:11:52
字體:
來源:轉載
供稿:網友

 我們經常會討論這樣的問題:什么時候數據存儲在飛鴿傳書堆棧(Stack)中,什么時候數據存儲在堆(Heap)中。我們知道,局部變量是存儲在堆棧中的;debug時,查看堆??梢灾篮瘮档恼{用順序;函數調用時傳遞參數,事實上是把參數壓入堆棧,聽起來,堆棧象一個大雜燴。那么,堆棧(Stack)到底是如何工作的呢? 本文將詳解C/C++堆棧的工作機制。閱讀時請注意以下幾點:

    1)本文討論的語言是 Visual C/C++,由于高級語言的堆棧工作機制大致相同,因此對其他高級語言如C#也有意義。

    2)本文討論的堆棧,是指程序為每個線程分配的默認堆棧,用以支持程序的運行,而不是指程序員為了實現算法而自己定義的堆棧。

    3)  本文討論的平臺為intel x86。

    4)本文的主要部分將盡量避免涉及到匯編的知識,在本文最后可選章節,給出前面章節的反編譯代碼和注釋。

    5)結構化異常處理也是通過堆棧來實現的(當你使用try…catch語句時,使用的就是c++對windows結構化異常處理的擴展),但是關于結構化異常處理的主題太復雜了,本文將不會涉及到。

document_thumb_thumb[4]從一些基本的知識和概念開始

    1) 程序的堆棧是由處理器直接支持的。在intel x86的系統中,堆棧在內存中是從高地址向低地址擴展(這和自定義的堆棧從低地址向高地址擴展不同),如下圖所示:

image

    因此,棧頂地址是不斷減小的,越后入棧的數據,所處的地址也就越低。

    2) 在32位系統中,堆棧每個數據單元的大小為4字節。小于等于4字節的數據,比如字節、字、雙字和布爾型,在堆棧中都是占4個字節的;大于4字節的數據在堆棧中占4字節整數倍的空間。

    3) 和堆棧的操作相關的兩個寄存器是EBP寄存器和ESP寄存器的,本文中,你只需要把EBP和ESP理解成2個指針就可以了。ESP寄存器總是指向堆棧的棧頂,執行PUSH命令向堆棧壓入數據時,ESP減4,然后把數據拷貝到ESP指向的地址;執行POP命令時,首先把ESP指向的數據拷貝到內存地址/寄存器中,然后ESP加4。EBP寄存器是用于訪問堆棧中的數據的,它指向堆棧中間的某個位置(具體位置后文會具體講解),函數的參數地址比EBP的值高,而函數的局部變量地址比EBP的值低,因此參數或局部變量總是通過EBP加減一定的偏移地址來訪問的,比如,要訪問函數的第一個參數為EBP+8。

    4) 堆棧中到底存儲了什么數據? 包括了:函數的參數,函數的局部變量,寄存器的值(用以恢復寄存器),函數的返回地址以及用于結構化異常處理的數據(當函數中有try…catch語句時才有,本文不討論)。這些數據是按照一定的順序組織在一起的,我們稱之為一個堆棧幀(Stack Frame)。一個堆棧幀對應一次函數的調用。在函數開始時,對應的堆棧幀已經完整地建立了(所有的局部變量在函數幀建立時就已經分配好空間了,而不是隨著函數的執行而不斷創建和銷毀的);在函數退出時,整個函數幀將被銷毀。

    5) 在文中,我們把函數的調用者稱為Caller(調用者),被調用的函數稱為Callee(被調用者)。之所以引入這個概念,是因為一個函數幀的建立和清理,有些工作是由Caller完成的,有些則是由Callee完成的。

document_thumb_thumb4開始討論堆棧是如何工作的 

    我們來討論堆棧的工作機制。堆棧是用來支持函數的調用和執行的,因此,我們下面將通過一組函數調用的例子來講解,看下面的代碼:

view sourcePRint?
01int foo1(int m, int n)
02{
03    int p=m*n;
04    return p;
05}
06int foo(int a, int b)
07{
08    int c=a+1;
09    int d=b+1;
10    int e=foo1(c,d);
11    return e;
12}
13  
14int main()
15{
16    int result=foo(3,4);
17    return 0;
18}

    這段代碼本身并沒有實際的意義,我們只是用它來跟蹤堆棧。下面的章節我們來跟蹤堆棧的建立,堆棧的使用和堆棧的銷毀。

 document_thumb_thumb4堆棧的建立

    我們從main函數執行的第一行代碼,即int result=foo(3,4); 開始跟蹤。這時main以及之前的函數對應的堆棧幀已經存在在堆棧中了,如下圖所示:

image

圖1

    參數入棧 

   當foo函數被調用,首先,caller(此時caller為main函數)把foo函數的兩個參數:a=3,b=4壓入堆棧。參數入棧的順序是由函數的調用約定(Calling Convention)決定的,我們將在后面一個專門的章節來講解調用約定。一般來說,參數都是從左往右入棧的,因此,b=4先壓入堆棧,a=3后壓入,如圖:

image

圖2

   返回地址入棧

    我們知道,當函數結束時,代碼要返回到上一層函數繼續執行,那么,函數如何知道該返回到哪個函數的什么位置執行呢?函數被調用時,會自動把下一條指令的地址壓入堆棧,函數結束時,從堆棧讀取這個地址,就可以跳轉到該指令執行了。如果當前"call foo"指令的地址是0x00171482,由于call指令占5個字節,那么下一個指令的地址為0x00171487,0x00171487將被壓入堆棧:

image

圖3

    代碼跳轉到被調用函數執行

    返回地址入棧后,代碼跳轉到被調用函數foo中執行。到目前為止,堆棧幀的前一部分,是由caller構建的;而在此之后,堆棧幀的其他部分是由callee來構建。

   EBP指針入棧

    在foo函數中,首先將EBP寄存器的值壓入堆棧。因為此時EBP寄存器的值還是用于main函數的,用來訪問main函數的參數和局部變量的,因此需要將它暫存在堆棧中,在foo函數退出時恢復。同時,給EBP賦于新值。

    1)將EBP壓入堆棧

    2)把ESP的值賦給EBP

image

圖4

    這樣一來,我們很容易發現當前EBP寄存器指向的堆棧地址就是EBP先前值的地址,你還會發現發現,EBP+4的地址就是函數返回值的地址,EBP+8就是函數的第一個參數的地址(第一個參數地址并不一定是EBP+8,后文中將講到)。因此,通過EBP很容易查找函數是被誰調用的或者訪問函數的參數(或局部變量)。

    為局部變量分配地址

    接著,foo函數將為局部變量分配地址。程序并不是將局部變量一個個壓入堆棧的,而是將ESP減去某個值,直接為所有的局部變量分配空間,比如在foo函數中有ESP=ESP-0x00E4,如圖所示:

image

圖5

     奇怪的是,在debug模式下,編譯器為局部變量分配的空間遠遠大于實際所需,而且局部變量之間的地址不是連續的(據我觀察,總是間隔8個字節)如下圖所示:

 image

圖6

    我還不知道編譯器為什么這么設計,或許是為了在堆棧中插入調試數據,不過這無礙我們今天的討論。

通用寄存器入棧

     最后,將函數中使用到的通用寄存器入棧,暫存起來,以便函數結束時恢復。在foo函數中用到的通用寄存器是EBX,ESI,EDI,將它們壓入堆棧,如圖所示:

image

圖7

   至此,一個完整的堆棧幀建立起來了。

 

 

document_thumb_thumb4堆棧特性分析

 

   上一節中,一個完整的堆棧幀已經建立起來,現在函數可以開始正式執行代碼了。本節我們對堆棧的特性進行分析,有助于了解函數與堆棧幀的依賴關系。

   1)一個完整的堆棧幀建立起來后,在函數執行的整個生命周期中,它的結構和大小都是保持不變的;不論函數在什么時候被誰調用,它對應的堆棧幀的結構也是一定的。

   2)在A函數中調用B函數,對應的,是在A函數對應的堆棧幀“下方”建立B函數的堆棧幀。例如在foo函數中調用foo1函數,foo1函數的堆棧幀將在foo函數的堆棧幀下方建立。如下圖所示:

image圖8 

  3)函數用EBP寄存器來訪問參數和局部變量。我們知道,參數的地址總是比EBP的值高,而局部變量的地址總是比EBP的值低。而在特定的堆棧幀中,每個參數或局部變量相對于EBP的地址偏移總是固定的。因此函數對參數和局部變量的的訪問是通過EBP加上某個偏移量來訪問的。比如,在foo函數中,EBP+8為第一個參數的地址,EBP-8為第一個局部變量的地址。

   4)如果仔細思考,我們很容易發現EBP寄存器還有一個非常重要的特性,請看下圖中:

image

圖9

   我們發現,EBP寄存器總是指向先前的EBP,而先前的EBP又指向先前的先前的EBP,這樣就在堆棧中形成了一個鏈表!這個特性有什么用呢,我們知道EBP+4地址存儲了函數的返回地址,通過該地址我們可以知道當前函數的上一級函數(通過在符號文件中查找距該函數返回地址最近的函數地址,該函數即當前函數的上一級函數),以此類推,我們就可以知道當前線程整個的函數調用順序。事實上,調試器正是這么做的,這也就是為什么調試時我們查看函數調用順序時總是說“查看堆棧”了。

document_thumb_thumb4返回值是如何傳遞的

    堆棧幀建立起后,函數的代碼真正地開始執行,它會操作堆棧中的參數,操作堆棧中的局部變量,甚至在堆(Heap)上創建對象,balabala….,終于函數完成了它的工作,有些函數需要將結果返回給它的上一層函數,這是怎么做的呢?

    首先,caller和callee在這個問題上要有一個“約定”,由于caller是不知道callee內部是如何執行的,因此caller需要從callee的函數聲明就可以知道應該從什么地方取得返回值。同樣的,callee不能隨便把返回值放在某個寄存器或者內存中而指望Caller能夠正確地獲得的,它應該根據函數的聲明,按照“約定”把返回值放在正確的”地方“。下面我們來講解這個“約定”:      1)首先,如果返回值等于4字節,函數將把返回值賦予EAX寄存器,通過EAX寄存器返回。例如返回值是字節、字、雙字、布爾型、指針等類型,都通過EAX寄存器返回。

    2)如果返回值等于8字節,函數將把返回值賦予EAX和EDX寄存器,通過EAX和EDX寄存器返回,EDX存儲高位4字節,EAX存儲低位4字節。例如返回值類型為__int64或者8字節的結構體通過EAX和EDX返回。

    3)  如果返回值為double或float型,函數將把返回值賦予浮點寄存器,通過浮點寄存器返回。

    4)如果返回值是一個大于8字節的數據,將如何傳遞返回值呢?這是一個比較麻煩的問題,我們將詳細講解:

        我們修改foo函數的定義如下并將它的代碼做適當的修改:

view sourceprint?
1MyStruct foo(int a, int b)
2{
3...
4}

 

         MyStruct定義為:

view sourceprint?
1struct MyStruct
2{
3    int value1;
4    __int64 value2;
5    bool value3;
6};

 

     這時,在調用foo函數時參數的入棧過程會有所不同,如下圖所示:

image

圖10

    caller會在壓入最左邊的參數后,再壓入一個指針,我們姑且叫它ReturnValuePointer,ReturnValuePointer指向當前ESP值下方很遠的一個地址,這個地址將用來存儲函數的返回值。函數返回時,把返回值拷貝到ReturnValuePointer指向的地址中,然后把ReturnValuePointer的地址賦予EAX寄存器。函數返回后,caller通過EAX寄存器找到ReturnValuePointer,然后通過ReturnValuePointer找到返回值。

    你或許會有這樣的疑問,函數返回后,對應的堆棧幀已經被銷毀,而ReturnValuePointer是在該堆棧幀中,不也應該被銷毀了嗎?對的,堆棧幀是被銷毀了,但是程序不會自動清理其中的值,因此ReturnValuePointer中的值還是有效的。

    但是,這里還有一個問題我沒有答案。ReturnValuePointer指向的地址是由caller決定的,而才caller并不知道callee對應的堆棧幀會有多大,如果callee對應的堆棧幀很大那么就可能會和返回值的地址重合。我還不知道VS編譯器通過什么策略來避免這個問題。

document_thumb_thumb4堆棧幀的銷毀

    當函數將返回值賦予某些寄存器或者拷貝到堆棧的某個地方后,函數開始清理堆棧幀,準備退出。堆棧幀的清理順序和堆棧建立的順序剛好相反:(堆棧幀的銷毀過程就不一一畫圖說明了)

   1)如果有對象存儲在堆棧幀中,對象的析構函數會被函數調用。

    2)從堆棧中彈出先前的通用寄存器的值,恢復通用寄存器。

    3)ESP加上某個值,回收局部變量的地址空間(加上的值和堆棧幀建立時分配給局部變量的地址大小相同)。

    4)從堆棧中彈出先前的EBP寄存器的值,恢復EBP寄存器。

    5)從堆棧中彈出函數的返回地址,準備跳轉到函數的返回地址處繼續執行。

    6)ESP加上某個值,回收所有的參數地址。

    前面1-5條都是由callee完成的。而第6條,參數地址的回收,是由caller或者callee完成是由函數使用的調用約定(calling convention )來決定的。下面的小節我們就來講解函數的調用約定。

document_thumb_thumb4函數的調用約定(calling convention)

    函數的調用約定(calling convention)指的是進入函數時,函數的參數是以什么順序壓入堆棧的,函數退出時,又是由誰(Caller還是Callee)來清理堆棧中的參數。有2個辦法可以指定函數使用的調用約定:

    1)在函數定義時加上修飾符來指定,如

view sourceprint?
1void __thiscall mymethod();
2{
3    ...
4}

 

    2)在VS工程設置中為工程中定義的所有的函數指定默認的調用約定:在工程的主菜單打開Project|Project Property|Configuration Properties|C/C++|Advanced|Calling Convention,選擇調用約定(注意:這種做法對類成員函數無效)。

    常用的調用約定有以下3種:

    1)__cdecl。這是VC編譯器默認的調用約定。其規則是:參數從右向左壓入堆棧,函數退出時由caller清理堆棧中的參數。這種調用約定的特點是支持可變數量的參數,比如printf方法。由于callee不知道caller到底將多少參數壓入堆棧,因此callee就沒有辦法自己清理堆棧,所以只有函數退出之后,由caller清理堆棧,因為caller總是知道自己傳入了多少參數。

    2)__stdcall。所有的Windows API都使用__stdcall。其規則是:參數從右向左壓入堆棧,函數退出時由callee自己清理堆棧中的參數。由于參數是由callee自己清理的,所以__stdcall不支持可變數量的參數。

    3) __thiscall。類成員函數默認使用的調用約定。其規則是:參數從右向左壓入堆棧,x86構架下this指針通過ECX寄存器傳遞,函數退出時由callee清理堆棧中的參數,x86構架下this指針通過ECX寄存器傳遞。同樣不支持可變數量的參數。如果顯式地把類成員函數聲明為使用__cdecl或者__stdcall,那么,將采用__cdecl或者__stdcall的規則來壓棧和出棧,而this指針將作為函數的第一個參數最后壓入堆棧,而不是使用ECX寄存器來傳遞了。

document_thumb_thumb4反編譯代碼的跟蹤(不熟悉匯編可跳過)

    以下代碼為和foo函數對應的堆棧幀建立相關的代碼的反編譯代碼,我將逐行給出注釋,可對照前文中對堆棧的描述:

    main函數中 int result=foo(3,4); 的反匯編:

view sourceprint?
1008A147E  push        4                     //b=4 壓入堆棧   
2008A1480  push        3                     //a=3 壓入堆棧,到達圖2的狀態
3008A1482  call        foo (8A10F5h)         //函數返回值入棧,轉入foo中執行,到達圖3的狀態 
4008A1487  add         esp,8                 //foo返回,由于采用__cdecl,由Caller清理參數
5008A148A  mov         dWord ptr [result],eax //返回值保存在EAX中,把EAX賦予result變量

 

    下面是foo函數代碼正式執行前和執行后的反匯編代碼

 

view sourceprint?
01008A13F0  push        ebp                  //把ebp壓入堆棧 
02008A13F1  mov         ebp,esp              //ebp指向先前的ebp,到達圖4的狀態
03008A13F3  sub         esp,0E4h             //為局部變量分配0E4字節的空間,到達圖5的狀態
04008A13F9  push        ebx                  //壓入EBX
05008A13FA  push        esi                  //壓入ESI
06008A13FB  push        edi                  //壓入EDI,到達圖7的狀態
07008A13FC  lea         edi,[ebp-0E4h]       //以下4行把局部變量區初始化為每個字節都等于cch
08008A1402  mov         ecx,39h 
09008A1407  mov         eax,0CCCCCCCCh 
10008A140C  rep stos    dword ptr es:[edi] 
11......                                      //省略代碼執行N行
12......
13008A1436  pop         edi                   //恢復EDI  
14008A1437  pop         esi                   //恢復ESI
15008A1438  pop         ebx                   //恢復EBX
16008A1439  add         esp,0E4h              //回收局部變量地址空間
17008A143F  cmp         ebp,esp               //以下3行為Runtime Checking,檢查ESP和EBP是否一致   
18008A1441  call        @ILT+330(__RTC_CheckEsp) (8A114Fh) 
19008A1446  mov         esp,ebp 
20008A1448  pop         ebp                   //恢復EBP 
21008A1449  ret                               //彈出函數返回地址,跳轉到函數返回地址執行                                            //(__cdecl調用約定,Callee未清

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表

圖片精選

亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
亚洲精品国产精品国产自| 国产精品色午夜在线观看| 国产精品网站视频| 韩日欧美一区二区| 久久综合伊人77777尤物| 日本一区二区三区四区视频| 精品人伦一区二区三区蜜桃网站| 第一福利永久视频精品| 韩剧1988免费观看全集| 欧美精品精品精品精品免费| 啊v视频在线一区二区三区| 精品久久久久久久久久久久久| 中文字幕在线观看日韩| 久久久国产精品一区| 亚洲а∨天堂久久精品喷水| 97精品国产97久久久久久| 国产成人精品免高潮在线观看| 中文综合在线观看| 欧美日韩亚洲国产一区| 国产亚洲欧洲高清| 日韩的一区二区| 亚洲女人初尝黑人巨大| 亚洲精品www久久久久久广东| 久久综合九色九九| 国产精品福利在线| 91精品国产一区| 性欧美暴力猛交69hd| 亚洲欧美中文日韩在线v日本| 亚洲精品视频二区| 久久人人爽人人爽爽久久| 欧美在线观看一区二区三区| 一个人看的www久久| 亚洲最大av网站| 久久91精品国产| 国产精品久久久久久久久粉嫩av| 日韩av一区在线观看| 国产午夜精品理论片a级探花| 国产综合视频在线观看| 欧美另类第一页| 欧美—级a级欧美特级ar全黄| 中文字幕视频一区二区在线有码| 美乳少妇欧美精品| 美女啪啪无遮挡免费久久网站| 狠狠综合久久av一区二区小说| 2020国产精品视频| 国产精品香蕉av| 久久久久久久久久久久久久久久久久av| 久久精品美女视频网站| 国产又爽又黄的激情精品视频| 久久久久久久91| 岛国av一区二区三区| 亚洲免费视频在线观看| 国产精品免费久久久久影院| 日韩精品免费在线视频| 国产999在线观看| 精品无人区太爽高潮在线播放| 日韩一区二区三区xxxx| 国产精品视频永久免费播放| 亚洲免费福利视频| 日韩电影视频免费| 福利精品视频在线| 久久精品国产免费观看| 亚洲国产高清福利视频| 欧美成人黑人xx视频免费观看| 国产成人精品久久二区二区| 日韩av资源在线播放| 国产精品网站视频| 亚洲精品动漫久久久久| 久久综合网hezyo| 亚洲一区亚洲二区| 欧美成人午夜激情在线| 欧美一区二区三区艳史| 亚洲免费影视第一页| 欧美在线播放视频| 欧美国产日韩中文字幕在线| 精品日本高清在线播放| 91豆花精品一区| 激情av一区二区| 91精品国产综合久久男男| 在线精品高清中文字幕| 亚洲国产又黄又爽女人高潮的| 亚洲美女av黄| 欧美成人午夜激情视频| 欧洲s码亚洲m码精品一区| 欧美日韩在线另类| 国产精品女人久久久久久| 日本久久久久久| 伊人久久久久久久久久| 91亚洲精品在线| 久久成人18免费网站| 97国产精品人人爽人人做| 超薄丝袜一区二区| 亚洲人午夜精品| 亚洲一区中文字幕在线观看| 日韩精品欧美国产精品忘忧草| 韩日欧美一区二区| 欧美极度另类性三渗透| 国产日韩欧美夫妻视频在线观看| 国产视频福利一区| 日韩欧美中文在线| 国产精品jvid在线观看蜜臀| 日韩精品极品在线观看| 日韩一区二区三区国产| 在线观看中文字幕亚洲| 国产午夜精品全部视频在线播放| 国产91在线播放| 日韩中文字幕在线视频播放| 国产婷婷成人久久av免费高清| 国产精欧美一区二区三区| 97涩涩爰在线观看亚洲| 成人精品视频99在线观看免费| 91精品国产91久久久久久久久| 色老头一区二区三区| 日韩av免费在线观看| 久久男人av资源网站| 亚洲一区二区三区在线免费观看| 成人a在线视频| 色婷婷av一区二区三区久久| 2019中文字幕在线免费观看| 亚洲尤物视频网| 亚洲日韩欧美视频一区| 亚洲电影av在线| 成人黄色在线观看| 国产成人91久久精品| 久久夜色精品国产欧美乱| 日韩av一区二区在线观看| 国产精品jvid在线观看蜜臀| 欧美视频在线观看免费网址| 国产欧美精品久久久| 亚洲aaa激情| 国产精品嫩草视频| 日韩中文在线不卡| 欧美极品美女电影一区| 中文在线资源观看视频网站免费不卡| 秋霞午夜一区二区| 亚洲区中文字幕| 色偷偷av一区二区三区乱| 日韩国产精品一区| 欧美美女18p| 亚洲最新av在线| 亚洲成人动漫在线播放| 全球成人中文在线| 日韩免费视频在线观看| 精品久久国产精品| 一本一本久久a久久精品牛牛影视| 亚洲国产精品人久久电影| 国产激情视频一区| 91视频九色网站| 国产成人福利视频| 九九热这里只有精品免费看| 91免费人成网站在线观看18| 久久男人资源视频| 亚洲成人激情视频| 国产精品视频一| 成人免费淫片视频软件| 免费99精品国产自在在线| 亚洲精品久久久久| 久久久av一区| 一区二区欧美日韩视频| 欧美自拍视频在线| 亚洲精品美女久久久| 51精品在线观看| 欧美日韩国产中文字幕| 国产日韩在线一区|