第19章 DLL基础
Windows API提供的所有函数都包含在DLL中。三个最重要的DLL:Kernel32.dll(管理内存、进程和线程)、Use32.dll(执行与用户界面相关的任务)、GDI32.dll(绘制图像和显示文字)。
19.1、DLL和进程的地址空间
第一层:DLL定位概要
在应用程序(或其他DLL)能调用一个DLL中函数前,必须将该DLL的文件映像映射到调用进程的地址空间中。两种方式:隐式载入时链接和显示运行时链接。
映射后,当调用DLL中一个函数时,该函数会在线程栈中取得传给他的参数,并用线程栈来存放它需要的局部变量。此外,DLL中函数创建的任何对象都为调用线程或进程所拥有。
19.2、纵观全局
第二层:理论流程概要
若一个EXE需要从另一个DLL模块中导入函数或变量,则需:
先构建DLL:
1)先创建一个头文件,包含在DLL中导出的函数原型、结构及符号。为构建该DLL,DLL的所有源文件需包含这个头文件。构建EXE时需同一个头文件。
2)创建源文件来实现DLL模块中导出的函数和变量。
3)构建DLL模块时,编译器会对每个源文件处理并生成一个.obj模块。
4)当所有.obj模块都创建完后,链接器将所有.obj模块内容合并,产生一个单独的DLL。
5)若链接器检测到DLL源文件输出了至少一个函数或变量,则链接器还会生成一个.lib文件。它只是列出所有被导出的函数或变量的符号名。
再构建EXE(可执行模块):
1)所有引用了导出的函数、变量、数据结构或符号的源文件中,必须包含DLL对应的头文件。
2)构建EXE时,编译器会对每个源文件处理并生成一个.obj模块。
3)编译完后,链接器会将所有.obj模块内容合并生成一个exe。该exe(可执行模块)包含一个导入段,其中列出了所有它需要的DLL模块,以及它从每个DLL模块中引用的符号。执行exe,OS的加载程序会执行5)。
4)加载程序先为新的进程创建一个虚拟地址空间,并将可执行模块映射到新进程的地址空间中。加载程序接着解析可执行模块的导入段。对导入段中列出的每个DLL,加载程序会在用户系统中对该DLL模块进行定位,并将该DLL映射到进程的地址空间中。由于DLL模块可从其他DLL模块中导入函数和变量,因此DLL模块可能有自己的导入段并需将它所有的DLL模块映射到进程的地址空间中。
加载程序将EXE和所有DLL映射到进程的地址空间后,进程的主线程可以开始执行。
第三层:实践详细流程
19.2.1、构建DLL模块
一个DLL可导出变量、函数或C++类。应避免导出变量。仅当导出的C++类的模块使用的编译器与导入的C++类的模块使用的编译器由同一厂商提供时,才可导出C++类。
巧妙之处(代码P515):DLL头文件中
#ifdef MYLIBAPI
#else
#define MYLIBAPI extern “C” _declspec(dllimport)
#endif
//导出的函数或变量
MYLIBAPI int Add(int nLeft, int nRight);
DLL源文件中
#define MYLIBAPI extern “C” _declspec(dllexport) //必须在dll头文件之前
#include “dll头文件”
EXE源文件中
#include “dll头文件” //不能定义MYLIBAPI宏
如此在DLL源文件中MYLIBAPI被定义为导出,EXE源文件中MYLIBAPI被定义为导入。
_declspec(dllexport):源文件中不必在被导出变量和函数前加此修饰。因编译器在解析头文件时会记住应导出哪些变量和函数。
_declspec(dllimport):DLL的头文件中加在导出的函数、变量或C++类前。非必需,但能略微提高效率。EXE的源文件中,编译器看到此符号,会知道该从DLL模块中导入该符号。
extern “C”:在编写C++代码时才使用(C不该使用)。因C++编译器通常会对函数名和变量名进行改编,链接时会出错。此修饰符是告诉编译器不要对变量名或函数进行改编。
_stdcall:即使是C,当用此约定时,Microsoft的编译器会对函数名进行改编。具体方法是:给函数名添加下划线前缀和一个特殊的后缀。该后缀由一个@符号跟作为参数传给函数的字节数组成。如:_declspec(dllexport) LONG _stdcall MyFunc(int a, int b);导出为_MyFunc@8。所以要防止改编。两种方式:创建一个.def文件,并在.def文件中包含一下类似下面段:
EXPORTS
MyFunc
第二种方法(建议不用)是导出未经改编的函数名。在DLL的源文件中加入:
#pragma comment(linker, “/export:MyFunc=_MyFunc@8”)
.def文件格式:
LIBRARY XX(dll名称这个并不是必须的,但必须确保跟生成的dll名称一样)
EXPORTS
[函数名] @ [函数序号]
导出类:注意导出类和使用导出类同导出函数和使用导出函数类似,但在导出类中不可使用extern “C”符号。
导出段:当Microsoft C/C++编译器看到__declspec(dllexport)修饰的变量、函数或C++类时,会在生成的.obj文件中嵌入一些额外的信息。当链接器在链接DLL的所有.obj文件时,会解析这些信息。链接器会在生成的DLL文件中嵌入一个导出符号表。这个导出段列出了导出的变量、函数或类的符号名。还会保存相对虚拟地址,表示每个符号可在DLL模块的何处找到。(DumpBin.exe工具加入-exports可查看一个DLL的导出段)
导入段:当链接器看到__declspec(dllimport)修饰的导入符号时,会在生成的可执行模块中嵌入一个特殊的段,它的名字叫导入段。导入段列出了该模块所需的DLL模块,以及它从每个DLL模块中引用的符号。(DumpBin.exe工具加入-imports可查看一个DLL的导出段)
/***********************************
Moudle: MyLib.h
***********************************/
#ifdef MYLIBAPI
//MYLIBAPI should be defined in all of the DLL's source code
//moudles before this header file is included.
//All functions/variables are being exported.
#else
//This header file is included by an EXE source code moudle/
//Indicate that all functions/variables are being imported.
#define MYLIBAPI extern "C" _declspec(dllimport)
#endif
//
//Define any data structures and symbols here.
//
//Define exported variables here.(Note:Avoid exporting variables.)
MYLIBAPI int g_nResult;
/
//Define exported function prototypes here.
MYLIBAPI int Add(int nLeft, int nRight);
End of File///
/****************************************
Moudle:MyLibFile1.cpp
****************************************/
#include <windows.h>
#define MYLIBAPI extern "C" _declspec(dllexport)
#include "MyLib.h"
/
int g_nResult;
int Add(int nLeft, int nRight)
{
g_nResult = nLeft + nRight;
return g_nResult;
}
///End of File/
19.2.2、构建可执行模块
1)包含dll的导出头文件:#include <>;注意不要定义MYLIBAPI宏。
2)包含lib文件:#pragma comment(lib, “”);为了让链接器确定代码中的导入符号来自哪个DLL。
3)直接使用导出的变量、函数或C++类。
19.2.3、运行可执行模块
OS的加载程序先为进程创建虚拟地址空间,然后将EXE映射到地址空间中,之后加载程序回检查EXE的导入段,对所需DLL进行定位并将它们映射到进程的地址空间中。
因导入段只包含DLL名称,不包含DLL路径,因此加载程序必须搜索DLL,顺序为:
1) 包含可执行文件的目录
2) Windows的系统目录,可通过GetSystemDirectory得到
3) 16位的系统目录,即Windows目录的System子目录
4) Windows目录,可通过GetWindowsDirectory得到
5) 进程的当前目录
6) PATH环境变量中列出的目录。
比如现在建立好了一个DLL导出了CMyClass类,客户也能正常使用这个DLL,假设CMyClass对象的大小为30字节。如果我们需要修改DLL 中的CMyClass类,让它有相同的函数和成员变量,但是给增加了一个私有的成员变量int类型,现在CMyClass对象的大小就是34字节了。当直接把这 个新的DLL给客户使用替换掉原来30字节大小的DLL,客户应用程序期望的是30字节大小的对象,而现在却变成了一个34字节大小的对象,糟糕,客户程序出错 了。
类似的问题,如果不是导出CMyClass类,而在导出的函数中使用了CMyClass,改变对象的大小仍然会有问题的。这个时候修改这个问题的唯一办法就是替 换客户程序中的CMyClass的头文件,全部重新编译整个应用程序,让客户程序使用大小为34字节的对象。
这就是一个严重的问题,有的时候如果没有客户程序的源代码,那么我们就不能使用这个新的DLL了。
具体用到时再baidu