深入體會__cdecl與__stdcall


在學習C++的過程中時常碰到WINAPI或者CALLBACK這樣的調用約定,每每覺得十分迷惑。究竟這些東西有什么用?不用他們又會不會有問題?經過在網上的一番搜尋以及自己動手后,整理成以下的學習筆記。
1.WINAPI與CALLBACK

    其實這兩者在Windows下是相同的,在windef.h中定義如下:


#ifdef _MAC
#define CALLBACK    PASCAL
#define WINAPI      CDECL
#define WINAPIV     CDECL
#define APIENTRY    WINAPI
#define APIPRIVATE  CDECL
#ifdef _68K_
#define PASCAL      __pascal
#else
#define PASCAL
#endif
#elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)
#define CALLBACK    __stdcall
#define WINAPI      __stdcall
#define WINAPIV     __cdecl
#define APIENTRY    WINAPI
#define APIPRIVATE  __stdcall
#define PASCAL      __stdcall
#else
#define CALLBACK
#define WINAPI
#define WINAPIV
#define APIENTRY    WINAPI
#define APIPRIVATE
#define PASCAL      pascal
#endif


    這里根據不同的系統版本選擇不同的定義,Windows的話對應#elif (_MSC_VER >= 800) || defined(_STDCALL_SUPPORTED)后面的那一段,可以看得出都為__stdcall。至於為什么Windows要對應__stdcall可能與系統本身的內存處理方式有關。現在問題就在於__stdcall是什么?
2.__stdcall與__cdecl
    在網上找到的資料如下:
    1)采用__cdecl約定時,函數參數按照從右到左的順序入棧,並且由調用函數者把參數彈出棧以清理堆棧。因此,實現可變參數的函數只能使用該調用約定。由於每一個使用__cdecl約定的函數都要包含清理堆棧的代碼,所以產生的可執行文件大小會比較大。
      2)采用__stdcall約定時,函數參數按照從右到左的順序入棧,被調用的函數在返回前清理傳送參數的棧,函數參數個數固定。由於函數體本身知道傳進來的參數個數,因此被調用的函數可以在返回前用一條ret n指令直接清理傳遞參數的堆棧。
   在看完這些描述后,有些問題必須解決的,首先什么是可變參數的函數,還有為什么__stdcall就不能調用可變參數的函數。
3.匯編詳解__stdcall與__cdecl
    1)准備  VS2008下查看程序反匯編代碼的方法
    首先創建一個可運行的工程,接着在程序中設置一個斷點,debug一下程序。在代碼區域的任意地方右鍵-->"轉到反匯編"即可看到程序的反匯編代碼。
    2)VS2008一些默認的設置
    在VS2008下,全局函數的約定是__cdecl,類的成員函數的約定是__stdcall。還有一般的Win32 API函數都是__stdcall。一般來說可以用__stdcall的就不會去使用__cdecl,這樣有更好的封裝性,因為入棧和清空棧的代碼在同一塊地方。至於為什么全局函數會是__cdecl,我還沒想出來。
    3)__cdecl約定的反匯編分析


 1 --- stdcalltest.cpp -------------------
 2 // StdcallTest.cpp : 定義控制台應用程序的入口點。
 3 //
 4 
 5 #include "stdafx.h"
 6 
 7 int Add(int a, int b)
 8 {
 9 004113A0  push        ebp  
10 004113A1  mov         ebp,esp 
11 004113A3  sub         esp,0C0h 
12 004113A9  push        ebx  
13 004113AA  push        esi  
14 004113AB  push        edi  
15 004113AC  lea         edi,[ebp-0C0h] 
16 004113B2  mov         ecx,30h 
17 004113B7  mov         eax,0CCCCCCCCh 
18 004113BC  rep stos    dword ptr es:[edi] 
19    return a + b;
20 004113BE  mov         eax,dword ptr [a] 
21 004113C1  add         eax,dword ptr [b] 
22 }
23 004113C4  pop         edi  
24 004113C5  pop         esi  
25 004113C6  pop         ebx  
26 004113C7  mov         esp,ebp 
27 004113C9  pop         ebp  
28 004113CA  ret              
29  
30 --- stdcalltest.cpp -------------------
31 
32 int _tmain(int argc, _TCHAR* argv[])
33 {
34 004113E0  push        ebp  
35 004113E1  mov         ebp,esp 
36 004113E3  sub         esp,0CCh 
37 004113E9  push        ebx  
38 004113EA  push        esi  
39 004113EB  push        edi  
40 004113EC  lea         edi,[ebp-0CCh] 
41 004113F2  mov         ecx,33h 
42 004113F7  mov         eax,0CCCCCCCCh 
43 004113FC  rep stos    dword ptr es:[edi] 
44     int sum = Add(1, 2);
45 004113FE  push        2    
46 00411400  push        1    
47 00411402  call        Add (41108Ch) 
48 00411407  add         esp,8 
49 0041140A  mov         dword ptr [sum],eax 
50     return 0;
51 0041140D  xor         eax,eax 
52 }
53 0041140F  pop         edi  
54 00411410  pop         esi  
55 00411411  pop         ebx  
56 00411412  add         esp,0CCh 
57 00411418  cmp         ebp,esp 
58 0041141A  call        @ILT+325(__RTC_CheckEsp) (41114Ah) 
59 0041141F  mov         esp,ebp 
60 00411421  pop         ebp  
61 00411422  ret              


    這里實現的是int Add(int a, int b),然后調用Add(1, 2)。從44行開始看,先push 2,再push 1,可見入棧是從右到左。接着call 41108Ch的內存,但在這段代碼中是找不到這個內存的。在call處按F11,會看到如下代碼:Add: 0041108C  jmp         Add (4113A0h) ,這里的4113A0h在上面代碼就可以找到在第9行了。至於為什么要這樣子?你可以認為VS在編譯的時候把函數的地址放在一段內存中比較好管理。而事實上函數的地址也確實是這樣子。為了更清楚,我把這段代碼也展示一下。


 1 0041100F  jmp         wmain (4113E0h) 
 2 00411014  jmp         _RTC_GetErrDesc (411BE0h) 
 3 00411019  jmp         __p__fmode (412796h) 
 4 0041101E  jmp         __security_check_cookie (4132F0h) 
 5 00411023  jmp         IsDebuggerPresent (4134AAh) 
 6 00411028  jmp         _RTC_Terminate (412760h) 
 7 0041102D  jmp         WideCharToMultiByte (4134BCh) 
 8 00411032  jmp         _RTC_AllocaHelper (411550h) 
 9 00411037  jmp         _RTC_GetErrorFuncW (411CA0h) 
10 0041103C  jmp         _RTC_NumErrors (411BD0h) 
11 00411041  jmp         __setusermatherr (412706h) 
12 00411046  jmp         Sleep (41349Eh) 
13 0041104B  jmp         GetModuleFileNameW (413510h) 
14 00411050  jmp         __security_init_cookie (412930h) 
15 00411055  jmp         SetUnhandledExceptionFilter (4134DAh) 
16 0041105A  jmp         _cexit (412A4Eh) 
17 0041105F  jmp         _CrtDbgReportW (412CD6h) 
18 00411064  jmp         VirtualQuery (413516h) 
19 00411069  jmp         atexit (4128F0h) 
20 0041106E  jmp         MultiByteToWideChar (4134C2h) 
21 00411073  jmp         _RTC_SetErrorType (411C00h) 
22 00411078  jmp         wmainCRTStartup (4117E0h) 
23 0041107D  jmp         _except_handler4 (412CF0h) 
24 00411082  jmp         _lock (413320h) 
25 00411087  jmp         GetProcAddress (4134CEh) 
26 0041108C  jmp         Add (4113A0h) 
27 00411091  jmp         _RTC_CheckStackVars (4114D0h) 
28 00411096  jmp         __report_gsfailure (413340h) 
29 0041109B  jmp         terminate (413302h) 
30 004110A0  jmp         _exit (412A42h) 


    你會發現,在不知不覺中,編譯器已經給你生成了這么多的函數,而Add僅僅在第26行出現過。看得出編譯器對編程有多重要了。閑話少說,回到代碼。在第二段代碼展示的28行處的指令是ret,這里沒有執行清棧。而在48行是add esp,8,這個操作將棧的指針修正到有參數入棧之前,這里的8就剛好是兩個int的大小。
    4)__stdcall約定的反匯編分析
    這里只做一點點改動,將Add的定義改為int __stdcall Add(int a, int b)。在反匯編代碼中不同的地方只有兩處,一是add esp,8這條語句沒有了,二是ret變為了ret 8。可見,清棧的工作變到了在函數里面。
    5)何為可變參數的函數
    我覺得必須先知道的是它的形式:type funcname(type para1, type para2, ...)。這里的"..."不是省略的意思,而是可變參數的函數必須這樣聲名。具體說明可參照http://hi.baidu.com/sunlit88/blog/item/272460da3f360f61d1164ea7.html。下面是我改了一些地方的代碼。


 1 // StdcallTest.cpp : 定義控制台應用程序的入口點。
 2 //
 3 
 4 #include "stdafx.h"
 5 #include <stdarg.h>
 6 
 7 int Add(int a, )
 8 {
 9     int count = 0, sum = 0, i = a;
10     va_list marker;
11     va_start(marker, a); //初始化
12     while(i != -1)
13     {
14         sum += i; //先加第一個參數
15         ++count;
16         i = va_arg(marker, int);//取下一個參數
17     }
18     va_end(marker);
19     return sum;
20 }
21 
22 int _tmain(int argc, _TCHAR* argv[])
23 {
24     int sum = Add(1, 2, 3, -1);
25     return 0;
26 }


    查看反匯編代碼也可以看出清棧的操作是在Add(1, 2, 3, -1)后執行的。本來我想試試寫成int __stdcall Add(int a, ...)會有什么后果的,誰知道VS在編譯的時候硬是把__stdcall方式改成__cdecl方式,看來編譯器也不笨啊,知道這種方式肯定會出問題,就把你的改過來了。不過這也是一個好的編譯器所需要做的事情,有時候你會發現自己寫的代碼與實際運行會有點點差別,那可能就是編譯器把自己覺得需要優化的東西優化后的結果。這時候我又想起了volatile這個關鍵字,它就是讓編譯器不要去優化的時候使用的。
    6)可變參函數為什么不能用__stdcall
    我覺得這個問題應該從編譯時和運行時來說,因為函數的代碼是在編譯的時候就已經在內存中寫好的,而當程序在編譯的時候,可變參不能告知代碼的ret n的n是多少。而add esp,n是在運行時執行的,所以知道n是多少。
4.寫在后面
    相對__cdecl和__stdcall還有很多約定,這里就不細說了。以前學匯編沒細學,現在才發現只有從最最低層的代碼才能看到程序的原貌。C++那層有時候還看不出問題,中間還有個編譯器在搞鬼。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM