程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> 關於C語言 >> C/C++堆棧指引(轉),堆棧指引

C/C++堆棧指引(轉),堆棧指引

編輯:關於C語言

C/C++堆棧指引(轉),堆棧指引


C/C++堆棧指引

Binhua Liu

document_thumb_thumb前言

    我們經常會討論這樣的問題:什麼時候數據存儲在堆棧(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的系統中,堆棧在內存中是從高地址向低地址擴展(這和自定義的堆棧從低地址向高地址擴展不同),如下圖所示:

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

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int foo1(int m, int n) {     int p=m*n;     return p; } int foo(int a, int b) {     int c=a+1;     int d=b+1;     int e=foo1(c,d);     return e; }   int main() {     int result=foo(3,4);     return 0; }

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

document_thumb_thumb4堆棧的建立

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

圖1

    參數入棧 

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

圖2

   返回地址入棧

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

圖3

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

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

   EBP指針入棧

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

    1)將EBP壓入堆棧

    2)把ESP的值賦給EBP

圖4

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

    為局部變量分配地址

    接著,foo函數將為局部變量分配地址。程序並不是將局部變量一個個壓入堆棧的,而是將ESP減去某個值,直接為所有的局部變量分配空間,比如在foo函數中有ESP=ESP-0x00E4,(根據燭秋兄在其他編譯環境上的測試,也可能使用push命令分配地址,本質上並沒有差別,特此說明)如圖所示:

圖5

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

 圖6

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved