__declspec(dllimport) 對【函數調用】編譯結果的影響

環境:vs2005 + xpsp3
作者:magictong
時間:2010-09-08
 
注:例子演示裏面都是以debug模式下的彙編來講,在release下因爲經過一些優化,過程會有一些區別,但是最終的結論是一樣的。
 
        __declspec本身就是microsoft對c++的擴展,因此後面的討論都是指在VS2005下編譯的結果,與__declspec(dllimport)相對的一個組合用法是__declspec(dllexport),__declspec(dllexport)是作用於PE文件導出函數、類、變量等等(如果你不用def文件導出函數,就必須使用__declspec(dllexport)來進行導出)。__declspec(dllimport)的具體作用在msdn上面講得比較模糊,如下Importing into an Application Using __declspec(dllimport)
        英文:
        Using __declspec(dllimport) is optional on function declarations, but the compiler produces more efficient code if you use this keyword. However, you must use __declspec(dllimport) for the importing executable to access the DLL's public data symbols and objects. Note that the users of your DLL still need to link with an import library.
        中文:
        在函數聲明上使用 __declspec(dllimport) 是可選操作,但如果使用此關鍵字,編譯器將生成更有效的代碼。但是,爲使導入的可執行文件能夠訪問 DLL 的公共數據符號和對象,必須使用 __declspec(dllimport)。請注意,DLL 的用戶仍然需要與導入庫鏈接。
所謂可選操作,就是可要可不要,但是如果使用了,編譯器會生成更高效的代碼。嗯,編譯器就是這麼一個意思。後來我測試了四種情況,來看看到底是什麼情況。
 
        情況一:EXE內部調用自己的swap函數,swap函數沒有使用__declspec(dllimport)修飾
  1. int swap(int& a, int& b);  
  2. int swap(int& a, int& b)  
  3. {   
  4.          a = a ^ b;  
  5.          b = a ^ b;  
  6.          a = a ^ b;  
  7.          return 0;  
  8. }   
int swap(int& a, int& b);
int swap(int& a, int& b)
{ 
         a = a ^ b;
         b = a ^ b;
         a = a ^ b;
         return 0;
} 

        對swap的調用生成的彙編代碼爲:
         swap(m, n);
00411423  lea         eax,[n]
00411426  push        eax 
00411427  lea         ecx,[m]
0041142A  push        ecx 
0041142B  call        swap (411113h)
00411430  add         esp,8
        看一下411113h 這個地址是一條jmp指令,跳轉到swap的真正地址:
0x00411113  e9 98 04 00 00
00411113  jmp         swap (4115B0h)
 
int swap(int& a, int& b)
{
004115B0  push        ebp 
004115B1  mov         ebp,esp
004115B3  sub         esp,0C0h
        因此,這種情況僅僅是正常的的函數調用。
 
        情況二:EXE內部調用自己的swap函數,swap函數使用__declspec(dllimport)修飾
        這種情況跟情況一生成的代碼基本一樣,唯一有點點區別是在編譯的時候在swap的實現處會出現下面的警告(大概意思就是聲明有點不一致,與編譯器的預期有點不一樣,因爲編譯器預期會是一個dll的導出函數,嗯,大概是這麼個情況,不用深究):
        warning C4273: 'swap' : inconsistent dll linkage
 
        情況三:EXE內部調用dll的add函數,add函數不使用__declspec(dllimport)修飾
  1. #if IMPORTDLLDLL_EXPORTS  
  2. #define API_DECLSPEC  __declspec(dllexport)  
  3. #else   
  4. #define API_DECLSPEC  
  5. #endif   
  6. // -------------------------------------------------------------------------  
  7.     
  8. API_DECLSPEC int __stdcall add(int a, int b);  
  9.     
  10. int __stdcall add(int a, int b)  
  11. {   
  12.          return a + b;  
  13. }  
#if IMPORTDLLDLL_EXPORTS
#define API_DECLSPEC  __declspec(dllexport)
#else 
#define API_DECLSPEC
#endif 
// -------------------------------------------------------------------------
  
API_DECLSPEC int __stdcall add(int a, int b);
  
int __stdcall add(int a, int b)
{ 
         return a + b;
}

        對add的調用生成的彙編代碼爲:
         add(m, n);
004113FC  mov         eax,dword ptr [n]
004113FF  push        eax 
00411400  mov         ecx,dword ptr [m]
00411403  push        ecx 
00411404  call        add (411127h)
        看下411127h這個地址也是一條jmp指令,跳轉到411620h
0x00411127  e9 f4 04 00 00
00411127  jmp         add (411620h)
        跳轉到411620h之後發現居然還不是add的地址,而是
00411620  jmp         dword ptr [__imp_add (4181E0h)]
        依然是一個jmp指令,跳轉到4181E0h指向的一個位置,看下內存,應該是跳轉到0x100110f0去執行: 0x004181E0  f0 10 01 10
        跟過去,居然還是一條跳轉指令,這次是跳轉到0x10011340:
100110F0  jmp         add (10011340h)
        再跟過去,這次終於對了:
int __stdcall add(int a, int b)
{
10011340  push        ebp 
10011341  mov         ebp,esp
10011343  sub         esp,0C0h
10011349  push        ebx 
        這次很糾結,中間多了幾個jmp指令,這個我們先不講,我們先看看情況四之後再討論。
 
        情況四:EXE內部調用dll的add函數,add函數使用__declspec(dllimport)修飾
  1. #if IMPORTDLLDLL_EXPORTS  
  2. #define API_DECLSPEC  __declspec(dllexport)  
  3. #else   
  4. #define API_DECLSPEC  __declspec(dllimport)  
  5. #endif   
  6. // -------------------------------------------------------------------------  
  7.     
  8. API_DECLSPEC int __stdcall add(int a, int b);  
  9.     
  10. int __stdcall add(int a, int b)  
  11. {   
  12.          return a + b;  
  13. }  
#if IMPORTDLLDLL_EXPORTS
#define API_DECLSPEC  __declspec(dllexport)
#else 
#define API_DECLSPEC  __declspec(dllimport)
#endif 
// -------------------------------------------------------------------------
  
API_DECLSPEC int __stdcall add(int a, int b);
  
int __stdcall add(int a, int b)
{ 
         return a + b;
}

        對add的調用生成的彙編代碼爲:
         add(m, n);
004113FC  mov         esi,esp
004113FE  mov         eax,dword ptr [n]
00411401  push        eax 
00411402  mov         ecx,dword ptr [m]
00411405  push        ecx 
00411406  call        dword ptr [__imp_add (4181E0h)]
        這次情況好像有點不一樣,直接call了4181E0h指向的一個位置,看下內存:
0x004181E0  f0 10 01 10
        也就是說跳轉到0x100110f0去執行,跟過去:
100110F0  jmp         add (10011340h)
        直接一個jmp指令到10011340h,再跟過去就是add的真正地址了:
int __stdcall add(int a, int b)
{
10011340  push        ebp 
10011341  mov         ebp,esp
10011343  sub         esp,0C0h
        這種情況下,對比情況三,情況四少了2條jmp指令……
 
        結論:
        看來msdn說的沒錯o(∩_∩)o ,確實使用了__declspec(dllimport)之後,生成的代碼更加高效。在對release代碼的查看中發現,最終會少一條jmp指令。
        其實整個來講還是比較好理解的,對於編譯器來講,你沒有__declspec(dllimport)這個來修飾函數聲明,它開始就會把函數當成一個本地函數來生成調用代碼(如:call add (411127h)),但是最後鏈接的時候發現這個函數是個動態鏈接庫裏面的函數,真正地址會存在輸入表裏面,因此中間就多了至少一條jmp指令來中轉。如果使用了__declspec(dllimport)來修飾,那麼編譯器知道,哦,是個外部的函數,函數的地址在輸入表裏面,雖然現在不知道函數真正地址,但是裝載之後該函數地址在輸入表中的位置是固定的,因此編譯器就直接生成調用輸入表中某個地方的函數地址的代碼了(如:call  dword ptr [__imp_add (4181E0h)])。
 
        【END】
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章