上篇啰里啰嗦地说了一大堆,其实所说的消息都是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函数。
AfxWndProcBase反汇编代码:
73D31B81 > MOV EAX,MFC42.73DC2CFE
73D31B86 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
73D31B8B PUSH ECX
73D31B8C PUSH ECX
73D31B8D PUSH MFC42.#2188_?CreateObject@?$CProcessLocal@V_AFX_BASE_>
73D31B92 MOV ECX,OFFSET MFC42._afxBaseModuleState
73D31B97 CALL MFC42.#3028_?GetData@CProcessLocalObject@@QAEPAVCNoTr>
73D31B9C PUSH EAX
73D31B9D LEA ECX,DWORD PTR SS:[EBP-14]
73D31BA0 CALL MFC42.#6467_??0AFX_MAINTAIN_STATE2@@QAE@PAVAFX_MODULE>
73D31BA5 PUSH DWORD PTR SS:[EBP+14]
73D31BA8 AND DWORD PTR SS:[EBP-4],0
73D31BAC PUSH DWORD PTR SS:[EBP+10]
73D31BAF PUSH DWORD PTR SS:[EBP+C]
73D31BB2 PUSH DWORD PTR SS:[EBP+8]
73D31BB5 CALL MFC42.#1578_?AfxWndProc@@YGJPAUHWND__@@IIJ@Z
73D31BBA MOV ECX,DWORD PTR SS:[EBP-10]
73D31BBD MOV EDX,DWORD PTR SS:[EBP-14]
73D31BC0 MOV DWORD PTR DS:[ECX+4],EDX
73D31BC3 MOV ECX,DWORD PTR SS:[EBP-C]
73D31BC6 MOV DWORD PTR FS:[0],ECX
73D31BCD LEAVE
73D31BCE RETN 10
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函数进行下一步处理。
AfxWndProc函数反汇编代码:
73D31BD1 > PUSH EBP
73D31BD2 MOV EBP,ESP
73D31BD4 CMP DWORD PTR SS:[EBP+C],360
73D31BDB JE MFC42.73D8BF8A
73D31BE1 PUSH DWORD PTR SS:[EBP+8]
73D31BE4 CALL MFC42.#2867_?FromHandlePermanent@CWnd@@SGPAV1@PAUHWND>
73D31BE9 PUSH DWORD PTR SS:[EBP+14]
73D31BEC PUSH DWORD PTR SS:[EBP+10]
73D31BEF PUSH DWORD PTR SS:[EBP+C]
73D31BF2 PUSH DWORD PTR SS:[EBP+8]
73D31BF5 PUSH EAX
73D31BF6 CALL MFC42.#1109_?AfxCallWndProc@@YGJPAVCWnd@@PAUHWND__@@I>
73D31BFB POP EBP
73D31BFC RETN 10
提示:
a. OD加载程序后,调出MFC42.dll模块,定位到AfxWndProc代码入口处。
b. 在入口PUSH EBP处设置条件断点[esp+8]==111,即可设置按钮点击事件断点。
c. [esp+4]==002407B4 && [esp+8]==202 可以为指定按钮设置点击断点(002407B4是按钮的句柄值)。
说明:此时设置条件断点就更方便了,[esp]是返回地址,[esp+4]是接收消息的窗口句柄,[esp+8]就是消息代码值
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前后要作一些处理。
AfxCallWndProc 函数反汇编代码:
73D31BFF > MOV EAX,MFC42.73DC2D82
73D31C04 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
73D31C09 SUB ESP,34
73D31C0C PUSH EBX
73D31C0D PUSH ESI
73D31C0E PUSH EDI
73D31C0F MOV ECX,OFFSET MFC42._afxThreadState
73D31C14 MOV DWORD PTR SS:[EBP-10],ESP
73D31C17 PUSH MFC42.#2202_?CreateObject@?$CThreadLocal@V_AFX_THREAD>
73D31C1C CALL MFC42.#3030_?GetData@CThreadLocalObject@@QAEPAVCNoTra>
...
73D31C62 PUSH DWORD PTR SS:[EBP+18]
73D31C65 MOV EAX,DWORD PTR DS:[EDI]
73D31C67 MOV ECX,EDI
73D31C69 PUSH DWORD PTR SS:[EBP+14]
73D31C6C PUSH ESI
73D31C6D CALL DWORD PTR DS:[EAX+A0] <D1.?WindowProc@CWnd@@MAEJIIJ mfc42:MFC42.DLL>
...
73D31C9A C2 1400 RETN 14
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函数完成。
CWnd::WindowProc()函数反汇编代码:
73D31CC8 > PUSH EBP
73D31CC9 MOV EBP,ESP
73D31CCB PUSH ECX
73D31CCC PUSH ESI
73D31CCD MOV ESI,ECX
73D31CCF LEA ECX,DWORD PTR SS:[EBP-4]
73D31CD2 AND DWORD PTR SS:[EBP-4],0
73D31CD6 MOV EAX,DWORD PTR DS:[ESI]
73D31CD8 PUSH ECX
73D31CD9 PUSH DWORD PTR SS:[EBP+10]
73D31CDC MOV ECX,ESI
73D31CDE PUSH DWORD PTR SS:[EBP+C]
73D31CE1 PUSH DWORD PTR SS:[EBP+8]
73D31CE4 CALL DWORD PTR DS:[EAX+A4] ;<D1.?OnWndMsg@CWnd@@ mfc42:MFC42.DLL>
73D31CEA TEST EAX,EAX
73D31CEC JNZ SHORT MFC42.73D31D04
73D31CEE PUSH DWORD PTR SS:[EBP+10]
73D31CF1 MOV EAX,DWORD PTR DS:[ESI]
73D31CF3 MOV ECX,ESI
73D31CF5 PUSH DWORD PTR SS:[EBP+C]
73D31CF8 PUSH DWORD PTR SS:[EBP+8]
73D31CFB CALL DWORD PTR DS:[EAX+A8] ; <D1.?DefWindowProcA@CWnd@@ mfc42:MFC42.DLL>
73D31CFB CALL DWORD PTR DS:[EAX+A8]
73D31D01 MOV DWORD PTR SS:[EBP-4],EAX
73D31D04 MOV EAX,DWORD PTR SS:[EBP-4]
73D31D07 POP ESI
73D31D08 LEAVE
73D31D09 RETN 0C
提示:
a. OD加载程序后,调出MFC42.dll模块,定位到WindowProc代码入口处。
b. 在入口PUSH EBP处设置条件断点[esp+4]==111,即可设置按钮点击事件断点。
说明: 1. 此时[esp]是返回地址,[esp+4]是消息代码值。
2. 由于,此时的接收消息窗口的句柄被CWnd类隐藏起来了,所以此时要设定指定按钮断点不太方便。
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()函数。
CWnd::OnWndMsg 部分反汇编代码:
73D31D0C > MOV EAX,MFC42.73DC2E0A
73D31D11 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
...
73D31D1E MOV EBX,DWORD PTR SS:[EBP+8] ;取出message参数
...
73D31D23 CMP EBX,111 ;是否为WM_COMMAND消息
73D31D29 MOV EDI,ECX
73D31D2B JE MFC42.73D31DBE
...
73D31DBE PUSH DWORD PTR SS:[EBP+10]
73D31DC1 MOV EAX,DWORD PTR DS:[EDI]
73D31DC3 PUSH DWORD PTR SS:[EBP+C]
73D31DC6 CALL DWORD PTR DS:[EAX+80] ; <D1.?OnCommand@CWnd@@ mfc42:MFC42.DLL>
...
提示:
73D31DC6 CALL DWORD PTR DS:[EAX+80],也是对点击按钮之类的WM_COMMAND消息设置断点较好的切入点,这里可以直接F2设置断点,更为方便,因为,只有在WM_COMMAND消息下,才有可能执行这条语句。
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()函数
CWnd::OnCommand()函数部分反汇编代码:
73D3291C > PUSH EBP
73D3291D MOV EBP,ESP
73D3291F SUB ESP,2C
73D32922 MOV EAX,DWORD PTR SS:[EBP+8]
…
73D3296B PUSH EBX
73D3296C PUSH EBX
73D3296D MOV ECX,ESI
73D3296F PUSH DWORD PTR SS:[EBP+8]
73D32972 PUSH EDI
73D32973 CALL DWORD PTR DS:[EAX+14] ;<D1.?OnCmdMsg@CDialog@@ mfc42:MFC42.DLL>
73D32976 POP EDI
73D32977 POP ESI
73D32978 POP EBX
73D32979 LEAVE
73D3297A RETN 8
提示:
在函数入口处,同样可以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;
...
}
CDialog::OnCmdMsg()函数部分反汇编代码:
73D38FAA > PUSH EBP
73D38FAB MOV EBP,ESP
…
73D38FBB PUSH DWORD PTR SS:[EBP+10]
73D38FBE PUSH EDI
73D38FBF PUSH EBX
73D38FC0 CALL MFC42.#4424_?OnCmdMsg@CCmdTarget@@UAEHIHPAXPAUAFX_CMD> ; CCmdTarget::OnCmdMsg()
73D38FC5 TEST EAX,EAX
73D38FC7 JNZ SHORT MFC42.73D38FE6
…
73D38FD8 RETN 10
对话框的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()函数,使它能为文档模板类提供命令消息。
73D3223C > PUSH EBP
73D3223D MOV EBP,ESP
…
73D32267 CALL DWORD PTR DS:[EAX+30] ; GetMessageMap 得到消息映射表地址
…
73D3227A CALL MFC42.#1145_?AfxFindMessageEntry@@YGPBUAFX_MSGMAP_ENT> ;寻找消息函数
73D3227F TEST EAX,EAX
73D32281 JNZ SHORT MFC42.73D32296 ; 找到,则转
…
73D32296 PUSH DWORD PTR SS:[EBP+14]
73D32299 PUSH DWORD PTR DS:[EAX+10]
73D3229C PUSH DWORD PTR SS:[EBP+10]
73D3229F PUSH DWORD PTR DS:[EAX+14] ;这个[EAX+14]就是消息函数的地址
73D322A2 PUSH DWORD PTR SS:[EBP+C]
73D322A5 PUSH DWORD PTR SS:[EBP+8]
73D322A8 PUSH EDI
73D322A9 CALL MFC42.73D3233C ;调用_AfxDispatchCmdMsg这个函数
…
这里GetMessageMap() 和 AfxFindMessageEntry() 两个函数就是搜索查寻消息函数在消息映射表中的位置,从而找出消 息函数的地址。关于这个两个函数代码分析,及消息映射表的结构,本文就不分析了(分析起来,又要啰里啰嗦地说了一大堆)。有兴趣者可自行参考相关资料,网 上很多(看雪论坛精华集上也有很多这方面的资料,而且写得很不错)。
提示:
个人认为,对于对话框程序,在这个函数函数入口处设置断点最好(请记住这个函数:CCmdTarget::OnCmdMsg()),因为:
一、不用设置条件断点,只有在发生WM_COMMAND消息后,才运行到此。
二、而且再继续往下运行到73D3229F PUSH DWORD PTR DS:[EAX+14],就得到了消息函数的地址。
或者,也可以往下到 73D322A9 CALL MFC42.73D3233C语句时F7跟进,再往下执行几条语句,就很容易来到WM_COMMAND消息函数代码处。见下面说明。
跟进73D3233C,执行几条语句,经过几次跳转后,很快就定位到了按钮事件代码处:
73D3233C PUSH EBP
73D3233D MOV EBP,ESP
...
73D3234E CMP EAX,28
73D32351 JBE SHORT MFC42.73D32390
73D32353 SUB EAX,29
73D32356 JE MFC42.73D8E55B
73D3235C SUB EAX,3
73D3235F JNZ MFC42.73D8E52F
…
73D3239E SUB EAX,0A
73D323A1 > JE SHORT MFC42.73D323D2
73D323A3 DEC EAX
73D323A4 JE MFC42.73D8E4FD
73D323AA SUB EAX,16
73D323AD JE SHORT MFC42.73D323C8
73D323AF SUB EAX,3
73D323B2 JNZ MFC42.73D8E4E7
…
73D323D2 MOV ECX,DWORD PTR SS:[EBP+8] ; Case C of Switch XXXXXXXX
73D323D5 CALL DWORD PTR SS:[EBP+14] ; D1.?OnButton1@CD1Dlg@@IAEXXZ D1Dlg.obj 进入按钮函数代码
73D323D8 ^ JMP SHORT MFC42.73D3237B
其实上面一段代码就是_AfxDispatchCmdMsg函数。
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"); }
注:
MFC另外两种常见的框架程序,SDI和MDI程序,其消息流程分析同此类似,其间过程大同小异,我就不再分析了。有兴趣者,可自行分析,这里只是提一下注意之点:
1. 对于SDI/MDI程序,最好在CWnd::OnCommand()函数入口处,设置断点,然后跟进CCmdTarget::OnCmdMsg()函数,就很快找到消息函数地址了。
2. 若要在CCmdTarget::OnCmdMsg()函数入口处设置断点。必须设置条件断点,如[esp+4]==3e8, 这里3e8是按钮的 ID值。由于在SDI和MDI程序正常运行时,经常有文档类,框架类消息从此流过,设置条件断点后,会引起OD运行特别慢。所以,如果确实要在此处设置断 点,注意:在需要设置断点时再设置断点,OD中断后,马上取消断点!
附录:MFC对Windows消息的截获过程
MFC在调用对话框的DoModal函数之时,在PreModal内部调用了AfxHookWindowCreate()函数(对于SDI/MDI程序, 是在调用CWnd::CreateEx()函数里面),这是一个安装WH_CBT类型钩子的函数。安装WH_CBT类型的钩子过程后,系统在下列情况下将 会首先调用此函数:激活窗口,创建或销毁窗口,最大化或最小化窗口,移动或缩放窗口,完成一个系统命令,从系统的消息队列里移除一个鼠标或者键盘事件,设 置键盘,设置输入焦点或者同步系统消息队列事件等。安装钩子后,窗口的消息走向就要发生变化。
安装构子函数AfxHookWindowCreate()源码如下:
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (pThreadState->m_pWndInit == pWnd) return;
if (pThreadState->m_hHookOldCbtFilter == NULL)
{
pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
if (pThreadState->m_hHookOldCbtFilter == NULL) AfxThrowMemoryException();
}
pThreadState->m_pWndInit = pWnd;
}
继续观察其中钩子过程的地址_AfxCbtFilterHook的源码,部分源码如下:
LRESULT CALLBACK _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{ _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (code != HCBT_CREATEWND)
{ // 等待HCBT_CREATEWND通过其他的钩子
return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);
}
…
if (!afxData.bWin4 && !bContextIsDLL && (pCtl3dState = _afxCtl3dState.GetDataNA()) != NULL && pCtl3dState->
m_pfnSubclassDlgEx != NULL && (dwFlags = AfxCallWndProc(pWndInit, hWnd, WM_QUERY3DCONTROLS)) != 0)
{ // 窗口类注册是用AfxWndProc进行的吗?
WNDPROC afxWndProc = AfxGetAfxWndProc();
BOOL bAfxWndProc = ((WNDPROC)GetWindowLong(hWnd, GWL_WNDPROC) == afxWndProc);
pCtl3dState->m_pfnSubclassDlgEx(hWnd, dwFlags);
if (!bAfxWndProc) //如果还没有编排到AfxWndProc则用AfxWndProc子类化窗口
{ //用标准的AfxWndProc子类化窗口
oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC,
(DWORD)afxWndProc);
*pOldWndProc = oldWndProc; //保存原窗口过程
}
}
…
}
从钩子函数中可以看到,如果它检测到注册窗口类时,使用的窗口过程不是AfxWndProc函数,则它将用此函数替代,并且通过这个设置,在调用:: CreateWindowEx后,将原窗口过程保存在窗口类的成员变量m_pfnSuper中,这样就形成了一个窗口过程链。在需要的时候,原窗口过程地 址可以通过窗口类成员函数GetSuperWndProcAddr获得。
这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息还是非队列消息,都将首先送到AfxWndProc窗口过程进行处理。经过消息分发之后仍没有被处理的消息,将送给原窗口过程处理。
注册时所使用的窗口过程果真不是AfxWndProc,而是DefWindowProc,因此钩子函数将完成这一替换任务。而调用函数AfxGetAfxWndProc所返回的结果正是函数AfxWndProc的地址,从其定义便可知这一事实:
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
return AfxGetModuleState()->m_pfnAfxWndProc;
#else
return &AfxWndProc;
#endif
}
至此,相信各位能够明白DispatchMessage最终将消息发到了AfxWndProc函数里,而非DefWindowProc里。AfxWndProc的作用是截获所有发送给窗口的消息(包括队列消息和非队列消息),所以实质上它是一个窗口消息分发器。
==============================================================================================
结束语:
我们的消息之旅到此结束了。相信各位仔细看完之后,不会再MFC的消息流程感到迷惑。MFC的消息就是沿着固定的路线,中间可能有分支,沿途经过一二十个站点,就能到达目的地 -- 消息函数地址处了。
各位看完之后,若有不明白或疑问之处,我很乐意同大家进行探讨!
这篇文章中我没有进行公式性条款归纳,因为我觉得没有这个必要,只要理解了MFC的这种消息处理机制,比任何归纳都重要。就可以从任意一个MFC函数切入,随意跟踪MFC程序,以不变应万变,任你程序消息函数处理如何隐藏,如何变化多端。我们都可以揪出来。
最后,本文肯定仅在很多不当和失误之处,若能得到各位高手指点一二,我的这份辛苦也不算白费了。
作者:szdbg
E-mail:szdbg@sina.com
【版权声明】 本文原创于看雪论坛,纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!
对于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函数。
AfxWndProcBase反汇编代码:
73D31B81 > MOV EAX,MFC42.73DC2CFE
73D31B86 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
73D31B8B PUSH ECX
73D31B8C PUSH ECX
73D31B8D PUSH MFC42.#2188_?CreateObject@?$CProcessLocal@V_AFX_BASE_>
73D31B92 MOV ECX,OFFSET MFC42._afxBaseModuleState
73D31B97 CALL MFC42.#3028_?GetData@CProcessLocalObject@@QAEPAVCNoTr>
73D31B9C PUSH EAX
73D31B9D LEA ECX,DWORD PTR SS:[EBP-14]
73D31BA0 CALL MFC42.#6467_??0AFX_MAINTAIN_STATE2@@QAE@PAVAFX_MODULE>
73D31BA5 PUSH DWORD PTR SS:[EBP+14]
73D31BA8 AND DWORD PTR SS:[EBP-4],0
73D31BAC PUSH DWORD PTR SS:[EBP+10]
73D31BAF PUSH DWORD PTR SS:[EBP+C]
73D31BB2 PUSH DWORD PTR SS:[EBP+8]
73D31BB5 CALL MFC42.#1578_?AfxWndProc@@YGJPAUHWND__@@IIJ@Z
73D31BBA MOV ECX,DWORD PTR SS:[EBP-10]
73D31BBD MOV EDX,DWORD PTR SS:[EBP-14]
73D31BC0 MOV DWORD PTR DS:[ECX+4],EDX
73D31BC3 MOV ECX,DWORD PTR SS:[EBP-C]
73D31BC6 MOV DWORD PTR FS:[0],ECX
73D31BCD LEAVE
73D31BCE RETN 10
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函数进行下一步处理。
AfxWndProc函数反汇编代码:
73D31BD1 > PUSH EBP
73D31BD2 MOV EBP,ESP
73D31BD4 CMP DWORD PTR SS:[EBP+C],360
73D31BDB JE MFC42.73D8BF8A
73D31BE1 PUSH DWORD PTR SS:[EBP+8]
73D31BE4 CALL MFC42.#2867_?FromHandlePermanent@CWnd@@SGPAV1@PAUHWND>
73D31BE9 PUSH DWORD PTR SS:[EBP+14]
73D31BEC PUSH DWORD PTR SS:[EBP+10]
73D31BEF PUSH DWORD PTR SS:[EBP+C]
73D31BF2 PUSH DWORD PTR SS:[EBP+8]
73D31BF5 PUSH EAX
73D31BF6 CALL MFC42.#1109_?AfxCallWndProc@@YGJPAVCWnd@@PAUHWND__@@I>
73D31BFB POP EBP
73D31BFC RETN 10
提示:
a. OD加载程序后,调出MFC42.dll模块,定位到AfxWndProc代码入口处。
b. 在入口PUSH EBP处设置条件断点[esp+8]==111,即可设置按钮点击事件断点。
c. [esp+4]==002407B4 && [esp+8]==202 可以为指定按钮设置点击断点(002407B4是按钮的句柄值)。
说明:此时设置条件断点就更方便了,[esp]是返回地址,[esp+4]是接收消息的窗口句柄,[esp+8]就是消息代码值
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前后要作一些处理。
AfxCallWndProc 函数反汇编代码:
73D31BFF > MOV EAX,MFC42.73DC2D82
73D31C04 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
73D31C09 SUB ESP,34
73D31C0C PUSH EBX
73D31C0D PUSH ESI
73D31C0E PUSH EDI
73D31C0F MOV ECX,OFFSET MFC42._afxThreadState
73D31C14 MOV DWORD PTR SS:[EBP-10],ESP
73D31C17 PUSH MFC42.#2202_?CreateObject@?$CThreadLocal@V_AFX_THREAD>
73D31C1C CALL MFC42.#3030_?GetData@CThreadLocalObject@@QAEPAVCNoTra>
...
73D31C62 PUSH DWORD PTR SS:[EBP+18]
73D31C65 MOV EAX,DWORD PTR DS:[EDI]
73D31C67 MOV ECX,EDI
73D31C69 PUSH DWORD PTR SS:[EBP+14]
73D31C6C PUSH ESI
73D31C6D CALL DWORD PTR DS:[EAX+A0] <D1.?WindowProc@CWnd@@MAEJIIJ mfc42:MFC42.DLL>
...
73D31C9A C2 1400 RETN 14
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函数完成。
CWnd::WindowProc()函数反汇编代码:
73D31CC8 > PUSH EBP
73D31CC9 MOV EBP,ESP
73D31CCB PUSH ECX
73D31CCC PUSH ESI
73D31CCD MOV ESI,ECX
73D31CCF LEA ECX,DWORD PTR SS:[EBP-4]
73D31CD2 AND DWORD PTR SS:[EBP-4],0
73D31CD6 MOV EAX,DWORD PTR DS:[ESI]
73D31CD8 PUSH ECX
73D31CD9 PUSH DWORD PTR SS:[EBP+10]
73D31CDC MOV ECX,ESI
73D31CDE PUSH DWORD PTR SS:[EBP+C]
73D31CE1 PUSH DWORD PTR SS:[EBP+8]
73D31CE4 CALL DWORD PTR DS:[EAX+A4] ;<D1.?OnWndMsg@CWnd@@ mfc42:MFC42.DLL>
73D31CEA TEST EAX,EAX
73D31CEC JNZ SHORT MFC42.73D31D04
73D31CEE PUSH DWORD PTR SS:[EBP+10]
73D31CF1 MOV EAX,DWORD PTR DS:[ESI]
73D31CF3 MOV ECX,ESI
73D31CF5 PUSH DWORD PTR SS:[EBP+C]
73D31CF8 PUSH DWORD PTR SS:[EBP+8]
73D31CFB CALL DWORD PTR DS:[EAX+A8] ; <D1.?DefWindowProcA@CWnd@@ mfc42:MFC42.DLL>
73D31CFB CALL DWORD PTR DS:[EAX+A8]
73D31D01 MOV DWORD PTR SS:[EBP-4],EAX
73D31D04 MOV EAX,DWORD PTR SS:[EBP-4]
73D31D07 POP ESI
73D31D08 LEAVE
73D31D09 RETN 0C
提示:
a. OD加载程序后,调出MFC42.dll模块,定位到WindowProc代码入口处。
b. 在入口PUSH EBP处设置条件断点[esp+4]==111,即可设置按钮点击事件断点。
说明: 1. 此时[esp]是返回地址,[esp+4]是消息代码值。
2. 由于,此时的接收消息窗口的句柄被CWnd类隐藏起来了,所以此时要设定指定按钮断点不太方便。
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()函数。
CWnd::OnWndMsg 部分反汇编代码:
73D31D0C > MOV EAX,MFC42.73DC2E0A
73D31D11 CALL MFC42.__EH_prolog ; JMP 到 MSVCRT._EH_prolog
...
73D31D1E MOV EBX,DWORD PTR SS:[EBP+8] ;取出message参数
...
73D31D23 CMP EBX,111 ;是否为WM_COMMAND消息
73D31D29 MOV EDI,ECX
73D31D2B JE MFC42.73D31DBE
...
73D31DBE PUSH DWORD PTR SS:[EBP+10]
73D31DC1 MOV EAX,DWORD PTR DS:[EDI]
73D31DC3 PUSH DWORD PTR SS:[EBP+C]
73D31DC6 CALL DWORD PTR DS:[EAX+80] ; <D1.?OnCommand@CWnd@@ mfc42:MFC42.DLL>
...
提示:
73D31DC6 CALL DWORD PTR DS:[EAX+80],也是对点击按钮之类的WM_COMMAND消息设置断点较好的切入点,这里可以直接F2设置断点,更为方便,因为,只有在WM_COMMAND消息下,才有可能执行这条语句。
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()函数
CWnd::OnCommand()函数部分反汇编代码:
73D3291C > PUSH EBP
73D3291D MOV EBP,ESP
73D3291F SUB ESP,2C
73D32922 MOV EAX,DWORD PTR SS:[EBP+8]
…
73D3296B PUSH EBX
73D3296C PUSH EBX
73D3296D MOV ECX,ESI
73D3296F PUSH DWORD PTR SS:[EBP+8]
73D32972 PUSH EDI
73D32973 CALL DWORD PTR DS:[EAX+14] ;<D1.?OnCmdMsg@CDialog@@ mfc42:MFC42.DLL>
73D32976 POP EDI
73D32977 POP ESI
73D32978 POP EBX
73D32979 LEAVE
73D3297A RETN 8
提示:
在函数入口处,同样可以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;
...
}
CDialog::OnCmdMsg()函数部分反汇编代码:
73D38FAA > PUSH EBP
73D38FAB MOV EBP,ESP
…
73D38FBB PUSH DWORD PTR SS:[EBP+10]
73D38FBE PUSH EDI
73D38FBF PUSH EBX
73D38FC0 CALL MFC42.#4424_?OnCmdMsg@CCmdTarget@@UAEHIHPAXPAUAFX_CMD> ; CCmdTarget::OnCmdMsg()
73D38FC5 TEST EAX,EAX
73D38FC7 JNZ SHORT MFC42.73D38FE6
…
73D38FD8 RETN 10
对话框的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()函数,使它能为文档模板类提供命令消息。
73D3223C > PUSH EBP
73D3223D MOV EBP,ESP
…
73D32267 CALL DWORD PTR DS:[EAX+30] ; GetMessageMap 得到消息映射表地址
…
73D3227A CALL MFC42.#1145_?AfxFindMessageEntry@@YGPBUAFX_MSGMAP_ENT> ;寻找消息函数
73D3227F TEST EAX,EAX
73D32281 JNZ SHORT MFC42.73D32296 ; 找到,则转
…
73D32296 PUSH DWORD PTR SS:[EBP+14]
73D32299 PUSH DWORD PTR DS:[EAX+10]
73D3229C PUSH DWORD PTR SS:[EBP+10]
73D3229F PUSH DWORD PTR DS:[EAX+14] ;这个[EAX+14]就是消息函数的地址
73D322A2 PUSH DWORD PTR SS:[EBP+C]
73D322A5 PUSH DWORD PTR SS:[EBP+8]
73D322A8 PUSH EDI
73D322A9 CALL MFC42.73D3233C ;调用_AfxDispatchCmdMsg这个函数
…
这里GetMessageMap() 和 AfxFindMessageEntry() 两个函数就是搜索查寻消息函数在消息映射表中的位置,从而找出消 息函数的地址。关于这个两个函数代码分析,及消息映射表的结构,本文就不分析了(分析起来,又要啰里啰嗦地说了一大堆)。有兴趣者可自行参考相关资料,网 上很多(看雪论坛精华集上也有很多这方面的资料,而且写得很不错)。
提示:
个人认为,对于对话框程序,在这个函数函数入口处设置断点最好(请记住这个函数:CCmdTarget::OnCmdMsg()),因为:
一、不用设置条件断点,只有在发生WM_COMMAND消息后,才运行到此。
二、而且再继续往下运行到73D3229F PUSH DWORD PTR DS:[EAX+14],就得到了消息函数的地址。
或者,也可以往下到 73D322A9 CALL MFC42.73D3233C语句时F7跟进,再往下执行几条语句,就很容易来到WM_COMMAND消息函数代码处。见下面说明。
跟进73D3233C,执行几条语句,经过几次跳转后,很快就定位到了按钮事件代码处:
73D3233C PUSH EBP
73D3233D MOV EBP,ESP
...
73D3234E CMP EAX,28
73D32351 JBE SHORT MFC42.73D32390
73D32353 SUB EAX,29
73D32356 JE MFC42.73D8E55B
73D3235C SUB EAX,3
73D3235F JNZ MFC42.73D8E52F
…
73D3239E SUB EAX,0A
73D323A1 > JE SHORT MFC42.73D323D2
73D323A3 DEC EAX
73D323A4 JE MFC42.73D8E4FD
73D323AA SUB EAX,16
73D323AD JE SHORT MFC42.73D323C8
73D323AF SUB EAX,3
73D323B2 JNZ MFC42.73D8E4E7
…
73D323D2 MOV ECX,DWORD PTR SS:[EBP+8] ; Case C of Switch XXXXXXXX
73D323D5 CALL DWORD PTR SS:[EBP+14] ; D1.?OnButton1@CD1Dlg@@IAEXXZ D1Dlg.obj 进入按钮函数代码
73D323D8 ^ JMP SHORT MFC42.73D3237B
其实上面一段代码就是_AfxDispatchCmdMsg函数。
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"); }
注:
MFC另外两种常见的框架程序,SDI和MDI程序,其消息流程分析同此类似,其间过程大同小异,我就不再分析了。有兴趣者,可自行分析,这里只是提一下注意之点:
1. 对于SDI/MDI程序,最好在CWnd::OnCommand()函数入口处,设置断点,然后跟进CCmdTarget::OnCmdMsg()函数,就很快找到消息函数地址了。
2. 若要在CCmdTarget::OnCmdMsg()函数入口处设置断点。必须设置条件断点,如[esp+4]==3e8, 这里3e8是按钮的 ID值。由于在SDI和MDI程序正常运行时,经常有文档类,框架类消息从此流过,设置条件断点后,会引起OD运行特别慢。所以,如果确实要在此处设置断 点,注意:在需要设置断点时再设置断点,OD中断后,马上取消断点!
附录:MFC对Windows消息的截获过程
MFC在调用对话框的DoModal函数之时,在PreModal内部调用了AfxHookWindowCreate()函数(对于SDI/MDI程序, 是在调用CWnd::CreateEx()函数里面),这是一个安装WH_CBT类型钩子的函数。安装WH_CBT类型的钩子过程后,系统在下列情况下将 会首先调用此函数:激活窗口,创建或销毁窗口,最大化或最小化窗口,移动或缩放窗口,完成一个系统命令,从系统的消息队列里移除一个鼠标或者键盘事件,设 置键盘,设置输入焦点或者同步系统消息队列事件等。安装钩子后,窗口的消息走向就要发生变化。
安装构子函数AfxHookWindowCreate()源码如下:
void AFXAPI AfxHookWindowCreate(CWnd* pWnd)
{
_AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (pThreadState->m_pWndInit == pWnd) return;
if (pThreadState->m_hHookOldCbtFilter == NULL)
{
pThreadState->m_hHookOldCbtFilter = ::SetWindowsHookEx(WH_CBT, _AfxCbtFilterHook, NULL, ::GetCurrentThreadId());
if (pThreadState->m_hHookOldCbtFilter == NULL) AfxThrowMemoryException();
}
pThreadState->m_pWndInit = pWnd;
}
继续观察其中钩子过程的地址_AfxCbtFilterHook的源码,部分源码如下:
LRESULT CALLBACK _AfxCbtFilterHook(int code, WPARAM wParam, LPARAM lParam)
{ _AFX_THREAD_STATE* pThreadState = _afxThreadState.GetData();
if (code != HCBT_CREATEWND)
{ // 等待HCBT_CREATEWND通过其他的钩子
return CallNextHookEx(pThreadState->m_hHookOldCbtFilter, code, wParam, lParam);
}
…
if (!afxData.bWin4 && !bContextIsDLL && (pCtl3dState = _afxCtl3dState.GetDataNA()) != NULL && pCtl3dState->
m_pfnSubclassDlgEx != NULL && (dwFlags = AfxCallWndProc(pWndInit, hWnd, WM_QUERY3DCONTROLS)) != 0)
{ // 窗口类注册是用AfxWndProc进行的吗?
WNDPROC afxWndProc = AfxGetAfxWndProc();
BOOL bAfxWndProc = ((WNDPROC)GetWindowLong(hWnd, GWL_WNDPROC) == afxWndProc);
pCtl3dState->m_pfnSubclassDlgEx(hWnd, dwFlags);
if (!bAfxWndProc) //如果还没有编排到AfxWndProc则用AfxWndProc子类化窗口
{ //用标准的AfxWndProc子类化窗口
oldWndProc = (WNDPROC)SetWindowLong(hWnd, GWL_WNDPROC,
(DWORD)afxWndProc);
*pOldWndProc = oldWndProc; //保存原窗口过程
}
}
…
}
从钩子函数中可以看到,如果它检测到注册窗口类时,使用的窗口过程不是AfxWndProc函数,则它将用此函数替代,并且通过这个设置,在调用:: CreateWindowEx后,将原窗口过程保存在窗口类的成员变量m_pfnSuper中,这样就形成了一个窗口过程链。在需要的时候,原窗口过程地 址可以通过窗口类成员函数GetSuperWndProcAddr获得。
这样,AfxWndProc就成为CWnd或其派生类的窗口过程。不论队列消息还是非队列消息,都将首先送到AfxWndProc窗口过程进行处理。经过消息分发之后仍没有被处理的消息,将送给原窗口过程处理。
注册时所使用的窗口过程果真不是AfxWndProc,而是DefWindowProc,因此钩子函数将完成这一替换任务。而调用函数AfxGetAfxWndProc所返回的结果正是函数AfxWndProc的地址,从其定义便可知这一事实:
WNDPROC AFXAPI AfxGetAfxWndProc()
{
#ifdef _AFXDLL
return AfxGetModuleState()->m_pfnAfxWndProc;
#else
return &AfxWndProc;
#endif
}
至此,相信各位能够明白DispatchMessage最终将消息发到了AfxWndProc函数里,而非DefWindowProc里。AfxWndProc的作用是截获所有发送给窗口的消息(包括队列消息和非队列消息),所以实质上它是一个窗口消息分发器。
==============================================================================================
结束语:
我们的消息之旅到此结束了。相信各位仔细看完之后,不会再MFC的消息流程感到迷惑。MFC的消息就是沿着固定的路线,中间可能有分支,沿途经过一二十个站点,就能到达目的地 -- 消息函数地址处了。
各位看完之后,若有不明白或疑问之处,我很乐意同大家进行探讨!
这篇文章中我没有进行公式性条款归纳,因为我觉得没有这个必要,只要理解了MFC的这种消息处理机制,比任何归纳都重要。就可以从任意一个MFC函数切入,随意跟踪MFC程序,以不变应万变,任你程序消息函数处理如何隐藏,如何变化多端。我们都可以揪出来。
最后,本文肯定仅在很多不当和失误之处,若能得到各位高手指点一二,我的这份辛苦也不算白费了。
作者:szdbg
E-mail:szdbg@sina.com
【版权声明】 本文原创于看雪论坛,纯属技术交流, 转载请注明作者并保持文章的完整, 谢谢!