消息机制

我们在初级班对于Win32 API的学习中,知道可以通过向窗口发送消息实现交互,但是我们始终没有从本质上了解什么是消息机制,从而也就无法回答如下这些问题。

images/download/attachments/3440655/image2023-9-18_10-55-18.png

因此为了弄清楚这些问题,我们就必须进入0环,从底层理解消息机制的本质。

消息队列

什么是消息队列

首先我们要理解什么是消息队列,我们可以编写运行如下代码,在桌面的左上角画上一个窗口,接着我们可以通过发送消息来与窗口进行互动:

#include <Windows.h>
 
typedef struct _Color
{
DWORD red;
DWORD green;
DWORD blue;
} Color;
 
typedef struct _WindowClass
{
DWORD x;
DWORD y;
DWORD width;
DWORD height;
Color color;
} WindowClass;
 
void PaintWindow(HDC hdc, WindowClass* window)
{
HBRUSH hBrush = (HBRUSH)GetStockObject(DC_BRUSH);
SelectObject(hdc, hBrush);
SetDCBrushColor(hdc, RGB(window->color.red, window->color.green, window->color.blue));
MoveToEx(hdc, window->x, window->y, NULL);
LineTo(hdc, window->x + window->width, window->y);
LineTo(hdc, window->x + window->width, window->y + window->height);
LineTo(hdc, window->x, window->y + window->height);
LineTo(hdc, window->x, window->y);
Rectangle(hdc, window->x, window->y, window->x + window->width, window->y + window->height + 1);
DeleteObject(hBrush);
}
 
int main()
{
char cMessage;
HWND hwnd;
HDC hdc;
WindowClass windowClass;
windowClass.x = 0;
windowClass.y = 0;
windowClass.width = 800;
windowClass.height = 400;
windowClass.color.red = 0xEF;
windowClass.color.green = 0xEB;
windowClass.color.blue = 0xDE;
hwnd = GetDesktopWindow();
hdc = GetWindowDC(hwnd);
for (;;)
{
PaintWindow(hdc, &windowClass);
cMessage = getchar();
switch (cMessage)
{
case 'a':
windowClass.color.red += 0x10;
windowClass.color.green += 0x10;
windowClass.color.blue += 0x10;
break;
case 'b':
windowClass.color.red -= 0x10;
windowClass.color.green -= 0x10;
windowClass.color.blue -= 0x10;
break;
}
}
getchar();
return 0;
}

编译运行这段代码,我们输入a或者b进行回车,一开始创建的窗口就会随着不同的指令进行颜色的切换。

images/download/attachments/3440655/image2023-10-8_20-30-43.png

这段代码是一个简单的交互程序,它通过接收键盘消息与窗口进行交互。然而,它有一个限制,即只能处理键盘消息,而无法处理鼠标或其他进程发来的消息。

因此我们希望接收并处理所有类型的消息,就需要提供一个容器,即消息队列。将所有消息都存放在消息队列中,进程再从消息队列中获取。

images/download/attachments/3440655/image2023-10-8_20-45-1.png

消息队列存放位置

用户空间

如果消息队列存在于用户空间,也就表示每个进程有一个独属于自己的消息队列,那么就需要有一个专用进程来对不同类型的消息进行分发。

Linux操作系统就采用了一种类似的机制,通过单独的进程专门负责接收消息,并将消息发送给不同的进程进行处理。但这种方法需要进行频繁的跨进程通信,可能导致效率下降。

images/download/attachments/3440655/image2023-10-8_20-56-46.png

内核空间

在Windows操作系统上微软采用了一种不同的策略。由于在0环(内核空间)中,不同进程的地址空间往往是相同的(即我们常说的高2G为共享内存),因此可以将消息队列存储在内核空间

在线程结构体的0x130偏移位有个成员Win32Thread,该成员在线程调用图形界面相关函数时,会指向一个名为_THREADINFO的结构体,该结构体中包含了消息队列。

images/download/attachments/3440655/image2023-10-8_21-5-37.png

由于该结构体是未公开的,所以我们也只能在ReactOS代码(https://sourceforge.net/projects/reactos/)中找到,如下代码中的成员MessageQueue就是消息队列:

#ifdef __cplusplus
typedef struct _THREADINFO : _W32THREAD
{
#else
typedef struct _THREADINFO
{
W32THREAD;
#endif
PTL ptl;
PPROCESSINFO ppi;
struct _USER_MESSAGE_QUEUE* MessageQueue;
struct tagKL* KeyboardLayout;
struct _CLIENTTHREADINFO * pcti;
struct _DESKTOP* rpdesk;
struct _DESKTOPINFO * pDeskInfo;
struct _CLIENTINFO * pClientInfo;
FLONG TIF_flags;
PUNICODE_STRING pstrAppName;
struct _USER_SENT_MESSAGE *pusmSent;
struct _USER_SENT_MESSAGE *pusmCurrent;
/* Queue of messages sent to the queue. */
LIST_ENTRY SentMessagesListHead; // psmsReceiveList
/* Last message time and ID */
LONG timeLast;
ULONG_PTR idLast;
/* True if a WM_QUIT message is pending. */
BOOLEAN QuitPosted;
/* The quit exit code. */
INT exitCode;
HDESK hdesk;
UINT cPaintsReady; /* Count of paints pending. */
UINT cTimersReady; /* Count of timers pending. */
struct tagMENUSTATE* pMenuState;
DWORD dwExpWinVer;
DWORD dwCompatFlags;
DWORD dwCompatFlags2;
struct _USER_MESSAGE_QUEUE* pqAttach;
PTHREADINFO ptiSibling;
ULONG fsHooks;
struct tagHOOK * sphkCurrent;
LPARAM lParamHkCurrent;
WPARAM wParamHkCurrent;
struct tagSBTRACK* pSBTrack;
/* Set if there are new messages specified by WakeMask in any of the queues. */
HANDLE hEventQueueClient;
/* Handle for the above event (in the context of the process owning the queue). */
PKEVENT pEventQueueServer;
LIST_ENTRY PtiLink;
INT iCursorLevel;
/* Last message cursor position */
POINT ptLast;
 
INT cEnterCount;
/* Queue of messages posted to the queue. */
LIST_ENTRY PostedMessagesListHead; // mlPost
WORD fsChangeBitsRemoved;
WCHAR wchInjected;
UINT cWindows;
UINT cVisWindows;
#ifndef __cplusplus /// FIXME!
LIST_ENTRY aphkStart[NB_HOOKS];
CLIENTTHREADINFO cti; // Used only when no Desktop or pcti NULL.
 
/* ReactOS */
 
/* Thread Queue state tracking */
// Send list QS_SENDMESSAGE
// Post list QS_POSTMESSAGE|QS_HOTKEY|QS_PAINT|QS_TIMER|QS_KEY
// Hard list QS_MOUSE|QS_KEY only
// Accounting of queue bit sets, the rest are flags. QS_TIMER QS_PAINT counts are handled in thread information.
DWORD nCntsQBits[QSIDCOUNTS]; // QS_KEY QS_MOUSEMOVE QS_MOUSEBUTTON QS_POSTMESSAGE QS_SENDMESSAGE QS_HOTKEY
 
LIST_ENTRY WindowListHead;
LIST_ENTRY W32CallbackListHead;
SINGLE_LIST_ENTRY ReferencesList;
ULONG cExclusiveLocks;
#if DBG
USHORT acExclusiveLockCount[GDIObjTypeTotal + 1];
#endif
#endif // __cplusplus
} THREADINFO;

在所有线程刚创建时,它们都是普通线程,可以通过使用_KTHREAD.ServiceTable,可以找到一张表KeServiceDescriptorTable。然而,当线程首次调用Win32k.sys(在0环中实现图形界面API)时,将调用PsConvertToGuiThread函数,该函数执行以下几个主要步骤:

  1. 扩展内核栈大小为64KB,因为普通内核栈只有12KB大小;

  2. 创建一个带有消息队列的结构体,并将其挂接到_KTHREAD结构体Win32Thread成员上;

  3. 将Thread.ServiceTable指向KeServiceDescriptorTableShadow,此时两个表都可见;

  4. 将所需的内存数据映射到当前进程的地址空间中。

总结

  1. 在Windows操作系统中,消息队列存储在0环(内核空间)中,并可以通过KTHREAD.Win32Thread找到。

  2. 并非所有线程都需要消息队列,只有GUI线程才会拥有消息队列。

  3. 每个GUI线程有且只有一个消息队列。

窗口与线程

了解消息队列与线程关系后,我们需要知道消息是从哪里来,又到哪里去,是谁来做这些消息传递的

消息来源

如下图所示,我们可以通过VC++6.0的工具Spy++用于捕捉窗口接收到的消息。当鼠标在窗口上移动、点击或键盘敲击时,就会产生消息。

images/download/attachments/3440655/image2023-10-9_9-48-4.png

除了键盘、鼠标外,消息还来自于其他进程,假设A进程使用了CreateWindow创建窗口,就会获得一个窗口句柄,窗口句柄具有全局特性(所有窗口对象都存在一张公共表中),这意味着一旦其他进程获取到窗口句柄,都可以利用SendMessage或PostMessage函数向A进程创建的窗口发送消息以进行交互。

消息去处

当一个消息产生时,肯定是需要通过消息监控来知道由消息产生了,然后再由监控的程序存储到窗口对应线程的消息队列中。

在Windows上0环(内核空间)Win32k.sys的2个线程分别对鼠标、键盘进行消息的监控,具体的我们可以看下如下这个函数InitInputImpl,在初始化Win32k.sys的服务时,会调用它来创建2个线程:KeyboardThreadMain、MouseThreadMain,也就是鼠标、键盘的监控线程。

images/download/attachments/3440655/image2023-10-9_10-25-34.png

NTSTATUS FASTCALL InitInputImpl(VOID)
{
NTSTATUS Status;
KeInitializeEvent(&InputThreadsStart, NotificationEvent, FALSE);
MasterTimer = ExAllocatePoolWithTag(NonPagedPool, sizeof(KTIMER), TAG_INPUT);
KeInitializeTimer(MasterTimer);
Status = PsCreateSystemThread(&RawInputThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&RawInputThreadId,RawInputThreadMain,NULL);
 
// 键盘输入线程:KeyboardThreadMain
Status = PsCreateSystemThread(&KeyboardThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&KeyboardThreadId,KeyboardThreadMain,NULL);
 
// 鼠标输入线程:MouseThreadMain
Status = PsCreateSystemThread(&MouseThreadHandle,THREAD_ALL_ACCESS,NULL,NULL,
&MouseThreadId,MouseThreadMain,NULL);
 
InputThreadsRunning = TRUE; // TRUE表示现在可以开始读取键盘鼠标输入
KeSetEvent(&InputThreadsStart, IO_NO_INCREMENT, FALSE);
 
return STATUS_SUCCESS;
}

如上代码也就解释了为什么有的时候程序卡死,但鼠标仍然可以移动,这是因为鼠标操作运行在独立的线程中。

消息队列的寻找

如下图所示,在打开了三个窗口的情况下,当鼠标进行点击和移动操作时,操作系统是如何准确地将消息发送给不同窗口所对应的消息队列呢?

images/download/attachments/3440655/Picture1.png

首先,图中不仅有三个窗口,每个进程的窗口内部的按钮、表格等也都是窗口的一部分。因此,一个进程可以拥有多个窗口,但这些窗口只能属于同一个进程

接着,我们可以看一下创建窗口的API到底是怎么调用的:CreateWindow - CreateWindowA/W - CreateWindowEx - _VerNtUserCreateWindowEx - _NtUserCreateWindow - _NtUserCreateWindow。

我们会发现创建窗口最终还是会通过系统调用到0环去,根据系统调用号(从0位开始,第12位为1)我们就知道它最终调用的就是Win32k.sys提供的服务。

images/download/attachments/3440655/image2023-10-9_10-40-45.png

因此,窗口就像进程和线程一样,实际上是一个处于0环的结构。窗口也有与之对应的内核结构体,即_WINDOW_OBJECT。(该结构体没有通过符号表导出,可以查看ReactOS的代码来查看该结构体)

在窗口对象_WINDOW_OBJECT中,存在一个名为pti的成员,其类型为_PTHREADINFO,指向_THREADINFO结构体。这个THREADINFO结构体正是前文提到的_KTHREAD.Win32Thread相关联的结构体。通过这种方式,就可以将线程与窗口联系在一起

在初始状态下,_HREAD.Win32Thread指向的值为空。然而,当线程调用Win32k.sys中的函数创建一个窗口时,_KTHREAD.Win32Thread将指向_THREADINFO结构体,从而将该线程由普通线程转变为GUI线程。此时,窗口对象对应的内核结构体WINDOW_OBJECT中的pti成员也会指向这个_THREADINFO结构体。而消息队列则位于THREADINFO结构体中,这就使得窗口可以访问所属线程的消息队列

消息的接收

窗口的创建过程

在3环创建窗口时,首先需要创建和注册一个窗口类对象,并注册和设置窗口的样式和过程函数。然后,通过调用CreateWindow函数来创建窗口。

images/download/attachments/3440655/image2023-10-9_20-48-14.png

实质上,CreateWindow只是一个3环的接口,最终调用的是位于Win32k.sys中的0环函数。在0环中,会为窗口创建一个名为_WINDOW_OBJECT的结构体,每个窗口都有一个这样的结构体。

消息队列的结构

当线程调用Win32k.sys提供的图形界面函数时,线程结构体_KTHREAD中的成员Win32Thread会指向一个名为_THREADINFO的结构体。在该结构体中有一个成员MessageQueue,即消息队列,其中包含7组队列(仅适用于旧版ReactOS),用于处理不同类型的消息。如下3个是比较常见的消息队列:

  1. SentMessagesListHead:接到SendMessage发来的消息。

  2. PostedMessagesListHead:接到PostMessage发来的消息。

  3. HardwareMessagesListHead:接到鼠标、键盘的消息。

GetMessage

我们在窗口创建后需要使用GetMessage、TranslateMessage和DispatchMessage来获取、转换和分发消息。如下代码所示,我们通常会这样去写:

MSG msg;
BOOL bRet;
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
// handle the error and possibly exit
sprintf(szOutBuff, "Error: %d", GetLastError());
OutputDebugString(szOutBuff);
return 0;
}
else
{
// 转换消息
TranslateMessage(&msg);
// 分发消息:就是给系统调用窗口处理函数
DispatchMessage(&msg);
}
}

GetMessage表面上的意思是获取消息,但实际功能不仅限于此,我们首先来看下该函数的语法:

BOOL GetMessage(
LPMSG lpMsg, // 返回从队列中获取的消息
HWND hWnd, // 过滤条件:指定接收消息的窗口
UINT wMsgFilterMin, // 过滤条件
UINT wMsgFilterMax // 过滤条件
);

GetMessage函数有4个参数,其后3个参数是过滤条件,第一个条件是用于指定接收消息的窗口。而第一个参数则是从消息队列中获取的消息。

GetMessage函数通过循环判断是否存在该窗口的消息,如果有,将消息存储到MSG结构体中,并从原始消息队列中删除该消息。然后,将消息传递给TranslateMessage和DispatchMessage函数进行处理。

我们可以在代码中,将TranslateMessage和DispatchMessage注释掉,来看一下没有这个两个函数进行消息的转换和分发,我们的窗口过程函数WindowProc是否仍然可以执行,接着另外一个程序发送消息:

images/download/attachments/3440655/image2023-10-9_21-49-46.png

images/download/attachments/3440655/image2023-10-9_21-50-14.png

我们运行这个窗口程序后再运行发送消息的程序,会发现窗口成功接收到了消息,并执行了对应的处理函数:

images/download/attachments/3440655/image2023-10-9_21-51-53.png

DispatchMessage函数用于将消息转发到窗口过程函数,以触发相应的处理逻辑。但是在这里我们通过实验发现GetMessage函数也会对消息进行处理

GetMessage函数调用的是Win32k.sys中的NtUserGetMessage函数,在该函数内部有如下的大致逻辑:

do
{
// 先判断SentMessagesListHead是否有消息,如果有就处理掉
do
{
....
KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
....
} while (SentMessagesListHead != NULL)
// 依次判断其他的6个队列,里面如果有消息就返回,没有则继续
} while(其他队列 != NULL)

在一个内部的do...while循环中,NtUserGetMessage首先会判断SentMessagesListHead中是否存在消息,如果有,则调用窗口回调函数处理它。然后,在处理完SentMessagesListHead中的所有消息之后,才会考虑其他六个队列中的消息。在这种情况下,将不会处理这些消息,而是直接将它们返回。因此,GetMessage也会对消息进行处理,但只会处理SentMessagesListHead中的消息

那么也就表示如果我们刚刚发送消息的代码使用的函数从SendMessage变成PostMessage,在没有TranslateMessage和DispatchMessage函数的情况下,GetMessage函数并不会处理这个消息。

SendMessage与PostMessage

接着我们来看一下这两个函数:SendMessage与PostMessage,这两者都是用于发送消息的,但不同之处在于前者是同步发送消息,后者是异步发送消息。

使用SendMessage函数发送消息,当GetMessage函数接收消息时,它会进入0环遍历SentMessagesListHead消息队列以检查是否有消息。如果有消息,则会进行处理;如果没有消息,则会立即返回。在有消息的情况下,必须完全处理完消息才能返回,否则SendMessage函数会一直阻塞在这里,直到接收到对方的执行结果并返回。

如下图所示,我们的消息发送程序使用SendMessage函数发送消息,当窗口程序的处理没有结束,即弹窗没有关闭,则消息发送程序一直停留在哪里,也不会关闭。

images/download/attachments/3440655/image2023-10-9_21-51-53.png

相比之下,当我们使用PostMessage函数发送消息时,GetMessage函数只是接收该消息,而不会进行处理。消息的处理由TranslateMessage和DispatchMessage函数负责。PostMessage函数不会等待对方返回处理结果,一旦发送完成就立即结束自身程序

消息的转换

我们再来看一下消息的转换,即TranslateMessage函数,该函数是针对键盘类消息的一种优化。如果没有使用它,键盘消息将属于WM_KEYDOWN类型,并以ASCII对应的十进制值打印出来。但是,通过使用它,键盘消息将被转换为WM_CHAR类型,并打印出按下的键盘符号。因此,TranslateMessage函数的使用与否对结果影响不大,它只是对消息类型进行转换。

我们可以添加对WM_KEYDOWN、WM_CHAR消息的处理,并且注释掉TranslateMessage函数和不注释掉进行对比,就会发现这一特征,需要主要的是在实际使用过程中如果你使用了TranslateMessage函数就只需要添加对WM_KEYDOWN消息的处理,不需要再对WM_CHAR消息进行处理,否则就会造成重复处理:

images/download/attachments/3440655/image2023-10-9_23-47-22.png

消息的分发

所谓消息的分发,核心点在于DispatchMessage函数,它会根据窗口句柄调用相关的窗口过程函数。在上一节的学习中我们了解到GetMessage函数除了接收消息还会处理SendMessage发送过来的消息,也就是SentMessagesListHead消息队列中的消息。

也就表示其他消息是交由DispatchMessage函数来处理的,DispatchMessage函数最终调用的是Win32k.sys中的NtUserDispatchMessage函数。该函数主要做了以下两件事情:

  1. 根据窗口句柄找到窗口对象_WINDOW_OBJECT;(UserGetWindowObject函数)

  2. 根据窗口对象获取窗口过程函数,并通过0环发起调用。

如下图所示,我们可以看见DispatchMessage函数与GetMessage函数(NtUserGetMessage函数)一样,最终都是调用KeUserModeCallback的回调函数进入3环,再去调用窗口过程函数。

images/download/attachments/3440655/image2023-10-9_23-21-19.png

并且我们会发现即使DispatchMessage函数的唯一参数MSG结构体,发挥了巨大的作用,该结构体里有窗口句柄、消息类型、消息参数等内容,具体的我们看如下定义。

typedef struct tagMSG {
HWND hwnd; // 窗口句柄
UINT message; // 消息类型
WPARAM wParam; // 消息参数
LPARAM lParam; // 消息参数
DWORD time; // 消息时间戳
POINT pt; // 鼠标坐标
#ifdef _MAC
DWORD lPrivate;
#endif
} MSG, *PMSG, NEAR *NPMSG, FAR *LPMSG;

默认的消息处理函数

消息无时不刻都在产生,我们在自定义窗口过程函数时候只需要对关注的消息进行处理,其他的我们都可以交给默认的消息处理函数,即DefWindowProc。

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
// 调用一个默认的消息处理函数,关闭、最小化、最大化都是由默认消息处理函数处理的
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}

内核回调机制

窗口过程的调用者

我们知道GetMessage函数再处理SentMessagesListHead消息队列中的消息时,以及DispatchMessage在处理其他消息队列中的消息时,都会调用窗口过程函数。除此之外内核代码也会调用窗口过程函数

我们可以注释如下图所示的代码,并且在窗口过程函数中加入一个输出,再编译运行窗口会发现,即使没有GetMessage、DispatchMessage函数,窗口过程函数仍然会被执行。

images/download/attachments/3440655/image2023-10-10_9-50-36.png

那么为什么内核代码需要调用窗口过程函数呢?假设有一个需求,当我们想要在窗口创建时去做一些事情时,由于窗口创建时是肯定没有消息的,因此GetMessage函数也没法获取到消息,DispatchMessage函数自然也就无法分发消息

所以只能借助于内核代码来调用回调函数,这里实际上就是CreateWindow发挥的作用,其进入0环调用的NtUserCreateWindowEx函数,该函数在窗口创建之前通过调用内核回调函数向窗口发送消息,这些消息不会进入消息队列,而是直接发送给窗口过程函数,消息类型为WM_CREATE

0到3跨环调用

从0环调用3环函数的有三种方法:

用户APC的执行:用户APC(Asynchronous Procedure Call)是一种用于异步执行用户模式代码的机制。在这种情况下,内核可以将用户模式函数作为一个APC请求提交给目标线程,并在目标线程处于用户模式执行时,将该函数插入到目标线程的执行流中。

用户异常的处理:当内核调试器和用户调试器均不存在或不处理时,如果发生用户模式异常,处理流程将从Ring 0(内核模式)转移到Ring 3(用户模式)。在这种情况下,操作系统会将异常传递给目标进程的异常处理程序,由用户模式代码处理该异常。

内核回调:内核回调是指在Ring 0的代码中调用窗口过程函数。这种调用是通过内核提供的机制实现的,允许Ring 0的代码向特定窗口的过程函数发送消息。通过这种方式,Ring 0的代码可以与用户模式的窗口过程进行交互,以实现特定的功能或处理特定事件。这种机制通常由操作系统提供,如SendMessage或PostMessage函数。

KeUserModeCallback

内核回调机制中,0环是通过KeUserModeCallback函数来调用3环函数的。在之前我们的分析NtUserDispatchMessage函数的调用时(NtUserDispatchMessage->IntDispatchMessage->co_IntCallWindowProc->KeUserModeCallback)也知道它会去调用KeUserModeCallback函数。

images/download/attachments/3440655/image2023-10-9_23-21-19.png

KeUserModeCallback函数的语法格式如下:

NTSTATUS NTAPI KeUserModeCallback(
IN ULONG RoutineIndex,
IN PVOID Argument,
IN ULONG ArgumentLength,
OUT PVOID * Result,
OUT PULONG ResultLength
)

其有5个参数,其中Argument是提供参数的,其中包含窗口过程函数、窗口句柄、消息类型、消息参数等等内容,这些参数也就对上在3环窗口过程函数所需要的参数了

images/download/attachments/3440655/image2023-10-10_10-20-41.png

RoutineIndex参数是一个索引,它与回到3环的落脚点有关,在当前代码里它是一个宏,点进去查看就会发现实际上是一个数字,还有其他许多别的索引值。

images/download/attachments/3440655/image2023-10-10_10-27-33.png

既然有索引,也就表示肯定有张表,可以通过索引在表中找到回到3环的落脚点。这张表就叫做回调函数表,其包含多个回调函数,根据不同的索引值RoutineIndex,KeUserModeCallback就可以调用表内不同的回调函数。

这些回调函数均由USER32.dll提供,回调函数表我们可以这样去寻找:FS:[0]->TEB→TEB.PEB(0x30偏移位)→PEB.KernelCallbackTable(0x2C偏移位),其实也就是PEB结构体中的成员KernelCallbackTable。

如下图所示随便找个程序放大OD里都可以找到回调函数表,回调函数实现的功能我们也可以推测出个大概,从Argument中取窗口过程函数地址,再将其他几个参数作为传参调用窗口过程函数

images/download/attachments/3440655/image2023-10-10_10-53-31.png