MFC程式訊息流程程

來源:互聯網
上載者:User

記得前一段時間,我剛接觸軟體破解和逆向這一行時,對於一些軟體不知從何處跟蹤按鈕訊息,試了好多方法,就是斷不下來,在系統模組中經常轉得暈頭轉向,而一無所獲。

MFC程式是一種常見類型的程式,我靜下心來,潛心研究了一下MFC訊息流程程。弄清原委之後,一切豁然開朗,發現跟蹤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的實現訊息分發模式的工具。

PumpMessage函數反組譯碼代碼:
73D31194 > 56 PUSH ESI
...
73D311A1 FF15 B0B6DC73 CALL DWORD PTR DS:[<&USER32.GetMessageA>]
73D311A7 85C0 TEST EAX,EAX
73D311A9 74 26 JE SHORT MFC42.73D311D1 ;收到WM_QUIT,退出程式
73D311AB 817E 38 6A030000 CMP DWORD PTR DS:[ESI+38],36A
73D311B2 74 1A JE SHORT MFC42.73D311CE
73D311B4 8B06 MOV EAX,DWORD PTR DS:[ESI]
73D311B6 57 PUSH EDI
73D311B7 8BCE MOV ECX,ESI
73D311B9 FF50 60 CALL DWORD PTR DS:[EAX+60] ; PreTranslateMessage (訊息預先處理)
73D311BC 85C0 TEST EAX,EAX
73D311BE 75 0E JNZ SHORT MFC42.73D311CE
73D311C0 57 PUSH EDI ;訊息預先處理返回FALSE
73D311C1 FF15 ACB6DC73 CALL DWORD PTR DS:[<&USER32.TranslateMessage>]
73D311C7 57 PUSH EDI
73D311C8 FF15 30B6DC73 CALL DWORD PTR DS:[<&USER32.DispatchMessageA>]
;
73D311CE 6A 01 PUSH 1 ;返回TRUE
73D311D0 58 POP EAX
73D311D1 5F POP EDI
73D311D2 5E POP ESI
73D311D3 C3 RETN

提示:
a. OD載入程式後,調出MFC42.dll模組,定位到PumpMessage代碼入口處。
b. 在CALL DWORD PTR DS:[EAX+60]這一條語句上設定條件斷點[[esp]+4]==202,即可設定滑鼠左鍵釋放斷點。
說明:call [eax+60]是調用PreTranslateMessage函數,入口參數為:MSG* pMsg,所以:
[esp]就是pMsg,而[[esp]]就是pMsg->hWnd , [[esp]+4]就是pMsg->Message
c. [[esp]]==002407B4 && [[esp]+4]==202 可以為指定按鈕設定點擊斷點。這裡002407B4是目標按鈕的控制代碼.

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
}

PreTranslateMessage函數反組譯碼部分代碼
73D313D0 > PUSH ESI
73D313D1 PUSH EDI
73D313D2 MOV EDI,DWORD PTR SS:[ESP+C]
73D313D6 CMP DWORD PTR DS:[EDI],0
73D313D9 JE MFC42.73D8E9A1
73D313DF CALL MFC42.#6575_?AfxGetMainWnd@@YGPAVCW>
73D313E4 MOV ESI,EAX
73D313E6 TEST ESI,ESI
73D313E8 JE SHORT MFC42.73D313ED
73D313EA MOV EAX,DWORD PTR DS:[ESI+20]
73D313ED PUSH EDI
73D313EE PUSH EAX
73D313EF CALL MFC42.#6367_?WalkPreTranslateTree@C>
73D313F4 TEST EAX,EAX
73D313F6 JNZ SHORT MFC42.73D31415
73D313F8 > TEST ESI,ESI
73D313FA JE SHORT MFC42.73D3140E
73D313FC PUSH DWORD PTR DS:[EDI]
73D313FE CALL MFC42.#2864_?FromHandle@CWnd@@SGPAV>
73D31403 MOV ECX,EAX
73D31405 CALL MFC42.#3815_?GetTopLevelParent@CWnd>
73D3140A CMP EAX,ESI
73D3140C JNZ SHORT MFC42.73D3141A
73D3140E XOR EAX,EAX
73D31410 POP EDI
73D31411 POP ESI
73D31412 RETN 4
提示:
a. OD載入程式後,調出MFC42.dll模組,定位到PreTranslateMessage代碼入口處。
b. 在函數入口處設定條件斷點[[esp+4]+4]==202,即可設定滑鼠左鍵釋放斷點。
說明:此函數的入口參數為:MSG* pMsg,在入口處時,[esp]是函數返回地址,所以:
[esp+4]就是pMsg,而[[esp+4]]就是pMsg->hWnd , [[esp+4]+4]就是pMsg->Message
c. [[esp+4]]==002407B4 && [[esp+4]+4]==202 可以為指定按鈕設定點擊斷點。這裡002407B4是目標按鈕的控制代碼.

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;  //訊息被某一視窗處理了,返回
}
if (hWnd == hWndStop) break; // got to hWndStop window without interest
}
return FALSE; // no special processing
}
正是這個if (pWnd->PreTranslateMessage(pMsg)) return TRUE; 才實現了MFC靈活的訊息分發處理機制。MFC程式各個視窗類別中重載的PreTranslateMessage虛函數,都是從這裡進來的。
MFC從當前訊息視窗類逐級向上搜尋執行各個類的PreTranslateMessage函數,只要有一個PreTranslateMessage函數
返回TRUE,WalkPreTranslateTree就中止搜尋,並返回TRUE,否則返回FALSE。
在PumpMessage函數中最終就是根據WalkPreTranslateTree函數的傳回值決定是否要由Windows系統進行訊息處理與分發。

WalkPreTranslateTree函數反組譯碼代碼如下:
73D31389 MOV EDI,EDI ; D1.0040308C
73D3138B PUSH ESI
73D3138C PUSH EDI
73D3138D MOV EDI,DWORD PTR SS:[ESP+10]
73D31391 MOV ESI,DWORD PTR DS:[EDI]
73D31393 JMP SHORT MFC42.73D313BD
73D31395 /PUSH ESI
73D31396 |CALL MFC42.#2867_?FromHandlePermanent@CWnd@@SGPAV> ;取得CWnd 指標值
73D3139B |TEST EAX,EAX ;CWnd * 值不為NULL時,則調用CWnd::PreTranslateMessage()
73D3139D |JE SHORT MFC42.73D313AE
73D3139F |MOV EDX,DWORD PTR DS:[EAX]
73D313A1 |PUSH EDI
73D313A2 |MOV ECX,EAX
73D313A4 |CALL DWORD PTR DS:[EDX+98] ;<JMP.&MFC42.#5280_?PreTranslateMessage > ;通過虛函數方式調用
73D313AA |TEST EAX,EAX
73D313AC |JNZ SHORT MFC42.73D313C8 ;訊息被處理了,返回TRUE
73D313AE |CMP ESI,DWORD PTR SS:[ESP+C]
73D313B2 |JE SHORT MFC42.73D313C1
73D313B4 |PUSH ESI ; /hWnd
73D313B5 |CALL DWORD PTR DS:[<&USER32.GetParent>] ; GetParent
73D313BB |MOV ESI,EAX
73D313BD TEST ESI,ESI
73D313BF JNZ SHORT MFC42.73D31395
73D313C1 XOR EAX,EAX ;返回FALSE
73D313C3 POP EDI
73D313C4 POP ESI
73D313C5 RETN 8
73D313C8 XOR EAX,EAX
73D313CA INC EAX ;返回 TRUE
73D313CB JMP SHORT MFC42.73D313C3
跟蹤說明:
在上面一句73D313A4 CALL DWORD PTR DS:[EDX+98] 設定按鈕點擊條件斷點: [[esp]]==002407B4 && [[esp]+4]==202
可以發現:當點擊按鈕後,按鈕點擊事件函數代碼就會在這條語句後執行,當按鈕事件函數代碼執行完畢後,CALL才會返回TRUE。

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); // 訊息流程入此處
}

CDialog::PreTranslateMessage()反組譯碼代碼如下:
73D468A4 > PUSH ESI
73D468A5 PUSH EDI
73D468A6 MOV EDI,DWORD PTR SS:[ESP+C]
73D468AA MOV ESI,ECX
73D468AC PUSH EDI
73D468AD CALL MFC42.#5290_?PreTranslateMessage@CWnd@@UAEHPAUtagMSG@>
73D468B2 TEST EAX,EAX
73D468B4 JNZ MFC42.73D8D490
73D468BA MOV ECX,ESI
73D468BC CALL MFC42.#3813_?GetTopLevelFrame@CWnd@@QBEPAVCFrameWnd@@>
73D468C1 TEST EAX,EAX
73D468C3 JNZ MFC42.73D8D429
73D468C9 CMP DWORD PTR DS:[EDI+4],100
73D468D0 JE SHORT MFC42.73D468DF
73D468D2 PUSH EDI
73D468D3 MOV ECX,ESI
73D468D5 CALL MFC42.#5278_?PreTranslateInput@CWnd@@QAEHPAUtagMSG@@@> ;訊息從流入此處
73D468DA POP EDI
73D468DB POP ESI
73D468DC RETN 4

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()函數進行下一步處理。
CWnd::PreTranslateInput()函數反組譯碼代碼如下:
73D34009 > MOV EDX,DWORD PTR SS:[ESP+4]
73D3400D MOV EAX,DWORD PTR DS:[EDX+4]
73D34010 CMP EAX,100
73D34015 JNB SHORT MFC42.73D34023
73D34017 CMP EAX,200
73D3401C JNB SHORT MFC42.73D34032
73D3401E XOR EAX,EAX
73D34020 RETN 4
73D34023 CMP EAX,108
73D34028 ^ JA SHORT MFC42.73D34017
73D3402A PUSH EDX
73D3402B CALL MFC42.#4047_?IsDialogMessageA@CWnd@@QAEHPAUtagMSG@@@Z
73D34030 ^ JMP SHORT MFC42.73D34020
73D34032 CMP EAX,209
73D34037 ^ JBE SHORT MFC42.73D3402A
73D34039 ^ JMP SHORT MFC42.73D3401E
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真是太牛了。

CWnd::IsDialogMessageA函數反組譯碼代碼:
73D468F5 > PUSH ESI
73D468F6 MOV ESI,ECX
73D468F8 TEST BYTE PTR DS:[ESI+29],1
73D468FC JNZ MFC42.73D8E273
73D46902 PUSH DWORD PTR SS:[ESP+8]
73D46906 PUSH DWORD PTR DS:[ESI+20]
73D46909 CALL DWORD PTR DS:[<&USER32.IsDialogMessageA>] ;進入系統核心
73D4690F POP ESI
73D46910 RETN 4
提示:
1 . OD中設斷: bp IsDialogMessageA MSG==202 , 則當滑鼠左鍵釋放時,會中斷在User32.IsDialogMessageA函數入口上。
2. 若已知按鈕的控制代碼,且要求當點擊該按鈕時,程式中斷在IsDialogMessageA上,則可以作如下設斷:
bp IsDialogMessageA [[esp+8]]==00060350 && MSG==202.
3. 中斷後,可以通過堆棧返回到CWnd::IsDialogMessageA函數代碼處。
=========================================================================================================
8. User32 核心處理,不分析
這裡面的過程,我們就當作一個黑匣子吧,不管它,一般情況下,也無需管它。因為我們百分之百相信它。
=========================================================================================================
當訊息到達此處時,又進入了MFC地界 .第8步之前,可以說是經常峰迴路轉,山重水複。第8步之後,是柳暗花明,可以一路高歌,直奔目的地了。

欲知後事如何,且聽下回分解!

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.