本文主要结合之前两篇介绍的“Windows挂钩”知识,讲解《Windows核心编程》一书中的“DIPS"示例程序和我的调试经验。
一、程序功能介绍
1,功能需求
DIPS.exe应用程序使用了Windows Hook来将一个DLL注入到Explorer.exe的地址空间中,并在内部给桌面的ListView控件发送LVM_GETITEM和LVM_GETITEMPOSITON消息,获取桌面的ListView的子项及其位置坐标,然后将这些信息写入系统注册表,待需要的时候重新取回。
之所以要“注入DLL”,是因为LVM_GETITEM消息要求我们在它的LPARAM参数中传入一个LV_ITEM数据结构。由于这个内存地址只对发送消息的进程又意义,因此接收消息的进程是无法使用它的。唯有将这个地址注入到接收消息的进程,才能发送成功。
2,名词解释
1)DIPS —— Desktop Item Position Saver (桌面图标位置保存工具)
2)LVM —— ListView Manage
3)Explorer.exe,就是我们俗称的“资源管理器”,Windows操作系统中,通过图形界面看到的所有东西,比如:桌面、C盘、D盘、文件夹等都是在这个“资源管理器”的投影。我们鼠标双击打开桌面上的应用程序,也是由Explorer.exe调用CreateProcess来实现。
此外,Explorer.exe这个名字是在“任务管理器”中可以看到的,事实上,它也是有窗口的,只不过它的窗口时隐藏的。我们可以通过VS自带的“spy++”工具来查看它的窗口。打开“spy++”,搜索(alt+F3)类“Progman”,注意,它区分大小写。
看上图左边,红圈中,“00010132”是它的窗口句柄,“Program Manager”是窗口的标题,“Progman”是窗口的类名。Progman窗口只有为一个自窗口“SHELLDLL_DefView”,而它也自由一个孙窗口“SysListView32”即桌面ListView。所以,DIPS中,通过如下方式来获取桌面窗口句柄:
HWND hWndLV = GetFirstChild(GetFirstChild(
FindWindow(TEXT("ProgMan"), NULL)));
上图右边是该窗口的右键属性,可以比较这个进程ID和 Explorer.exe在“任务管理器”中的PID,它们的值是相等的(需要进行进制换算)。
3,注册表项
通过“regedit"命令(register edit)可以打开系统注册表。DIPS需要添加一个注册表项”HKEY_CURRENT_USER\Software\Wintellect\Desktop Item Position Saver”。我们在调试的时候,可以先查看对应位置是否有这个项。
二、代码调试
Jeffrey Richter设计的这个DIPS程序展示了很多项比较高级的编程技巧,下面我将我的体会一一列出。
1,DIPS工程对DIPSLib工程的引用
安装Windows Hook,需要在实施进程和目标进程都加载Hook Procedure所在的DLL。但是,我找遍DIPS工程(实施进程),从代码到工程文件,都没找到它是怎么加载DIPSLib.dll文件的。一般来说,一个工程加载DLL文件,有以下几种方式:
1)在代码中显示调用LoadLibrary函数进行加载。
2)在代码中使用“#pragma comment(lib,"***")”伪指令,隐式加载某个DLL。
3)在“VS工程属性 -> 链接器 -> 附加依赖项”中填入待加载的DLL的对应lib文件名。kernel32.dll、user32.dll等系统DLL就是这么加入的。
但是,我检查了这几个地方,都没有发现。于是,我也用notepad++逐个打开DIPS工程下的各个文件,逐个搜索“DIPSLib”,终于在“DIPS.vcxproj”文件发现了它的踪影。原来,Jeffrey Richter应用了project reference的方法,建立了DIPS工程对DIPSLib工程的强依赖关系。参考MSDN文档:点击打开链接
下面,我将举例来说明这个方法。假设我要新建一个DIPS.exe和一个DIPSLib.dll工程,且前者需要加载后者,那么我们可以在一个solution中进行新建和管理这两个工程的代码。
1)先用VS正常新建DIPS.exe工程。
2)通过“右键解决方案 -> 添加 -> 新建项目”的方式添加DIPSLib.dll工程到同一个solution下。
3)在VS的“解决方案资源管理器”中,展开DIPS.exe的目录树,可以看到下面第一项就是“引用(reference)”,第二项是“外部依赖项”。”右键引用 -> 添加引用“即可打开一个对话框,该对话框中会列出同一个solution下的其他工程项目。我们只需要勾选”DIPSLib“,然后确认关闭对话框即可。
2,hInstDll的句柄
在MSDN的Installing and Releasing Windows Hook的示例代码中,它是通过LoadLibrary函数的返回值来获得hInstDll的句柄,然后传给SetWindowsHookEx作为第3个参数。
而Jeffrey将SetWindowsHookEx函数也放到了DLL中,他巧妙地利用DLL的入口点获取DLL自身的句柄,如下:
BOOL WINAPI DllMain(<span style="color:#ff0000;">HINSTANCE hInstDll</span>, DWORD fdwReason, PVOID fImpLoad) {
switch (fdwReason) {
case DLL_PROCESS_ATTACH:
// DLL is attaching to the address space of the current process.
<span style="color:#ff0000;">g_hInstDll = hInstDll;</span>
break;
注意,g_hInstDll是非共享的全局变量,这样,在DIPS进程和在目标进程中的g_hInstDll的值是不一样的。
此外,由于SetWindowsHookEx和GetMsgProc都位于DLL中,也就不需要像MSDN的示例代码一样通过GetProcAddress函数来获取GetMsgProc的地址了。
3,内存映射文件共享数据段
DIPSLib利用了内存映射文件的自定义共享段的方式来在DLL的不同实例之间,即实施进程和目标进程之间,来共享数据(全局变量)。如下:
// Instruct the compiler to put the g_hHook data variable in
// its own data section called Shared. We then instruct the
// linker that we want to share the data in this section
// with all instances of this application.
#pragma data_seg("Shared")
HHOOK g_hHook = NULL;
DWORD g_dwThreadIdDIPS = 0;
#pragma data_seg()
// Instruct the linker to make the Shared section
// readable, writable, and shared.
#pragma comment(linker, "/section:Shared,rws")
4,client and server模式
DIPS利用Hook Procedure函数,在目标进程创建了一个隐藏的模态对话框"Wintellect DIPS"。
// Create the DIPS Server window to handle the client request.
CreateDialog(g_hInstDll, MAKEINTRESOURCE(IDD_DIPS), NULL, Dlg_Proc);
这个对话框的Dlg_Proc函数通过共享的线程ID向DIPS发消息,而DIPS通过FindWindow获得该对话框的句柄,然后通过SendMessage相这个对话框发消息,它们之间形成一个client and server的关系。
5,利用线程消息队列来进行线程同步
DIPS和"Wintellect DIPS"模态对话框之间有一个跨进程的同步,Jeffrey巧妙地利用了线程消息队列来同步,而不是自定义一个锁(需要跨进程)。
// Wait for the DIPS server window to be created.
MSG msg;
GetMessage(&msg, NULL, 0, 0);
查看MSDN的文档,对GetMesage说明如下:
Retrieves a message from the calling thread's message queue. The function dispatches incoming sent messages until a posted message is available for retrieval.
Unlike GetMessage, the PeekMessage function does not wait for a message to be posted before returning.
此外,在《深入浅出MFC》一书中,侯老师展示了“在消息循环的时候,用PeekMessage替代GetMessage,并调用OnIdle函数进行空闲时间的后台工作”。
6,读写系统注册表
在DIPSLib的SaveListViewItemPositions和RestoreListViewItemPositions中,展示了对系统注册表的读写。大家可以看源码学习一下。
7,SendMessage和PostMessage的区别
SendMessage需要等WndProcedure函数返回,而PostMessage仅仅是将消息发送到指定线程的消息队列返回。MSDN文档说明如下:
Sends the specified message to a window or windows. The SendMessage function calls the window procedure for the specified window and does not return until the window procedure has processed the message.
To send a message and return immediately, use the SendMessageCallback or SendNotifyMessage function. To post a message to a thread's message queue and return immediately, use the PostMessage or PostThreadMessage function.
8,WM_GETMESSAGE和GetMsgProc
SetWindowsHookEx函数的第一个参数表示Hook的类型,对应监视不同的系统事件(event)。DIPS使用的是WM_GETMESSAGE类型的Hook,查看MSDN文档:
| Installs a hook procedure that monitors messages posted to a message queue. For more information, see the GetMsgProc hook procedure. |
GetMsgProc 的说明:
An application-defined or library-defined callback function used with the SetWindowsHookEx function. The system calls this function whenever the GetMessage orPeekMessage function has retrieved a message from an application message queue. Before returning the retrieved message to the caller, the system passes the message to the hook procedure.
The HOOKPROC type defines a pointer to this callback function. GetMsgProc is a placeholder for the application-defined or library-defined function name.
因此,DIPS在调用SetHook给目标线程安装好Hook后,第一件事就是给目标线程发消息,主动激活它的HookProcedure。
// The hook was installed successfully; force a benign message to
// the thread's queue so that the hook function gets called.
bOk = PostThreadMessage(dwThreadId, WM_NULL, 0, 0);
此外,在HookProcedure函数中,为了避免被其他消息击中多次,它定义了一个静态的计数器。
LRESULT WINAPI GetMsgProc(int nCode, WPARAM wParam, LPARAM lParam) {
static BOOL bFirstTime = TRUE;
if (bFirstTime) {
...... // only once
}
return(CallNextHookEx(g_hHook, nCode, wParam, lParam));
}