基础:
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编程