两个应用程序之间的相互 SendMessage-ing 如何工作?

How does reciprocal SendMessage-ing between two applications work?

假设我有 2 个应用程序,A 和 B。它们每个都在主线程中创建一个 window,并且没有其他线程。

当按下应用程序 A window 的 "close" 按钮时,会发生以下情况:

  1. 应用程序 A 收到一条 WM_CLOSE 消息并像这样处理它:

    DestroyWindow(hWnd_A);
    return 0;
    
  2. WM_DESTROY 上,应用程序 A 的行为类似于:

    SendMessage(hWnd_B, WM_REGISTERED_MSG, 0, 0); //key line!!
    PostQuitMessage(0);
    return 0;
    
  3. WM_REGISTERED_MSG 应用程序 B 运行:

    SendMessage(hWnd_A, WM_ANOTHER_REGISTERED_MSG, 0, 0);
    return 0;
    
  4. WM_ANOTHER_REGISTERED_MSG 上应用程序 A 运行:

    OutputDebugString("Cannot print this");
    return 0;
    

就是这样。

MSDN看到,当消息发送到另一个线程创建的window时,调用线程被阻塞,它只能处理非队列消息。

现在,由于上面的代码有效并且没有挂起,我猜应用程序 B 对 SendMessage 的调用(第 3 点)向应用程序 A 的 window 过程发送了一个非排队消息,在应用程序 B 的主线程 的上下文中处理它 。实际上,在第 4 点 OutputDebugString 中没有显示任何调试输出。

这一点也得到了证明,即在第 2 点的 key line 中将 SendMessage 替换为 SendMessageTimeoutSMTO_BLOCK 标志,使得整个事情实际上是阻塞的. (参见 SendMessagedocumentation

那么,我的问题是:

  • 实际上,非队列消息只是简单地直接调用进程 B 中 SendMessage 对 window 过程的简单直接调用?

  • SendMessage如何知道何时发送排队或非排队的消息?


更新

还是不明白A是怎么处理的WM_ANOTHER_REGISTERED_MSG。我期望的是,当发送该消息时,A 的线程应该等待其对 SendMessage 到 return.

的调用

有什么见解吗?


给读者的建议

我建议阅读 Adrian 的回答作为 RbMm 的介绍,它遵循相同的思路,但更详细。

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 中的 SendMessage 等待它发送的消息(从 A 到 B)完成,但是,在等待期间,它能够发送消息从其他线程发送到此线程。

当在同一线程上为 window 调用 SendMessage 时,我们将其视为最终导致目标 windowproc 并最终 returns 给来电者。

但是当消息跨越线程边界时,就没那么简单了。它变得像一个客户端-服务器应用程序。 SendMessage 打包消息并通知目标线程它有消息要处理。到那时,它会等待。

目标线程最终(我们希望)到达一个让步点,在该点检查该信号、获取消息并处理它。然后目标线程发出信号,表明它已完成工作。

原始线程看到 "I'm done!" 信号和 returns 结果值。对于 SendMessage 的调用者来说,它看起来只是一个函数调用,但实际上它被编排为将消息编组到另一个线程并将结果编组回来。

几个 Windows API 调用是 "yield points," 检查是否有消息从另一个线程发送到当前线程的地方。最著名的是 GetMessagePeekMessage,但某些类型的等待——包括 SendMessage 内的等待——也是屈服点。正是这个让步点,使得 A 可以在等待 B 处理完第一条消息的同时响应 B 发回的消息。

这是 A 在收到 B 返回的 WM_ANOTHER_REGISTERED_MSG 时(第 4 步)调用堆栈的一部分:

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)

您可以看到 OnClose 仍然在 SendMessageW 中,但是嵌套在其中,它从 B 获取回调消息并将其路由到A的window程序。

所描述的行为确实有效。

How does SendMessage know when to send queued or non-queued messages?

来自 Nonqueued Messages

Some functions that send nonqueued messages are ... SendMessage ...

所以SendMessage总是发送非队列消息。

来自SendMessage 文档:

However, the sending thread will process incoming nonqueued messages while waiting for its message to be processed.

这意味着window过程可以在SendMessage调用中被调用。并处理从另一个线程通过 SendMessage 发送的传入消息。这是如何实施的?

当我们向另一个线程window调用SendMessage消息时,它进入内核模式。内核模式总是记住用户模式堆栈指针。我们切换到内核堆栈。当我们 return 从内核模式到用户模式时 - 内核通常 return 回到点,从用户模式调用它的地方并保存堆栈。但存在和例外。其中之一:

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

这是导出但未记录的 api。然而,它一直被 win32k.sys 用于调用 window 过程。 api 是如何工作的?

首先,它在当前栈帧下面分配额外的内核栈帧。而不是获取保存的用户模式堆栈指针并在其下方复制一些数据(参数)。最后我们从内核退出到用户模式,但不是指向,从调用内核的地方而是为了特殊(从 ntdll.dll 导出)函数 -

void
KiUserCallbackDispatcher
(
    IN ULONG RoutineIndex,
    IN PVOID Argument,
    IN ULONG ArgumentLength
);

并且堆栈在下方 堆栈指针,我们从那里提前进入内核。 KiUserCallbackDispatcher 调用 RtlGetCurrentPeb()->KernelCallbackTable[RoutineIndex](Argument, ArgumentLength) - 通常这是 user32.dll 中的某个函数。该函数已经调用了相应的 window 过程。从 window 过程我们可以回调内核 - 因为 KeUserModeCallback 分配额外的内核帧 - 我们将进入这个帧内的内核而不是破坏以前的。当 window 过程 return - 再次特殊 api 调用

__declspec(noreturn)
NTSTATUS
NTAPI
ZwCallbackReturn
(
    IN PVOID Result OPTIONAL,
    IN ULONG ResultLength,
    IN NTSTATUS Status
);

这个api(如果没有错误)绝不能return - 在内核端 - 分配的内核帧被取消分配,我们return到之前的内核堆栈里面KeUserModeCallback。所以我们最终 return 从 KeUserModeCallback 被调用的地方开始。然后我们回到用户模式,正好从我们调用内核的地方开始,在同一个堆栈上。

window程序是如何在里面调用调用GetMessage的?正是通过这一点。呼叫流程是:

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

与阻止 SendMessage 完全相同。

所以当thread_A发送message_1thread_B 通过 SendMessage - 我们进入内核,信号 gui event_B,在 thread_B[=144= 】 等着。并开始等待当前线程的 gui event_A。如果 thread_B 执行消息检索代码(调用 GetMessagePeekMessageKeUserModeCallback[=192= 中调用]。结果执行了 window 程序。这里它调用 SendMessage 发送一些 message_2thread_A 回来。结果我们设置 event_A 等待 thread_A 并开始等待 event_Bthread_A会被唤醒并呼叫KeUserModeCallback。 it Window 程序将与此消息一起调用。当它 return(假设这次我们不再调用 SendMessage)时,我们再次发回信号 event_B 并开始等待 event_A。 现在 thread_B return 来自 SendMessage 然后 return 来自 window 过程 - 最终处理原始 message_1。将 event_A 设置。 thread_ASendMessage 觉醒并 return。接下来是呼叫流程:

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

还请注意,当我们处理 WM_DESTROY 消息时 - window 仍然有效并调用处理传入消息。我们可以实现下一个演示:首先我们不需要 2 个进程。具有 2 个线程的单进程绝对足够了。和这里不需要的特殊注册消息。为什么不使用 say WM_APP 作为测试消息?

  1. thread_A 从自己 WM_CREATE 创建 thread_B 并传递自己的 window 处理它。
  2. thread_B 创建自己 window,但是在 WM_CREATE 上只是 return -1(因为创建失败 window)
  3. thread_B 来自 WM_DESTROY 调用 SendMessage(hwnd_A, WM_APP, 0, hwnd_B)(将自身 hwnd 作为 lParam)
  4. thread_A 获得 WM_APP 并调用 SendMessage(hwnd_B, WM_APP, 0, 0)
  5. thread_B 得到了 WM_APP(所以 WindowProc 被递归调用,在下面的堆栈上 WM_DESTROY
  6. thread_B 打印 "Cannot print this" 和 return 自己的 ID 到 thread_A
  7. thread_A return从呼叫 SendMessage 和 return 自己的 ID 到 thread_B
  8. thread_B return从 SendMessage 内部调用 WM_DESTROY

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)\n", GetCurrentThreadId(), _AddressOfReturnAddress(), wParam, lParam);

        if (lParam)
        {
            DbgPrint("%x:%p>Send WM_APP(0)\n", GetCurrentThreadId(), _AddressOfReturnAddress());
            LRESULT r = SendMessage((HWND)lParam, WM_APP, 0, 0);
            DbgPrint("%x:%p>SendMessage=%p\n", GetCurrentThreadId(), _AddressOfReturnAddress(), r);
            PostMessage(hWnd, WM_NULL, 0, 0);
        }
        else
        {
            DbgPrint("%x:%p>Cannot print this\n", 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)\n", GetCurrentThreadId(), _AddressOfReturnAddress(), hWnd);
            LRESULT r = SendMessage(hWndSendTo, WM_APP, 0, (LPARAM)hWnd);
            DbgPrint("%x:%p>SendMessage=%p\n", 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\n", 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\n", GetCurrentThreadId());
}

我得到下一个输出:

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

递归调用 WM_APP

thread_B 最有趣的堆栈跟踪