下面我們來看看 Windows 平臺下應用程序是怎么調用 Windows 提供的底層 API 服務運行的。
我們編寫 Win32SDK 程序時,需要彈出對話框以作出友好的選擇, MessageBox 這個 API 函數就可以實現該功能。在開頭要添加 <windows.h> , 因為其包含了眾多的 API 函數聲明頭文件。為了探究這個小小的 MessageBox 是怎么彈出來的,我們右擊 MessageBox ,選擇“ Go to definition of MessageBox( 轉到定義 ) ”將打開 <winuser.h> 中 MessageBox 定義處。 MessageBox(A/W) 的函數原型聲明如下:
// WINUSER.H
WINUSERAPI
int
WINAPI
MessageBoxA (
HWND hWnd ,
LPCSTR lpText ,
LPCSTR lpCaption ,
UINT uType );
WINUSERAPI
int
WINAPI
MessageBoxW (
HWND hWnd ,
LPCWSTR lpText ,
LPCWSTR lpCaption ,
UINT uType );
#ifdef UNICODE
#define MessageBox MessageBoxW
#else
#define MessageBox MessageBoxA
#endif // !UNICODE
我們在使用 Windows 窗口操作系統時,經常會蹦出大大小小的 窗口, MessageBox 只是 Windows 作為提示的對話框窗口單元。那么, MessageBoxW 這個 API 函數到底在哪里實現的呢?應用程序是如何調用系統接口函數的呢?
動態鏈接庫( .dll )
初窺 DLL
實際上 Windows API 函數是定義在一些 DLL 中的, DLL 實現了代碼封裝,從這個角度來看 DLL 才是真正意義上的 API 函數包,它是非開源 Windows 操作系統提供給我們的底層接口。 DLL 的編制與具體的編程語言及編譯器無關。
動態鏈接庫 dll 文件( Linux 中與之對應的是的 .so ) 存放在 C:/WINDOWS/system 目錄和 C:/WINDOWS/system32 目錄下,它在被應用程序調用時才同程序相鏈接。
其中 Windows 系統最重要的 DLL 是 User32.dll 、 Gdi32.dll 和 Kernel32.dll 這三個庫文件。這三個庫文件中的 API 函數大都在頭文件 <windows.h> 中進行了聲明。從功能上進行分類, User32.dll ( Windows XP USER API Client DLL )定義了窗口管理函數,包括窗口的創建、顯示、設置和移動等; Gdi32.dll ( GDI Client DLL )定義了圖形設備函數( GDI ),實現與設備無關的繪圖功能; Kernel32.dll ( Windows NT BASE API Client DLL )定義了系統服務函數,包括諸如內存調度、進程管理等與操作系統有關的底層功能。我們可以通過三種方式來查看 DLL 文件中的導出函數信息:( 1 )使用 Windows 系統的 dumpbin 命令(在命令行中: C:/WINDOWS/system32> dumpbin ntdll.dll /exports );( 2 )使用 VC 自帶的 Depends 工具( Microsoft Visual Studio 6.0 Tools à Depends 或 Visual Studio 2005 命令行提示 à depends );( 3 ) Dll 函數查看器 ( ViewDll 或 ExeScope )。以下使用 VC 自帶的 Depends 工具查看 user32.dll 中 MessageBox 函數系列。
DLL 與 API
正如 Java 的跨平臺( Write Once, Run Everywhere )需要 JVM 的支持一樣, C/C++ 成為跨平臺的編程語言,依賴各個平臺(包括操作系統和編譯器)對 C/C++ 標準函數庫的具體平臺實現。
VS 編譯器自帶的標準 C 函數庫 <stdio.h> , <stdlib.h> , <string.h> , <math.h> 中聲明的函數可以到 C:/Program Files/Microsoft Visual Studio/VC98/CRT/SRC( 或 C:/Program Files/Microsoft Visual Studio 8/VC/crt/src) 中查看相關實現源代碼。 strcat.c 文件提供了 strcat 和 strcpy 函數的源碼 , 在 Windows 系統中, C: / WINDOWS/system32/ ntdll.dll 提供了 strcat 和 strcpy 這兩個 CRT API 的底層實現。用戶對 C 函數的調用最終通過調用底層 API 來完成真正的功能。例如 C 標準庫函數 create 用于創建文件,但它是靠調用 CreateFile ( kernel32.dll ) 函數來完成創建文件功能的; beginthread( process.h,thread.c ) 需要調用 CreateThread ( kernel32.dll ) 函數來完成線程的創建。
DLL 的移植升級
Windows 將遵循下面的搜索順序來定位 DLL : 包含 EXE 文件的目錄 > 進程的當前工作目錄 >Windows 系統目錄 >Windows 目錄 > 列在 Path 環境變量中的一系列目錄。
如果你在本機編寫一個 Windows 應用程序,移植到其他機子上 ( 當然也是 Windows 操作系統 ) ,有可能因為缺少相關 DLL 文件而無法執行。因為 DLL 是動態鏈接,就是隨用隨加載,這就是為什么我們玩 3D 游戲時經常彈出缺少 d3dx9***.dll 的錯誤提示。如果啟動的程序調用了一個過期的 DLL 文件或不匹配的 DLL 文件,則會出現“未定義的動態鏈接調用”消息。
動態鏈接庫除了實現代碼的共享外,其模塊封裝特性使得應用程序在調用一個 DLL 的不同版本時,只要導出的函數名相同就不必進行重新編譯鏈接。這樣,軟件產品在更新或升級時,客戶程序不必進行改動。在開發軟件產品時,對于通用功能的函數,一般以 DLL 的形式來實現。 Windows 設備驅動程序就是體現上述特點的動態鏈接庫。
DLL 的調用
動態鏈接庫的調用方式又分為隱式調用(也稱靜態調用,需要 .lib 文件)和顯式調用(也稱動態調用, LoadLibrary à GetProcAddress à FreeLibrary )。
那么,在我們編寫的程序中,如何調用 DLL 中的 API 呢?既然用戶對 C 函數的調用最終是通過調用底層 API 來完成真正的功能,那么,我們在編寫第一個 Hello World 程序時就已經不知不覺地調用了 DLL 。實際上 <stdio.h> 中定義的 printf 函數由 msvcrt.dll 函數導出,而 *printf 系列函數在 ntdll.dll 中有具體底層實現。
DLL 文件包括了具體實現的代碼編譯后的結果(二進制的機器碼),而頭文件中的代碼主要是 DLL 庫文件 導出函數的原型聲明。 <winuser.h> 即主要對 user32.dll 中導出的函數做聲明索引, 所以若要使用 MessageBox(A/W) , 需 #include <winuser.h> (已被 <windows.h> 包含)。正如調用 printf 函數需要 #include <stdio.h> 。
在調用 printf 函數或 MessageBox 函數時,我們僅僅包含了聲明頭文件,沒有顯式 LoadLibrary à GetProcAddress ,那我們是如何是隱式調用(定位) DLL 中的 API 的呢?實際上我們在利用 VC 向導生成一個 Win32 Console Application 時,向導已經為項目設置了 link 項( Project Settings à Link à Input à Object/library modules ),其中默認鏈接 kernel32.lib 、 user32.lib 、 gdi32.lib 等。
靜態鏈接庫( .lib )
目標代碼集靜態鏈接庫
在早期庫的組織形式相對簡單,里面的目標代碼只能夠進行靜態鏈接,所以我們稱為“靜態庫”,靜態庫的結構比較簡單,其實就是把原來的目標代碼( *.obj) 集合在一起,鏈接程序 LINKER 根據每一份目標代碼的符號表查找相應的符號(函數和變量的名字),找到的話就把該函數里面需要定位的進行定位,然后將整塊函數代碼放進可執行文件里,若是找不到需要的函數就報錯退出。標準 Turbo C2.0 中的 C 庫函數,例如 scanf 、 printf 、 memcpy 、 strcpy 等,就是使用的靜態庫技術。
以下是 C 程序的編譯鏈接過程: (1) 執行 cl /c main.c;cl /c lib1.c;cl /c lib2.c 生成了 main.obj lib1.obj lib2.obj 三個文件; (2) 執行 link /lib lib1.obj;link /lib lib2.obj 生成了 2 個文件 lib1.lib lib2.lib ; (3) 執行 link main.obj lib1.lib lib2.lib 生成 main.exe 。
靜態鏈接 lib 庫( Linux 中與之對應的是的 .a ) 的兩個特點:
( 1 )鏈接后產生的可執行文件包含了所有需要調用的函數的代碼,因此占用磁盤空間較大。
( 2 )如果有多個(調用相同庫函數的)進程在內存中同時運行,內存中就存有多份相同的庫函數代碼,因此占用內存空間較多。
DLL 隱式調用靜態鏈接庫
使用 DLL 隱式鏈接時,可執行程序鏈接到一個包含 DLL 導出函數信息的輸入庫文件 (.LIB 文件 ) 。操作系統在加載使用可執行程序時加載 DLL 。可執行程序直接通過函數名像調用其他源文件中的函數一樣調用 DLL 中的導出函數。
我們可以用記事本打開 C:/Program Files/Microsoft Visual Studio/VC98/Lib 中的 USER32.LIB 文件,其中有
__imp__MessageBoxA@16_ MessageBoxW @16 // 這里 16 為參數的字節數
? _ MessageBoxW @16 USER32.dll USER32.dll/ 889206797
靜態鏈接庫 lib 文件中存放的是接口函數的入口地址, dll 中存放的是函數實體。當我們隱式調用 dll 時,需要在 Link 選項指明其對應的 lib 庫。 lib 告訴編譯器你的 dll 都導出了什么函數,以及這些函數的相對地址,運行的時候就根據這些信息就可以找到 dll 中相應的 API 。
除了在“ Project Settings à Link à Input à object/library modules” 中填寫靜態鏈接庫( *.lib )外,我們還可以通過 #pragma comment 宏顯示輸入 *.lib 庫文件, 例如在網絡編程中需要添加 WS2_32.LIB 庫,則可以在文件的開頭包含頭文件后 #pragma comment ( lib , "WS2_32.LIB" ) 引入靜態鏈接庫文件。
由于 我們 經常 要調用一些第三方廠商或其他編譯器編寫的動態鏈接庫,但是一般都不提供源文件或 .lib 文件。我們若知道相應 API 的函數原型,通過 LoadLibrary à GetProcAddress 以實現正確的調用。如果隱式調用,則需要 lib 文件,可使用 DLL2LIB 工具生成 DLL 對應的 LIB 文件。
在編寫 MFC 項目時,我們打開 Project Settings à General 的 Microsoft Foundation Classes 里面有兩種鏈接方式: Use MFC in a Static Library , Use MFC in a Shared Library 。對應在 Visual Studio 2005 中“項目屬性 à 配置屬性 à 常規 à MFC 的使用”中設置鏈接方式。
如果選擇 Use MFC in a Shared Library 的話,你編譯后的程序中不包含 MFC 庫,所以文件會比較小,但是如果你的程序直接移到一個沒有安裝過 MFC 的機器上時,可能會導致找不到 MFC 的 DLL ,故發布時要帶 MFC 得 DLL 文件。如果選擇 Use MFC in a Static Library ,那么編譯后的程序就直接包含了 MFC 的靜態鏈接庫(目標代碼集,相當于基于源碼級集成),文件可能會大一些,但是可以直接移到其他機器上運行,即發布時不用帶 MFC 的 DLL 文件。
參考:
《 動態鏈接庫 DLL 的創建和使用 》
更多文章、技術交流、商務合作、聯系博主
微信掃碼或搜索:z360901061

微信掃一掃加我為好友
QQ號聯系: 360901061
您的支持是博主寫作最大的動力,如果您喜歡我的文章,感覺我的文章對您有幫助,請用微信掃描下面二維碼支持博主2元、5元、10元、20元等您想捐的金額吧,狠狠點擊下面給點支持吧,站長非常感激您!手機微信長按不能支付解決辦法:請將微信支付二維碼保存到相冊,切換到微信,然后點擊微信右上角掃一掃功能,選擇支付二維碼完成支付。
【本文對您有幫助就好】元
