MFC消息机制逆向追踪(上)
本文目的就是以一个MFC的标准对话框程序为例,同时从源码和反汇编代码两方面来研究MFC消息的流程走向,弄清MFC消息路径的所有站点,这样就可以任意定位MFC的所有消息事件,可以从任一站点切入,进行跟踪分析MFC的处理过程。甚至可以从PumpMessage大本营出发,一直全程跟踪,做到心中有数,不慌不乱。
关于对话框的启动过程,其过程很简单,程序进入WinMain函数之后,会调用对话框的DoModal函数,然后就进入RunModalLoop函数,消息循环在这里就开始了,限于篇幅,本文不作多说,有兴趣者可看看MFC源码。本篇重点在于分析MFC的消息分发处理的过程。
先看一下RunModalLoop函数部分源码:
int CWnd::RunModalLoop(DWORD dwFlags) { ... for (;;) { ... do { if (!AfxGetThread()->PumpMessage()) // pump message, but quit on WM_QUIT { AfxPostQuitMessage(0); return -1; } ... } while (::PeekMessage(pMsg, NULL, NULL, NULL, PM_NOREMOVE)); } ... } |
这里,AfxGetThread()->PumpMessage()是MFC消息处理的大本营,MFC程序的所有消息就是从这里开始,经过重重路径转换,翻山越岭,中途直达Windows系统内核,再返回到MFC地界,又途经不少周折,才找到最终目的地 – 消息函数地址。可谓是山重水复疑无路,柳暗花明又一村。
一个按钮点击事件的过程如下:
CWinThread::PumpMessage -> CWnd::PretranslateMessage -> CWnd::WWalkPreTranslateMessate -> CD1Dlg::PreTranslateMessage -> CDialog::PreTranslateMessage -> CWnd::PreTranslateInput -> CWnd::IsDialogMessageA -> USER32内核 - > AfxWndProcBase -> AfxWndProc -> AfxCallWndProc -> CWnd::WindowProc -> CWnd::OnWndMsg -> CWnd::OnCommand -> CDialog::OnCmdMsg -> CCmdTarget::OnCmdMsg -> _AfxDispatchCmdMsg -> CD1Dlg::OnButton1()
VC下,可以随手写一个标准的对话框程序,上面放一个按钮,点击按钮后,弹出一个消息框。我们现在就从PumpMessage()开始,来分析这中间的消息流程:
1. CWinThread::PumpMessage函数 (消息泵)
BOOL CWinThread::PumpMessage() { //GetMessage 当消息为WM_QUIT时,返回0,其它消息时,返回TRUE,有错误时,返回-1 if (!::GetMessage(&m_msgCur, NULL, NULL, NULL)) return FALSE; if (m_msgCur.message != WM_KICKIDLE && !PreTranslateMessage(&m_msgCur)) { ::TranslateMessage(&m_msgCur); ::DispatchMessage(&m_msgCur); } return TRUE; } |
PumpMessage只有在接收到WM_QUIT消息时,才返回FALSE,其它情况,返回TRUE。由于CWinThread::PumpMessage()函数负责从消息队列中获取消息、翻译消息以及分发消息等,因此习惯将此函数称之为“消息泵”。
在PumpMessage函数中,PreTranslateMessage函数至关重要,正是有了这个PreTranslateMessage(),才使得MFC能够灵活的控制消息的分发模式,可以说,PreTranslateMessage()就是MFC的实现消息分发模式的工具。
2. CWinThread::PreTranslateMessage函数
BOOL CWinThread::PreTranslateMessage(MSG* pMsg) { // if this is a thread-message, short-circuit this function if (pMsg->hwnd == NULL && DispatchThreadMessageEx(pMsg)) return TRUE; CWnd* pMainWnd = AfxGetMainWnd(); // 通过WalkPreTranslateTree 进行消息分发 if (CWnd::WalkPreTranslateTree(pMainWnd->GetSafeHwnd(), pMsg)) return TRUE; // 消息分发处理关键 if (pMainWnd != NULL) { CWnd* pWnd = CWnd::FromHandle(pMsg->hwnd); if (pWnd->GetTopLevelParent() != pMainWnd) return pMainWnd->PreTranslateMessage(pMsg); //程序主框架处理消息 } return FALSE; // no special processing } |
3. CWnd::WalkPreTranslateTree函数
CWnd::WalkPreTranslateTree()的所使用的策略很简单,拥有该消息窗口最先获得该消息的处理权,如果它不想对该消息进行处理(该窗口对象的PreTranslateMessage()函数返回FALSE),就将处理权交给它的父亲窗口,如此向树的根部遍历,直到遇到 hWndStop(在CWinThread::PreTranslateMessage()中,hWndStop表示的是线程主窗口的句柄)。
记住这个消息处理权的传递方向,是由树的某个一般节点或叶子节点向树的根部传递!
BOOL PASCAL CWnd::WalkPreTranslateTree(HWND hWndStop, MSG* pMsg) { for (HWND hWnd = pMsg->hwnd; hWnd != NULL; hWnd = ::GetParent(hWnd)) //从当前窗口到父窗口,逐层往上 { CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); if (pWnd != NULL) { // target window is a C++ window
//消息被某一窗口处理了,返回 if (pWnd->PreTranslateMessage(pMsg)) return TRUE; } // got to hWndStop window without interest if (hWnd == hWndStop) break; } return FALSE; // no special processing } |
正是这个if (pWnd->PreTranslateMessage(pMsg)) return TRUE; 才实现了MFC灵活的消息分发处理机制。MFC程序各个窗口类中重载的PreTranslateMessage虚函数,都是从这里进来的。
MFC从当前消息窗口类逐级向上搜索执行各个类的PreTranslateMessage函数,只要有一个PreTranslateMessage函数返回TRUE,WalkPreTranslateTree就中止搜索,并返回TRUE,否则返回FALSE。
在PumpMessage函数中最终就是根据WalkPreTranslateTree函数的返回值决定是否要由Windows系统进行消息处理与分发。
4. CD1Dlg::PreTranslateMessage函数
BOOL CDialog::PreTranslateMessage(MSG* pMsg) {... return CDialog::PreTranslateMessage(pMsg); } |
若接收消息的窗口类重载了PreTranslateMessage函数,则此时会调用它,否则就进入第5步。实际应用中,这里很有可能是消息流程的一个分水岭,可能走向两条不同的道路。这完全取决于应用程序新增的代码,若应用程序在这里返回TRUE,消息流程就返回去了。否则,就会继续往下执行。
在跟踪按钮消息时,此处应作为一个注意点,而设置断点的最佳位置是在上一步WalkPreTranslateTree函数中所说的位置,跟踪下来,注意消息流程的走向。
5. CDialog::PreTranslateMessage函数
BOOL CDialog::PreTranslateMessage(MSG* pMsg) { if (CWnd::PreTranslateMessage(pMsg)) return TRUE; CFrameWnd* pFrameWnd = GetTopLevelFrame(); if (pFrameWnd != NULL && pFrameWnd->m_bHelpMode) return FALSE; if (pMsg->message == WM_KEYDOWN && (pMsg->wParam == VK_ESCAPE || pMsg->wParam == VK_CANCEL) && (::GetWindowLong(pMsg->hwnd, GWL_STYLE) & ES_MULTILINE) &&_AfxCompareClassName(pMsg->hwnd, _T("Edit"))) { HWND hItem = ::GetDlgItem(m_hWnd, IDCANCEL); if (hItem == NULL || ::IsWindowEnabled(hItem)) { SendMessage(WM_COMMAND, IDCANCEL, 0); return TRUE; } } return PreTranslateInput(pMsg); // 消息流入此处 } |
6. CWnd::PreTranslateInput函数
BOOL CWnd::PreTranslateInput(LPMSG lpMsg) { if ((lpMsg->message < WM_KEYFIRST || lpMsg->message > WM_KEYLAST) && (lpMsg->message < WM_MOUSEFIRST || lpMsg->message > WM_MOUSELAST)) // 过滤消息 return FALSE; return IsDialogMessage(lpMsg); } |
从源码中可以看出,这个函数是对消息进行过滤,对于按键消息和鼠标消息,直接返回FALSE,然后再返回到PumpMessge函数中,调用 TranslageMessage()和DispatchMessage()函数,进行消息转换和分发,再进入MFC。对于其它消息,则调用CWnd:: IsDialogMessage()函数进行下一步处理。
7. CWnd::IsDialogMessageA函数
BOOL CWnd::IsDialogMessage(LPMSG lpMsg) { if (m_nFlags & WF_OLECTLCONTAINER) return afxOccManager->IsDialogMessage(this, lpMsg); else return ::IsDialogMessage(m_hWnd, lpMsg); } |
这里会转进User32.IsDialogMessageA函数,从而转入系统内核,由Windows系统再来负责将消息的分发传送到各个目标窗口。
注:User32.IsDialogMessage并不是象它的名字那样用来检查对话框消息的,而是用来解释或转换消息的。更贴切的名字应该是 TranslateDialogMessage。CWnd::IsDialogMessage实际上是一个以LPMSG作为参数,再加上内部的 m_hWnd参数来调用User32.IsDialogMessage的打包函数。这样,MFC中每一个对话框都会解释自己的输入。所以,若同时运行五个对话框,每一个对话框的PreTranslateMessage都会自动调用User32.IsDialogMessage,而且运转良好,完全可以不用我们编程处理,MFC真是太牛了。
8. User32 内核处理,不分析
这里面的过程,我们就当作一个黑匣子吧,不管它,一般情况下,也无需管它。因为我们百分之百相信它。
当消息到达此处时,又进入了MFC地界 .第8步之前,可以说是经常峰回路转,山重水复。第8步之后,是柳暗花明,可以一路高歌,直奔目的地了。
欲知后事如何,且听下回分解!
MFC消息机制逆向追踪(下)
上篇啰里啰嗦地说了一大堆,其实所说的消息都是PostMessage方式的。MFC中还有另外一种很常见的消息发送方式,就是SendMessage函数。这个消息起始路径和上篇所讲的完全不一样。这种方式下,前面的7个站点均不执行,而是直接进入第8站点:User32内核,从第8站点出来后,这两种消息方式走上了同一条道路,进入第9个站点或第10个站点了,真是殊道同归。
对于MFC窗口程序,所有窗口都使用同一窗口过程 : AfxWndProcBase(第9个站点)或AfxWndProc(第10个站点)。如果程序是动态链接到MFC DLL(定义了_AFXDLL),则AfxWndProcBase被用作窗口过程,否则AfxWndProc被用作窗口过程。而在 AfxWndProcBase中,最终也是调用AfxWndProc函数。
所以,可以说,第10个站点:AfxWndProc函数是MFC的所有消息必经之点。
可以作如下测试:在Button1事件代码中加入: SendMessage(WM_COMMAND,IDC_BUTTON2,0); 这是往 Button2发送点击消息,当点击Button1时,跟进Button1的事件代码流程,再跟进SendMessage函数的内部代码,可以发现,和上面所讲是完全一样的。
各位可能有疑问了,消息从User32内核出来之后,应该是由Windows系统自动发往各个窗口的消息处理函数,但这里怎么会全部进入了AfxWndProc()函数呢?这涉及到了钩子函数,有兴趣者,请看本文附录,正文不作多说。现在继续进入消息之旅:
请看以下源码:
9. AfxWndProcBase函数
LRESULT CALLBACK AfxWndProcBase(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) { AFX_MANAGE_STATE(_afxBaseModuleState.GetData()); return AfxWndProc(hWnd, nMsg, wParam, lParam); } |
AfxWndProcBase首先使用宏AFX_MANAGE_STATE设置正确的模块状态,然后调用AfxWndProc。
说明:如果程序是动态链接到MFC DLL(定义了_AFXDLL),则AfxWndProcBase被用作窗口过程,否则AfxWndProc被用作窗口过程。从源码可以知道,在AfxWndProcBase中,最终也是调用AfxWndProc函数。
10. AfxWndProc函数 - 是所有的CWnd类及其派生类的WndProc
LRESULT CALLBACK AfxWndProc(HWND hWnd, UINT nMsg, WPARAM wParam, LPARAM lParam) { if (nMsg == WM_QUERYAFXWNDPROC) return 1; CWnd* pWnd = CWnd::FromHandlePermanent(hWnd); return AfxCallWndProc(pWnd, hWnd, nMsg, wParam, lParam); } |
AfxWndProc()要做的第一件事是找到目标窗口的CWnd对象。一旦找到CWnd对象,就会立刻调用AfxCallWndProc()。
这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息,还是非队列消息,都送到AfxWndProc窗口过程来处理(如果使用MFC DLL,则AfxWndProcBase被调用,然后是AfxWndProc)。
Windows消息送给AfxWndProc窗口过程之后,AfxWndProc得到HWND窗口对应的MFC窗口对象,然后,调用AfxCallWndProc函数进行下一步处理。
11. AfxCallWndProc函数
LRESULT AFXAPI AfxCallWndProc(CWnd* pWnd, HWND hWnd, UINT nMsg,WPARAM wParam = 0, LPARAM lParam =0) { _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData(); //存储标志符和参数,因为MFC内部需要这些参数和信息,但用户不需关心 MSG oldState = pThreadState->m_lastSentMsg; pThreadState->m_lastSentMsg.hwnd = hWnd; pThreadState->m_lastSentMsg.message = nMsg; pThreadState->m_lastSentMsg.wParam = wParam; pThreadState->m_lastSentMsg.lParam = lParam; … //委派到窗口的WindowProc lResult = pWnd->WindowProc(nMsg, wParam, lParam); … return lResult; } |
AfxCallWndProc函数把消息送给CWnd类或其派生类的对象。该函数主要是把消息和消息参数(nMsg、wParam、lParam)传递给 MFC窗口对象的成员函数WindowProc(pWnd->WindowProc)作进一步处理。如果是WM_INITDIALOG消息,则在调用WindowProc前后要作一些处理。
12. CWnd::WindowProc函数
AfxWndProc和 AfxCallWndProc都是AFX的API函数。而WindowProc已经是CWnd的一个方法。所以可以注意到在 WindowProc中已经没有关于句柄或者是CWnd的参数了。至此,消息已经正式登堂入室,步入MFC的大厅了。真是辛苦啊!
其源码如下:
LRESULT CWnd::WindowProc(UINT message, WPARAM wParam, LPARAM lParam) { LRESULT lResult = 0; if (!OnWndMsg(message, wParam, lParam, &lResult)) // OnWndMsg做了大部分工作 lResult = DefWindowProc(message, wParam, lParam); return lResult; } |
CWnd::WindowProc先发送消息到OnWndMsg()函数,它试图在类中为该消息寻找一个处理函数;如果未被处理,则调用 DefWindowProc()函数。DefWindowProc()是缺省的窗口过程,所有不能或者没有被OnWndMsg处理的函数都将交由它处理。
CWnd::WindowProc是一个虚拟函数,程序员可以在CWnd的派生类中覆盖它,改变MFC分发消息的方式。例如,MFC的 CControlBar就覆盖了WindowProc,对某些消息作了自己的特别处理,其他消息处理由基类的WindowProc函数完成。
13. CWnd::OnWndMsg函数 (这个函数很长,此处仅选一部分)
BOOL CWnd::OnWndMsg(UINT message, WPARAM wParam, LPARAM lParam, LRESULT* pResult) { LRESULT lResult = 0; if (message == WM_COMMAND) { if (OnCommand(wParam, lParam)) // 命令消息从此处流进 { lResult = 1; goto LReturnTrue; } return FALSE; } if (message == WM_NOTIFY) { NMHDR* pNMHDR = (NMHDR*)lParam; if (pNMHDR->hwndFrom != NULL && OnNotify(wParam, lParam, &lResult)) goto LReturnTrue; return FALSE; } if (message == WM_ACTIVATE) _AfxHandleActivate(this, wParam, CWnd::FromHandle((HWND)lParam)); ... } |
CWnd::OnWndMsg()函数的功能首先按字节对消息进行排序,对于WM_COMMAND消息,调用OnCommand()消息响应函数,对于 WM_NOTIFY消息调用OnNotify()消息响应函数。任何被遗漏的消息将是一个窗口消息。OnWndMsg()函数搜索类的消息映像,以找到一个能处理任何窗口消息的处理函数。
如果OnWndMsg()函数不能找到这样的处理函数的话,则把消息返回到WindowProc()函数,由它将消息发送给DefWindowProc()函数。
14. CWnd::OnCommand函数
BOOL CWnd::OnCommand(WPARAM wParam, LPARAM lParam) { UINT nID = LOWORD(wParam); HWND hWndCtrl = (HWND)lParam; int nCode = HIWORD(wParam); ... return OnCmdMsg(nID, nCode, NULL, NULL); //通过虚函数调用,直接进入了重载的的CD2Dlg::OnCmdMsg函数 } |
该函数查看这是不是一个控件通知(lParam参数不为NULL,如果lParam参数为空的话,说明该消息不是控件通知),如果它是, OnCommand()函数会试图将消息映射到制造通知的控件;如果他不是一个控件通知(或者如果控件拒绝映射的消息)OnCommand()就会调用 OnCmdMsg()函数
提示:
在函数入口处,同样可以F2直接设置断点,定位WM_COMMAND消息。
15. CD2Dlg::OnCmdMsg函数(如果重载了的话)
BOOL CD2Dlg::OnCmdMsg(UINT nID, int nCode, void* pExtra, AFX_CMDHANDLERINFO* pHandlerInfo) { ... return CDialog::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo); } |
16. CDialog::OnCmdMsg()函数:
BOOL CDialog::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo) { if (CWnd::OnCmdMsg(nID, nCode, pExtra, pHandlerInfo)) //从这里直接进入了CCmdTarget::OnCmdMsg() return TRUE; ... } |
对话框的OnCmdMsg其实也是重载了CCmdTarget::OnCmdMsg()函数。
17. CCmdTarget::OnCmdMsg()函数
BOOL CCmdTarget::OnCmdMsg(UINT nID, int nCode, void* pExtra,AFX_CMDHANDLERINFO* pHandlerInfo) { const AFX_MSGMAP* pMessageMap; const AFX_MSGMAP_ENTRY* lpEntry; UINT nMsg = 0; nMsg = HIWORD(nCode); nCode = LOWORD(nCode); if (nMsg == 0) nMsg = WM_COMMAND; //查看消息映射是否自己所需 for (pMessageMap = GetMessageMap(); pMessageMap != NULL;pMessageMap = (*pMessageMap->pfnGetBaseMap)()) { lpEntry = AfxFindMessageEntry(pMessageMap->lpEntries, nMsg, nCode, nID); if (lpEntry != NULL) { //有匹配的消息映射时,会进行如下调用: return _AfxDispatchCmdMsg(this, nID, nCode,lpEntry->pfn, pExtra, lpEntry->nSig,pHandlerInfo); } } return FALSE; } |
根据接收消息的类,OnCmdMsg()函数将在一个称为命令传递(Command Routing)的过程中潜在的传递命令消息和控件通知。例如:如果拥有该窗口的类是一个框架类,则命令和通知消息也被传递到视图和文档类,并为该类寻找一个消息处理函数。
CCmdTarget是MFC消息映射体系结构的基类。正是通过这个体系结构,才将命令或者消息映射到开发人员所写的命令处理函数或者消息响应函数。
OnCmdMsg()实际上是CCmdTarget的成员函数,而不是CWnd的成员函数。认实这一点很重要,因为它允许任何从CCmdTarget派生的类接收一个命令消息,即使那些没有一个窗口的类也可以。如,当你跟踪MFC的SDI或MDI程序消息流程时,会发现没有窗口的文档类处理消息时,也会重载OnCmdMsg()函数,使它能为文档模板类提供命令消息。
这里GetMessageMap() 和 AfxFindMessageEntry() 两个函数就是搜索查寻消息函数在消息映射表中的位置,从而找出消息函数的地址。关于这个两个函数代码分析,及消息映射表的结构,本文就不分析了(分析起来,又要啰里啰嗦地说了一大堆)。有兴趣者可自行参考相关资料,网上很多(看雪论坛精华集上也有很多这方面的资料,而且写得很不错)。
18. _AfxDispatchCmdMsg()函数, 找到按钮消息函数处
AFX_STATIC BOOL AFXAPI _AfxDispatchCmdMsg(CCmdTarget* pTarget, UINT nID, int nCode, AFX_PMSG pfn, void* pExtra, UINT nSig, AFX_CMDHANDLERINFO* pHandlerInfo) { union MessageMapFunctions mmf; mmf.pfn = pfn; BOOL bResult = TRUE; switch (nSig) { case AfxSig_vv: // normal command or control notification ASSERT(CN_COMMAND == 0); // CN_COMMAND same as BN_CLICKED ASSERT(pExtra == NULL); (pTarget->*mmf.pfn_COMMAND)(); //从这里执行下面的CD2Dlg::OnButton1()函数 break; case AfxSig_bv: ... ... } |
这里,通过调用全局函数_AfxDispatchCmdMsg,来调用具体的消息处理函数。这样便完成了从产生消息到调用消息响应函数的全过程。其参数分别介绍如下。
pTarget:该参数是指向处理消息的对象。
nID:该参数是命令ID。
nCode:该参数是通知消息等,对于一个命令消息,该变量将赋值为CN_COMMAND(相当于0)。
pfn:该参数是消息处理函数地址。
pExtra:该参数用于存放一些有用的信息,它取决于当前正被处理的消息类型。如果是控件通知WM_NOTIFY,则是指向NMHDR的AFX_NOTIFY结构的指针;如果是菜单项和工具栏更新,则它是指向CCmdUI派生类的指针;如果是其他类型,则为空。
nSig:该参数定义消息处理函数的调用变量。在AFXMSG_.H中,为nSig预定义了60多个值,例如,nSig值为iww,则在调用消息处理函数前,使OnWndMsg()格式化wParam和lParam为两个UINT变量,返回值为整型。
pHandlerInfo:该参数是一个指针,指向AFX_CMDHANDLERINFO结构。
前6个参数(除了pExtra以外)都是输入参数,而参数pExtra和pHandlerInfo既可以用作输出参数,也可以用作输入参数。
该函数主要完成的任务是:首先,它检查参数pHandlerInfo是否空,如果不空,则用pTarget和pfn填充它所指向的结构,并且返回 TRUE;其次,如果pHandlerInfo空,则进行消息处理函数的调用。它根据参数nSig的值,把参数pfn的类型转换为要调用的消息处理函数的类型。
如果在视图中没有找到相应的消息处理函数,则将会交由文档类来进行处理。
19. 执行我们的按钮消息函数
void CD1Dlg::OnButton1() { AfxMessageBox("OK"); }
至此,相信各位能够明白DispatchMessage最终将消息发到了AfxWndProc函数里,而非DefWindowProc里。AfxWndProc的作用是截获所有发送给窗口的消息(包括队列消息和非队列消息),所以实质上它是一个窗口消息分发器。