Windows动态链接库编程笔记

基础:
1.DLL 与进程关系
DLL 的文件映像被映射到调用进程的地址空间中 ,DLL 的函数供进程中运行的所有线程使用 . 这时 ,DLL 几乎将失去它作为 DLL 的全部特征 . 对于进程中的线程来说 ,DLL 的代码和数据看上去就像恰巧是在进程的地址空间中的额外代码和数据一样 .1
当一个线程调用 DLL 函数时 , DLL 函数要查看线程的堆栈 , 以便检索它传递的参数 , 并将线程的堆栈用于它需要的任何局部变量 . 此外 ,DLL 中函数的代码创建的任何对象均由调用线程所拥有 , DLL 本身从来不拥有任何东西 . DLL 中所使用的资源会被系统看作是进程所有 .
注意必须注意的是 , 单个地址空间是由一个可执行模块和若干个 DLL 模块组成的 . 这些模块中 , 有些可以链接到静态版本的 C/C++ 运行期库 , 有些可以链接到一个 DLL 版本的 C/C++ 运行期库 , 而有些模块(如果不是用 C/C++ 编写的话)则根本不需要 C/C++ 运行期库 . 许多开发人员经常会犯一个常见的错误 , 因为他们忘记了若干个 C/C++ 运行期库可以存在于单个地址空间中 . ,DLL 中所申请的资源在释放 DLL 时应该由 DLL 自行归还 .
创造 DLL:
1) 建立带有输出原型 / 结构 / 符号的头文件 . 在构建可执行文件的时候需要用到同一个头文件 ;
2) 建立实现输出函数 / 变量的 C/C++ 源文件 ;
3) 编译器为每个 C/C++ 源文件生成 .obj 模块 ;
4) 链接程序将生成 DLL .obj 模块链接起来 ;
5) 如果至少输出一个函数 / 变量 , 那么链接程序也生成 lib 文件 . 并不包含任何函数或变量 . 它只是列出了所有被导出的函数和变量的符号名 . 这个文件可与头文件一起构建可执行模块 ( 可选 );
创造 EXE:
6) 建立带有输入原型 / 结构 / 符号的头文件 . 可包含由 DLL 的开发人员所创建的头文件 ( 隐式加载 ?) 或者自定义 DLL 导出函数原型 ( 显示加载 ?);
7) 建立引用输入函数 / 变量的 C/C++ 源文件 ;
8) 编译器为每个 C/C++ 源文件生成 .obj 源文件 ;
9) 链接程序将各个 .obj 模块链接起来 , 产生一个 .exe 文件 ( 它包含了所需要 DLL 模块的名字和输入符号的列表 , 包括 DLL 所需 DLL);
运行应用程序 :
10) 加载程序为 .exe 创建地址空间 ;
11) 加载程序将需要的 DLL 加载到地址空间中进程的主线程开始执行 , 应用程序启动运行 .
即就是 DLL 和可执行模块都已构建完毕 . 当运行可执行模块的时候 , 操作系统的加载程序会为新的进程创建一个虚拟地址空间 , 并将可执行模块映射到新进程的地址空间中 . 加载程序接着解析可执行模块的导入段 . 对导入段中列出的每个 DLL, 加载程序会在用户的系统中对该 DLL 模块进行定位 , 并将该 DLL 映射到进程的地址空间中 . 注意 , 由于 DLL 模块可以从其它 DLL 模块中导入函数和变量 , 因此 DLL 模块可能有自己的导入段并需要将它所需的 DLL 模块映射到进程的地址空间中 .

-------------------------

1 可用 WinHex 打开一个 DLL 文件 , 然后使用 WinHex 的一个功能 : 拷贝文件到 C 代码 , 这是 DLL 的全部内容就变成无符号数组的形式 . 可用 API 将此内容写入到新文件即可复原  DLL。


高级:

2.DLL 模块的显示载入和符号链接
HMODULE  LoadLibrary(PCTSTR  pszDLLPathName);
HMODULE  LoadLibraryEx(PCTSTR  pszDLLPathName,  HANDLE  hFile,  DWORD  dwFlags)
LoadLibraryEx 函数有两个额外的参数: hFile dwFlags. 参数 hFile 是保留的 , NULL. 参数 dwFlags 可以被设为 0, 或下列标志的组合:
A.DONT_RESOLVE_DLL_REFERENCES 标志 , 告诉系统只需将 DLL 映射到调用进程的地址空间 , 不要调用 DllMain. 同时 , 系统不会将那些额外的 DLL( DLL 模块所需要包含的其他 DLL) 自动载入到进程的地址空间中 . 这样调用 DLL 到处的任何函数时 , 会面临很大的风险:代码所依赖的内部数据结构可能尚未初始化 , 或者代码所引用的 DLL 尚未载入 .
B.LOAD_LIBRARY_AS_DATAFILE 标志 , 告诉系统将 DLL 作为数据文件映射到进程的地址空间中 . 就只对文件进行映射这点而言 , 和上一个标志相似 .( 实际上当系统将一个 DLL 映射到进程的地址空间中的时候 , 会检查 DLL 中的一些信息来决定应该给文件中不同的段指定何种页面保护属性 . 如果不指定这个标志 , 那么系统会认为需要执行文件中的代码 , 并用相应的方式来设置页面保护属性 .) 举个例子 , 例如一个 DLL 使用这个标志载入的 , 那么对这个 DLL 调用 GetProcessAddress 的时候 , 返回值将是 NULL, GetLastError 将会返回 ERROR_MOD_NOT_FOUND. 该标志非常有用 , 可以加载资源文件使用 , DLL , 或者另一个 EXE 文件中 .
C.LOAD_LIBRARY_AS_DATAFILE_EXCLUSIVE 标志 , 和上个标志相似 , 唯一不同之处在于 DLL 文件是以独占访问模式打开的 , 从而禁止任何其他应用程序在当前应用程序使用该 DLL 文件的时候对其进行修改 . 该标志能提供更好的安全性 .
D.LAOD_LIBRARY_AS_IMAGE_RESOURCE 标志 , 与标志 2 相似 , 但也有一点略有不同:当系统载入 DLL 的时候 , 会对相对虚拟地址 (relative virtual address, 简称 RVA) 进行修复 . 这样 ,RVA 就可以直接使用 , 而不必再根据 DLL 载入到的内存地址来对它们进程转换了 . 当需要对 DLL 进行解析来遍历其中的 PE(protable executable) 段时 , 这个标志特别有用 .
E.LOAD_WITH_ALTERED_SEARCH_PATH 标志 , 用来改变 LoadLibraryEx 在对指定的 DLL 进行定位时所使用的搜索算法 . 通常 LoadLibraryEx 会更具运行可执行模块中列出的顺序来搜索文件 . 但是指定了这个标志后 , 函数会根据传给 pszDLLPathName 参数的值 , 用三种不同的算法来搜索文件 .
a. 不包含 \ 字符 , 那么使用 19 章的标准搜索路径来对 DLL 进行搜索 .
b. 如果 pszDLLPathName 包含 \ 字符 , 那么取决于该路径是全路径还是相对路径 . 全路径或网络共享路径 (\\server\share\BetaBinLibrary.dll) 的话 , 函数会试图直接载入该 DLL 文件 . 如果对应的文件不存在 , 那么直接返回 NULL, 这时错误码为 ERROR_MOD_NOT_FOUND. 如果是相对路径的话 , 会把下面文件夹与 pszDLLPathName 连接起来:进程的当前目录 .Windows 的系统目录 (system32).16 Windows 系统目录 (system).Windows 目录 .PATH 环境变量总列出的目录 . 如果参数中出现 “.” “..”, 那恶魔在搜索过程中的每一个步骤都会将它们考虑在内来构建一个相对路径 . 例如 , 如果将 TEXT("..\\BetaBinLibrary.dll") 作为参数传入 , 那么函数会在下列位置搜索 BetaBinLibrary.dll :保护当前目录的文件夹 . 包含 Windows 的系统目录的文件夹 ( Windows 目录 ). 包含 16 Windows 系统目录的文件夹 . 保护 Windows 目录的文件夹 ( 通常是磁盘的根目录 ).PATH 环境变量中列出的每个目录的上一层文件夹 .
c. 在构建应用程序的时候 , 如果不希望用 LOAD_WITH_ALTERED_SERACH_PATH 标志来调用 LoadLibraryEx, 或者不希望改变应用程序的当前目录 , 而是希望让它从一个众所周知的文件夹中动态地载入 DLL, 那么应该调用 SetDllDirectory, 并将程序库所在的文件夹作为参数传入 . 这个函数告诉 LoadLibrary LoadLibraryEx 在搜素的时候使用下面的算法:进程的当前目录 . 通过 SetDllDirectory 所设置的文件夹 .Windows 的系统目录 .16 Windows 系统目录 .Windows 目录和 PATH 环境变量中列出的目录 . 该搜索算法允许我们将应用程序和共享的 DLL 保存在一个预先定义好的目录中 , 由于应用程序的当前目录是可以设置的 , 比如在快捷方式中 , 因此该算法可以避免从应用程序的当前目录中意外地载入同名 DLL 的风险 .
F.LOAD_IGNORE_CODE_AUTHZ_LEVEL 标志 , 用来关闭 WinSafer 所提供的验证 , 是在 XP 中引入的 , 其设计目的是为了对代码在执行过程中可以拥有的特权加以控制 .UAC 特性已经取代了这项特性 .
通过 FreeLibrary 函数可以显式地将 DLL 从进程的地址空间中卸载 , 传入的 HMODULE 值用来标识我们想要卸载的 DLL.
还有个 FreeLibraryAndExitThread 函数 , Kernel32.dll 中实现如下:
VOID FreeLibraryAndExitThread(HMODULE hInstDll, DWORD dwExitCode)
{
FreeLibrary(hInstDll);
ExitThread(dwExitCode);
}
用于 DLL 会创建一个线程 , 当线程完成了它的工作后 , 可以先后调用 FreeLibrary ExitThread 来从进程的地址空间中撤销对 DLL 的映射并终止线程 . 分别调用会出现问题 .DLL 被撤销了 ExitThread 的调用代码也不存在 , 线程试图执行的是不存在的代码 , 将引发访问违规 , 并导致整个进程被终止 . 通过 Kernel32.dll,ExitThread 的代码还在 Kernel.dll , 则可以继续执行 .
线程可以调用 GetModuleHandle 函数来检测一个 DLL 是否已经被映射到了进程的地址空间中 .
始终应该记住的是 , 即便 LoadLibrary LoadLibraryEx 载入的 DLL 理应是磁盘上的同一个文件 , 也不能将它们的返回的映射地址互换使用 . 混用 LoadLibrary LoadLibraryEx 可能会导致将同一个 DLL 映射到同一个地址空间中的不同位置 .1
当我们用 LOAD_LIBRARY_AD_DATAFILE,LOAD_LIBRARY_AD_DATAFILE_EXCLUSIVE LOAD_LIBRARY_AS_IMAGE_RESOURCE 标志调用 LoadLibraryEx 的时候 ,OS 会先检测该 DLL 是否已经被 LoadLibrary LoadLibraryEx( 但没有使用这些标志 ) 载入过 . 如果已经被载入过 , 那么函数会返回地址空间中 DLL 原先已经被映射的地址 . 但是 , 如果 DLL 尚未被载入 , 那么 Windows 会将该 DLL 载入到地址空间中一个可用的地址 , 但并不认为它是一个完全载入的 DLL. 这时如果用这个模块句柄来调用 GetModuleFileName, 那么得到的返回值将为 0. 这时一种非常好的方法 , 可以让我们知道与一个 DLL 想对应的模块句柄并不包含动态函数 , 因此无法通过 GetProAddress 来得到函数地址并对函数进行调用 .( 因此 , 先调用有这些标志的 Ex 函数 , 再普通调用加载 , 就会出现多次加载了 .)

3 入口函数
函数名 DllMain 是区分大小写的 . 一个 DLL 可以有一个入口点函数 . 系统会在不同的时候调用这个入口点函数 , 具体什么时候我们马上就会介绍 . 这些调用是通知性质的 .
参数 hInstDll 包含该 DLL 实例的句柄 . _tWinMain hInstExe 参数相似 , 这个值表示一个虚拟内存地址 ,DLL 的文件映像就被映射到进程地址空间中的这个位置 . 通常将这个参数保存在一个全局变量中 , 这样在调用资源载入函数 ( 比如 DialogBox LoadString) 的时候 , 就可以使用它 . 如果 DLL 是隐式载入的 , 那么最后一个参数 fImpLoad 的值不为零 , 如果 DLL 是显式载入的 , 那么 fImpLoad 的值将为零 .
参数 fdwReason 表示系统调用入口点函数的原因 . 这个参数可能是下列 4 个值之一: DLL_PROCESS_ATTACH,DLL_PROCESS_DETACH,DLL_THREAD_ATTACH DLL_THREAD_DETACH.
必须记住 ,DLL 使用 DllMain 函数来对自己进行初始化 .DllMain 函数执行的时候 , 同一个地址空间中的其他 DLL 可能还没有执行它们的 DllMain. 这意味着它们尚未初始化 , 因此应该避免在 DllMain 里与其他 Dll 发生交互 .
Platform SDK 文档说 DllMain 函数只应该执行简单的初始化 , 比如设置线程句柄存储区 , 创建内核对象 , 打开文件等 . 避免调用 User.Shell.ODBC.COM.RPC 以及套接字函数 , 这是因为保护这些函数的 DLL 可能尚未初始化完毕 , 或者函数可能会在内部调用 LoadLibrary(Ex) 从而产生循环依赖 .
如果要创建全局或静态 C++ 对象 , 会存在同样的问题 , 因为在 DllMain 函数被调用的同时 , 这些对象的构造函数和析构函数也会被调用 .2
1.DLL_PROCESS_ATTACH 通知 , 系统第一次将一个 DLL 映射到进程的地址空间中 . 只有当 DLL 的文件映像第一次被映射的时候 , 才会这样 . 如果之后一个线程再调用 LoadLibrary(Ex) 来载入一个已经被映射到进程的地址空间的 DLL, 那么 OS 只不过是递增该 DLL 的使用计数 , 而不会再次用 DLL_PROCESS_ATTACH 来调用 DllMain 函数 . DllMain 处理 DLL_PROCESS_ATTACH 通知的时候 ,DllMain 的返回值用来表示该 DLL 的初始化是否成功 . 如果 fdwReason 是任何其他值 , 系统会忽略 DllMain 的返回值 .
系统将创建进程的主线程并用这个线程来调用每个 DLL DllMain 函数 , 同时传入 DLL_PROCESSESS_ATTACH. 当所有已映射的 DLL 都完成了对该通知的处理后 , 系统会先让进程的主线程开始执行可执行模块的 C/C++ 运行时的启动代码 (startup code), 然后执行可执行模块的入口点函数 (_tmain _tWinMain). 如果任何一个 DLL DllMain 函数返回 FALSE, 也就是说初始化没有成功 , 那么系统会把所欲的文件映像从地质空间中清除 , 向用户显示一个消息框 , 终止整个进程 .
显式载入 DLL 的时候 , 进程调用 LoadLibrary(Ex) 的时候 , 系统会对指定的 DLL 进行定位 , 并将该 DLL 映射到进程的地址空间中 . 然后系统会调用 LoadLibrary(Ex) 的线程来调用 DLL DllMain 函数 , 并传入 DLL_PROCESS_ATTACH . DLL DllMain 函数完成了对通知的处理后 , 系统会让 LoadLibrary(Ex) 调用返回 , 这样线程就可以继续正常执行 . 如果 DllMain 函数返回 FALSE, 也就是说初始化不成功 , 那么系统会自动从进程的地址空间中撤销对 DLL 文件映像的映射 , 并让 LoadLibrary(Ex) 返回 NULL.
2.DLL_PROCESS_DETACH 通知 , 当系统将一个 DLL 从进程的地址空间中撤销映射时 . DLL 处理这个通知的时候 , 应该执行与进程相关的清理工作 .( 如果在处理 DLL_PROCESS_ATTACH 通知的时候返回 FALSE 耳背消除 , 则不会有 DLL_PROCESS_DETACH 通知 .)DLL 可能会阻碍进程的终止 . 例如 , DllMain 收到 DLL_PROCESS_DETACH 通知的时候 , 有可能会进入无线循环 . 只有当每个 DLL 都处理完 DLL_PROCESS_DETACH 通知之后 , 操作系统才会真正地终止进程 .( 如果进程终止是因为系统中的某个线程调用 TerminateProcess, 系统便不会用 DLL_PROCESS_DETACH 来调用 DLL DllMain 函数 .)
3.DLL_THREAD_ATTACH, 当进程创建一个线程的时候 , 系统会检查当前映射到该进程的地址空间中的所有 DLL 文件映像 , 并用该标志来调用每个 DLL DllMain 函数 . 告诉 DLL 需要执行与线程相关的初始化 . 新创建的线程负责执行所有 DLL DllMain 函数中的代码 . 只有当所有 DLL 都完成了对该通知的处理之后 , 系统才会让新线程开始执行它的线程函数 .
当系统将一个新的 DLL 映射到
进程的地址空间中时 , 系统不会让任何已有的线程用 DLL_THREAD_ATTACH 来调用该 DLL DllMain 函数 . 只有在创建新线程的时候 ,DLL 已经被映射到进程的地址空间中才会产生此标志通知 .
此外 , 进程被创建是收到 DLL_PROCESS_ATTACH 通知 , 不会收到 DLL_THREAD_ATTACH 通知 . 既是主线程不会收到这个 .
4.DLL_THREAD_DETACH, 线程终止的时候 , 系统不会立即终止该线程 , 而会让这个即将终止的线程用 DLL_THREAD_DETACH 来调用所有已映射 DLL DllMain 函数 . 类似的 ,DLL 也可可能会妨碍线程的终止 . 比如进入无限循环 , 毕竟只有当每个 DLL 都处理完该通知之后 ,OS 才会真正地终止线程 .
DllMain 的序列化调用 , 一个 DLL DllMain 会按照被通知顺序被一个个线程执行 , 最先通知在执行中 , 其余被挂起 . 如果在执行 DllMain , 又出现创建新线程再等待其终止 , 那么就会造成死锁 .
DisableThreadLibraryCalls 函数 , 告诉系统不想让某个指定的 DLL DllMain 函数发送 DLL_THREAD_ATTACH DLL_THREAD_DETACH 通知 .
当系统创建进程的时候 , 会同时创建一个锁 ( Vista 中是一个关键段 ). 每个进程都有自己的锁 —— 多个进程不会共享同一个锁 . 当进程中的线程调用映射到进程地址空间中的 DLL DllMain 函数时 , 会用这个锁来同步各个线程 (DllMain 的序列化调用 ).
在程序调用 CreateThread 的时候 , 系统会首先创建线程内核对象和线程栈 . 然后系统会在内部调用 WatiForSingleOjbect 函数 , 并传入进程的互斥量对象的句柄 . 当新线程得到互斥量的所有权后 , 系统会让新线程用 DLL_THREAD_ATTACH 来调用每个 DLL DllMain 函数 . 只有在这个时候 , 系统才会调用 ReleaseMutex 来放弃对进程的互斥对象的所有权 .
C/C++ 运行库的 DLL 启动代码的工作
在链接 DLL 的时候 , 链接器会将 DLL 的入口函数的地址嵌入到生成的 DLL 文件映像中 . 在默认情况下 , 如果用的是 MS 链接器并制定了 /DLL 开关 , 那么链接器会认为入口点函数的函数名是 _DllMainCRTStartup. 这个函数包含在 C/C++ 运行库中 , 在链接 DLL 的时候会被静态地链接到 DLL 的文件映像 .( 即便用的是 C/C++ 运行库的 DLL 版本 , 对这个函数的链接仍然会是静态的 .)
系统将 DLL 的文件映像映射到进程的地址空间中时 , 实际上调用的是 _DllMainCRTStartup 函数 , 而不是我们的 DllMain 函数 . 在将所有的通知都转发到 _DllMainCRTStartup 函数之前 , 为了支持 /GS 开关提供的安全性特性 ,_DllMainCRTStartup 函数会对 DLL_PROCESS_ATTACH 通知进程处理 ._DllMainCRTStartup 函数会初始化 C/C++ 运行库 , 并确保在 _DllMainCRTStartup 收到 DLL_PROCESS_ATTACH 通知的时候 , 所有全局或静态 C++ 对象都已经构造完毕 .( 作用类似于进程开始是 CRT 提供的启动代码 .)
DLL 的源代码中实现 DllMain 函数并不是必须的 . 如果没有自己的 DllMain 函数 , 那么可以使用 C/C++ 运行库提供的 DllMain 函数 , 它的实现看起来大致如下 ( 如果静态链接到 C/C++ 运行库 )
BOOL  WINAPI  DllMain(HINSTANCE  hInstDll,  DWORD  fdwReason,  PVOID  fImpLoad)
{
      if(fdwReason  ==  DLL_PROCESS_ATTACH)
           DisableThreadLibraryCalls(hInstDll);
      return (TRUE);
}
在链接 DLL 的时候 , 如果链接器无法在 DLL .obj 文件中找到一个名为 DllMain 的函数 , 那么它会链接 C/C++ 运行库的 DllMain 函数 .

-------------------------

1 如果 LoadLibraryEx 不使用任何标志位 , 那么等价于 LoadLibrary 否则两个所得到的句柄是不一样的 .


参考资料:
1、《windows核心编程第五版》之DLL编程

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值