前言
进程之间空间独立,每个进程最大2G地址空间,只有内存映像文件和系统组件才能映射到这个区域。
有些情况下,必须打破进程的界限,访问另一个进程的地址空间,这些情况包括:
- 当你想要为另一个进程创建的窗口建立子类时。
- 当你需要调试帮助时(例如,当你需要确定另一个进程正在使用哪个 D L L时)。
- 当你想要挂接其他进程时。
一、插入DLL:一个例子
目的
为由另一个进程创建的窗口建立一个子类。
你可能记得,建立子类就能够改变窗口的行为特性。若要建立子类,只需要调用 SetWindowLongPtr函数,改变窗口的内存块中的窗口过程地址,指向一个新的(你自己的)WndProc。
当调用的SetWindowLongPtr函数,建立一个窗口的子类时,你告诉系统,发送到或者显示在hWnd设定的窗口中的所有消息都应该送MySubclassProc,而不是送往窗口的正常窗口过程:
SetWindowLongPtr(hwnd,GWLP_WNDPROC,MySubclassProc);
当系统需要将消息发送到指定窗口的 WndProc时,要查看它的地址,然后直接 调用WndProc。在本例中,系统发现MySubclassProc函数的地址与窗口相关联,因此就直接调用MySubclassProc函数。
SetWindowLongPtr函数
用GetClassLong和SetClassLong函数可以读写窗口类的数据;
用GetWindowLong和SetWindowLong可以读写指定窗口实例的数据。使用这些接口,可以在运行时读取或修改窗口类或窗口实例的窗口过程地址。
该函数改变指定窗口的属性。
函数也将指定的一个值设置在窗口的额外存储空间的指定偏移位置。
LONG_PTR SetWindowLongPtr( [in] HWND hWnd, [in] int nIndex, [in] LONG_PTR dwNewLong );
hWnd
:窗口句柄,间接给出窗口所属的类。
nIndex
:指定将设定的大于等于0的偏移值。
nlndex | 说明 |
GWL_EXSTYLE | 设定一个新的扩展风格。更多信息,请见CreateWindowEx。 |
GWL_STYLE | 设定一个新的窗口风格。 |
GWL_WNDPROC | 为窗口消息处理过程设置一个新的地址。 |
GWL_HINSTANCE | 设置一个新的应用程序实例句柄。 |
GWL_ID | 设置一个新的窗口标识符。 |
GWL_USERDATA | 设置与该窗口相关的用户数据。这些用户数据可以在程序创建该窗口时被使用。用户数据的初始值为0。当hWnd参数标识了一个对话框时,也可使用下列值:DWL_DLGPROC设置对话框过程的新地址。 |
DWL_MSGRESULT | 设置对话框中的消息处理程序的返回值。 |
DWL_USER | 设置的应用程序所私有的新的额外信息,例如句柄或指针。 |
dwNewLong
:指定的替换值。
如果函数成功,则返回所指定的偏移量的前一个值。
如果函数失败,则返回0。
GetWindowLongPtr函数
GetWindowLongPtr函数是在指定的窗口中获取信息。也可以在指定window内存偏移量的情况下获取值。
LONG_PTR GetWindowLongPtr( HWND hWnd, int nIndex );
hWnd
:欲获取信息的窗口(或属于窗口的类)的句柄。
nIndex
:为欲获取的信息指定值。
如果函数执行成功,将返回读取的值。
如果执行失败,将返回零,要获取更多扩展信息,请调用GetLastError.
主要问题
为另一个进程创建的窗口建立子类时遇到的问题是:
- 建立子类的过程位于另一个进程的地址空间中,可能造成内存访问的违规。
下图显示了一个简化了的图形,说明窗口过程是如何接受消息的。进程 A正在运行,并且已经创建了一个窗口。
窗口过程是如何接受消息的:
- 文件 User32.dll被映射到进程A的地址空间中。对User32.dll文件的映射是为了接收和发送在进程 A中运行的任何线程创建的任何窗口中发送和显示的消息。
- 当User32.dll的映像发现一个消息时,它首先要确定窗口的WndProc的地址,然后调用该地址,传递窗口的句柄、消息和wParam和lParam值。
- 当WndProc处理该消息后,User32 .dll便循环运行,并等待另一个窗口消息被处理。
A进程 | B进程 |
EXE file: LRESUlT WndProc(HWND hend,UNIT uMsg,…){…} |
EXE file void Somefunc(void) { HWND hwnd=Findwindow(“class A”,NULL); SetWindowLongPtr(hwnd,GWLP_WNDPROC,MySubclassProc); } |
USER32.DLL file: LONG DispatchMessage(CONST MSG*msg) { LONG lRet; WNDPROC lpfnWndProc= (WNDPROC)GetWindowLongPtr(msg,hwnd,GWLP_WNDPROC); lRet=lpfnWndProc(msg.hwnd,msg.message,msg.wParam,mag.lParam); return lRet; } |
USER32.DLL file: … |
现在假设你的进程是进程B,你想为进程A中的线程创建的窗口建立子类:
- 你在进程 B中的代码必须首先确定你想要建立子类的窗口的句柄。这个操作使用的方法很多。上图 显示的例子只是调用FindWindow函数来获得需要的窗口。
- 进程B中的线程调用SetWindowLongPtr函数,试图改变窗口的WndProc的地址。(SetWindowLongPtr函数中的代码要查看是否有一个进程正在试图改变另一个进程创建的窗口的WndProc地址,然后将忽略这个函数的调用。 本节末尾会讲到,微软内部实现限制的)
如果SetWindowLongPtr函数能够改变窗口的 WndProc,那将出现什么情况呢?
系统将把MySubclassProc的地址与特定的窗口关联起来。然后,当有一条消息被发送到这个窗口中时,进程A中的User32.dll代码将检索该消息,获得 MySubclassProc的地址,并试图调用这个地址。但是,这时可能遇到一个大问题。MySubclassProc将位于进程B的地址空间中,而进程 A却是个活动进程。
显然,如果User32想要调用该地址,它就要调用进程 A的地址空间中的一个地址,这就可能造成内存访问的违规。
为了避免这个问题的产生,应该让系统知道 MySubclassProc是在进程B的地址空间中,然后,在调用子类的过程之前,让系统执行一次上下文转换(即切换到进程B地址空间)。
Microsoft没有实现这个辅助函数功能,原因是:
- 应用程序很少需要为其他进程的线程创建的窗口建立子类。大多数应用程序只是为它们自己创建的窗口建立子类,Windows的内存结构并不阻止这种创建操作。
- 切换活动进程需要占用许多CPU时间。
- 进程B中的线程必须执行MySubclassProc中的代码。系统究竟应该使用哪个线程呢?是现有的线程,还是新线程呢?
-User32.dll怎样才能说明与窗口相关的地址是用于另一个进程中的过程,还是用于同一个进程中的过程呢?
由于对这个问题的解决并没有什么万全之策,因此 Microsoft决定不让 Set WindowsLongPtr改变另一个进程创建的窗口过程。
不过仍然可以为另一个进程创建的窗口建立子类—只需要用另一种方法来进行这项操作。这并不是建立子类的问题,而是进程的地址空间边界的问题。
如果能将你的子类过程的代码放入进程A的地址空间,就可以方便地调用SetWindowLongPtr函数,将进程A的地址传递给MySubclassProc函数。我将这个方法称为将DLL“插入”进程的地址空间。有若干种方法可以用来进行这项操作。下面将逐个介绍它们。
二、使用注册表来插入DLL
cmd 输入 regedit
快速打开注册表。
以下是通过注册表路径会加载DLL的注册表键:
计算机\HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion\Windows\AppInit_DLLs
注意:
Windows 98 Windows 98将忽略注册表的这个关键字。在Windows 98下,无法使用该方法插入DLL。
使用方法
以下是加入D:\MyDLL.dll载入:
AppInit_DLLs该关键字的值包含一个DLL文件名或者一组DLL文件名(用空格或逗号隔开)。
过程
- 当重新启动计算机及 Windows进行初始化时,系统将保存这个关键字的值。
- 然后,当User32.dll库被映射到进程中时,它将接收到一个 DLL_PROC ESS_ATTACH通知。
- 当这个通知被处理时,User32.dll便检索保存的这个关键字中的值,并且为字符串中指定的每个 D L L调用LoadLibrary函数。当每个库被加载时,便调用与该库相关的 DllMain函数(fdwReason为DLL_PROCESS_ATTACH)
这样,每个库就能够对自己进行初始化。
由于插入的 D L L在进程的寿命期中早早地就进行了加载,因此在调用函数时应该格外小心。调用 kernel32.dll
中的函数时应该不会出现什么问题,不过调用其他 D L L中的函数时就可能产生一些问题。
- User32.dll并不检查每个库是否已经加载成功,或者初始化是否取得成功。
注册表注入DLL的不足
在插入D L L时所用的所有方法中,这是最容易的一种方法。要做的工作只是将一个值添加到一个已经存在的注册表关键字中。
不过这种方法也有它的某些不足:
- 修改值后必须重新启动你的计算机: 由于系统在初始化时要读取这个关键字的值,因此在修改这个值后必须重新启动你的计算机—即使退出后再登录,也不行。当然,如果从这个关键字的值中删除 D L L,那么在计算机重新启动之前,系统不会停止对库的映射操作。
- 你的D L L只会映射到使用User32.dll的进程中: 所有基于GUI的应用程序均使用User32.dll,不过大多数基于 CUI的应用程序并不使用它。因此,如果需要将 D L L插入编译器或链接程序,这种方法将不起作用。
- 你的D L L将被映射到每个基于 G U I的应用程序中,但是必须将你的库插入一个或几个进程中。 你的D L L映射到的进程越多,“容器”进程崩溃的可能性就越大。毕竟在这些进程中运行的线程是在执行你的代码。如果你的代码进入一个无限循环,或者访问的内存不正确,就会影响代码运行时所在进程的行为特性和健壮性。因此,最好将你的库插入尽可能少的进程中。
- 你的D L L将被映射到每个基于 G U I的应用程序中。这与上面的问题相类似。在理想的情况下,你的D L L只应该映射到需要的进程中,同时,它应该以尽可能少的时间映射到这些进程中。假设在用户调用你的应用程序时你想要建立 WordPad的主窗口的子类。在用户调用你的应用程序之前,你的 D L L不必映射到WordPad的地址空间中。如果用户后来决定终止你的应用程序的运行,那么你必须撤消 WordPad的主窗口。在这种情况下,你的D L L将不再需要被插入WordPad的地址空间。最好是仅在必要时保持D L L的插入状态。
三、使用Windows挂钩来插入DLL
可以使用挂钩将D L L插入进程的地址空间。为了使挂钩能够像它们在 1 6位Windows中那样工作,微软不得不设计了一种方法,使得D L L能够插入另一个进程的地址空间中。
SetWindowsHookEx函数
该函数将一个应用程序定义的挂钩处理过程安装到挂钩链中去,你可以通过安装挂钩处理过程来对系统的某些类型事件进行监控,这些事件与某个特定的线程或系统中的所有事件相关。
HHOOK WINAPI SetWindowsHookEx( int idHook, \\钩子类型 HOOKPROC lpfn, \\回调函数地址 HINSTANCE hMod, \\实例句柄 DWORD dwThreadId \\线程ID );
idHook
: 指明要安装的挂钩的类型。
lpfn
: 指明窗口准备处理一个消息时系统应该调用的函数的地址。
hMod
:包含GetMsgProc函数的D L L实例句柄。
dwThreadId
:线程ID,0用于指明要挂接所有 G U I线程。
返回值
:
若此函数执行成功,则返回值就是该挂钩处理过程的句柄;若此函数执行失败,则返回值为NULL(0).若想获得更多错误信息,请调用GetLastError函数.
UnhookWindowsHookEx函数
删除挂钩链中安装的钩子。
BOOL WINAPI UnhookWindowsHookEx( __in HHOOK hhk);
hhk
:要删除的钩子的句柄。这个参数是上一个函数SetWindowsHookEx的返回值.
GetWindowThreadProcessId函数
找出某个窗口的创建者(线程或进程),返回创建者的标志符。
DWORD GetWindowThreadProcessId( HWND hWnd, LPDWORD lpdwProcessId );
hWnd
: (向函数提供的)被查找窗口的句柄.
lpdwProcessId
:[out] 进程号的存放地址(变量地址)
返回值
:返回线程号,注意,lpdwProcessId 是存放进程号的变量。返回值是线程号,lpdwProcessId 是进程号存放处。