How does reciprocal SendMessage-ing between two applications work?
假设我有 2 个应用程序,A 和 B。它们每个都在主线程中创建一个窗口,并且没有其他线程。
当应用程序A的窗口的"关闭"按钮被按下时,会发生以下情况:
应用程序 A 接收到
1 2 | DestroyWindow(hWnd_A); return 0; |
在
1 2 3 | SendMessage(hWnd_B, WM_REGISTERED_MSG, 0, 0); //key line!! PostQuitMessage(0); return 0; |
在
1 2 | SendMessage(hWnd_A, WM_ANOTHER_REGISTERED_MSG, 0, 0); return 0; |
在
1 2 | OutputDebugString("Cannot print this"); return 0; |
就是这样。
从 MSDN 中,我读到当消息发送到另一个线程创建的窗口时,调用线程被阻塞,它只能处理非排队消息。
现在,由于上面的代码可以工作并且没有挂起,我猜想从应用程序 B(第 3 点)对
这也被以下事实证明:将
那么,我的问题是:
-
实际上,非排队消息只是简单地直接调用进程 B 中
SendMessage 进行的窗口过程吗? -
SendMessage 如何知道何时发送排队或非排队的消息?
更新
不过,我还是不明白 A 是如何处理
有什么见解吗?
给读者的建议
我建议阅读 Adrian 的回答作为 RbMm 的介绍,它遵循相同的思路,但更详细。
所描述的行为确实很有效。
How does
SendMessage know when to send queued or non-queued
messages?
来自非排队消息
Some functions that send nonqueued messages are ... SendMessage
...
所以
并来自
However, the sending thread will process incoming nonqueued messages
while waiting for its message to be processed.
这意味着可以在
当我们调用
1 2 3 4 5 6 7 8 9 10 11 | NTSYSCALLAPI NTSTATUS NTAPI KeUserModeCallback ( IN ULONG RoutineIndex, IN PVOID Argument, IN ULONG ArgumentLength, OUT PVOID* Result, OUT PULONG ResultLenght ); |
这是导出但未记录的 api。但是它一直被 win32k.sys 用于调用窗口过程。这个 api 是如何工作的?
首先它在当前内核栈帧之下分配额外的内核栈帧。而不是保存用户模式堆栈指针并在其下方复制一些数据(参数)。最后我们从内核退出到用户模式,但没有指出内核被调用的地方,而是特殊的(从 ntdll.dll 导出)函数 -
1 2 3 4 5 6 7 | void KiUserCallbackDispatcher ( IN ULONG RoutineIndex, IN PVOID Argument, IN ULONG ArgumentLength ); |
并且堆栈位于堆栈指针下方,我们从这里提前进入内核。
的特殊 api
1 2 3 4 5 6 7 8 9 | __declspec(noreturn) NTSTATUS NTAPI ZwCallbackReturn ( IN PVOID Result OPTIONAL, IN ULONG ResultLength, IN NTSTATUS Status ); |
这个 api(如果没有错误)永远不能返回 - 在内核端 - 分配的内核帧被释放,我们返回到
真的是如何在调用
1 2 3 4 5 6 7 8 9 10 11 12 13 | GetMessage... --- kernel mode --- KeUserModeCallback... push additional kernel stack frame --- user mode --- (stack below point from where GetMessage enter kernel) KiUserCallbackDispatcher WindowProc ZwCallbackReturn -- kernel mode -- pop kernel stack frame ...KeUserModeCallback --- user mode --- ...GetMessage |
与阻塞
所以当 thread_A 通过
现在 thread_B 从
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | thread_A thread_B ---------------------------------------------------- GetMessage... wait(event_B) SendMessage(WM_B)... set(event_B) wait(event_A) begin process WM_B... KeUserModeCallback... KiUserCallbackDispatcher WindowProc(WM_B)... SendMessage(WM_A)... set(event_A) wait(event_B) begin process WM_A... KeUserModeCallback... KiUserCallbackDispatcher WindowProc(WM_A)... ...WindowProc(WM_A) ZwCallbackReturn ...KeUserModeCallback set(event_B) ...end process WM_A wait(event_A) ...SendMessage(WM_A) ...WindowProc(WM_B) ZwCallbackReturn ...KeUserModeCallback set(event_A) ...end process WM_B wait(event_B) ...SendMessage(WM_B) ...GetMessage |
还请注意,当我们处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 | ULONG WINAPI ThreadProc(PVOID hWnd); struct WNDCTX { HANDLE hThread; HWND hWndSendTo; }; LRESULT CALLBACK WindowProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) { WNDCTX* ctx = reinterpret_cast<WNDCTX*>(GetWindowLongPtrW(hWnd, GWLP_USERDATA)); switch (uMsg) { case WM_NULL: DestroyWindow(hWnd); break; case WM_APP: DbgPrint("%x:%p>WM_APP:(%p, %p)\ ", GetCurrentThreadId(), _AddressOfReturnAddress(), wParam, lParam); if (lParam) { DbgPrint("%x:%p>Send WM_APP(0)\ ", GetCurrentThreadId(), _AddressOfReturnAddress()); LRESULT r = SendMessage((HWND)lParam, WM_APP, 0, 0); DbgPrint("%x:%p>SendMessage=%p\ ", GetCurrentThreadId(), _AddressOfReturnAddress(), r); PostMessage(hWnd, WM_NULL, 0, 0); } else { DbgPrint("%x:%p>Cannot print this\ ", GetCurrentThreadId(), _AddressOfReturnAddress()); } return GetCurrentThreadId(); case WM_DESTROY: if (HANDLE hThread = ctx->hThread) { WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); } if (HWND hWndSendTo = ctx->hWndSendTo) { DbgPrint("%x:%p>Send WM_APP(%p)\ ", GetCurrentThreadId(), _AddressOfReturnAddress(), hWnd); LRESULT r = SendMessage(hWndSendTo, WM_APP, 0, (LPARAM)hWnd); DbgPrint("%x:%p>SendMessage=%p\ ", GetCurrentThreadId(), _AddressOfReturnAddress(), r); } break; case WM_NCCREATE: SetLastError(0); SetWindowLongPtr(hWnd, GWLP_USERDATA, reinterpret_cast<LONG_PTR>(reinterpret_cast<CREATESTRUCT*>(lParam)->lpCreateParams)); if (GetLastError()) { return 0; } break; case WM_CREATE: if (ctx->hWndSendTo) { return -1; } if (ctx->hThread = CreateThread(0, 0, ThreadProc, hWnd, 0, 0)) { break; } return -1; case WM_NCDESTROY: PostQuitMessage(0); break; } return DefWindowProc(hWnd, uMsg, wParam, lParam); } static const WNDCLASS wndcls = { 0, WindowProc, 0, 0, (HINSTANCE)&__ImageBase, 0, 0, 0, 0, L"lpszClassName" }; ULONG WINAPI ThreadProc(PVOID hWndSendTo) { WNDCTX ctx = { 0, (HWND)hWndSendTo }; CreateWindowExW(0, wndcls.lpszClassName, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, &ctx); return 0; } void DoDemo() { DbgPrint("%x>test begin\ ", GetCurrentThreadId()); if (RegisterClassW(&wndcls)) { WNDCTX ctx = { }; if (CreateWindowExW(0, wndcls.lpszClassName, 0, 0, 0, 0, 0, 0, HWND_MESSAGE, 0, 0, &ctx)) { MSG msg; while (0 < GetMessage(&msg, 0, 0, 0)) { DispatchMessage(&msg); } } UnregisterClassW(wndcls.lpszClassName, (HINSTANCE)&__ImageBase); } DbgPrint("%x>test end\ ", GetCurrentThreadId()); } |
我得到了下一个输出:
1 2 3 4 5 6 7 8 9 | d94>test begin 6d8:00000008884FEFD8>Send WM_APP(0000000000191BF0) d94:00000008880FF4F8>WM_APP:(0000000000000000, 0000000000191BF0) d94:00000008880FF4F8>Send WM_APP(0) 6d8:00000008884FEB88>WM_APP:(0000000000000000, 0000000000000000) 6d8:00000008884FEB88>Cannot print this d94:00000008880FF4F8>SendMessage=00000000000006D8 6d8:00000008884FEFD8>SendMessage=0000000000000D94 d94>test end |
thread_B 在
上递归调用时最有趣的堆栈跟踪
Still, I do not understand how does A process WM_ANOTHER_REGISTERED_MSG. What I would expect is that when that message is sent, A's thread should be waiting for its call to
SendMessage to return.
A 中的
当
但是当消息跨越线程边界时,就没有那么简单了。它变得像一个客户端-服务器应用程序。
目标线程最终(我们希望)到达一个屈服点,它检查该信号,获取消息并处理它。然后目标线程发出信号表明它已经完成了工作。
原始线程看到"I\\'m done!"信号并返回结果值。对于
几个 Windows API 调用是"让步点",用于检查是否有消息从另一个线程发送到当前线程。最著名的是
这是 A 的调用堆栈的一部分,当它从 B 接收到
1 2 3 4 5 6 7 8 9 10 11 12 | A.exe!MyWnd::OnFromB(unsigned int __formal, unsigned int __formal, long __formal, int & __formal) A.exe!MyWnd::ProcessWindowMessage(HWND__ * hWnd, unsigned int uMsg, unsigned int wParam, long lParam, long & lResult, unsigned long dwMsgMapID) A.exe!ATL::CWindowImplBaseT<ATL::CWindow,ATL::CWinTraits<114229248,262400> >::WindowProc(HWND__ * hWnd, unsigned int uMsg, unsigned int wParam, long lParam) atlthunk.dll!AtlThunk_Call(unsigned int,unsigned int,unsigned int,long) atlthunk.dll!AtlThunk_0x00(struct HWND__ *,unsigned int,unsigned int,long) user32.dll!__InternalCallWinProc@20() user32.dll!UserCallWinProcCheckWow() user32.dll!DispatchClientMessage() user32.dll!___fnDWORD@4() ntdll.dll!_KiUserCallbackDispatcher@12() user32.dll!SendMessageW() A.exe!MyWnd::OnClose(unsigned int __formal, unsigned int __formal, long __formal, int & __formal) |
你可以看到