下面我们来看看 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 的创建和使用 》