四個dll檔案引發的“血案”——呼叫DLL中的函式
喵哥專案的合作公司最近給喵哥出了個難題——專案中鐳射雷達的模組是公司一個工程師負責的,工程師比較務實,在網上一個VB.NET程式碼的基礎修改了一些細節,就交差了,的確可以用,但是最近工程師退出了這個專案,boss打算讓喵哥接手這個模組,喵哥很慌,但還是硬著頭皮上了。
面臨的問題
1.一個用VB.NET(我不熟悉的語言)編寫的程式; 因此我打算把它改寫成VC++的形式
2.只有四個dll檔案,沒有lib和h,當時的我更加慌了; 想著怎麼得到lib和h
3.所以我需要在VC++ 中呼叫四個dll的函式。
解決問題
從一種語言改寫到另一種語言,最好的方法是撇開語言的束縛,把程式的功能和執行過程摸清楚,把一些api函式認真記下來,以便以後知道用哪個函式。由於程式比較簡單,所以搞起來挺快的。
然後就遇到麻煩了,怎麼呼叫dll檔案中的函式?我之前寫的程式都是先包含.h和.lib,然後把dll檔案放在程式可讀的路徑下就可以完美的呼叫函數了。但是,現在只有四個光禿禿的dll,怎麼搞?
dll檔案是個啥
DLL(Dynamic Link Library)檔案為動態連結庫檔案,又稱“應用程式拓展”,是軟體檔案型別。在Windows中,許多應用程式並不是一個完整的可執行檔案,它們被分割成一些相對獨立的動態連結庫,即DLL檔案,放置於系統中。當我們執行某一個程式時,相應的DLL檔案就會被呼叫。一個應用程式可使用多個DLL檔案,一個DLL檔案也可能被不同的應用程式使用,這樣的DLL檔案被稱為共享DLL檔案。值得一提的是,Linux下動態庫是.so,靜態庫是.a。
dll不像exe可以獨立執行,而是被其他程式呼叫,這種使用的特性使得dll經常用於程式碼複用來提高軟體開發的效率。並且dll的暗盒特性使得它相較於提供原始碼實現程式碼複用的手段有以下幾個優點:
1.不會暴露原始碼;
2.不會造成與程式設計師的程式碼發生命名衝突;
3.體量小;
4.容易更新。
怎麼呼叫dll
終於到了重頭戲,“血案”的根源就是呼叫dll函數出了問題。
通常有兩種呼叫dll的方法:一種是隱式呼叫,一種是顯式呼叫。
隱式呼叫
隱式呼叫的方法需要採用靜態載入,需要dll、h、lib,敢情喵哥之前一直用隱式呼叫。。。真·結廬在人間,而無車馬喧·的“隱士”。
隱式呼叫要把h檔案的路徑包含到專案->屬性->配置屬性->VC++ 目錄-> 在“包含目錄”;
lib檔案的路徑包含到專案->屬性->配置屬性->VC++ 目錄-> 在“庫目錄”
然後把需要用到的lib檔名拷貝到專案->屬性->配置屬性->連結器->輸入-> 在“附加依賴項”
需要注意的是dll檔案最好放在工程路徑和可執行檔案生成路徑下,不過一些大公司的api在安裝某些軟體時,會把dll檔案的路徑新增到環境變數中去,所以有時看似不需要管dll檔案,實則不然。
隱式呼叫比較簡單粗暴,適合初學者使用,但是如果沒有lib檔案和標頭檔案怎麼辦?
由dll檔案生成lib和h
1.在VS的命令列工具中執行
dumpbin -exports lmsapi.dll>lmsapi.def
這是在VS2013命令列裡執行dumpbin -exports lmsapi.dll顯示的介面,生成的def檔案也是這個樣子的,可見不是所有的dll檔案都是[email protected]@的形式。
2.把lmsapi.def改成如下形式
LIBRARY"example"
EXPORT
lmsapi_close_terminal @1
lmsapi_config = _wsprintfA @2
lmsapi_console_out @3
lmsapi_create_crc @4
lmsapi_get_laser_type @5
lmsapi_laser_data_create @6
lmsapi_laser_data_destroy @7
後面的@1,@2是按照函式順序排列。
3.然後執行lib.exe/def:lmsapi.def,可以生成lmsapi.lib和lmsapi.exp檔案。lib就可以直接用了。
#pragma comment(lib,"lmsapi.lib")
4.新建一個lmsapi.tmp,裡面儲存函式名
lmsapi_close_terminal @1
lmsapi_config = _wsprintfA @2
lmsapi_console_out @3
lmsapi_create_crc @4
lmsapi_get_laser_type @5
lmsapi_laser_data_create @6
lmsapi_laser_data_destroy @7
然後執行undname.exe lmsapi.tmp>lmsapi.txt,從而把函式名解析到lmsapi.txt中。
5.需要用大佬的軟體(還沒要到),或者自己手動把格式改成.h檔案中宣告的形式,或者類的形式。
顯式呼叫
顯式呼叫在應用程式在執行過程中隨時可以載入DLL檔案,也可以隨時解除安裝DLL檔案,這是隱式連結所無法作到的,所以顯式連結具有更好的靈活性,對於解釋性語言更為合適。
typedef bool(*pConnect)(string ip, int port);
HMODULE hMod1 = LoadLibrary(_T("SICK_Communication.dll"));
HMODULE hMod2 = LoadLibrary(_T("SICK_FileManagement.dll"));
HMODULE hMod3 = LoadLibrary(_T("SICK_LMS5xx-PRO_Library.dll"));
HMODULE hMod4 = LoadLibrary(_T("SICKwork.dll"));
if (hMod1 == NULL || hMod2 == NULL || hMod3 == NULL || hMod4 == NULL)
{
AfxMessageBox(_T("載入動態連結庫失敗!"), MB_OKCANCEL | MB_ICONINFORMATION);
}
else
{
pConnect fp1 = pConnect(GetProcAddress(hMod1, (LPCSTR)1));
if (fp1 != NULL)
{
}
else
{
AfxMessageBox(_T("提取dll中的函式失敗!"), MB_OKCANCEL | MB_ICONINFORMATION);
}
}
顯式呼叫的問題:在DLL檔案中,dll工程中函式名稱在編譯生成DLL的過程中發生了變化(C++編譯器),在DLL檔案中稱變化後的字元為“name標示”。GetProcAddress中第二個引數可以由DLL檔案中函式的順序獲得,或者直接使用DLL檔案中的”name標示”,這個標示可以通過Dumpbin.exe小程式檢視。如果C++編譯器下,想讓函式名更規範(和原來工程中一樣)。
更一般的顯式呼叫
為了解決上部分最後的問題,可以使用 extern “C” 為dll工程中的函式建立C連線,簡單的示例工程如下。在DLL建立的工程中,新增cpp檔案
#ifdef __cplusplus // if used by C++ code
extern "C" { // we need to export the C interface
#endif
__declspec(dllexport) int addfun(int a, int b)
{
return a+b;
}
#ifdef __cplusplus
}
#endif
#include <windows.h>
#include <iostream>
using namespace std;
void main()
{
typedef int(*FUNA)(int,int);
HMODULE hMod = LoadLibrary("cdll.dll");//dll路徑
if (hMod)
{
FUNA addfun = (FUNA)GetProcAddress(hMod, TEXT("addfun"));//直接使用原工程函式名
if (addfun != NULL)
{
cout<<addfun(5, 4)<<endl;
}
else
{
cout<<"ERROR on GetProcAddress"<<endl;
}
FreeLibrary(hMod);
}
else
cout<<"ERROR on LoadLibrary"<<endl;
}
然而,以上兩種方法都不適用於喵哥的程式,後來發現我的dll是.NET的,C#和VB可以很好的應用,但是喵哥用在VC上是沒法實現。主要現象是。
喵哥想生成lib但是,生成的def(其中一個過程)中沒有任何函式名,用Dependency也是看不到函式,所以沒法轉換。所以無法採用隱式呼叫。
又由於無法看到函式,所以不知道特定函式的指標位置或者函式在dll中的標識,所以顯式呼叫也無法進行。
因此文中的例子是另外一個dll檔案,是可以完成這些操作的,但是生成.h檔案還是很麻煩。
附
dumpbin和undname是微軟vs自帶的兩個小工具。 前者可以用於檢視obj、ilb、dll等檔案的符號表,後者可以用於根據Name Mangling之後的字串反推函式原始宣告。 在排查LINK 2019連結錯誤時,這兩個命令較為有用。