1. 程式人生 > 實用技巧 >dll = MinGW gcc 生成動態連結庫 dll 的一些問題彙總

dll = MinGW gcc 生成動態連結庫 dll 的一些問題彙總

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

網路上關於用 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。

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

//dlltest.c
int Double(int x)
{
return x * 2;
}

下面的命令列將這個程式碼編譯成 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

//main.c

include <stdio.h>

int Double(int x);
int main(void)
{
printf("Hello :%d\n", Double(333));
return 0;
}

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。那麼程式碼可以這麼寫:

//m2.c
define UNICODE 1

include <windows.h>

include <stdio.h>

typedef int (*INT_FUNC)(int);
int main(void)
{
INT_FUNC db;
HINSTANCE hInstLibrary = LoadLibrary(L"dlltest.dll");
printf("LoadLibrary\n");
db = (INT_FUNC)GetProcAddress(hInstLibrary, "Double");

printf("Hello :%d\n", db(333));
FreeLibrary(hInstLibrary); 

return 0;

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

gcc m2.c -o m2.exe

執行的結果也是正確的。

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

用VS2010 的除錯功能,看了看反彙編的結果。看似都是正常的。

int _tmain(int argc, _TCHAR* argv[])
{
int a;
a = Double(333);
01021000 push 14Dh
01021005 call _Double (1021024h)
printf("Hello :%d\n", a);
0102100A push eax
0102100B push offset string "Hello :%d\n" (10220F4h)
01021010 call dword ptr [__imp__printf (10220A0h)]
01021016 add esp,0Ch
getchar();
01021019 call dword ptr [__imp__getchar (102209Ch)]
return 0;
0102101F xor eax,eax
}

單步跟進_Double 函式後是這樣的:

_Double:
01021024 jmp dword ptr ds:[1020000h]
0102102A nop
0102102B nop
Jmp 語句後:

00905A4D ???
00905A4E ???
00905A4F ???
00905A50 ???
00905A51 ???
00905A52 ???
00905A53 ???
可是在Debug 模式下:

_Double:
011B144C jmp dword ptr [__imp__Double (11B8340h)]
011B1452 nop
011B1453 nop
011B1454 int 3
011B1455 int 3
Jmp 語句後:

6C101560 push ebp
6C101561 mov ebp,esp
6C101563 mov eax,dword ptr [ebp+8]
6C101566 add eax,eax
6C101568 pop ebp
6C101569 ret
而從下圖可以看出,dlltest.dll 被載入到 6C100000 是正確的。

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

include <windows.h>

typedef int (INT_FUNC)(int);
int _tmain(int argc, _TCHAR
argv[])
{
INT_FUNC db;
HINSTANCE hInstLibrary = LoadLibrary(L"dlltest.dll");
printf("LoadLibrary ");
db = (INT_FUNC)GetProcAddress(hInstLibrary, "Double");

printf("Hello :%d\n", db(333));
FreeLibrary(hInstLibrary); 
getchar();
return 0;

}

這個程式碼用 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 函式呼叫約定需要修改程式程式碼。

//dlltest.c
int _stdcall Double(int x)
{
return x * 2;
}

//main.c

include <stdio.h>

int _stdcall Double(int x);
int main(void)
{
printf("Hello :%d\n", Double(333));
return 0;
}
編譯命令是不變的。但是需要注意的是,這時生成的dll 檔案中的函式名是有變化的。可以參看下圖。原來是Double 現在變成了 Double@4,變成了這種類似 C++ 函式的名字了。但是這樣並不影響使用。

網上關於生成和使用dll 的文章都會寫到,生成dll 是函式宣告需新增 __declspec(dllexport),而使用dll時函式宣告要使用__declspec(dllimport)。大家都看到了,我前面的程式碼中這兩個都沒有用到。那麼這兩個宣告有什麼用呢。下面就做個測試。

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

//dlltest.c
int __declspec(dllexport) _stdcall Double(int x);

int _stdcall Double(int x)
{
return x * 2;
}

編譯命令如下:

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

M.c 檔案不變:

//m.c

include <stdio.h>

int _stdcall Double(int x);

int main(void)
{
printf("Hello :%d\n", Double(333));
return 0;
}

編譯命令如下:

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

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

修改一下m.c 。

//m.c

include <stdio.h>

int __declspec(dllimport) _stdcall Double(int x);

int main(void)
{
printf("Hello :%d\n", Double(333));
return 0;
}

編譯命令如下:

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

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

再修改一下main.c 。

//main.c

include <stdio.h>

int __declspec(dllexport) _stdcall Double(int x);

int main(void)
{
printf("Hello :%d\n", Double(333));
return 0;
}

編譯命令如下:

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

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

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

//dlltest.c
int Double(int x);
int xxx = 123;
int Double(int x)
{
return x * 2;
}

//m.c

include <stdio.h>

int Double(int x);
extern int xxx;
int main(void)
{
printf("Hello :%d\n", Double(333));
printf("%d", xxx);
return 0;
}

編譯:

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) ,結果就不一樣了。

//dlltest.c
int Double(int x);
int __declspec(dllexport) xxx = 123;
int Double(int x)
{
return x * 2;
}

//m.c

include <stdio.h>

int Double(int x);
extern int xxx;
int main(void)
{
printf("Hello :%d\n", Double(333));
printf("%d", xxx);
return 0;
}
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)

//m.c

include <stdio.h>

int Double(int x);
int __declspec(dllimport) xxx;
int main(void)
{
printf("Hello :%d\n", Double(333));
printf("%d", xxx);
return 0;
}

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

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

qq_33264243:__declspec關鍵字好像是Microsoft c++的關鍵字 在VS中編譯dll時如果沒有dllexport和def預設不會生成匯入庫2年前回復