MinGW gcc 生成動態鏈接庫 dll 的一些問題匯總(由淺入深,很詳細)


網絡上關於用 MinGW gcc 生成動態鏈接庫的文章很多。介紹的方法也都略有不同。這次我在一個項目上剛好需要用到,所以就花了點時間將網上介紹的各種方法都實驗了一遍。另外,還根據自己的理解試驗了些網上沒有提到的方法。這里,我就將這兩天獲得的成果總結一下。

 

首先說一下我的開發環境:

gcc version 4.9.2 (Rev1, Built by MSYS2 project)

Target: i686-w64-mingw32

Thread model: posix

--disable-sjlj-exceptions  --with-dwarf2

 

另外,為了試驗生成的 dll 是否通用。測試代碼時還用到了 Visual Stdio 2010。

在試驗一種新的功能時,我一般會從最簡單的代碼開始。

 

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //dlltest.c  
  2. int Double(int x)  
  3. {  
  4.     return x * 2;  
  5. }  

 

 

下面的命令行將這個代碼編譯成 dll。

gcc dlltest.c -shared -o dlltest.dll -Wl,--out-implib,dlltest.lib

其中 -shared 告訴gcc dlltest.c 文件需要編譯成動態鏈接庫。-Wl 表示后面的內容是ld 的參數,需要傳遞給 ld。 --out-implib,dlltest.lib 表示讓ld 生成一個名為 dlltest.lib 的導入庫。

如果還需要 .def 文件,則上面的命令行可以寫為:

gcc dlltest.c -shared -o dlltest.dll -Wl,--output-def,dlltest.def,--out-implib,dlltest.a

 

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //main.c  
  2. #include <stdio.h>  
  3. int Double(int x);  
  4. int main(void)  
  5. {  
  6.         printf("Hello :%d\n", Double(333));  
  7.         return 0;  
  8. }  

 

 

gcc main.c dlltest.lib -o main.exe

 

運行結果為:

Hello :666

說明生成的dlltest.dll是正確的。另外,也可以用dependecy walker 查看相互調用的關系。

 

實際上,如果我們的dll文件只是被MinGW gcc使用。都不需要生成 dlltest.lib。直接在編譯的時候將 dlltest.dll 加進去就行了。

gcc main.c dlltest.dll -o main.exe

如果在程序中動態加載dll。那么代碼可以這么寫:

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //m2.c  
  2. define UNICODE 1  
  3.   
  4. #include <windows.h>  
  5. #include <stdio.h>  
  6.   
  7. typedef int (*INT_FUNC)(int);  
  8. int main(void)  
  9. {  
  10.     INT_FUNC db;  
  11.     HINSTANCE hInstLibrary = LoadLibrary(L"dlltest.dll");  
  12.     printf("LoadLibrary\n");  
  13.     db = (INT_FUNC)GetProcAddress(hInstLibrary, "Double");  
  14.       
  15.     printf("Hello :%d\n", db(333));  
  16.     FreeLibrary(hInstLibrary);   
  17.       
  18.     return 0;  
  19. }  

編譯的時候更不需要dlltest.lib 了,甚至都不需要 dlltest,dll。

gcc m2.c -o m2.exe

運行的結果也是正確的。

那么這個dll 可以被其他c編譯器使用嗎?利用VC 2010來測試表明,可以生成exe文件。如果是生成Debug模式的exe文件,執行是正常的。但是改為release模式后,每次運行都會報錯。


 

用VS2010 的調試功能,看了看反匯編的結果。看似都是正常的。

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     int a;  
  4.     a = Double(333);  
  5. 01021000  push        14Dh    
  6. 01021005  call        _Double (1021024h)    
  7.     printf("Hello :%d\n", a);  
  8. 0102100A  push        eax    
  9. 0102100B  push        offset string "Hello :%d\n" (10220F4h)    
  10. 01021010  call        dword ptr [__imp__printf (10220A0h)]    
  11. 01021016  add         esp,0Ch    
  12.     getchar();  
  13. 01021019  call        dword ptr [__imp__getchar (102209Ch)]    
  14.     return 0;  
  15. 0102101F  xor         eax,eax    
  16. }  

 

單步跟進_Double 函數后是這樣的:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. _Double:  
  2. 01021024  jmp         dword ptr ds:[1020000h]    
  3. 0102102A  nop    
  4. 0102102B  nop    

Jmp 語句后:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. 00905A4D  ???    
  2. 00905A4E  ???    
  3. 00905A4F  ???    
  4. 00905A50  ???    
  5. 00905A51  ???    
  6. 00905A52  ???    
  7. 00905A53  ???    

可是在Debug 模式下:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. _Double:  
  2. 011B144C  jmp         dword ptr [__imp__Double (11B8340h)]    
  3. 011B1452  nop    
  4. 011B1453  nop    
  5. 011B1454  int         3    
  6. 011B1455  int         3    

Jmp 語句后:

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. 6C101560  push        ebp    
  2. 6C101561  mov         ebp,esp    
  3. 6C101563  mov         eax,dword ptr [ebp+8]    
  4. 6C101566  add         eax,eax    
  5. 6C101568  pop         ebp    
  6. 6C101569  ret    

而從下圖可以看出,dlltest.dll 被加載到 6C100000 是正確的。

 

沒有想明白為什么會這樣,看來還需要努力,到目前為止只成功了一小步。不過,如果是動態調用dll,卻沒有問題。

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. #include <windows.h>  
  2.   
  3. typedef int (*INT_FUNC)(int);  
  4. int _tmain(int argc, _TCHAR* argv[])  
  5. {  
  6.     INT_FUNC db;  
  7.     HINSTANCE hInstLibrary = LoadLibrary(L"dlltest.dll");  
  8.     printf("LoadLibrary ");  
  9.     db = (INT_FUNC)GetProcAddress(hInstLibrary, "Double");  
  10.       
  11.     printf("Hello :%d\n", db(333));  
  12.     FreeLibrary(hInstLibrary);   
  13.     getchar();  
  14.     return 0;  
  15. }  

 

這個代碼用 VC2010 編譯執行一點問題都沒有。很是奇怪。在網上查找了一番,發現可能是 MinGW gcc 生成的 lib 文件與 VC 生成的lib 文件有些細微的差別,導致在VC環境下,Debug模式下工作正常,而Release 模式工作卻不正常。為了驗證這個結論,又找了些資料學會了如何從dll文件生成VC下可用的lib文件。

下面的方法參考了這篇博客:

http://blog.sina.com.cn/s/blog_4f183d960100gqfj.html

生成VC下可用的lib文件需要有 def 文件,前面已經說過 -Wl,--output-def,dlltest.def  就可以生成對應的def 文件。

有了def文件之后,利用VS2010 提供的lib.exe可以生成對應的lib文件。

lib /machine:ix86 /def:dlltest.def

將生成的dlltest.lib 文件拷到VC項目中。編譯,運行,一切正常。

我們知道 WinAPI 函數是符合 Pascal 函數調用約定的,也就是所謂的 stdcall。而剛才生成的dll 中的函數是使用的 C語言函數調用約定(__cdecl )。如果將其改為Pascal 函數調用約定需要修改程序代碼。

 

[plain]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //dlltest.c  
  2. int _stdcall Double(int x)  
  3. {  
  4.     return x * 2;  
  5. }  
  6.   
  7. //main.c  
  8. #include <stdio.h>  
  9. int _stdcall Double(int x);  
  10. int main(void)  
  11. {  
  12.         printf("Hello :%d\n", Double(333));  
  13.         return 0;  
  14. }  

編譯命令是不變的。但是需要注意的是,這時生成的dll 文件中的函數名是有變化的。可以參看下圖。原來是Double 現在變成了 Double@4,變成了這種類似 C++ 函數的名字了。但是這樣並不影響使用。

 

網上關於生成和使用dll 的文章都會寫到,生成dll 是函數聲明需添加 __declspec(dllexport),而使用dll時函數聲明要使用__declspec(dllimport)。大家都看到了,我前面的代碼中這兩個都沒有用到。那么這兩個聲明有什么用呢。下面就做個測試。

首先在生成dll 的代碼中增加:

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //dlltest.c  
  2. int __declspec(dllexport) _stdcall Double(int x);  
  3.   
  4. int _stdcall Double(int x)  
  5. {  
  6.     return x * 2;  
  7. }  

 

 編譯命令如下:

gcc dlltest.c -shared -o dlltest.dll -Wl,--output-def,dlltest.def,--out-implib,dlltest.a

 

M.c 文件不變:

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //m.c  
  2. #include <stdio.h>  
  3.   
  4. int _stdcall Double(int x);  
  5.   
  6. int main(void)  
  7. {  
  8.         printf("Hello :%d\n", Double(333));  
  9.         return 0;  
  10. }  

 

編譯命令如下:

gcc m.c dlltest.a -o m2.exe

 

編譯沒有問題,執行也沒有問題。

修改一下m.c 。

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //m.c  
  2. #include <stdio.h>  
  3.   
  4. int __declspec(dllimport) _stdcall Double(int x);  
  5.   
  6. int main(void)  
  7. {  
  8.         printf("Hello :%d\n", Double(333));  
  9.         return 0;  
  10. }  

 

編譯命令如下:

Gcc m.c dlltest.a -o m2.exe

 

編譯沒有問題,執行也沒有問題。

再修改一下main.c 。

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //main.c  
  2. #include <stdio.h>  
  3.   
  4. int __declspec(dllexport) _stdcall Double(int x);  
  5.   
  6. int main(void)  
  7. {  
  8.         printf("Hello :%d\n", Double(333));  
  9.         return 0;  
  10. }  

 

編譯命令如下:

Gcc main.c dlltest.a -o m2.exe

 

編譯沒有問題,執行也沒有問題。 這個實驗說明__declspec(dllexport)對於函數聲明其實是沒什么作用的。我也比較過生成的代碼的反匯編結果,也是沒區別的。並不像有些人所說增加了__declspec(dllexport)之后生成的代碼能夠更精煉。當然,這也可能是現在編譯器的優化能力越來越強的結果。早期編譯器跟不上,可能還是有區別的。

 

但是__declspec(dllexport)對於輸出變量是有影響的。看下面的測試代碼:

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //dlltest.c  
  2. int Double(int x);  
  3. int xxx = 123;  
  4. int Double(int x)  
  5. {  
  6.     return x * 2;  
  7. }  
  8.   
  9. //m.c  
  10. #include <stdio.h>  
  11.   
  12. int Double(int x);  
  13. extern int xxx;  
  14. int main(void)  
  15. {  
  16.         printf("Hello :%d\n", Double(333));  
  17.         printf("%d", xxx);  
  18.         return 0;  
  19. }  


編譯:

 

gcc dlltest.c -shared -o dlltest.dll -Wl,--output-def,dlltest.def,--out-implib,dlltest.a

lib /machine:ix86 /def:dlltest.def

gcc m.c dlltest.a -o mm.exe

這樣是可以編譯執行的,說明dlltest.a 中包含了足夠的信息。

但是第三句改為:

gcc m.c dlltest.lib -o mm.exe

 

則會有如下的錯誤,這也說明dlltest.a 和dlltest.lib 確實有些很小的差異。

undefined reference to `xxx'

collect2.exe: error: ld returned 1 exit status

 

VS2010 編譯也是類似的錯誤,無法找到符號 xxx的定義。即使是main.c中增加了如下的聲明:extern int __declspec(dllimport) xxx;

結果也是類似的:error LNK2001: 無法解析的外部符號 __imp__xxx

說明dlltest.lib 中就沒有 xxx 的相關信息,不可能訪問到dlltest.dll 中的xxx。

如果將dll的全局變量聲明中增加 __declspec(dllexport) ,結果就不一樣了。

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //dlltest.c  
  2. int Double(int x);  
  3. int  __declspec(dllexport)  xxx = 123;  
  4. int Double(int x)  
  5. {  
  6.     return x * 2;  
  7. }  
  8.   
  9. //m.c  
  10. #include <stdio.h>  
  11.   
  12. int Double(int x);  
  13. extern int xxx;  
  14. int main(void)  
  15. {  
  16.         printf("Hello :%d\n", Double(333));  
  17.         printf("%d", xxx);  
  18.         return 0;  
  19. }  

gcc dlltest.c -shared -o dlltest.dll -Wl,--output-def,dlltest.def,--out-implib,dlltest.a

lib /machine:ix86 /def:dlltest.def

gcc m.c dlltest.a -o mm.exe

編譯成功, 

gcc m.c dlltest.lib -o mm.exe

還是失敗的。

 

在VS2010中編譯 m.c,仍然是失敗的。報的錯誤是:

 error LNK2001: 無法解析的外部符號 _xxx

m.c 做一些修改。增加 __declspec(dllimport)

[cpp]  view plain  copy
 
 在CODE上查看代碼片派生到我的代碼片
  1. //m.c  
  2. #include <stdio.h>  
  3.   
  4. int Double(int x);  
  5. int __declspec(dllimport) xxx;  
  6. int main(void)  
  7. {  
  8.         printf("Hello :%d\n", Double(333));  
  9.         printf("%d", xxx);  
  10.         return 0;  
  11. }  

 

VS2010 中編譯就可以通過。另外,再多說一句,我試驗的結果表明,__declspec(dllimport) 與__declspec(dllexport) 對於編譯來說似乎沒有任何區別,字面上的區別完全是給程序員自己看的。

至此,MinGW gcc 生成 dll 的常見問題就都解決了。

 

http://blog.csdn.net/liyuanbhu/article/details/42612365


免責聲明!

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



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