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函數。
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
【版權聲明】 本文原創於看雪論壇,純屬技術交流, 轉載請注明作者並保持文章的完整, 謝謝!