繁体   English   中英

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

[英]How does reciprocal SendMessage-ing between two applications work?

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

当按下应用程序A窗口的“关闭”按钮时,将发生以下情况:

  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上 ,我读到将消息发送到另一个线程创建的窗口时,调用线程被阻止,并且它只能处理非排队消息。

现在,由于上述代码可以正常工作并且不会挂起,我猜想从应用程序B调用SendMessage (第3点)会将非排队消息发送到应用程序A的窗口过程,该过程在应用程序B的主线程的上下文中进行处理。 实际上,在第4点中没有显示带有OutputDebugString调试输出。

这也得到了以下事实的证明:在点2的key line ,用带有SMTO_BLOCK标志的SendMessageTimeout替换SendMessage会使整个事情实际上被阻塞。 (请参阅SendMessage 文档

然后,我的问题是:

  • 实际上,非排队消息仅仅是对进程B中SendMessage进行的窗口过程的简单直接调用吗?

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


UPDATE

仍然,我不明白A如何处理WM_ANOTHER_REGISTERED_MSG 我期望的是,当发送该消息时,A的线程应该正在等待其对SendMessage调用返回。

有见识吗?


读者建议

我建议阅读Adrian的答案,作为对RbMm答案的介绍,该答案遵循相同的思路,但会更加详细。

所描述的行为确实很好。

SendMessage如何知道何时发送已排队或未排队的消息?

来自非排队消息

发送非排队消息的一些功能是... SendMessage ...

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

以及来自SendMessage文档:

但是,发送线程将在等待其消息处理的同时处理传入的非排队消息。

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

当我们将SendMessage消息调用到另一个线程窗口时,它将进入内核模式。 内核模式始终记住用户模式堆栈指针。 然后我们切换到内核堆栈。 当我们从内核返回到用户模式时-内核通常返回到用户模式从那里调用它并保存到堆栈的位置。 但是存在和例外。 其中之一:

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导出)-

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

堆栈位于堆栈指针下方 ,这是我们较早进入内核的位置。 KiUserCallbackDispatcher调用RtlGetCurrentPeb()->KernelCallbackTable[RoutineIndex](Argument, ArgumentLength) -通常这是user32.dll中的某些函数。 该函数已经调用了相应的窗口程序。 从窗口过程中,我们可以回叫内核-因为KeUserModeCallback分配了其他内核框架-我们将在此框架内进入内核,而不会损坏先前的内核。 当窗口过程返回时-再次调用特殊的api

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

此api(如果没有错误)必须永远不会返回内核侧-已分配的内核帧已取消分配,并且我们返回到KeUserModeCallback内部的先前内核堆栈。 所以我们最终从调用KeUserModeCallback地方返回。 然后我们回到用户模式,恰好从我们调用内核的那一点开始,就在同一堆栈上。

实际上,如何 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通过SendMessagemessage_1发送到thread_B时 ,我们进入内核,信号为gui event_Bthread_B等待了它。 并开始在gui event_A上等待当前线程。 如果thread_B执行消息检索代码(调用GetMessagePeekMessage ),则在thread_B中调用KeUserModeCallback 结果执行了它的窗口过程。 在这里,它调用SendMessage将一些message_2发送回thread_A 结果,我们将event_A设置为其中thread_A等待并在event_B上开始等待。 thread_A将被唤醒并调用KeUserModeCallback 该消息将调用Windows过程。 当它返回时(假设这次我们不再调用SendMessage ),我们再次发信号给event_B并开始等待event_A 现在thread_BSendMessage返回,然后从窗口过程返回-完成句柄原始message_1 将设置为event_A thread_A唤醒并从SendMessage返回。 呼叫流程将是下一个:

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消息时-窗口仍然有效,并调用处理传入消息。 我们可以实现下一个演示:首先,我们不需要两个过程。 具有2个线程的绝对足够的单个进程。 不需要特殊的注册消息。 为什么不使用说WM_APP作为测试消息?

  1. thread_A从自我WM_CREATE创建thread_B,并通过自己的窗口句柄。
  2. thread_B创建自己的窗口,但是在WM_CREATE简单地返回-1(对于失败的创建窗口)
  3. 来自WM_DESTROY thread_B调用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 (因此在堆栈下面WM_DESTROY递归调用WindowProc
  6. thread_B打印“无法打印此文件”,并将自身ID返回给thread_A
  7. thread_A从调用返回SendMessage ,返回自己的ID,以thread_B
  8. WM_DESTROY内部的调用SendMessage返回的thread_B

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

thread_B的最有趣的外观堆栈跟踪时,它递归调用上WM_APP

在此处输入图片说明

仍然,我不明白A如何处理WM_ANOTHER_register_MSG。 我期望的是,当发送该消息时,A的线程应该正在等待其对SendMessage调用返回。

A中的SendMessage 正在等待发送(从A到B)的消息完成,但是在等待时,它能够将其他线程发送的消息分派到该线程。

当在同一线程上为窗口调用SendMessage ,我们将其视为一串函数调用,这些函数最终导致目标windowproc并最终返回到调用者。

但是,当消息跨越线程边界时,就不是那么简单。 它变得像一个客户端服务器应用程序。 SendMessage打包消息并向目标线程发出信号,通知它有消息要处理。 在这一点上,它等待。

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

原始线程看到“我完成了!” 发出信号并返回结果值。 对于SendMessage的调用者来说,它看起来像是一个函数调用,但实际上是经过编排的,以将消息封送给另一个线程,然后将结果封送回去。

几个Windows API调用是“屈服点”,用于检查是否有消息从另一个线程发送到当前线程。 最著名的是GetMessagePeekMessage ,但是某些类型的等待(包括SendMessage内部的等待)也是屈服点。 正是这个屈服点使A可以在等待B完成对第一条消息的处理的同时,对从B发回的消息做出响应。

当它从B返回WM_ANOTHER_REGISTERED_MSG时,这是A的调用堆栈的一部分(步骤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的窗口过程。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM