反彙編系列(二)——堆棧篇

    要反彙編程序,不可避免要接觸到堆棧,你首先得會查看堆棧,知道堆棧在某一時刻的確切內容。首先,我們講述一些與堆棧相關的基礎知識。


1、堆棧基礎

    彙編語言中的“堆棧”的含義與數據結構中堆棧的含義不同,儘管從操作上來說,它們都是“後進先出”,這個不用贅述。彙編中有一個寄存器esp指向當前棧頂,而棧底的位置是不變的,整個程序運行過程中,通過操作esp來操作堆棧,進行堆棧的壓入、彈出及平衡操作。

    

    一些堆棧操作術語:

  • 壓入:將一個變量壓入到堆棧;
  • 彈出:將一個變量從堆棧中彈出;
  • 平衡堆棧:當函數調用完成後,進行局部變量的釋放、返回值的正確處理等;
    這些操作都是針對 esp操作完成的,所以單次棧的操作可以認爲基本沒有額外開銷。

2、堆棧類型

    根據架構的不同,有兩種基本的堆棧類型:向上生長和向下生長的堆棧。

  • 向上生長堆棧:堆棧向高地址增長,當向棧中壓入元素時,esp增加。棧頂地址 >= 棧底地址。
  • 向下生長堆棧:堆棧向低地址增長,當向棧中壓入元素時,esp減小。棧頂地址 <= 棧底地址。
Windows平臺下堆棧向下生長,所以我們後續的章節中默認堆棧向下生長。一個典型的向下生長的堆棧圖如下:


3、函數調用
堆棧主要用於函數調用,我們知道,一個函數調用時的例程如下:
  1. 將函數參數入棧;
  2. 將返回處的代碼地址入棧(段內調用一般將eip入棧,段間調用將 cs、eip 依次入棧);
  3. jmp到被調函數代碼地址處開始執行;
  4. 被調函數分配局部變量內存;
  5. 執行被調函數代碼,存好返回值;
  6. 平衡堆棧。
C/C++ 標準中,參數的入棧順序是沒有規定的,也就是說具體入棧順序依賴於具體實現。如:
int f1()
{
    cout<<"In f1()"<<endl;
    return 1;
}

int f2()
{
    cout<<"In f2()"<<endl;
    return 2;
}

int foo(int a, int b)
{
    // ...
}

foo(f1(), f2());
    像這段代碼,我們不知道先執行f1還是先執行f2,因爲不同的編譯器編譯出的結果可能不同。用Microsoft Visual C++ 編譯出的代碼,先執行f2,後執行f1,也就是說,MSVC編出的代碼,參數從右往左依次入棧。

4、調用約定
談到函數調用,就不可避免的談到函數調用約定。主流的調用約定有:PASCAL__cdecl__stdcall等:
  • PASCAL:參數從左往右入棧,被調用者平衡堆棧;
  • __cdecl:即 C 調用約定,參數從右往左入棧,調用者平衡堆棧;
  • __stdcall:即標準調用約定,參數從右往左入棧,被調者平衡堆棧;
由於__cdecl約定由調用者平衡堆棧,所以生成的最終二進制文件較大,而且不同的編譯器編出的Dll協同工作差(思考爲什麼)
但是,在某些函數如printfsprintf等變長參數的函數中,由於被調者(printf) 不知道傳遞進來的參數有幾個,只有調用者知道,所以堆棧平衡操作交由調用者完成,所以必須使用__cdecl調用約定。

5、結語
我們詳述了堆棧相關的基本知識,在後續的篇幅中,這些知識將非常關鍵。下一章將結合一個實際實例介紹IDA Pro逆向出的代碼,從而對反彙編有一個基本的瞭解。





發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章