VC 中的內聯匯編(一):被調用函數的結構與堆棧

VC中的C函數調用有分 _cdecl 與 _stdcall 以及 _fastcall,

_fastcall 在此文中不討論


其實 不只 vc, gcc也是三種都支持, 但在linux中不支持stdcall


_cdecl  : 由調用者清除被調用函數的參數堆棧


要使用時的關鍵字這樣加:

int _ cdecl Fun(int a, int b);

( GCC是 int __attribute__((cdecl))Fun(int a, int b); )

先介紹 _cdecl, 一個很蠢的例子,C代碼是:

int _cdecl cdeclSub(volatile int a, volatile int b, volatile int*pOut)
{
 
  *pOut = a + b;
 
  return a +b; 
}

他的匯編代碼如下: (只寫出整個函數本身 提示組譯器/連結器的部份不寫)
當然寫法有很多種 這裡只列出其中一種, 在此32位元匯編代碼為例



            PUSH EBP;
 
          MOV EBP,ESP;           

 
          PUSH EBX;
 
          PUSH ECX;           
 
          MOV EAX,DWORD PTR [EBP + 8];

 
          MOV EBX,DWORD PTR [EBP + 12];
 
          SUB EAX,EBX;     
 
          MOV ECX,DWORD PTR [EBP + 16];
 
          MOV DWORDPTR [ECX], EAX;
 
         
 
          POP ECX;
 
          POP EBX;
 
            POP EBP;           
 
          RET ;

這裡說明如下
 
ESP : 棧基指針 傳入的參數,區域變量, 返回地址....都存放在這裡,

        PUSH XXX一下, ESP就會後退四字節,  然後XXX複製到ESP之上( [ESP_新] == XXX ,ESP_新 = ESP_舊 - 4);

 反之    POP XXX 就會把當前[ESP]的值傳入 XXX, 然後ESP值加四( XXX == [EBP_舊], EBP_新 = EBP_舊 +4)


EBP : 提供個EBP寄存器, 這樣可以把某個時間的ESP值存入EBP, 以方便寫代碼.

    因為直接操作ESP蠻麻煩的,推入拿出ESP值便會改來改去, 一下頭就暈了,


函數的返回值是靠EAX來傳遞, RET命令是跳轉到當前[ESP]值的地址(精確來講是EIP值)上, 既callXXXFun的下一行對應到的EIP值

注意這幾行

PUSH EBP;  
MOV EBP, ESP;

PUSH EBX;

PUSH ECX;
:
:
POP ECX;
POP EBX;
POP EBP;      
RET ;


這裡詳細說明如下:

初入函數時的[ESP_原]  為此函數之返回地址

PUSH EBP;    
                            => 這時 ESP  = ESP_原 - 4 且[ESP] = EBP_原

MOV EBP, ESP;

                            =>  這時 [EBP] = EBP原 = [ESP]= [ESP_原 -4] 


//這例子中沒有
SUB ESP, 區域變量總大小; 

 
              => 這時 ESP = ESP_原 - 4 - 區域變量總大小,這樣做的目地是若等等還要再操作ESP值時(如調用函數), 不會洗到區域變量值


(PUSH EXX)*n;
 
              => 把等下會用到通用寄存器之目前值都存起來 這時 ESP = ESP_原 - 4 - 區域變量總大小- n*4
                            這例子中 等等會用到 ECX, EBX, 所以把它門的當前值推入ESP棧裡
                            EAX雖然也有用到 但EAX是用來傳送函數返回值的, 所以不用存入棧裡了

:

:
:

(POP EXX)*n;
                            => 恢復通用寄存器的值, 這時 ESP = ESP_原 - 4 - 區域變量總大小
                          這例子要恢復EBX, ECX

//這例子中沒有
ADD ESP, 區域變量總大小; 

 
                        => 這時 ESP = ESP_原 - 4, 所以這行也可以用 MOV, ESP,EBP;代替


POP EBP;
         
=> 
[ESP_原 - 4] 的值是EBP_原,所這動作後, ESP = ESP_原, 然後EBP值也恢復了

RET;
                          => 依[ESP_原]的值, 返回調用此函數之所在



以上結構清處了 接下來講解傳入的參數值在哪裡


PUSH EBP; 


MOV EBP, ESP;

這時的 [EBP] = EBP原 = [ESP] = [ESP_原 - 4]

而[ESP_原]是要返回的EIP值

依調用慣例(也很自然可以推論),  [ESP_原 + 4] 是 a(第一個引數),  [ESP_原 +8]是 b(第二個引數)...以此類推

所以 [EBP + 8] 的值是a ( 注意[EBP + 8]是佔用了 ESP + 8 ~ ESP + 11格的位置) , [EBP +12]是b

這樣就看的懂下面這幾行了 


        MOV EAX, DWORD PTR [EBP + 8]; // EAX = a

            MOV EBX,DWORD PTR [EBP + 12]; // EBX = b

            SUB EAX,EBX;      // EAX -= EBX

            MOV ECX,DWORD PTR [EBP + 16]; //ECX = pOut

            MOV DWORDPTR [ECX], EAX; //[ECX] =EAX

之後代碼就沒在動到 EAX值了 而EAX值確實是計算結果 所以就直接RET


而若要用VC內聯匯編  (強烈建議不要與調用此函數的C/C++代碼放在同一個檔案裡, 不然編譯器會優化掉造成運行時或結果出問題)

前面的
PUSH EBP;
MOV EBP, ESP; 
         
以及
POP EBP;           
RET

編譯器會幫忙做掉

所以不需要也 不可以添加這些代碼

當然也可以要求編譯器不要自動添加, 使用 __declspec( naked ) 關鍵字於函數之前, 就可要求編譯器不要畫蛇添足 之後文章會更詳細解釋

這時內聯匯編如下

int _cdecl cdeclSub(volatile int a, volatile int b, volatile int*pOut)
{
 
  // *pOut= a + b;
   // return a +b;

      __asm
      {     
          // PUSHEBP;
      //MOV EBP,ESP;      


            PUSH EBX;
            PUSH ECX;           
            MOV EAX, DWORD PTR [EBP + 8];

            MOV EBX, DWORD PTR [EBP + 12];
            SUB EAX,EBX;     
            MOV ECX,DWORD PTR [EBP + 16];
            MOV DWORDPTR [ECX], EAX;
           
            POP ECX;
            POP EBX;
                                   
       //POP EBP;      
       //RET;
               
      }   
}

以上是 cdecl



stdcall : 被調用的函數, 負責清除之參數之堆暫空間

個人在此會覺的_stdcall有點怪怪的, 傳入的參數的暫存區是由被調用者清除? 明明就是調用者把參數壓棧的啊...這樣那邊的代碼看起來不就不對稱了?

   很詭異, 不過就是這樣規定

所以同函數的匯編結果如下:

       PUSH EBP;
 
          MOV  EBP, ESP;           

 
          PUSH EBX;
 
          PUSH ECX;           
 
          MOV EAX,DWORD PTR [EBP + 8];

 
          MOV EBX,DWORD PTR [EBP + 12];
 
          SUB EAX,EBX;     
 
          MOV ECX, DWORD PTR [EBP + 16];
 
          MOV DWORDPTR [ECX], EAX;
 
         
 
          POP ECX;
 
          POP EBX;
 
                     
 
          POP EBP;           

 
          RET 12;


只有紅色不一樣

  RET 12 是這樣的 :

      將當前[ESP- 12 ]值當代碼地址解釋, 轉跳到該處, ,再將ESP的值減掉該數字

也就是 RET 12 等於 RET; 再SUB ESP, 12;這兩行指令


12 這值是來自於 3*4, 三個參數(既 a, b , c), 一個佔用四字節, 現在把他們全清掉, 也不用再POP XXX了



 若沒有 __declspec( naked ) 關鍵字 編譯器會自動添加:

  PUSH EBP;
  MOV EBP, ESP;         
 
  POP EBP;           
  RET12;

所以以上代碼, 不能添加於內聯匯編中


另外, 可能會覺的很怪, 直接把ESP後退12格,那不是把那三個值放棄不要? 那上面的值怎辦?


是這樣的: C是以傳值/傳址做調用, 不是傳參考, 所以用來裝填函數參數的空間本來就是一次性(免洗)的


棧上的值是什麼, 在離開函數後 已完全不重要


當然 若要搞出個自家的傳參考, 也是可以, 只是這樣不符合一般性調用慣例






你可能感兴趣的:(VC 中的內聯匯編(一):被調用函數的結構與堆棧)