Windows消息机制学习笔记(四)—— 内核回调机制
要点回顾
1)GetMessage不仅能够取出消息,还能处理SentMessagesListHead队列中的消息
2)DispatchMessage用于处理其它队列中的消息。
内核调用
描述:窗口过程函数除了会在消息循环中被调用,一些0环的代码也可以直接发起调用。
例如:窗口初始化时、窗口创建时、窗口显示时。
目的:使窗口在被初始化、被创建、被显示时,用户能够有机会做一些事情。
实验1:理解内核调用
第一步:编译并运行以下代码
注意:运行前在while(GetMessage(&msg, NULL, 0, 0))
这行设置断点。
#include <stdio.h>
#include <windows.h>
#define DPRINTF_BUF_SZ 1024
void OutputDebugStringf(char *fmt, ...)
{
#ifdef _DEBUG
va_list args;
char buf[DPRINTF_BUF_SZ];
va_start(args, fmt);
vsprintf(buf, fmt, args);
OutputDebugString(buf);
#endif
}
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
switch(uMsg)
{
case WM_DESTROY:
{
ExitProcess(0);
return 0;
}
}
OutputDebugStringf("消息类型:%x \n", uMsg);
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
){
//窗口的类名
TCHAR className[] = "My First Window";
//创建一个自己的窗口
WNDCLASS wndclass = {0};
wndclass.hbrBackground = (HBRUSH)COLOR_MENU;
wndclass.lpfnWndProc = WindowProc;
wndclass.lpszClassName = className;
wndclass.hInstance = hInstance;
//注册
RegisterClass(&wndclass);
//创建窗口
HWND hwnd = CreateWindow(
className,
TEXT("测试窗口"),
WS_OVERLAPPEDWINDOW,
10,
10,
600,
300,
NULL,
NULL,
hInstance,
NULL);
if(hwnd == NULL)
return 0;
//显示窗口
ShowWindow(hwnd, SW_SHOW);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
//TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
运行结果:
第二步:修改窗口过程函数,重新运行
注意:仍然在while(GetMessage(&msg, NULL, 0, 0))
这行设置断点。
新窗口过程函数:
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
switch(uMsg)
{
case WM_CREATE:
{
MessageBoxA(NULL, "窗口被创建了", "CREATE", MB_OK);
return 0;
}
case WM_DESTROY:
{
ExitProcess(0);
return 0;
}
}
OutputDebugStringf("消息类型:%x \n", uMsg);
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
运行结果:
不难发现,程序并未进入消息循环,却调用了窗口过程函数,输出了消息与弹窗,说明消息不是只有当调用GetMessage与DispatchMessage时才处理。
KeUserModeCallback
描述:
1)从0环调用3环函数的几种方式:
- APC
- 异常
内核回调
APC与异常回三环的落脚点比较单一,而消息机制需要处理的情况较多,不能使用同一个逻辑进行处理,因此消息机制使用内核回调调用三环函数。
2)回到3环的落脚点:
- APC:
ntdll!KiUserApcDispatcher
- 异常:
ntdll!KiUserExceptionDispatcher
3)内核回调在3环的落脚点:KeUserModeCallback
PEB+0x2C指向回调函数地址表。
4)凡是有窗口的程序就有可能0环直接调用3环的程序。
声明:
NTSTATUS
NTAPI
KeUserModeCallback(
IN ULONG FunctionID, //索引,指向三环落脚点
IN PVOID InputBuffer, //包含窗口回调函数与相关重要信息
IN ULONG InputLength,
OUT PVOID *OutputBuffer,
OUT PULONG OutputLength
);
FunctionID索引列表:
#define USER32_CALLBACK_WINDOWPROC (0)
#define USER32_CALLBACK_SENDASYNCPROC (1)
#define USER32_CALLBACK_LOADSYSMENUTEMPLATE (2)
#define USER32_CALLBACK_LOADDEFAULTCURSORS (3)
#define USER32_CALLBACK_HOOKPROC (4)
#define USER32_CALLBACK_EVENTPROC (5)
#define USER32_CALLBACK_LOADMENU (6)
#define USER32_CALLBACK_CLIENTTHREADSTARTUP (7)
#define USER32_CALLBACK_MAXIMUM (7)
实验2:在OD中查看回调函数地址表
第一步:加载程序
第二步:在内存中查看TEB
第三步:查看PEB数据
PEB位于TIB+0x30位置
第四步:查看回调地址表
回调地址表位于PEB+0x2C位置
总结
- 不是所有消息都是在进入消息循环后由GetMessage和DispatchMessage进行处理的,部分内核代码也能够调用窗口处理函数。
- 内核代码通过KeUserModeCallback调用窗口处理函数,由第一个参数FunctionID决定回到三环时的落脚点。
- FunctionID的值为回调地址表的索引,回调地址表位于PEB+0x2C位置,每个值为一个函数地址。