MFC应用程序
下面是MFC应用程序的运行流程,通过MFC库中代码进行分析:
首先在HelloWorld.cpp中定义全局对象theApp:CHelloWorldApp theApp;。调用CWinApp和CHelloWorldApp的构造函数后,进入WinMain函数(位于appmodul.cpp中)。
- extern "C" int WINAPI
- _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,
- _In_ LPTSTR lpCmdLine, int nCmdShow)
- #pragma warning(suppress: 4985)
- {
- // call shared/exported WinMain
- return AfxWinMain(hInstance, hPrevInstance, lpCmdLine, nCmdShow);
- }
在TCHAR.h中,有此定义:#define _tWinMain WinMain,所以这里的_tWinMain就是WinMain函数。它调用了AfxWinMain函数(位于WinMain.cpp中)。
- int AFXAPI AfxWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPTSTR lpCmdLine, int nCmdShow)
- {
- .............略
- // App global initializations (rare)
- if (pApp != NULL && !pApp->InitApplication())
- goto InitFailure;
- if (!pThread->InitInstance())
- {
- .........略
- }
- // Run函数位于THRDCORE.cpp中,由此函数进入消息循环
- nReturnCode = pThread->Run();
- ..............略
- return nReturnCode;
- }
上面InitInstance函数的代码如下:
- BOOL CTestApp::InitInstance()
- {
- .............略
- CSingleDocTemplate* pDocTemplate;
- pDocTemplate = new CSingleDocTemplate(
- IDR_MAINFRAME,
- RUNTIME_CLASS(CTestDoc),
- RUNTIME_CLASS(CMainFrame), // main SDI frame window
- RUNTIME_CLASS(CTestView));
- if (!pDocTemplate)
- return FALSE;
- AddDocTemplate(pDocTemplate);
- // Parse command line for standard shell commands, DDE, file open
- CCommandLineInfo cmdInfo;
- ParseCommandLine(cmdInfo);
- //ProcessShellCommand位于AppUI2.cpp中,注册并创建窗口
- if (!ProcessShellCommand(cmdInfo))
- return FALSE;
- m_pMainWnd->ShowWindow(SW_SHOW);
- m_pMainWnd->UpdateWindow();
- return TRUE;
- }
InitInstance中的ProcessShellCommand函数又调用了CMainFrame的LoadFrame函数注册并创建了窗口,执行完ProcessShellCommand函数以后,调用了m_pMainWnd的ShowWindow和UpdateWindow函数显示并更新框架窗口。这些是不是与上面的SDK程序十分类似?
接下来该是消息循环了,上面的AfxWinMain函数中调用了pThread的Run函数(位于THRDCORE.cpp中),在Run中包含了消息循环。Run函数的代码如下:
- int CWinThread::Run()
- {
- .............略
- // phase2: pump messages while available
- do
- {
- // pump message, but quit on WM_QUIT
- if (!PumpMessage())
- return ExitInstance();
- // reset "no idle" state after pumping "normal" message
- if (IsIdleMessage(&m_msgCur))
- {
- bIdle = TRUE;
- lIdleCount = 0;
- }
- } while (::PeekMessage(&m_msgCur, NULL, NULL, NULL, PM_NOREMOVE));
- ..............略
- }
- BOOL CWinThread::PumpMessage()
- {
- return AfxInternalPumpMessage();
- }
- BOOL AFXAPI AfxInternalPumpMessage()
- {
- _AFX_THREAD_STATE *pState = AfxGetThreadState();
- if (!::GetMessage(&(pState->m_msgCur), NULL, NULL, NULL))
- {
- .............略
- }
- ...............略
- if (pState->m_msgCur.message != WM_KICKIDLE && !AfxPreTranslateMessage(&(pState->m_msgCur)))
- {
- ::TranslateMessage(&(pState->m_msgCur));
- ::DispatchMessage(&(pState->m_msgCur));
- }
- return TRUE;
- }
我们看到PumpMessage中通过调用GetMessage、TranslateMessage、DispatchMessage等建立了消息循环并投递消息。
窗口过程函数AfxWinProc形式如下:
- LRESULT CALLBACK AfxWndProc(HWND hWnd,UINT nMsg,WPARAM wParam, LPARAM lParam)
- {
- ……
- CWnd*pWnd=CWnd::FromHandlePermanent(hWnd);
- ReturnAfxCallWndProc(pWnd,hWnd,nMsg,wParam,lParam);
- }
两者运行过程对比
到此,通过对比可以发现,MFC应用程序的运行流程与SDK程序是类似的,都是先进行一些初始化过程,再注册并创建窗口,然后显示、更新窗口,最后进入消息循环,消息都由窗口过程函数处理。现在大家是不是觉得有些头绪了?在运行流程上有基本的掌握即可。
二.MFC应用程序框架主要类之间的关系
在第二讲中,给大家演示了如何利用应用程序向导生成单文档应用程序框架,可以看到程序的基本框架和必要的代码都自动生成了,上一讲又讲解了文件组成结构,实际上在前面自动生成的框架中比较重要的类包括以下几个:CHelloWorldApp、CMainFrame、CHelloWorldDoc和CHelloWorldView,至于其他的类比如CClassView、CFileView等都是在框架窗口(CMainFrame)上创建的面板等,不是必要的。
鸡啄米就四个主要类的关系简单讲下,CHelloWorldApp类处理消息,将收到的消息分发给相应的对象。CMainFrame是视图CHelloWorldView的父窗口,视图CHelloWorldView就显示在CMainFrame的客户区中。视图类CHelloWorldView用来显示文档类CHelloWorldDoc中的数据,并根据对视图类的操作修改文档类的数据。一个视图类只能跟一个文档类相联系,而一个文档类可以跟多个视图类相联系。关于视图类和文档类的关系后面会详细讲解。
本节VC++/MFC编程入门教程内容比较多,主要是让大家对MFC应用程序的运行原理有大概的了解。对于以后的MFC开发有很多好处。如果有问题请在鸡啄米博客留言交流。谢谢。
用工具:
IDA4.04、ExeScope、LoadPE、VC++6.0
一、准备知识:消息映射声明的解释
成员变量有两个:
第一个是_messageEntries
第二个是messageMap。
第一个成员变量的声明:
AFX_MSGMAP_ENTRY _messageEntries[]
这是一个AFX_MSGMAP_ENTRY 类型的数组变量,是一个静态成员变量,用来容纳类的消息映射条目。一个消息映射条目可以用AFX_MSGMAP_ENTRY结构来描述。
AFX_MSGMAP_ENTRY结构的定义如下:
struct AFX_MSGMAP_ENTRY
{
//Windows消息ID
UINT nMessage;
//控制消息的通知码
UINT nCode;
//Windows Control的ID
UINT nID;
//如果是一定范围的消息被映射,则nLastID指定其范围
UINT nLastID;
UINT nSig;//消息的动作标识
//响应消息时应执行的函数(routine to call (or special value))
AFX_PMSG pfn;
};
从上述结构可以看出,每条映射有两部分的内容:第一部分是关于消息ID的,包括前四个域;第二部分是关于消息对应的执行函数,包括后两个域。
在上述结构的六个域中,pfn是一个指向CCmdTarger成员函数的指针。函数指针的类型定义如下:
typedef void (AFX_MSG_CALL CCmdTarget::*AFX_PMSG)(void);
当使用一条或者多条消息映射条目初始化消息映射数组时,各种不同类型的消息函数都被转换成这样的类型:不接收参数,也不返回参数的类型。因为所有可以有消息映射的类都是从CCmdTarge派生的,所以可以实现这样的转换。
nSig是一个标识变量,用来标识不同原型的消息处理函数,每一个不同原型的消息处理函数对应一个不同的nSig。在消息分发时,MFC内部根据nSig把消息派发给对应的成员函数处理,实际上,就是根据nSig的值把pfn还原成相应类型的消息处理函数并执行它。
第二个成员变量的声明
AFX_MSGMAP messageMap;
这是一个AFX_MSGMAP类型的静态成员变量,从其类型名称和变量名称可以猜出,它是一个包含了消息映射信息的变量。的确,它把消息映射的信息(消息映射数组)和相关函数打包在一起,也就是说,得到了一个消息处理类的该变量,就得到了它全部的消息映射数据和功能。AFX_MSGMAP结构的定义如下:
struct AFX_MSGMAP
{
//得到基类的消息映射入口地址的数据或者函数
#ifdef _AFXDLL
//pfnGetBaseMap指向_GetBaseMessageMap函数
const AFX_MSGMAP* (PASCAL* pfnGetBaseMap)();
#else
//pBaseMap保存基类消息映射入口_messageEntries的地址
const AFX_MSGMAP* pBaseMap;
#endif
//lpEntries保存消息映射入口_messageEntries的地址
const AFX_MSGMAP_ENTRY* lpEntries;
};
从上面的定义可以看出,通过messageMap可以得到类的消息映射数组_messageEntries和函数_GetBaseMessageMap的地址(不使用MFC DLL时,是基类消息映射数组的地址)。
成员函数 _GetBaseMessageMap() 用来得到基类消息映射的函数。
成员函数 GetMessageMap() 用来得到自身消息映射的函数。
二、通过IDA找到VC++ PE 的 AFX_MSGMAP_ENTRY数组
综上所述,我们可以通过IDA反汇编VC++ PE,搜索:GetRuntimeClass,会发现几个如下的数据结构:
004DF698 -----------------------------------------------------------------------------------------
004DF698 下面是消息映射
004DF698 D0 FB 47 00 off_4DF698 dd offset sub_47FBD0 ; DATA XREF: sub_47FBF0+Co
004DF69C A0 F6 4D 00 dd offset Message_0
004DF6A0 11 BD 00 00 Message_0 dd 0BD11h ; DATA XREF: .rdata:004DF69Co
004DF6A4 02 00 00 00 nCode_0 dd 2
004DF6A8 00 00 00 00 nID_0 dd 0 控件ID
004DF6AC 00 00 00 00 nLastID_0 dd 0
004DF6B0 0C 00 00 00 nSig_0 dd 0Ch
004DF6B4 10 FC 47 00 pfn_0 dd 47FC10h 响应函数
以下每6个dword代表一个
004DF6B8 04 02 00 00 dd 204h ; WM_RBUTTONDOWN
004DF6BC 00 00 00 00 dd 0
004DF6C0 00 00 00 00 dd 0
004DF6C4 00 00 00 00 dd 0
004DF6C8 31 00 00 00 dd 31h
004DF6CC 60 FC 47 00 dd 47FC60h ;WM_RBUTTONDOWN响应函数
004DF6D0 11 01 00 00 dd 111h
004DF6D4 00 00 00 00 dd 0
004DF6D8 20 80 00 00 dd 8020h
004DF6DC 20 80 00 00 dd 8020h
004DF6E0 0C 00 00 00 dd 0Ch
004DF6E4 70 11 48 00 dd 481170h
004DF6E8 11 01 00 00 dd 111h
004DF6EC 00 00 00 00 dd 0
004DF6F0 21 80 00 00 dd 8021h
004DF6F4 21 80 00 00 dd 8021h
004DF6F8 0C 00 00 00 dd 0Ch
004DF6FC B0 11 48 00 dd 4811B0h
004DF700 11 01 00 00 dd 111h
004DF704 00 00 00 00 dd 0
004DF708 22 80 00 00 dd 8022h
004DF70C 22 80 00 00 dd 8022h
004DF710 0C 00 00 00 dd 0Ch
004DF714 D0 12 48 00 dd 4812D0h
004DF718 11 01 00 00 dd 111h
004DF71C 00 00 00 00 dd 0
004DF720 23 80 00 00 dd 8023h
004DF724 23 80 00 00 dd 8023h
004DF728 0C 00 00 00 dd 0Ch
004DF72C 50 13 48 00 dd 481350h
004DF730 11 01 00 00 dd 111h
004DF734 00 00 00 00 dd 0
004DF738 25 80 00 00 dd 8025h
004DF73C 25 80 00 00 dd 8025h
004DF740 0C 00 00 00 dd 0Ch
004DF744 90 13 48 00 dd 481390h
004DF748 11 01 00 00 dd 111h
004DF74C 00 00 00 00 dd 0
004DF750 26 80 00 00 dd 8026h
004DF754 26 80 00 00 dd 8026h
004DF758 0C 00 00 00 dd 0Ch
004DF75C C0 14 48 00 dd 4814C0h
004DF760 01 02 00 00 dd 201h
004DF764 00 00 00 00 dd 0
004DF768 00 00 00 00 dd 0
004DF76C 00 00 00 00 dd 0
004DF770 31 00 00 00 dd 31h
004DF774 80 FF 47 00 dd 47FF80h
004DF778 00 01 00 00 dd 100h
004DF77C 00 00 00 00 dd 0
004DF780 00 00 00 00 dd 0
004DF784 00 00 00 00 dd 0
004DF788 10 00 00 00 dd 10h
004DF78C 90 15 48 00 dd 481590h
004DF790 00 00 00 00 dd 0 ;6个全00代表结束
004DF794 00 00 00 00 dd 0
004DF798 00 00 00 00 dd 0
004DF79C 00 00 00 00 dd 0
004DF7A0 00 00 00 00 dd 0
004DF7A4 00 00 00 00 dd 0
004DF7A8 BA 8A 4C 00 off_4DF7A8 dd offset j_?GetRuntimeClass@CStatic@@UBEPAUCRuntimeClass@@XZ
004DF7A8 ; DATA XREF: sub_47FA50+33o
004DF7A8 ; sub_47FB50+24o
004DF7A8 ; CStatic::GetRuntimeClass(void)
因为各个类可能都有消息循环,所以,可能会找到很多个,但只要仔细查找控件ID,就很容易找到这个控件的响应代码。不管这个控件是菜单还是按钮,都可以找到。这样就可以对这段响应代码修改或替换了。
同理,知道了本原理,也可以很容易的给某个控件加上一个子控件(如按钮等)。
Spring.W
2006.2