1. 程式人生 > >如何使用g++編譯呼叫dll的c++程式碼

如何使用g++編譯呼叫dll的c++程式碼

本文將有以下4個部分來講如何使用g++編譯呼叫dll的c++程式碼。

1.如何呼叫dll

2.動態連結和靜態連結的區別

3.g++的編譯引數以及如何編譯呼叫dll的c++程式碼

4.總結

1.如何呼叫dll

    動態連結庫(Dynamic Link Library),簡稱DLL。DLL 是一個包含可由多個程式同時使用的程式碼和資料的庫。它允許程式共享執行特殊任務所必需的程式碼和其他資源,一般來說,DLL是一種磁碟檔案,以.dll、.DRV、.FON、.SYS和許多以.EXE為副檔名的系統檔案都可以是DLL。它由全域性資料、服務函式和資源組成,在執行時被系統載入到呼叫程序的虛擬空間中,成為呼叫程序的一部分。

DLL的呼叫可以分為兩種:一種是隱式呼叫(需要.lib和.dll),一種是顯示呼叫(需要.dll)。

隱式載入就是在程式編譯的時候就將dll編譯到可執行檔案中。實現隱式連結很容易,只要將匯入函式關鍵字_declspec(dllimport)函式名等寫到應用程式相應的標頭檔案中就可以了。下面將通過一個例子來講解隱式連結呼叫Dlltest.dll庫中的Min函式。

首先新建一個專案為TestDll,在Dlltest.h、Dlltest.cpp檔案中分別輸入如下程式碼:

Dlltest.h的程式碼如下:

//隱式載入Dlltest
#pragma comment(lib,"Dlltest.lib")

//
宣告外部函式 extern "C"_declspec(dllimport) int Max(int a,int b); extern "C"_declspec(dllimport) int Min(int a,int b);

Dlltest.cpp的程式碼如下:

#include<stdio.h>
#include"Dlltest.h"
int  main()
{
   int a;
   a=min(1,2);
   printf("比較的結果為%d\n",a);
   return 0;
}

 在生成Dlltest.exe檔案之前,要先將Dlltest.dll和Dlltest.lib拷貝到debug同目錄(工程根目錄)下,也可以拷貝到windows的System目錄下。如果DLL使用的是def檔案,要刪除Dlltest.h檔案中關鍵字extern "C"。Dlltest.h檔案中的關鍵字pragma commit是要Visual C++的編譯器在link時,連結到Dlltest.lib檔案,當然,開發人員也可以不使用#pragma comment(lib,"Dlltest.lib")語句,而直接在工程的Setting->Link頁的Object/Moduls欄填入Dlltest.lib既可。 

顯式載入是指在程式執行過程中,需要用到dll裡的函式時,再動態載入dll到記憶體中,這種載入方式因為是在程式執行後再載入的,dll的維護更容易,使得程式如果需要更新,很多時候直接更新dll,而不用重新安裝程式。使用這種方式使應用程式在執行過程中隨時可以載入DLL檔案,也可以隨時解除安裝DLL檔案。

下面講一個通過顯式連結呼叫DLL中的Max函式的例子。

#include <stdio.h>
#include <windows.h>
int main()
{
typedef int(*pMax)(int a,int b);
typedef int(*pMin)(int a,int b);
HINSTANCE hDLL;
int A;
PMax Max;
//載入動態連結庫MyDll.dll檔案
HDLL=LoadLibrary("Dlltest.dll");

Max=(pMax)GetProcAddress(hDLL,"Max");
A=Max(5,8);
Printf("比較的結果為%d\n",A);

//解除安裝MyDll.dll檔案;
FreeLibrary(hDLL);
}

  在上例中使用型別定義關鍵字typedef,定義指向和DLL中相同的函式原型指標,然後通過LoadLibray()將DLL載入到當前的應用程式中並返回當前DLL檔案的控制代碼,然後通過GetProcAddress()函式獲取匯入到應用程式中的函式指標,函式呼叫完畢後,使用FreeLibrary()解除安裝DLL檔案。在編譯程式之前,首先要將DLL檔案拷貝到工程所在的目錄或Windows系統目錄下。

學習參考連結:

DLLs in Visual C++:

https://docs.microsoft.com/en-us/previous-versions/1ez7dh12%28v%3dvs.140%29

Walkthrough: Creating and Using a Dynamic Link Library (C++):

https://docs.microsoft.com/en-us/previous-versions/ms235636%28v%3dvs.140%29

Using Run-Time Dynamic Linking:

https://docs.microsoft.com/zh-cn/windows/desktop/Dlls/using-run-time-dynamic-linking

 

2.動態連結和靜態連結的區別

靜態連結方法:#pragma comment(lib, "test.lib") ,靜態連結的時候,載入程式碼就會把程式會用到的動態程式碼或動態程式碼的地址確定下來
靜態庫的連結可以使用靜態連結,動態連結庫也可以使用這種方法連結匯入庫

動態連結方法:LoadLibrary()、GetProcessAddress()FreeLibrary(),使用這種方式的程式並不在一開始就完成動態連結,而是直到真正呼叫動態庫程式碼時,載入程式才計算(被呼叫的那部分)動態程式碼的邏輯地址,然後等到某個時候,程式又需要呼叫另外某塊動態程式碼時,載入程式又去計算這部分程式碼的邏輯地址,所以,這種方式使程式初始化時間較短,但執行期間的效能比不上靜態連結的程式。

靜態庫連結時搜尋路徑順序:

1. ld會去找g++命令中的引數-L
2. 再找g++的環境變數LIBRARY_PATH
3. 再找內定目錄 /lib /usr/lib /usr/local/lib 這是當初compile g++時寫在程式內的

動態連結時、執行時搜尋路徑順序:

1. 編譯目的碼時指定的動態庫搜尋路徑
2. 環境變數LD_LIBRARY_PATH指定的動態庫搜尋路徑
3. 配置檔案/etc/ld.so.conf中指定的動態庫搜尋路徑
4. 預設的動態庫搜尋路徑/lib
5. 預設的動態庫搜尋路徑/usr/lib

有關環境變數:
LIBRARY_PATH環境變數:指定程式靜態連結庫檔案搜尋路徑
LD_LIBRARY_PATH環境變數:指定程式動態連結庫檔案搜尋路徑

靜態連結和動態連結的優缺點: 

靜態連結庫的優點 

     (1) 程式碼裝載速度快,執行速度略比動態連結庫快; 

     (2) 只需保證在開發者的計算機中有正確的.LIB檔案,在以二進位制形式釋出程式時不需考慮在使用者的計算機上.LIB檔案是否存在及版本問題。

動態連結庫的優點 

     (1) 更加節省記憶體並減少頁面交換;

     (2) DLL檔案與EXE檔案獨立,只要輸出介面不變(即名稱、引數、返回值型別和呼叫約定不變),更換DLL檔案不會對EXE檔案造成任何影響,因而極大地提高了可維護性和可擴充套件性;

     (3) 不同程式語言編寫的程式只要按照函式呼叫約定就可以呼叫同一個DLL函式;

     (4)適用於大規模的軟體開發,使開發過程獨立、耦合度小,便於不同開發者和開發組織之間進行開發和測試。

 不足之處

     (1) 使用靜態連結生成的可執行檔案體積較大,包含相同的公共程式碼,造成浪費;

     (2) 使用動態連結庫的應用程式不是自完備的,它依賴的DLL模組也要存在,如果使用載入時動態連結,程式啟動時發現DLL不存在,系統將終止程式並給出錯誤資訊。而使用執行時動態連結,系統不會終止,但由於DLL中的匯出函式不可用,程式會載入失敗;速度比靜態連結慢。當某個模組更新後,如果新模組與舊的模組不相容,那麼那些需要該模組才能執行的軟體,統統不能正常執行

 

3.g++的編譯引數

G++手冊:https://linux.die.net/man/1/g++

編譯分為3步,首先對原始檔進行預處理,這個過程主要是處理一些#號定義的命令或語句(如巨集、#include、預編譯指令#ifdef等),生成*.i檔案;然後進行編譯,這個過程主要是進行詞法分析、語法分析和語義分析等,生成*.s的彙編檔案;最後進行彙編,這個過程比較簡單,就是將對應的彙編指令翻譯成機器指令,生成可重定位的二進位制目標檔案。

使用g++編譯連結示例:

g++ -E main.c -o main.i  #預編譯,生成main.i檔案
g++ -S main.i            #編譯,生成main.S檔案
g++ -c main.S            #彙編,生成main.o檔案
g++ main.o -o main       #連結,生成可執行檔案  

g++ 命令的基本用法如下:

g++ [options] [filenames]

    選項指定編譯器怎樣進行編譯。

g++常用引數:

1)-E引數

  -E 選項指示編譯器僅對輸入檔案進行預處理。當這個選項被使用時, 前處理器的輸出被送到標準輸出而不是儲存在檔案裡.

2)-S引數

  -S 編譯選項告訴 g++ 在為 C 程式碼產生了組合語言檔案後停止編譯。 g++ 產生的組合語言檔案的預設副檔名是 .s 。

3)-c引數

  -c 選項告訴 g++ 僅把原始碼編譯為目的碼。預設時 g++ 建立的目的碼檔案有一個 .o 的副檔名。 

4)-o引數 

    -o 編譯選項來為將產生的可執行檔案用指定的檔名。

5)-O引數

  -O 選項告訴 g++ 對原始碼進行基本優化。這些優化在大多數情況下都會使程式執行的更快。 -O2 選項告訴g++ 產生儘可能小和儘可能快的程式碼。 如-O2,-O3,-On(n 常為0--3);

-O1  主要進行跳轉和延遲退棧兩種優化;

-O2 除了完成-O1的優化之外,還進行一些額外的調整工作,如指令調整等。

-O3 則包括迴圈展開和其他一些與處理特性相關的優化工作。

選項將使編譯的速度比使用 -O 時慢, 但通常產生的程式碼執行速度會更快。

6)除錯選項-g和-pg

  g++ 支援數種除錯和剖析選項,常用到的是 -g 和 -pg 。

  -g 選項告訴 g++ 產生能被 GNU 偵錯程式使用的除錯資訊以便除錯你的程式。g++ 提供了一個很多其他 C 編譯器裡沒有的特性, 在 g++ 裡你能使-g 和 -O (產生優化程式碼)聯用。

 -pg 選項告訴 g++ 在編譯好的程式里加入額外的程式碼。執行程式時, 產生 gprof 用的剖析資訊以顯示你的程式的耗時情況。

7) -l引數和-L引數

  -l引數就是用來指定程式要連結的庫,-l引數緊接著就是庫名,那麼庫名跟真正的庫檔名有什麼關係呢?

就拿數學庫來說,他的庫名是m,他的庫檔名是libm.so,很容易看出,把庫檔名的頭lib和尾.so去掉就是庫名了。

-L引數跟著的是庫檔案所在的目錄名。再比如我們把libtest.so放在/aaa/bbb/ccc目錄下,那連結引數就是-L/aaa/bbb/ccc -ltest

8 -include-I引數

      -include用來包含標頭檔案,但一般情況下包含標頭檔案都在原始碼裡用#i nclude xxxxxx實現,-include引數很少用。

       -I引數是用來指定標頭檔案目錄,/usr/include目錄一般是不用指定的,g++知道去那裡找,但 是如果標頭檔案不在/usr/icnclude裡我們就要用-I引數指定了,比如標頭檔案放在/myinclude目錄裡,那編譯命令列就要加上-I/myinclude引數了,如果不加你會得到一個"xxxx.h: No such file or directory"的錯誤。-I引數可以用相對路徑,比如標頭檔案在當前目錄,可以用-I.來指定。

9-Wall-w  -v引數

     -Wall 打印出gcc提供的警告資訊

     -w     關閉所有警告資訊

     -v      列出所有編譯步驟

我們可以使用visual studio,還有Dev C++來編譯呼叫dll的c++程式,方便快捷。

VS的編譯器名稱為MSVC。MSVC官方文件為:

https://docs.microsoft.com/en-us/visualstudio/msbuild/msbuild-tasks-specific-to-visual-cpp?view=vs-2017

msvc的編譯器cl.exe 

msvc的連結器link.exe 

Dev-C++的編譯器名稱為Mingw(包含了g++和gcc)

那我們如何使用g++編譯呼叫dll的c++程式呢?

我要呼叫的dll名稱為winusb_dll_spr_x64.dll(64位),使用的是動態連結。呼叫dll的c++程式碼如下:

#include <Windows.h>
#include<iostream>
#include<stdlib.h>

using namespace std;

int main(int argc, char* argv[])

{
        //open_handle open_usb;	
	HINSTANCE hDllInst = LoadLibrary("winusb_dll_spr_x64.dll");
	//HINSTANCE hDllInst = LoadLibrary("winusb_dll_x86.dll");
	cout<<"hDllInst:"<<hDllInst<<endl; 
	

	if(NULL!=GetProcAddress(hDllInst, "open")()){
		cout<<"GetProcAddress success";
	}else{
		cout<<"fail"<<endl; 
	
	} 
	
    return 0;
}

 在Dev c++的執行結果為:

使用g++編譯,按住win+R,開啟cmd(你要確保已經配置g++的環境變數),輸入如下指令:

g++ -E test2.cpp -o test.i
g++ -S test.i
g++ -c test.S
g++ test.o -o test.exe
test.exe

在cmd下執行顯示hDllInst0 ,test.exe已停止工作,執行截圖如下所示:

 

我將程式碼改為呼叫winusb_dll_x86.dll32位的)

HINSTANCE hDllInst = LoadLibrary("winusb_dll_x86.dll");
cout<<"hDllInst:"<<hDllInst<<endl;

重新使用g++編譯。

g++ -c test2.cpp

g++ test2.o -o test2.exe

test2.exe

Cmd下執行test2.exe輸出為hDllInst:0x6a510000(呼叫dll成功。)

4.總結

我在dev c++和g++下編譯了32位版本和64位版本的可執行程式,當編譯的可執行程式為64位版本的,而呼叫的dll為64位版本時,執行不會出錯。當編譯的可執行程式為32位版本的,而呼叫的dll為64位時,執行可執行程式會出現loadlibrary的返回值為0,可執行程式停止工作。