Windows应用程序捆绑核心编程
作者: 张正秋 发表时间:
2007-4-24 19:56:50 所属类型:系统编程
|
3.1 引言
( 本章字数:3585 更新时间:2007-4-26
19:08:26)
|
本章提要
Ø
进程通信概述
Ø
通过自定义消息通信
Ø
通过WM_COPYDATA消息通信
Ø
使用内存读写函数和内存映射文件通信
Ø
使用动态链接库(DLL)通信
Ø
使用Windows剪贴板通信
Ø
使用动态数据交换(DDE)通信
3.1 引言
进程之间的通信(IPC,
Inter-Process
Communication)是应用程序之间捆绑最为基础的技术。它涉及两个或两个以上进程之间进行通信联系。通过信息交换,一个应用程序的功能可以得到扩展,例如一个应用程序可以控制另外一个应用程序等。
随着Windows技术的发展,应用程序的种类越来越多,要想使这些程序发挥更大的作用,程序间的通信就显得越来越重要了。目前,已有很多这种技术,但对这些技术文字描述性的文章比较多,而涉及到这些技术的具体实现则较少,因此,看了那些文章后,很多读者仍然较糊涂。为此,本章着重于实例分析,使读者看完后能够马上编写类似的程序。同时,通过实例学习编程也是一种非常有效的方法。
进程之间通信方法较多,在不同的计算机语言下,这些通信方法的实现过程也不一样。本章将介绍几种常用的和实用的通信方法。在进行两个程序间通信时,必须先运行接收数据的程序(服务程序,可以看成是一个服务器),以便发送程序可以找到接收程序的进程窗口句柄。以下所介绍的进程间通信程序,在没有特别说明的情况下,接收数据的程序都是先行启动。如果不这样,则需要在发送程序中使用一个工作线程或者时钟控制,来检测是否有所指定的接收进程存在。
程序之间的通信包括本地进程通信和远程进程通信,本章主要介绍初级的本地进程通信,而高级的和远程或网络的进程间的通信将在下一章做专门介绍。本章所介绍的每种通信方法都给出了一个实例,用VC++语言进行实现,并且都在Windows
XP系统下进行了运行。同时给出了发送数据程序和接收数据程序的主要源代码。图3.1和图3.2是本章开发的进程之间通信程序的演示对话框。
进程间通信是一个非常复杂的过程,涉及的问题也较多,例如进程互斥和同步等,这些问题在本书中不做介绍。
发送数据程序对话框 接收数据程序对话框
|
在学习编写进程之间通信的程序之前,有必要先介绍一些有关的背景知识,这样有助于读者对一些有关进程间通信方面的概念有较深入的了解。
3.2.1 Windows进程间标准通信技术的发展
我们既可以用非标准的进程间通信技术,如Windows消息、内存映射和内存共享等,也可以用标准的通信技术。微软标准进程间通信技术的发展过程如下所述。
(1)进程间通信初期
自从有Windows操作系统后,剪贴板(Clipboard)首先解决了不同程序间的通信问题(由剪贴板作为数据交换中心,进行复制、粘贴的操作)。但是剪贴板传递的都是“死”数据,应用程序开发者得自行编写、解析数据格式的代码。于是动态数据交换(Dynamic
Data
Exchange,DDE)的通信协定应运而生,它可以让应用程序之间自动获取彼此的最新数据。但是,解决彼此之间的数据格式转换仍然是程序员沉重的负担。对象的链接和嵌入(Object
Linking and
Embedded,OLE)的诞生把原来应用程序的数据交换提高到“对象交换”,这样程序间不但获得数据,而且也可以获得彼此的对象,并且可以直接使用彼此数据内容。
(2)OLE(对象链接与嵌入)
1991年制定的OLE1.0规范主要解决多个应用程序之间的通信和消息传递问题,微软希望第三方开发商能够遵守这个规范,以便在当时的Windows平台上的应用程序能够相互协调工作,更大地提高工作效率。然而事与愿违,只有很少的软件开发商支持它。为此,微软于1993年发布了新的规范——OLE2.0,它在原有的基础上完善并增强了以下各方面的性能:①OLE自动化,一个程序有计划地控制另一个程序的能力;②OLE控件,小型的组件程序,可嵌入到另外的程序,提供自己的专有功能;③OLE文档,完善了早期的混合文档功能,不仅支持简单的链接和嵌入,还支持在位激活、拖放等功能。
(3)ActiveX战略
同OLE1.0相比,OLE2.0得到了很多软件厂商的支持。许多程序设计人员编写了大量的实现OLE自动化服务器功能的组件(不一定是EXE文件),这些组件一般不求功能齐全、强大,而是可以实现专门的功能,被其他程序编程控制,由此承袭OLE的名字,称为OLE控件。它们在文件名中的扩展名一般为ocx(OLE
Control
Extension)。微软刚刚赢得广大软件厂商的支持,使OLE技术深入人心,然而天不如人愿,国际互联网的超速发展让比尔·盖茨始料未及。加上早期的OLE1.0不得人心,导致后来的人们总把在Word中插入一个图形当作OLE技术的全部,各类资料在介绍新OLE技术时命名也不统一,造成很大的混乱。针对这些情况,微软在1996年重新制订了一个关于OLE的规
范——OLE 96。这个规范扩展了
OLE控件的能力,并贯彻微软的Internet战略使它更易于在网络环境中使用,还考虑命名混淆的问题,重新给OLE控件贴上一个标签——ActiveX控件。不仅如此,以前的什么OLE文档也相应称为ActiveX
文档了。总之,为了满足Internet战略,微软把OLE换成了ActiveX,企图使人们重新看待新的OLE——ActiveX,把它看成网络上的解决软件组件问题的标准。
(4)OLE/ActiveX与COM/DCOM比较
OLE/ActiveX名称比COM/DCOM更为我们熟悉,其实OLE和ActiveX是商业名称,它们的发展过程为OLE→ActiveX(网络OLE)。COM和DCOM是纯技术名词,它们是OLE/ActiveX基础,其发展过程为COM→DCOM(网络COM),其中COM(Component
Object
Model,组件对象模式)是在OLE2.0中建立的规范。OLE/ActiveX不仅可以实现进程之间的通信,而且可以创建进程,它们是“类厂”组件对象。
3.2.2 应用程序与进程
应用程序和进程在概念上是有一定区别的,前者是静态的程序代码,而后者是动态的实体。只有应用程序加载到系统中后才能成为一个进程。Windows进程分为独立进程和共享进程两种。一般情况下,人们把独立运行的程序称为进程,其实这只是独立进程。在实际情况下也常常遇到另一种情况,即一个应用程序可能启动多个进程,一个进程空间可以运行多个程序,这就是共享进程。例如,同一个应用程序重复运行就启动了多个进程;而在一个进程中调用其他程序,或者通过程序挂钩,这就使同一个进程空间里运行了多个程序。这里着重讨论独立进程之间的通信。
3.2.3 进程之间通信的类型
根据不同的标准,进程之间通信类型有不同的划分方法。实际中也有多种划分方法,这里只给出几种划分方法如下所述。
(1)低级通信和高级通信
·
低级通信:只能传递状态和整数值(控制信息),包括进程互斥和同步所采用的信号量和管程机制。其第一个缺点为传送信息量小、效率低,每次通信传递的信息量固定,若传递较多信息则需要进行多次通信;第二个缺点是编程复杂,直接实现通信的细节,容易出错。
·
高级通信:能够传送任意数量的数据,包括共享存储区、管道、消息等。
(2)直接通信和间接通信
l
直接通信:信息直接传递给接收方,如管道,在发送时,指定接收方的地址或标识,也可以指定多个接收方或广播式地址;在接收时,允许接收来自任意发送方的消息,并在读出消息的同时获取发送方的地址。
l
间接通信:借助于收发双方进程之外的共享数据结构作为通信中转,如剪贴板。通常接收和发送方的数目可以是任意的。
(3)本地通信和远程通信
l
本地通信方式:这种通信又称之为同机通信,它是在同一台计算机上的程序之间进行的,也就是说客户进程和服务进程位于同一台计算机上。
l
远程通信方式:这种通信又称之为网间的进程通信,要解决的是不同主机进程间的相互通信问题(可把同机进程通信看作是其中的特例)。在这种通信中,首先要解决的是网络间的进程标识问题。同一主机上,不同进程可用进程号(process
ID)唯一标识。但在网络环境下,各主机独立分配的进程号不能唯一标识该进程。例如,主机A赋予某个进程号5,在B机中也可以存在5号进程,因此,“5号进程”这句话就没有意义了。其次,操作系统支持的网络协议众多,不同协议的工作方式不同,地址格式也不同。因此,网间的进程通信还要解决多重协议的识别问题。
|
Windows程序与其他类型程序的区别就是使用消息,例如键盘或鼠标消息等,在DOS系统下的程序没有定义消息。在Windows操作系统中,消息不但可以用于进程内的通信,也可以用于进程间的通信。这里重点介绍进程间的消息通信。
3.3.1 通过自定义消息实现进程间通信的方法
消息分为两种,即系统消息和用户(程序设计者)自定义消息。系统消息定义从0到0x3FF,可以使用0x400到0x7FFF定义自己的消息。Windows把0x400定义为WM_USER。如果想定义自己的一个消息,可以在WM_USER上加上一个值。
还有一种自定义窗口消息的方法是用RegisterWindowsMessage()函数来注册这个消息。与在WM_USER上加上某个数相比,它的好处是不必考虑所表示的消息标识符是否超出工程的允许范围。
要想用消息实现进程间通信,则需要在这两个程序中定义或注册相同的消息,才能保证数据通信顺利进行。用户自定义消息的方法如下:
l
#define WM_COMM WM_USER+100
const UINT wm_nRegMsg=RegisterWindowMessage("reg_data");
有了这两种定义的消息后,可以用如下的方法来发送消息。
pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg);
pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg);
其中pWnd为接收这个消息的窗口句柄,uMsg为要通过消息发送的数据,是长整型。这两个消息的发送可以分别在一个发送函数里实现,其具体的做法见以后的实例。通过实验可知,这两种自定义消息的发送效果是一样的。
在接收消息的程序中,除与发送消息的程序需要定义相同的消息外,还需要定义相应的消息映射和消息映射函数,消息的映射方法如下:
ON_MESSAGE(WM_COMM,OnUserReceiveMsg)
ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg)
与以上消息映射对应的函数定义如下:
void CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM
lParam)
{
// 增加用户自定义程序代码
…
}
//--------------------------------------------------------------------
void CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam)
{
// 增加用户自定义程序代码
…
}
其中OnUserReceiveMsg()函数为WM_COMM消息的映射函数,OnRegReceiveMsg()函数为wm_nRegMsg消息的映射函数。可以看出,这两种消息的映射函数形式是一样的。
3.3.2 通过自定义消息实现进程间通信的实例
为说明以自定义消息实现进程之间的通信,作者用VC++
编写了这样的程序。有两个对话框程序,其中一个为发送程序,另一个为接收程序。在这两个程序中分别定义了两个消息WM_COMM和wm_nRegMsg,在CDataSendDlg类中增加了用于发送数据的两个函数,即void
CDataSendDlg::OnSendUsermsg()和void CDataSendDlg::OnSendRegmsg()。它们的源代码如下:
void CDataSendDlg::OnSendUsermsg()
{
UpdateData(); // 更新数据
CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); //
查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox(TEXT("Unable to find DataRecv."));
return;
}
UINT uMsg;
uMsg=atoi(m_strUserMsg);
pWnd->SendMessage(WM_COMM,NULL,(LPARAM)uMsg); // 发送.
}
//--------------------------------------------------------------------
void CDataSendDlg::OnSendRegmsg()
{
UpdateData(); // 更新数据
CWnd *pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); //
查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox("Unable to find DataRecv.");
return;
}
UINT uMsg;
uMsg=atoi(m_strRegMsg);
pWnd->SendMessage(wm_nRegMsg,NULL,(LPARAM)uMsg); // 发送
}
在接收数据的程序中要做三件事:①定义自定义消息;②定义消息映射表;③定义消息映射函数。自定义消息的方法如前面所述。在CDataRecvDlg类中增加了两个用于接收数据的函数,即void
CDataRecvDlg::OnUserReceiveMsg()和void CDataRecvDlg::
OnRegReceiveMsg()。消息映射表如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg,
CDialog)
//{{AFX_MSG_MAP(CDataRecvDlg)
ON_MESSAGE(WM_COMM,OnUserReceiveMsg)
ON_REGISTERED_MESSAGE(wm_nRegMsg,OnRegReceiveMsg)
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
消息映射表中的映射函数名的格式是一样的。其实它们可以用同一个函数,为了说明方便,这里把它们的名字取为不一样。接收数据程序中的消息映射函数的源代码如下:
void
CDataRecvDlg::OnUserReceiveMsg(WPARAM wParam,LPARAM lParam)
{
m_strUserMsg.Format("%d\n",int(lParam));
UpdateData(FALSE); // 更新数据
}
//--------------------------------------------------------------------
void
CDataRecvDlg::OnRegReceiveMsg(WPARAM wParam,LPARAM lParam)
{
m_strRegMsg.Format("%d\n",int(lParam));
UpdateData(FALSE); // 更新数据
}
从上面的实例中可以看出,以自定义消息来进行进程之间的通信存在一定的局限性,即所发送的数据只能是长整型,而对于字符串,则不能进行通信。要进行字符串或大批量的数据的传输,则需要采用其他的通信方法。
|
对于少量数据可以用WM_COPYDATA方便地实现通信。由于SendMessage()是阻塞的,只有接收方响应了消息,SendMessage()才能返回,否则一直阻塞。所以,对于大量数据来说,用SendMessage()就容易造成窗口假死。
3.4.1 通过WM_COPYDATA消息实现进程间通信的方法
在Win32中,WM_COPYDATA消息主要目的是允许在进程间传递只读数据。SDK文档推荐用户使用SendMessage()函数,接收方在数据复制完成前不返回,这样发送方就不可能删除和修改数据。这个函数的原型如下:
SendMessage(WM_COPYDATA,wParam,lParam)
其中wParam设置为包含数据的窗口句柄,lParam指向一个COPYDATASTRUCT的结构,其定义为:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;
DWORD cbData;
PVOID lpData;
}COPYDATASTRUCT;
其中dwData为自定义数据,
cbData为数据大小,
lpData为指向数据的指针。需要注意的是,WM_COPYDATA消息保证发送的数据从原进程复制到目标进程。但是,WM_COPYDATA消息不能发送HDC、HBITMAP之类的东西,它们对于目标进程来说是无效的。目标进程得到这些数据不能在原进程作任何事情,因为它们属于不同的进程。
与其他进程通信方法一样,要实现进程间的数据通信,在发送数据的程序中,首先要找到接收数据进程的窗口句柄pWnd,可以用CWnd::FindWindow(NULL,_
T("DataRecv"))函数来得到,其中字符串"DataRecv"为接收数据的程序名。然后用SendMessage()函数发送数据,其具体的做法见后面的实例。
在接收数据的程序中,首先在消息映射表中增加WM_COPYDATA消息映射,然后定义消息映射函数,其函数的格式为:
BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd,
COPYDATASTRUCT* pCopyDataStruct)
{
// 增加用户自定义程序代码
…
}
3.4.2 通过WM_COPYDATA消息实现进程间通信的实例
与前面所说的自定义消息不一样,WM_COPYDATA消息是Win32提供的消息。与自定义消息相比较,WM_COPYDATA消息可以传递一个较大的数据块。这里仍然用两个对话框程序来实现WM_COPYDATA消息的通信。
以下分别给出发送数据程序的发送函数和接收数据程序的接收函数。在发送数据的对话框类CDataSendDlg中,用MFC
ClassWizard工具或者手工的方法增加函数void CDataSendDlg::OnSendCopydata(),其具体代码如下:
void CDataSendDlg::OnSendCopydata()
{
UpdateData(); // 更新数据
CWnd
*pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox("Unable to find
DataRecv.");
return;
}
COPYDATASTRUCT cpd; // 给COPYDATASTRUCT结构赋值
cpd.dwData = 0;
cpd.cbData =
m_strCopyData.GetLength();
cpd.lpData =
(void*)m_strCopyData.GetBuffer(cpd.cbData);
pWnd->SendMessage(WM_COPYDATA,NULL,(LPARAM)&cpd); // 发送
}
在用MFC
AppWizard(exe)创建接收数据的对话框程序后,生成对话框类CDataRecvDlg。在这个类中,首先要定义接收WM_COPYDATA消息的映射,可以用ClassWizard工具来增加,也可以手动增加,但手动增加需要修改三个地方:①在消息映射表中增加ON_WM_COPYDATA();②增加成员函数BOOL
CDataRecvDlg::OnCopyData();③在CDataRecvDlg类中增加WM_COPYDATA消息映射函数的定义。
WM_COPYDATA消息的映射如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog)
//{{AFX_MSG_MAP(CDataRecvDlg)
ON_WM_COPYDATA()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
CDataRecvDlg::OnCopyData()函数的定义如下:
BOOL
CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT*
pCopyDataStruct)
{
m_strCopyData=(LPSTR)pCopyDataStruct->lpData;
// 获得实际长度的字符串
m_strCopyData=m_strCopyData.Left(pCopyDataStruct->cbData);
// 更新数据
UpdateData(FALSE);
return CDialog::OnCopyData(pWnd,
pCopyDataStruct);
}
其中m_strCopyData为接收到的字符串,pCopyDataStruct为COPYDATASTRUCT结构指针。注意由pCopyDataStruct直接得到的m_strCopyData字符串长度可能不是实际发送的字符串长度,需要用发送字符串时所给定的字符串长度来进一步确定,其长度由pCopyDataStruct
->cbData来得到。
|
对于少量数据可以用WM_COPYDATA方便地实现通信。由于SendMessage()是阻塞的,只有接收方响应了消息,SendMessage()才能返回,否则一直阻塞。所以,对于大量数据来说,用SendMessage()就容易造成窗口假死。
3.4.1 通过WM_COPYDATA消息实现进程间通信的方法
在Win32中,WM_COPYDATA消息主要目的是允许在进程间传递只读数据。SDK文档推荐用户使用SendMessage()函数,接收方在数据复制完成前不返回,这样发送方就不可能删除和修改数据。这个函数的原型如下:
SendMessage(WM_COPYDATA,wParam,lParam)
其中wParam设置为包含数据的窗口句柄,lParam指向一个COPYDATASTRUCT的结构,其定义为:
typedef struct tagCOPYDATASTRUCT{
DWORD dwData;
DWORD cbData;
PVOID lpData;
}COPYDATASTRUCT;
其中dwData为自定义数据,
cbData为数据大小,
lpData为指向数据的指针。需要注意的是,WM_COPYDATA消息保证发送的数据从原进程复制到目标进程。但是,WM_COPYDATA消息不能发送HDC、HBITMAP之类的东西,它们对于目标进程来说是无效的。目标进程得到这些数据不能在原进程作任何事情,因为它们属于不同的进程。
与其他进程通信方法一样,要实现进程间的数据通信,在发送数据的程序中,首先要找到接收数据进程的窗口句柄pWnd,可以用CWnd::FindWindow(NULL,_
T("DataRecv"))函数来得到,其中字符串"DataRecv"为接收数据的程序名。然后用SendMessage()函数发送数据,其具体的做法见后面的实例。
在接收数据的程序中,首先在消息映射表中增加WM_COPYDATA消息映射,然后定义消息映射函数,其函数的格式为:
BOOL CDataRecvDlg::OnCopyData(CWnd* pWnd,
COPYDATASTRUCT* pCopyDataStruct)
{
// 增加用户自定义程序代码
…
}
3.4.2 通过WM_COPYDATA消息实现进程间通信的实例
与前面所说的自定义消息不一样,WM_COPYDATA消息是Win32提供的消息。与自定义消息相比较,WM_COPYDATA消息可以传递一个较大的数据块。这里仍然用两个对话框程序来实现WM_COPYDATA消息的通信。
以下分别给出发送数据程序的发送函数和接收数据程序的接收函数。在发送数据的对话框类CDataSendDlg中,用MFC
ClassWizard工具或者手工的方法增加函数void CDataSendDlg::OnSendCopydata(),其具体代码如下:
void CDataSendDlg::OnSendCopydata()
{
UpdateData(); // 更新数据
CWnd
*pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程
if(pWnd==NULL){
AfxMessageBox("Unable to find
DataRecv.");
return;
}
COPYDATASTRUCT cpd; // 给COPYDATASTRUCT结构赋值
cpd.dwData = 0;
cpd.cbData =
m_strCopyData.GetLength();
cpd.lpData =
(void*)m_strCopyData.GetBuffer(cpd.cbData);
pWnd->SendMessage(WM_COPYDATA,NULL,(LPARAM)&cpd); // 发送
}
在用MFC
AppWizard(exe)创建接收数据的对话框程序后,生成对话框类CDataRecvDlg。在这个类中,首先要定义接收WM_COPYDATA消息的映射,可以用ClassWizard工具来增加,也可以手动增加,但手动增加需要修改三个地方:①在消息映射表中增加ON_WM_COPYDATA();②增加成员函数BOOL
CDataRecvDlg::OnCopyData();③在CDataRecvDlg类中增加WM_COPYDATA消息映射函数的定义。
WM_COPYDATA消息的映射如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog)
//{{AFX_MSG_MAP(CDataRecvDlg)
ON_WM_COPYDATA()
//}}AFX_MSG_MAP
END_MESSAGE_MAP()
CDataRecvDlg::OnCopyData()函数的定义如下:
BOOL
CDataRecvDlg::OnCopyData(CWnd* pWnd, COPYDATASTRUCT*
pCopyDataStruct)
{
m_strCopyData=(LPSTR)pCopyDataStruct->lpData;
// 获得实际长度的字符串
m_strCopyData=m_strCopyData.Left(pCopyDataStruct->cbData);
// 更新数据
UpdateData(FALSE);
return CDialog::OnCopyData(pWnd,
pCopyDataStruct);
}
其中m_strCopyData为接收到的字符串,pCopyDataStruct为COPYDATASTRUCT结构指针。注意由pCopyDataStruct直接得到的m_strCopyData字符串长度可能不是实际发送的字符串长度,需要用发送字符串时所给定的字符串长度来进一步确定,其长度由pCopyDataStruct
->cbData来得到。
|
对于ReadProcessMemory()和WriteProcessMemory()函数的通信方法,在第1章已做介绍。并用它说明了C指针的意义,但有两点需要改进:①接收程序在接收数据时所用的指针代码值不需要事先给定;②内存大小是可以变化的。这里将对内存读写函数的通信方法做一点改进。
3.5.1 使用内存映射文件通信的方法采用内存映射(File
Mapping)机制可以将整个文件映射为进程虚拟地址空间的一部分来加以访问。这种方法和实例前面已做了详细介绍,这里不再重复。 3.5.2
使用内存读写函数实现进程间通信的方法要使接收程序获得发送程序的数据指针,可以通过发送消息方法来进行,即通过消息把数据指针从发送程序传递到接收程序。也可以用第1章所介绍的方法:先获得发送程序中的被发送数据指针,然后把这个指针直接赋值给接收数据的程序。但这种方法在实际操作中较困难,使用起来不方便。要使用发送消息的方法来传递指针,就需要定义一个用户消息。可用如下的自定义消息来传递指针,即
const UINT wm_nMemMsg=RegisterWindowMessage("mem_data");
要通过内存来传递数据,还必须要在内存中申请一定的内存空间,这一点很重要。用变量定义的方法只能申请有限的固定的内存空间,例如,定义一个char变量只能在内存里申请到一个字节的内存空间,定义一个int
变量只能在内存里申请到4个字节的内存空间。如果要分配一块内存空间存放数据,可以调用GlobalAlloc()或者VirtualAllocEx()等来实现。
3.5.3
使用内存读写函数实现进程间通信的实例自定义消息和内存读写函数(ReadProcessMemory()和WriteProcessMemory())相结合,利用它们各自的长处进行通信。自定义消息通信只能传递一个长整型数值,而内存读写函数却需要一个内存读写地址,并且缺少一个传递数据指针的方法。这样它们正好可以“合作”,来进行大批量的数据传递工作。
要进行这种方式的通信,同样需要编写两个对话框程序,并且在这两个程序中分别定义一个相同的用于传递指针的消息wm_nMemMsg。这里借用前面所使用的发送数据对话框类CDataSendDlg和接收数据对话框类CDataRecvDlg。在CDataSendDlg中,用MFC
ClassWizard工具或手动增加成员函数void CDataSendDlg::OnSendMem(),其源代码如下: void
CDataSendDlg::OnSendMem() { UpdateData(); // 更新数据 CWnd
*pWnd=CWnd::FindWindow(NULL,_T("DataRecv")); // 查找DataRecv进程 if(pWnd==NULL){
AfxMessageBox("Unable to find DataRecv."); return; } // 获取进程号 DWORD PID;
GetWindowThreadProcessId(pWnd->m_hWnd, (DWORD*)&PID ); HANDLE hProcess =
OpenProcess (PROCESS_ALL_ACCESS,FALSE,PID); // 分配虚拟内存 LPVOID lpBaseAddress;
lpBaseAddress = VirtualAllocEx(hProcess, 0, BUFFER_SIZE, MEM_COMMIT,
PAGE_READWRITE); char data[BUFFER_SIZE]; strcpy(data,m_strMem); //
把字符串写入hProcess进程的内存 WriteProcessMemory(hProcess, lpBaseAddress, data,
BUFFER_SIZE, NULL); // 发送基址给DataRecv进程
pWnd->SendMessage(wm_nMemMsg,NULL,(LPARAM)lpBaseAddress); // 等待接收程序接收数据
Sleep(100); // 释放虚拟内存 VirtualFreeEx(hProcess,lpBaseAddress, 0, MEM_RELEASE); }
从以上程序中可以看出如何使用WriteProcessMemory()和wm_nMemMsg消息来发送字符串m_strMem。这段程序中,首先,寻找接收数据的程序DataRecv的窗口指针pWnd和进程句柄hProcess,再用VirtualAllocEx()函数在这个进程中申请虚拟内存空间。然后,用WriteProcessMemory()把字符串m_strMem存放入虚拟内存,并且通过消息wm_nMemMsg把所申请的内存空间起始地址发送给数据接收程序。最后,当数据接收程序接收到数据后,用VirtualFreeEx()释放所申请的虚拟内存。
在数据接收程序的对话框类CDataRecvDlg中,需要定义wm_nMemMsg消息映射,它在消息映射表中的表示方法如下:
BEGIN_MESSAGE_MAP(CDataRecvDlg, CDialog) //{{AFX_MSG_MAP(CDataRecvDlg)
ON_REGISTERED_MESSAGE(wm_nMemMsg,OnRegMemMsg) //}}AFX_MSG_MAP END_MESSAGE_MAP()
在数据接收对话框类CDataRecvDlg中,用MFC ClassWizard工具或手动增加消息映射函数void
CDataRecvDlg::OnRegMemMsg(),其定义如下: void CDataRecvDlg::OnRegMemMsg(WPARAM
wParam,LPARAM lParam) { LPVOID lpBaseAddress=(LPVOID)lParam; //
把字符串写入hProcess进程的内存 HANDLE hProcess=GetCurrentProcess(); char data[BUFFER_SIZE];
ReadProcessMemory(hProcess, lpBaseAddress, data,BUFFER_SIZE, NULL);
m_strMem=data; // 更新数据 UpdateData(FALSE); }
|
动态链接库(Dynamic Link
Library,DLL)不仅可以用来共享程序代码,而且可以用来共享数据。用DLL共享程序代码的方法以后再做介绍,这里只说明如何利用它共享数据。 3.6.1
DLL概述 目前,动态链接库有Win32 DLL 和Win16 DLL之分,它们的特性不相同。但是,对于同一个DLL,不管Win32 DLL还是Win16
DLL的代码装入内存实际只有一次。这里必须强调的是对同一个DLL的调用才是这样。如果同一个DLL有多个副本,且每个DLL副本分别被不同的程序调用时,那么每个副本都会被装入内存一次。装入内存后,虽然说它们名字相同,但它们是多份副本,因此不是对同一个DLL的调用。在这种情况下就不能用这个DLL实现进程之间的通信。例如在c:\DLL_A目录下有动态库文件shared.
dll和应用程序user1.exe,而在另外一个c:\DLL_B目录下有动态库文件shared.dll和应用程序user2.exe,那么就不能通过shared.dll来实现user1.exe和user2.exe之间的通信。
Win32把DLL装入全局内存并把DLL映射到每个程序的地址空间,并且不需要把DLL映射到每个进程相同的地址空间上。DLL成为载入它的进程的一部分,而不像在Win16中,成为系统的一部分。
对于Win16来说,在DLL中共享数据是很容易的,因为访问DLL的每个应用程序都可以得到它的全局静态变量,如图3.3所示。在Win32中,对每个载入DLL的进程,DLL获取一个该进程的唯一的全局静态变量的副本,如图3.4所示。
3.6.2
使用DLL通信的方法从上面可以看出,对于Win32的DLL,所有载入DLL的应用程序只能共享程序代码,不能共享数据,必须要采取一种方法才能使这些程序之间共享数据。当然访问数据时要注意同步问题。
要想使Win32 DLL的数据区能设计成共享的存储区,可以通过#pragma
data_seg指令建立一个新段来做到这一点,实际上是告诉编译器包含段中的特定变量。然而,仅此不足以做到数据共享,还必须把段中将要共享的变量告诉连接器。可通过如下命令来
实现: (1)指定DEF文件在SECTIONS段下的名字,如下所示: SECTIONS 共享段名 READ WRITE SHARED (2)#pragma
comment(linker,"/SECTION: 共享段名,RWS") 下面的例子示范了如何初始化一个全局变量: #pragma
data_seg("MyShared") UINT m_glnData=0; #pragma data_seg() #pragma
comment(linker,"/SECTION:MyShared,RWS")
注意要初始化变量。初始化变量很重要,因为编译器将把所有未初始化数据存放在.bss段。把变量安排在与用户预期不同的段中,则它们就不能被共享,除非明确地指出要共享的是.bss段。
关于共享数据段名称MyShared,完全可以用其他的名称,如MYDATA、.MYSC等,但建议不要与PE文件的固定的段名相同,以免程序运行时出错。
最后一点要强调的是,进行通信的程序要使用同一个DLL文件。如果使用的是相同DLL文件的不同副本,则不能实现进程之间的通信。 3.6.3
使用DLL通信的实例与前面所讲的通信方法不同,本实例使用DLL实现进程间的通信。以下是一个用于生成DLL文件的头文件DllObj.h。其具体代码如下: //
DllObj.h:头文件 // #ifndef _DLLOBJ_H_INCLUDED #define _DLLOBJ_H_INCLUDED #include
#ifdef _cplusplus #define _DLLCOM_ extern "C" _declspec (dllexport)
#else #define _DLLCOM_ _declspec (dllexport) #endif _DLLCOM_LPSTR
GetValueString(); _DLLCOM_void SetValueString(LPCSTR str); #endif
其中SetValueString()和GetValueString()函数分别用于向所指定的共享存储区里写入和读取字符串。_DLLCOM_
用于定义DLL中函数的输出。可以看出,SetValueString()和GetValueString()函数的写法与其他DLL文件的写法没有什么不同。但是,这两个函数所用到的共用字符串变量m_strString的表示方法则是用DLL实现内存数据共享的关键。
现在来看看如何制作一个这样的DLL文件。先用MFC AppWizard(dll)生成一个dllcom
模板(可以取其他的名称),然后把以下的代码包含在一个dllcom.cpp文件中,再用VC++编译器进行编译和连接,就可以生成dllcom.dll和dllcom.lib文件。生成这个DLL文件的核心代码如下:
#pragma data_seg("MyShared") char m_strString[256]=TEXT(""); volatile bool
bInCriticalSection=FALSE; #pragma data_seg() #pragma
comment(linker,"/SECTION:MyShared,RWS") CCriticalSection cs; // 从内存中读取字符串
_DLLCOM_ LPSTR GetValueString() { while(bInCriticalSection) // 等待 Sleep(1);
return m_strString; } // 把字符串存储到共享内存中 _DLLCOM_ void SetValueString(LPCSTR str) {
while(bInCriticalSection) // 等待 Sleep(1); cs.Lock(); bInCriticalSection = TRUE;
strcpy(m_strString,str); bInCriticalSection = FALSE; cs.Unlock(); }
其中bInCriticalSection为进程访问数据时的同步标识。
在制作了一个用于进程间通信的DLL文件后,就可以利用它实现进程之间的通信。可以设计两个应用程序,dlluser1和dlluser2,在这两个程序中把动态库文件头DllObj.h和动态库dllcom.lib包含其中,即
#include "DllObj.h" #pragma comment(lib,"dllcom.lib") 然后,用MFC
VC++编译器进行编译和连接,这样就可以用SetValueString()和GetValue- String()函数进行通信了。
|
Windows剪贴板是一种比较简单同时也是开销比较小的IPC(进程间通信)机制。Windows系统支持剪贴板IPC的基本机制是由系统预留的一块全局共享内存,用来暂存各个进程间进行交换的数据。提供数据的进程创建一个全局内存块,并将要传送的数据移到或复制到该内存块;而接受数据的进程(也可以是提供数据的进程本身)获取此内存块的句柄,并完成对该内存块数据的读取。
在Windows系统和其他工具软件中有自带的使用剪贴板的命令。例如,在Microsoft Word
中,组合键Ctrl+C用于文字的复制、组合键Ctrl+X用于对文字的剪切、组合键Ctrl+V用于对文字的粘贴。使用这些命令可以很方便地对所选择字符串进行复制和移动。然而,这里关心的是如何在编写应用程序时使用剪贴板实现进程间的通信。
3.7.1 使用剪贴板实现进程间通信的方法可以使用剪贴板函数实现进程间的数据传输。常用的剪贴板函数有: // 打开剪贴板 BOOL
OpenClipboard(); // 关闭剪贴板 BOOL CloseClipboard(); // 清空剪贴板,并将所有权分配给打开剪贴板的进程 BOOL
EmptyClipboard( ); // 按指定数据格式放置剪贴板数据,用之前必须使用OpenClipboard函数 HANDLE
SetClipboardData(UINT uFormat, HANDLE hMem); // 检测是否已经包含了所需要的数据 BOOL
IsClipboardFormatAvailable(UINT uFormat); // 获取指定剪贴板数据 HANDLE GetClipboardData(
UINT uFormat); 其中uFormat 为剪贴板格式,见MSDN(微软开发者网络)描述,hMem为所申请的内存控制句柄。
文本剪贴板和位图剪贴板是比较常用的。其中,文本剪贴板是包含具有格式CF_TEXT的字符串的剪贴板,是最经常使用的剪贴板之一。在文本剪贴板中传递的数据是不带任何格式信息的ASCII字符。若要将文本传送到剪贴板,可以先分配一个可移动全局内存块,然后将要复制的文本内容写入到此内存区域,最后调用剪贴板函数如OpenClipboard()、SetClipboardData()将数据放置到剪贴板。从剪贴板获取文本的过程与之类似,首先用OpenClipboard()函数打开剪贴板并获取剪贴板的数据句柄,如果数据存在就复制其数据到程序变量。由于GetClipboardData()获取的数据句柄属于剪贴板,因此用户程序必须在调用CloseClipboard()函数之前使用它。
大多数应用程序对图形数据采取是位图剪贴板数据格式。位图剪贴板的使用与文本剪贴板的使用类似,只是数据格式要指明为CF_BITMAP,而且在使用SetClipboardData()或GetClipboardData()函数时交给剪贴板或从剪贴板返回的是设备相关位图句柄。
3.7.2
使用剪贴板实现进程间通信的实例剪贴板中可以存放许多类型的数据,其中包括标准文本格式、位图格式、RTF格式等,由于类型比较多,这里只给出经常使用的文本格式的实例,其他的数据类型的操作方法基本类似。同样,用
VC++ 编写两个对话框应用程序。为了方便,仍然借用前面所使用的对话框类CDataSendDlg和CDataRecvDlg。
为了把文本放置到剪贴板上,在CDataSendDlg中,用MFC ClassWizard工具或者用手工的方法增加函数void
CDataSendDlg::OnSendClipboard(),其源代码如下: void CDataSendDlg::OnSendClipboard() {
UpdateData(); // 更新数据 CString strData=m_strClipBoard; // 获得数据 // 打开系统剪贴板 if
(!OpenClipboard()) return; // 使用之前,清空系统剪贴板 EmptyClipboard(); //
分配一内存,大小等于要复制的字符串的大小,返回到内存控制句柄 HGLOBAL hClipboardData; hClipboardData =
GlobalAlloc(GMEM_DDESHARE, strData.GetLength()+1); //
内存控制句柄加锁,返回值为指向那内存控制句柄所在的特定数据格式的指针 char * pchData; pchData =
(char*)GlobalLock(hClipboardData); // 将本地变量的值赋给全局内存 strcpy(pchData,
LPCSTR(strData)); // 给加锁的全局内存控制句柄解锁 GlobalUnlock(hClipboardData); //
通过全局内存句柄将要复制的数据放到剪贴板上 SetClipboardData(CF_TEXT,hClipboardData); // 使用完后关闭剪贴板
CloseClipboard(); } 在数据接收程序的CDataRecvDlg类中,用与前面所用的同样的方法,增加从剪贴板上获取文本的函数,即void
CDataRecvDlg::OnRecvClipboard(),其源代码如下: void CDataRecvDlg::OnRecvClipboard() {
// 打开系统剪贴板 if (!OpenClipboard()) return; // 判断剪贴板上的数据是否是指定的数据格式 if
(IsClipboardFormatAvailable(CF_TEXT)|| IsClipboardFormatAvaila- ble(CF_OEMTEXT))
{ // 从剪贴板上获得数据 HANDLE hClipboardData = GetClipboardData(CF_TEXT); //
通过给内存句柄加锁,获得指向指定格式数据的指针 char *pchData = (char*)GlobalLock(hClipboardData); //
本地变量获得数据 m_strClipBoard = pchData; // 给内存句柄解锁 GlobalUnlock(hClipboardData); }
else { AfxMessageBox("There is no text (ANSI) data on the Clipboard."); } //
使用完后关闭剪贴板 CloseClipboard(); // 更新数据 UpdateData(FALSE); }
使用剪贴板通信的方法与使用发送消息通信的方法所经历的过程是不一样的,前者是把所共享的数据先放在剪贴板上,然后由接收数据的程序去获取,而后者是直接把共享数据发送到接收数据的程序。因此,在使用剪贴板通信方法时,接收数据程序的对话框上需要增加一个获取数据的命令按钮,而使用消息通信方法则不需要这个按钮。
|
动态数据交换(Dynamic
Data Exchange,DDE)也是一种进程间通信形式。它最早是随着Windows
3.1由美国微软公司提出的。当前大部分软件仍就支持DDE,但近10年间微软公司已经停止发展DDE技术,只保持对DDE技术给予兼容和支持。但我们仍然可以利用DDE技术编写自己的数据交换程序。
3.8.1 使用DDE技术通信原理
两个同时运行的程序间通过DDE方式交换数据时是客户/服务器关系,一旦客户和服务器建立起来连接关系,则当服务器中的数据发生变化后就会马上通知客户。通过DDE方式建立的数据连接通道是双向的,即客户不但能够读取服务器中的数据,而且可以对其进行修改。
DDE和剪贴板一样既支持标准数据格式(如文本、位图等),又可以支持自定义的数据格式。但它们的数据传输机制却不同,一个明显区别是剪贴板操作几乎总是用作对用户指定操作的一次性应答,如从菜单中选择粘贴命令。尽管DDE也可以由用户启动,但它继续发挥作用,一般不必用户进一步干预。
DDE有三种数据交换方式,即
(1)冷连接(Cool
Link):数据交换是一次性数据传输,与剪贴板相同。当服务器中的数据发生变化后不通知客户,但客户可以随时从服务器读写数据;
(2)温连接(Warm Link):当服务器中的数据发生变化后马上通知客户,客户得到通知后将数据取回;
(3)热连接(Hot Link):当服务器中的数据发生变化后马上通知客户,同时将变化的数据直接送给客户。
DDE
客户程序向DDE 服务器程序请求数据时,它必须首先知道服务器的名称(即DDE
Service名)、DDE主题名称(Topics名),还要知道请求哪一个数据项的项目名称(Items名)。DDE
Service名应该具有唯一性,否则容易产生混乱。通常DDE
Service就是服务器的程序名称,但不是绝对的,它是由程序设计人员在程序内部设定好的,并不是通过修改程序名称就可以改变的。Topics名和Items名也是由DDE
Service在其内部设定好的,所有服务程序的Service名、Topics名都是注册在系统中,当一个客户向一个服务器请求数据时,客户必须向系统报告服务器的Service名和Topics名。只有当Service名、Topics名与服务器内部设定的名称一致时,系统才将客户的请求传达给服务器。
当服务名和Topics名相符时,服务器马上判断Items名是否合法。如果请求的Item名是服务器中的合法数据项,服务器即建立此项连接,建立连接的数据发生数值变化后,服务器会及时通知客户。一个服务器可以有多个Topics名,Items名的数量也不受限制。
DDE交换可以发生在单机或网络中不同计算机的应用程序之间。开发者还可以定义定制的DDE数据格式,进行应用程序之间特别目的IPC,它们有更紧密耦合的通信要求。大多数基于Windows的应用程序都支持DDE。但DDE有个明显的缺点就是,通信效率低下,当通信量较大时数据刷新速度慢,在数据较少时DDE较实用。
3.8.2 如何使用DDEML编写程序
早期的DDE基于消息机制,应用程序间的消息传递需程序员调度。由于DDE消息通信牵涉的操作细节颇多,实现完全的DDE协议不是非常容易的事情,而且不同的开发者对协议的解释也略有不同。为了使用方便起见,微软提供DDE管理库(The
DDE Management Library,
简称DDEML)。DDEML专门协调DDE通信,给DDE应用程序提供句柄字符串和数据交换的服务,消除了早期由于DDE协议不一致所引起的问题。
使用DDEML开发的应用程序(客户/服务器)无论在运行一致性方面,还是在程序相互通信方面,性能均优于没有使用DDEML的应用程序。而且DDEML的应用使得开发支持DDE的应用程序容易了许多,因为
DDEML(这是个
DLL)担起了内务府总管的工作。使用DDEML后,实际上客户和服务器之间的多数会话并不是直达对方的,而是经由DDEML中转,即用Callback函数处理DDE交易(Transaction),而早期的消息通信是直接的。
在调用其他DDEML函数前,客户/服务器必须调用DdeInitialize()函数,以获取实例标识符,注册DDE
Callback函数,并为Callback函数指定事务过滤。对于服务器,在使用DdeInitialize()初始化后,调用DdeCreateStringHandle()建立Service名、Topics名和Items名等标识的句柄,再通过DdeNameService()在操作系统中注册服务器的名字。根据这些句柄,客户就可以使用它提供的DDE服务了。
为了执行某个DDE任务,许多DDEML函数需要获得字符串的访问权。例如:一个客户在调用DdeConnect()函数来请求同服务器建立会话时,必须指定Service名和Topics名。可以通过调用DdeCreateStringHandle()函数来获取特定字符串句柄。例如:
HSZ hszServName =
DdeCreateStringHandle(idInst,"MyServer",CP_WINANSI);
HSZ hszSysTopic =
DdeCreateStringHandle(idInst,SZDDESYS_TOPIC,CP_WINANSI);
一个应用程序的DDE回调函数在大多DDE事务中接收多个字符串句柄。比如:在XTYP_REQUEST事务处理期间,一个DDE
服务器接收两个字符串句柄:一个标识Topics名字符串,另一个标识Items名字符串。可以通过调用DdeQueryString()函数来获取相应于字符串句柄的字符串长度,并且复制字符串到应用程序定义的buffer中。例如:
DWORD idInst;
DWORD cb;
HSZ hszServ;
PSTR pszServName;
cb = DdeQueryString(idInst, hszServ, (LPSTR) NULL, 0, CP_WINANSI)
+ 1;
pszServName = (PSTR) LocalAlloc(LPTR, (UINT) cb);
DdeQueryString(idInst, hszServ, pszServName, cb, CP_WINANSI);
根据微软MSDN,现有的基于消息DDE协议的应用程序与DDEML应用程序是相容的,也就是说,基于消息通信的DDE应用程序可以与DDEML应用程序对话和交易。在使用DDEML时,必须在源程序文件中包括ddeml.h头文件,连接user32.lib文件,并保证ddeml.dll文件正确的系统路径。
|
由上面的介绍可知,可以编写基于消息DDE应用程序,也可以编写应用DDEML的应用程序。对于前者,实现的方法较复杂,这里不做介绍。这里介绍一个应用DDEML编写的DDE通信实例。
为了便于管理,这里把这个程序封装成一个CMyDde类,下面介绍这个类。CMyDde类头文件如下: // DDE.h: 定义CMyDde类 // #ifndef
_DDE_H_INCLUDED #define _DDE_H_INCLUDED #include class CMyDde { public:
CMyDde(); ~CMyDde(); // 静态回调成员函数 static HDDEDATA CALLBACK DdeCallback(UINT
iType,UINT iFmt, HCONV hConv,HSZ hsz1,HSZ hsz2, HDDEDATA hData,DWORD
dwData1,DWORD data2); void DdeCall(UINT iType, LPCSTR szSvr,LPCSTR
szTopic,LPCSTR szAtom); void DdeServer(CString strReply); void DdeClient(CString
strRequest); CString GetReply() { return m_strReply;} CString GetRequest() {
return m_strRequest;} private: static CMyDde* fakeThis; DWORD idInst; CString
AppName; CString m_strReply; CString m_strRequest; }; #endif
其中包含了ddeml.h头文件,DdeCallback()为static回调函数。之所以使用static,是因为DdeInitialize()函数的需要,否则编译会出错。
对于服务程序,使用类中的DdeServer()函数。在这个函数中用DdeInitialize()调用回调函数DdeCallback(),注册服务名MyDDEService,以便客户程序与服务程序取得联系。在DdeInitialize()中设置事务过滤,例如以下的DdeServer()函数中,在DdeInitialize()中设置CBF_FAIL_POKES,表示XTYP_
POKES事件将被过滤掉。DdeServer()函数的代码 如下: void CMyDde::DdeServer(CString strReply) {
m_strReply=strReply; fakeThis=this; // 建立DDE
DdeInitialize(&idInst,DdeCallback,APPCLASS_STANDARD| CBF_FAIL_ADVISES|
CBF_FAIL_POKES| CBF_SKIP_REGISTRATIONS| CBF_SKIP_UNREGISTRATIONS,0L); //
注册服务名MyDDEService,使该程序作为DDE服务器 AppName="MyDDEService"; HSZ
hszService=DdeCreateStringHandle(idInst,AppName,0);
DdeNameService(idInst,hszService,NULL,DNS_REGISTER); } 回调函数(Callback
function)大量用于Windows的系统服务,通过它,程序员可以安装设备驱动程序和消息过滤系统,以控制Windows的有效使用。以下是DDE服务程序的回调函数源代码:
HDDEDATA CALLBACK CMyDde::DdeCallback(UINT iType, UINT iFmt,HCONV hConv, HSZ
hsz1, // Topic. HSZ hsz2, // atom. HDDEDATA hData,DWORD dwData1,DWORD data2) {
char szBuffer[100]; switch(iType) { // 建立交易连接 case XTYP_CONNECT: // 获得应用名
DdeQueryString(fakeThis->idInst,hsz2, szBuffer,sizeof(szBuffer),0); //
如果此应用不能被此服务器支持,返回NULL if(strcmp(szBuffer,fakeThis->AppName)) return NULL; //
获得topic名 DdeQueryString(fakeThis->idInst,hsz1, szBuffer,sizeof(szBuffer),0);
// 如果连接成功,返回1 return (HDDEDATA)1; case XTYP_REQUEST: // 获得topic名
DdeQueryString(fakeThis->idInst,hsz1, szBuffer,sizeof(szBuffer),0);
if(strcmp(szBuffer,"query")==0) { // 获得Item 名
DdeQueryString(fakeThis->idInst,hsz2, szBuffer,sizeof(szBuffer),0);
strcpy(szBuffer,fakeThis->m_strReply); return
DdeCreateDataHandle(fakeThis->idInst,
(LPBYTE)szBuffer,sizeof(szBuffer),0,hsz2,CF_TEXT,0); } break; case XTYP_EXECUTE:
// 获得topic名 DdeQueryString(fakeThis->idInst,hsz1,
szBuffer,sizeof(szBuffer),0); if(strcmp(szBuffer,"data")==0) { // 获得数据
DdeGetData(hData, (LPBYTE)szBuffer, 40L, 0L);
fakeThis->m_strRequest=szBuffer; return (HDDEDATA)1; } break; } return NULL;
} 其中只使用了三个选项,即XTYP_CONNECT、XTYP_REQUEST和XTYP_
EXECUTE,还有其他的一些选项,见微软的MSDN说明。XTYP_CONNECT响应于客户程序使用的DdeConnect()函数。XTYP_REQUEST和XTYP_EXECUTE分别响应于客户程序中使用DdeClientTransaction()函数的XTYP_REQUEST和XTYP_
EXECUTE选项。在服务程序中,对于XTYP_REQUEST选项,可以用DdeCreateDataHandle函数向客户程序发送数据,而XTYP_EXECUTE则不能。而对于XTYP_EXECUTE选项,可以用DdeGetData()函数从客户获取数据,而XTYP_REQUEST则不能。
在服务程序中用DdeQueryString()函数从客户程序中获得Topics名和Items名,先得到Topics名,然后得到Items名。在本实例中XTYP_REQUEST选项的Topics名是“query”,Items名为“1”,而XTYP_EXECUTE选项的Topics名是“data”,Items名为“1”,但Items名都没有被利用。
以下是用于客户程序的主函数,也需要用DdeInitialize()函数初始化,并设置过滤类型。其中使用了类型调用函数DdeCall()。DdeClient()函数的代码如下:
void CMyDde::DdeClient(CString strRequest) { m_strRequest=strRequest; idInst=0;
DdeInitialize(&idInst,NULL,APPCLASS_STANDARD| CBF_FAIL_ADVISES|
CBF_FAIL_POKES| CBF_SKIP_REGISTRATIONS| CBF_SKIP_UNREGISTRATIONS,0L);
DdeCall(XTYP_EXECUTE,TEXT("MyDDEService"),TEXT("data"),TEXT("1"));
DdeCall(XTYP_REQUEST,TEXT("MyDDEService"),TEXT("query"),TEXT("1")); }
在类型调用的DdeCall()函数中,首先获得Service名、Topics名和Items名的字符串句柄,然后用DdeConnect()函数与服务程序连接。如果连接成功,就可以用DdeClientTransaction()
函数和用XTYP_REQUEST和XTYP_EXECUTE类型向服务程序发送数据。其中,对于XTYP_REQUEST,可以用DdeGetData()函数从服务程序获得数据。最后用DdeDisconnect()函数断开与服务程序的连接,并且用DdeFreeStringHandle()函数释放Service名、Topics名和Items名的字符串句柄。DdeCall()函数的源代码如下:
void CMyDde::DdeCall(UINT iType,LPCSTR szSvr,LPCSTR szTopic,LPCSTR szItem) { HSZ
hszServName = DdeCreateStringHandle(idInst,szSvr,CP_WINANSI); HSZ hszTopic =
DdeCreateStringHandle(idInst,szTopic,CP_WINANSI); HSZ hszItem =
DdeCreateStringHandle(idInst,szItem,CP_WINANSI); HCONV hConv=
DdeConnect(idInst,hszServName,hszTopic,NULL); HDDEDATA hData; DWORD dwResult;
char szBuffer[100]; DWORD dwLength; switch(iType) { case XTYP_REQUEST: //
向服务器发送请求 hData = DdeClientTransaction(NULL,0,hConv, hszItem, CF_TEXT, iType,
5000, &dwResult); // 从服务器取得返回值 dwLength = DdeGetData(hData,
(LPBYTE)szBuffer,sizeof(szBuffer), 0); if (dwLength > 0) m_strReply=szBuffer;
break; case XTYP_EXECUTE: strcpy(szBuffer,m_strRequest); // 向服务器发送执行命令 hData =
DdeClientTransaction((LPBYTE)szBuffer, sizeof(szBuffer), hConv, hszItem,
CF_TEXT, iType, 5000, &dwResult); break; } DdeDisconnect(hConv);
DdeFreeStringHandle(idInst,hszServName); DdeFreeStringHandle(idInst,hszTopic);
DdeFreeStringHandle(idInst,hszItem); }
|
3.9 本章小结
( 本章字数:652 更新时间:2007-4-26
19:08:27)
|
本章首先介绍了一些有关进程间通信的背景知识,便于读者弄清一些名词和概念,对进程间的通信有更深入的了解。
本章介绍了简单的本地进程之间的通信技术,它们也是较为实用的通信技术。还有另外一些简单的通信技术没有介绍,例如,使用一个临时文件、使用Windows注册表等,这些技术较容易,可以自己研究。一些高级的进程间通信技术将在以后章节介绍。
还把自定义消息、WM_COPYDATA消息、内存读写函数、FileMapping和剪贴板通信技术用一个服务程序和一个客户程序实现,如图3.1和图3.2所示。其中的数据发送类型如这两张图所示,并且发送字符串的方法可以用来发送较大的数据。
|
参考文献
( 本章字数:335 更新时间:2007-4-26
19:08:27)
|
[1] Kate Gregory著.
Visual C++ 6 开发使用手册. 前导工作室译. 北京:机械工业出版 社,西蒙与舒斯特国际出版公司,1999
[2] Scott
Stanfield, Ralph Arvesen 著. Visual C++ 4 开发人员指南. 华译工作室译.
北京:机械工业出版社,西蒙与舒斯特国际出版公司,1997
|
2.1 引言
( 本章字数:1270 更新时间:2007-4-26
19:08:26)
|
本章提要
· PE文件格式概述
· PE文件结构
· 如何获取PE文件中的OEP
· 如何获取PE文件中的资源
· 如何修改PE文件使其显示MessageBox的实例
2.1 引言
通常Windows下的EXE文件都采用PE格式。PE是英文Portable
Executable的缩写,它是一种针对于微软Windows NT、Windows
95和Win32s系统,由微软公司设计的可执行的二进制文件(DLLs和执行程序)格式,目标文件和库文件通常也是这种格式。这种格式由TIS(Tool
Interface
Standard)委员会(Microsoft、Intel、Borland、Watcom、IBM等)在1993进行了标准化。显然,它参考了一些UNIXes和VMS的COFF(Common
Object File Format)格式。
认识可执行文件的结构非常重要,在DOS下是这样,在Windows系统下更是如此。了解了这种结构后就可以对可执行程序进行加密、加壳和修改等,一些黑客也利用了这些技术。为了使读者对PE文件格式有进一步的认识,本章从一个程序员的角度出发再次介绍PE文件格式。如果已经熟悉这方面的知识,可以跳过这一章。
|
认识PE文件,既要懂得它的结构布局,又要知道它是如何装载到计算机内存中的。下面分别对它们进行说明。
2.2.1 PE文件结构布局找到文件中某一结构信息有两种定位方法。第一种是通过链表方法,对于这种方法,数据在文件的存放位置比较自由。第二种方法是采用紧凑或固定位置存放,这种方法要求数据结构大小固定,它在文件中的存放位置也相对固定。在PE文件结构中同时采用以上两种方法。 因为在PE文件头中的每个数据结构大小是固定的,因此能够编写计算程序来确定某一个PE文件中的某个参数值。在编写程序时,所用到的数据结构定义,包括数据结构中变量类型、变量位置和变量数组大小都必须采用Windows提供的原型。图2.1所示的PE文件结构的总体层次分布如下: PE文件结构总体层次分布 · DOS MZ
Header 所有
PE文件(甚至32位的DLLs)必须以简单的DOS MZ
header开始,它是一个IMAGE_DOS_HEADER结构。有了它,一旦程序在DOS下执行,DOS就能识别出这是有效的执行体,然后运行紧随MZ
Header之后的DOS Stub。 · DOS
Stub DOS
Stub实际上是个有效的EXE,在不支持PE文件格式的操作系统中,它将简单显示一个错误提示,类似于字符串“This program requires
Windows”或者程序员可根据自己的意图实现完整的DOS代码。大多数情况下DOS Stub由汇编器/编译器自动生成。 · PE
Header 紧接着DOS
Stub的是PE
Header。它是一个IMAGE_NT_HEADERS结构。其中包含了很多PE文件被载入内存时需要用到的重要域。执行体在支持PE文件结构的操作系统中执行时,PE装载器将从DOS
MZ header中找到PE header的起始偏移量。因而跳过DOS Stub直接定位到真正的文件头 PE header。 ·
Section Table PE
Header之后是数组结构Section Table(节表)。如果PE文件里有5个节,那么此Section
Table结构数组内就有5个(IMAGE_SECTION_HEADER)成员,每个成员包含对应节的属性、文件偏移量、虚拟偏移量等。排在节表中的最前面的第一个默认成员是text,即代码节头。通过遍历查找方法可以找到其他节表成员(节表头)。 ·
Sections PE文件的真正内容划分成块,称为Sections(节)。每个标准节的名字均以圆点开头,但也可以不以圆点开头,节名的最大长度为8个字节。Sections是以其起始位址来排列,而不是以其字母次序来排列。通过节表提供的信息,可以找到这些节。程序的代码,资源等就放在这些节中。 节的划分是基于各组数据的共同属性,而不是逻辑概念。每节是一块拥有共同属性的数据,比如代码/数据、读/写等。如果PE文件中的数据/代码拥有相同属性,它们就能被归入同一节中。节名称仅仅是个区别不同节的符号而已,类似“data”,“code”的命名只为了便于识别,唯有节的属性设置决定了节的特性和功能。 2.2.2 PE文件内存映射在Windows系统下,当一个PE应用程序运行时,这个PE文件在磁盘中的数据结构布局和内存中的数据结构布局是一致的。系统在载入一个可执行程序时,首先是Windows装载器(又称PE装载器)把磁盘中的文件映射到进程的地址空间,它遍历PE文件并决定文件的哪一部分被映射。其方式是将文件较高的偏移位置映射到较高的内存地址中。磁盘文件一旦被装入内存中,其某项的偏移地址可能与原始的偏移地址有所不同,但所表现的是一种从磁盘文件偏移到内存偏移的转换,如图2.2所示。 PE文件内存映射 当PE文件被加载到内存后,内存中的版本称为模块(Module),映射文件的起始地址称为模块句柄(hModule),可以通过模块句柄访问内存中的其他数据结构。这个初始内存地址也称为文件映像基址(ImageBase)。载入一个PE程序的主要步骤如下: (1)当PE文件被执行时,PE装载器首先为进程分配一个4GB的虚拟地址空间,然后把程序所占用的磁盘空间作为虚拟内存映射到这个4GB的虚拟地址空间中。一般情况下,会映射到虚拟地址空间中0x400000的位置。装载一个应用程序的时间比一般人所设想的要少,因为装载一个PE文件并不是把这个文件一次性地从磁盘读到内存中,而是简单地做一个内存映射,映射一个大文件和映射一个小文件所花费的时间相差无几。当然,真正执行文件中的代码时,操作系统还是要把存在于磁盘上的虚拟内存中的代码交换到物理内存(RAM)中。但是,这种交换也不是把整个文件所占用的虚拟地址空间一次性地全部从磁盘交换到物理内存中,操作系统会根据需要和内存占用情况交换一页或多页。当然,这种交换是双向的,即存在于物理内存中的一部分当前没有被使用的页,也可能被交换到磁盘中。 (2)PE装载器在内核中创建进程对象和主线程对象以及其他内容。 (3)PE装载器搜索PE文件中的Import
Table(引入表),装载应用程序所使用的动态链接库。对动态链接库的装载与对应用程序的装载方法完全类似。 (4)PE装载器执行PE文件首部所指定地址处的代码,开始执行应用程序主线程。 2.2.3 Big-endian和Little-endianPE
Header中IMAGE_FILE_HEADER的成员Machine 中的值,根据winnt.h中的定义,对于Intel
CPU应该为0x014c。但是用十六进制编辑器打开PE文件时,看到这个WORD显示的却是4c 01。其实4c 01就是0x014c,只不过由于Intel
CPU是Little-endian,所以显示出来是这样的。对于Big-endian和Little-endian,请看下面的例子。一个整型int变量,长度为4个字节。当这个整形变量的值为0x12345678时,对于Big-endian来说,显示的是{12,34,45,78},而对于Little-endian来说,显示的却是{78,45,34,12}。注意Intel使用的是Little-endian。 2.2.4 3种不同的地址PE文件的各种结构中,涉及到很多地址、偏移。有些是指在文件中的偏移,有些
是指在内存中的偏移。以下的第一种是指在文件中的地址,第二、三种是指在内存中的地址。 第一种,文件中的地址。比如用十六进制编辑器打开PE文件,看到的地址(偏移)就是文件中的地址,使用某个结构的文件地址,就可以在文件中找到该结构。 第二种,当文件被整个映射到内存时,例如某些PE分析软件,把整个PE文件映射到内存中,这时是内存中的虚拟地址(VA)。如果知道在这个文件中某一个结构的内存地址的话,那么它等于这个PE文件被映射到内存的地址加上该结构在文件中的地址。 第三种,当执行PE时,PE文件会被载入器载入内存,这时经常需要的是RVA。例如知道一个结构的RVA,那么程序载入点加上RVA就可以得到该结构的内存地址。比如,如果PE文件装入虚拟地址(VA)空间的0x400000处,某一结构的RVA
为0x1000,那么其虚拟地址为0x401000。 PE文件格式要用到RVA,主要是为了减少PE装载器的负担。因为每个模块都有可能被重载到任何虚拟地址空间,如果让PE装载器修正每个重定位项,这肯定是个梦魇。相反,如果所有重定位项都使用RVA,那么PE装载器就不必操心那些东西了,即它只要将整个模块重定位到新的起始VA。这就像相对路径和绝对路径的概念:RVA类似相对路径,VA就像绝对路径。 注意,RVA和VA是指内存中,不是指文件中。是指相对于载入点的偏移而不是一个内存地址,只有RVA加上载入点的地址,才是一个实际的内存地址。
|
在win32
SDK的文件winnt.h中有PE文件格式的定义。本文所用到的变量,如果没有特别说明,都在文件winnt.h中定义。
有关一些PE头文件结构一般都有32位和64位之分,如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64等,除了在64位版本中的一些扩展域外,这些结构总是一样的。是采用32位还是64位,需要用#define
_WIN64来定义,如果没有这种定义,则采用的是32位的文件结构。编译器将根据此定义选择相应的编译模式。 2.3.1 MS-DOS头部MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下:
l // 此结构包含于WINNT.H中 // typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部 WORD e_magic; // 魔术数字 WORD e_cblp; // 文件最后页的字节数 WORD e_cp; // 文件页数 WORD e_crlc; // 重定义元素个数 WORD e_cparhdr; // 头部尺寸,以段落为单位 WORD e_minalloc; // 所需的最小附加段 WORD e_maxalloc; // 所需的最大附加段 WORD e_ss; // 初始的SS值(相对偏移量) WORD e_sp; // 初始的SP值 WORD e_csum; // 校验和 WORD e_ip; // 初始的IP值 WORD e_cs; // 初始的CS值(相对偏移量) WORD e_lfarlc; // 重分配表文件地址 WORD e_ovno; // 覆盖号 WORD e_res[4]; // 保留字 WORD e_oemid; // OEM标识符(相对e_oeminfo) WORD e_oeminfo; // OEM信息 WORD e_res2[10]; // 保留字 LONG e_lfanew; // 新exe头部的文件地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; l 其中第一个域e_magic,被称为魔术数字,它用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其他的域对于MS-DOS操作系统来说都有用,但是对于Windows
NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。 2.3.2 IMAGE_NT_HEADER头部PE
Header是紧跟在MS-DOS头部和实模式程序残余之后的,描述它内容的结构 如下: l typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //
PE文件头标志:"PE\0\0" IMAGE_FILE_HEADER FileHeader; // PE文件物理分布的信息 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE文件逻辑分布的信息 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 紧接PE文件头标志之后是PE文件头结构,由20个字节组成,它被定义为: l typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER 20 l 其中请注意这个文件头部的大小已经定义在这个包含文件之中了,这样一来,想要得到这个结构的大小就很方便了。 Machine:表示该程序要执行的环境及平台,现在已知的值如表2.1所示。 应用程序执行的环境及平台代码 IMAGE_FILE_MACHINE_I386(0x14c) | Intel
80386 处理器以上 | 0x014d | Intel
80486 处理器以上 | 0x014e | Intel
Pentium 处理器以上 | 0x0160 | R3000(MIPS)处理器,big endian | IMAGE_FILE_MACHINE_R3000(0x162) | R3000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_R4000(0x166) | R4000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_R10000(0x168) | R10000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_ALPHA(0x184) | DEC
Alpha AXP处理器 | IMAGE_FILE_MACHINE_POWERPC(0x1f0) | IBM
Power PC,little endian |
|
|
NumberOfSections:段的个数。 TimeDateStamp:文件建立的时间。可用这个值来区分同一个文件的不同的版本,即使它们的商业版本号相同。这个值的格式并没有明确的规定,但是很显然地大多数的C编译器都把它定为从1970.1.1
00:00:00以来的秒数(time_t)。这个值有时也被用做绑定输入目录表。注意:一些编译器将忽略这个值。 PointerToSymbolTable及NumberOfSymbols:用在调试信息中,用途不太明确,不过它们的值总为0。 SizeOfOptionalHeader:可选头的长度(sizeof
IMAGE_OPTIONAL_HEADER),可以用它来检验PE文件的正确性。 Characteristics:是一个标志的集合,其大部分位用于OBJ或LIB文件中。 文件头下面就是可选择头,这是一个叫做IMAGE_OPTIONAL_HEADER的结构,由224个字节组成。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。可选头部包含了很多关于可执行映像的重要信息。例如,初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等。IMAGE_
OPTIONAL_HEADER结构如下: l #define
IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct _IMAGE_OPTIONAL_HEADER
{ // // 标准域 // WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// // NT附加域 // DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment; WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; DWORD SizeOfImage;
DWORD SizeOfHeaders; DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER,
*PIMAGE_OPTIONAL_HEADER; l 其中参数含义如下所述。 Magic:这个值好像总是0x010b。 MajorLinkerVersion及MinorLinkerVersion:链接器的版本号,这个值不太可靠。 SizeOfCode:可执行代码的长度。 SizeOfInitializedData:初始化数据的长度(数据段)。 SizeOfUninitializedData:未初始化数据的长度(bss段)。 AddressOfEntryPoint:代码的入口RVA地址,程序从这儿开始执行,常称为程序的原入口点OEP(Original
Entry Point)。 BaseOfCode:可执行代码起始位置。 BaseOfData:初始化数据起始位置。 ImageBase:载入程序首选的RVA地址。这个地址可被Loader改变。 SectionAlignment:段加载后在内存中的对齐方式。 FileAlignment:段在文件中的对齐方式。 MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系统版本。 MajorImageVersion及MinorImageVersion:程序版本。 MajorSubsystemVersion及MinorSubsystemVersion:子系统版本号,这个域系统支持。例如,程序运行于NT下,子系统版本号如果不是4.0,对话框不能显示3D风格。 Win32VersionValue:这个值总是为0。 SizeOfImage:程序调入后占用内存大小(字节),等于所有段的长度之和。 SizeOfHeaders:所有文件头长度之和,它等于从文件开始到第一个段的原始数据之间的大小。 CheckSum:校验和,仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它。 Subsystem:一个标明可执行文件所期望的子系统的枚举值。 DllCharacteristics:DLL状态。 SizeOfStackReserve:保留堆栈大小。 SizeOfStackCommit:启动后实际申请的堆栈数,可随实际情况变大。 SizeOfHeapReserve:保留堆大小。 SizeOfHeapCommit:实际堆大小。 LoaderFlags:目前没有用。 NumberOfRvaAndSizes:下面的目录表入口个数,这个值也不可靠,可用常数IMAGE_NUMBEROF_DIRECTORY_ENTRIES来代替它,这个值在目前Windows版本中设为16。注意,如果这个值不等于16,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。 DataDirectory:是一个IMAGE_DATA_DIRECTORY数组,数组元素个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,结构如下: l typedef struct _IMAGE_DATA_DIRECTORY
{ DWORD VirtualAddress; //
起始RVA地址 DWORD Size; //
长度 } IMAGE_DATA_DIRECTORY,
*PIMAGE_DATA_DIRECTORY; 2.3.3 IMAGE_SECTION_HEADER头部PE文件格式中,所有的节头部位于可选头部之后。每个节头部为40个字节长,并且没有任何填充信息。节头部被定义为以下的结构: l #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER
{ BYTE
Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如".text" union { DWORD PhysicalAddress;
// 物理地址 DWORD VirtualSize;
// 真实长度 } Misc; DWORD VirtualAddress;
// RVA DWORD
SizeOfRawData; // 物理长度 DWORD
PointerToRawData; // 节基于文件的偏移量 DWORD
PointerToRelocations; // 重定位的偏移 DWORD
PointerToLinenumbers; // 行号表的偏移 WORD
NumberOfRelocations; // 重定位项数目 WORD
NumberOfLinenumbers; // 行号表的数目 DWORD Characteristics;
// 节属性 } IMAGE_SECTION_HEADER,
*PIMAGE_SECTION_HEADER; l 其中IMAGE_SIZEOF_SHORT_NAME等于8。注意,如果不是这个值,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。
|
在win32
SDK的文件winnt.h中有PE文件格式的定义。本文所用到的变量,如果没有特别说明,都在文件winnt.h中定义。
有关一些PE头文件结构一般都有32位和64位之分,如IMAGE_NT_HEADERS32和IMAGE_NT_HEADERS64等,除了在64位版本中的一些扩展域外,这些结构总是一样的。是采用32位还是64位,需要用#define
_WIN64来定义,如果没有这种定义,则采用的是32位的文件结构。编译器将根据此定义选择相应的编译模式。 2.3.1 MS-DOS头部MS-DOS头部占据了PE文件的头64个字节,描述它内容的结构如下:
l // 此结构包含于WINNT.H中 // typedef struct _IMAGE_DOS_HEADER { // DOS的.EXE头部 WORD e_magic; // 魔术数字 WORD e_cblp; // 文件最后页的字节数 WORD e_cp; // 文件页数 WORD e_crlc; // 重定义元素个数 WORD e_cparhdr; // 头部尺寸,以段落为单位 WORD e_minalloc; // 所需的最小附加段 WORD e_maxalloc; // 所需的最大附加段 WORD e_ss; // 初始的SS值(相对偏移量) WORD e_sp; // 初始的SP值 WORD e_csum; // 校验和 WORD e_ip; // 初始的IP值 WORD e_cs; // 初始的CS值(相对偏移量) WORD e_lfarlc; // 重分配表文件地址 WORD e_ovno; // 覆盖号 WORD e_res[4]; // 保留字 WORD e_oemid; // OEM标识符(相对e_oeminfo) WORD e_oeminfo; // OEM信息 WORD e_res2[10]; // 保留字 LONG e_lfanew; // 新exe头部的文件地址 } IMAGE_DOS_HEADER, *PIMAGE_DOS_HEADER; l 其中第一个域e_magic,被称为魔术数字,它用于表示一个MS-DOS兼容的文件类型。所有MS-DOS兼容的可执行文件都将这个值设为0x5A4D,表示ASCII字符MZ。MS-DOS头部之所以有的时候被称为MZ头部,就是这个缘故。还有许多其他的域对于MS-DOS操作系统来说都有用,但是对于Windows
NT来说,这个结构中只有一个有用的域——最后一个域e_lfnew,一个4字节的文件偏移量,PE文件头部就是由它定位的。 2.3.2 IMAGE_NT_HEADER头部PE
Header是紧跟在MS-DOS头部和实模式程序残余之后的,描述它内容的结构 如下: l typedef struct _IMAGE_NT_HEADERS { DWORD Signature; //
PE文件头标志:"PE\0\0" IMAGE_FILE_HEADER FileHeader; // PE文件物理分布的信息 IMAGE_OPTIONAL_HEADER32 OptionalHeader; // PE文件逻辑分布的信息 } IMAGE_NT_HEADERS32, *PIMAGE_NT_HEADERS32; 紧接PE文件头标志之后是PE文件头结构,由20个字节组成,它被定义为: l typedef struct _IMAGE_FILE_HEADER { WORD Machine; WORD NumberOfSections; DWORD TimeDateStamp; DWORD PointerToSymbolTable; DWORD NumberOfSymbols; WORD SizeOfOptionalHeader; WORD Characteristics; } IMAGE_FILE_HEADER, *PIMAGE_FILE_HEADER; #define IMAGE_SIZEOF_FILE_HEADER 20 l 其中请注意这个文件头部的大小已经定义在这个包含文件之中了,这样一来,想要得到这个结构的大小就很方便了。 Machine:表示该程序要执行的环境及平台,现在已知的值如表2.1所示。 应用程序执行的环境及平台代码 IMAGE_FILE_MACHINE_I386(0x14c) | Intel
80386 处理器以上 | 0x014d | Intel
80486 处理器以上 | 0x014e | Intel
Pentium 处理器以上 | 0x0160 | R3000(MIPS)处理器,big endian | IMAGE_FILE_MACHINE_R3000(0x162) | R3000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_R4000(0x166) | R4000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_R10000(0x168) | R10000(MIPS)处理器,little endian | IMAGE_FILE_MACHINE_ALPHA(0x184) | DEC
Alpha AXP处理器 | IMAGE_FILE_MACHINE_POWERPC(0x1f0) | IBM
Power PC,little endian |
|
|
NumberOfSections:段的个数。 TimeDateStamp:文件建立的时间。可用这个值来区分同一个文件的不同的版本,即使它们的商业版本号相同。这个值的格式并没有明确的规定,但是很显然地大多数的C编译器都把它定为从1970.1.1
00:00:00以来的秒数(time_t)。这个值有时也被用做绑定输入目录表。注意:一些编译器将忽略这个值。 PointerToSymbolTable及NumberOfSymbols:用在调试信息中,用途不太明确,不过它们的值总为0。 SizeOfOptionalHeader:可选头的长度(sizeof
IMAGE_OPTIONAL_HEADER),可以用它来检验PE文件的正确性。 Characteristics:是一个标志的集合,其大部分位用于OBJ或LIB文件中。 文件头下面就是可选择头,这是一个叫做IMAGE_OPTIONAL_HEADER的结构,由224个字节组成。虽然它的名字是“可选头部”,但是请确信:这个头部并非“可选”,而是“必需”的。可选头部包含了很多关于可执行映像的重要信息。例如,初始的堆栈大小、程序入口点的位置、首选基地址、操作系统版本、段对齐的信息等。IMAGE_
OPTIONAL_HEADER结构如下: l #define
IMAGE_NUMBEROF_DIRECTORY_ENTRIES 16 typedef struct _IMAGE_OPTIONAL_HEADER
{ // // 标准域 // WORD Magic;
BYTE MajorLinkerVersion;
BYTE MinorLinkerVersion;
DWORD SizeOfCode;
DWORD SizeOfInitializedData;
DWORD SizeOfUninitializedData;
DWORD AddressOfEntryPoint;
DWORD BaseOfCode;
DWORD BaseOfData;
// // NT附加域 // DWORD ImageBase;
DWORD SectionAlignment;
DWORD FileAlignment; WORD MajorOperatingSystemVersion;
WORD MinorOperatingSystemVersion;
WORD MajorImageVersion;
WORD MinorImageVersion;
WORD MajorSubsystemVersion;
WORD MinorSubsystemVersion;
DWORD Win32VersionValue; DWORD SizeOfImage;
DWORD SizeOfHeaders; DWORD CheckSum;
WORD Subsystem;
WORD DllCharacteristics;
DWORD SizeOfStackReserve;
DWORD SizeOfStackCommit;
DWORD SizeOfHeapReserve;
DWORD SizeOfHeapCommit;
DWORD LoaderFlags;
DWORD NumberOfRvaAndSizes;
IMAGE_DATA_DIRECTORY
DataDirectory[IMAGE_NUMBEROF_DIRECTORY_ENTRIES]; } IMAGE_OPTIONAL_HEADER,
*PIMAGE_OPTIONAL_HEADER; l 其中参数含义如下所述。 Magic:这个值好像总是0x010b。 MajorLinkerVersion及MinorLinkerVersion:链接器的版本号,这个值不太可靠。 SizeOfCode:可执行代码的长度。 SizeOfInitializedData:初始化数据的长度(数据段)。 SizeOfUninitializedData:未初始化数据的长度(bss段)。 AddressOfEntryPoint:代码的入口RVA地址,程序从这儿开始执行,常称为程序的原入口点OEP(Original
Entry Point)。 BaseOfCode:可执行代码起始位置。 BaseOfData:初始化数据起始位置。 ImageBase:载入程序首选的RVA地址。这个地址可被Loader改变。 SectionAlignment:段加载后在内存中的对齐方式。 FileAlignment:段在文件中的对齐方式。 MajorOperatingSystemVersion及MinorOperatingSystemVersion:操作系统版本。 MajorImageVersion及MinorImageVersion:程序版本。 MajorSubsystemVersion及MinorSubsystemVersion:子系统版本号,这个域系统支持。例如,程序运行于NT下,子系统版本号如果不是4.0,对话框不能显示3D风格。 Win32VersionValue:这个值总是为0。 SizeOfImage:程序调入后占用内存大小(字节),等于所有段的长度之和。 SizeOfHeaders:所有文件头长度之和,它等于从文件开始到第一个段的原始数据之间的大小。 CheckSum:校验和,仅用在驱动程序中,在可执行文件中可能为0。它的计算方法Microsoft不公开,在imagehelp.dll中的CheckSumMappedFile()函数可以计算它。 Subsystem:一个标明可执行文件所期望的子系统的枚举值。 DllCharacteristics:DLL状态。 SizeOfStackReserve:保留堆栈大小。 SizeOfStackCommit:启动后实际申请的堆栈数,可随实际情况变大。 SizeOfHeapReserve:保留堆大小。 SizeOfHeapCommit:实际堆大小。 LoaderFlags:目前没有用。 NumberOfRvaAndSizes:下面的目录表入口个数,这个值也不可靠,可用常数IMAGE_NUMBEROF_DIRECTORY_ENTRIES来代替它,这个值在目前Windows版本中设为16。注意,如果这个值不等于16,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。 DataDirectory:是一个IMAGE_DATA_DIRECTORY数组,数组元素个数为IMAGE_NUMBEROF_DIRECTORY_ENTRIES,结构如下: l typedef struct _IMAGE_DATA_DIRECTORY
{ DWORD VirtualAddress; //
起始RVA地址 DWORD Size; //
长度 } IMAGE_DATA_DIRECTORY,
*PIMAGE_DATA_DIRECTORY; 2.3.3 IMAGE_SECTION_HEADER头部PE文件格式中,所有的节头部位于可选头部之后。每个节头部为40个字节长,并且没有任何填充信息。节头部被定义为以下的结构: l #define IMAGE_SIZEOF_SHORT_NAME 8 typedef struct _IMAGE_SECTION_HEADER
{ BYTE
Name[IMAGE_SIZEOF_SHORT_NAME]; // 节表名称,如".text" union { DWORD PhysicalAddress;
// 物理地址 DWORD VirtualSize;
// 真实长度 } Misc; DWORD VirtualAddress;
// RVA DWORD
SizeOfRawData; // 物理长度 DWORD
PointerToRawData; // 节基于文件的偏移量 DWORD
PointerToRelocations; // 重定位的偏移 DWORD
PointerToLinenumbers; // 行号表的偏移 WORD
NumberOfRelocations; // 重定位项数目 WORD
NumberOfLinenumbers; // 行号表的数目 DWORD Characteristics;
// 节属性 } IMAGE_SECTION_HEADER,
*PIMAGE_SECTION_HEADER; l 其中IMAGE_SIZEOF_SHORT_NAME等于8。注意,如果不是这个值,那么这个数据结构大小就不能固定下来,也就不能确定其他变量位置。
|
OEP(Original Entry
Point)是每个PE文件被加载时的起始地址,如何获得这个地址很重要,因为修改程序中的这个值是文件加壳和脱壳时的必须步骤,一些黑客程序也是通过修改OEP值来获得对目标程序的控制权从而实施攻击。下面分别介绍如何通过文件直接访问和通过内存映射访问读取OEP值的方法,并给出完整的程序代码。
2.4.1
通过文件读取OEP值获得OEP值的最简单方法是,直接从一个PE文件中读取OEP。根据以上对PE文件结构的介绍可知,OEP是PE文件的IMAGE_OPTIONAL_HEADER结构的AddressOfEntryPoint成员,在偏移此结构头40个字节处。而IMAGE_OPTIONAL_
HEADER在PE文件的起始位置由IMAGE_DOS_HEADER的e_lfanew成员来计算。注意,以上两个结构在PE文件中不是紧跟在一起的,它之间是DOS
Stub,而在每个PE文件DOS Stub的长度可能不一定相等。在PE文件的头部是IMAGE_
DOS_HEADER结构,读取这个结构可以得到e_lfanew的值,因而可以得到IMAGE_
OPTIONAL_HEADER在PE文件中的位置,也就得到了OEP值。以下是通过文件访问的方法读取OEP的程序代码,即: l // 通过文件读取OEP值
BOOL ReadOEPbyFile(LPCSTR szFileName) { HANDLE hFile; // 打开文件 if ((hFile =
CreateFile(szFileName, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_VALUE) { printf("Can~t not open
file.\n"); return FALSE; } DWORD dwOEP,cbRead; IMAGE_DOS_HEADER
dos_head[sizeof(IMAGE_DOS_HEADER)]; if (!ReadFile(hFile, dos_head,
sizeof(IMAGE_DOS_HEADER), &cbRead, NULL)){ printf("Read image_dos_header
failed.\n"); CloseHandle(hFile); return FALSE; } int
nEntryPos=dos_head->e_lfanew+40; SetFilePointer(hFile, nEntryPos, NULL,
FILE_BEGIN); if (!ReadFile(hFile, &dwOEP, sizeof(dwOEP), &cbRead,
NULL)){ printf("read OEP failed.\n"); CloseHandle(hFile); return FALSE; } //
关闭文件 CloseHandle(hFile); // 显示OEP地址 printf("OEP by file:%d\n",dwOEP); return
TRUE; } 2.4.2
通过内存映射读取OEP值获得OEP值的另一种方法是通过内存映射来实现,此方法也需要熟悉PE的文件结构。与直接访问PE的方法不同,内存映射的方法首先把PE文件映射到计算机的内存,再通过内存的基指针获得IMAGE_DOS_HEADER的头指针,由此再获得IMAGE_
OPTIONAL_HEADER指针,这样就可以得到AddressOfEntryPoint的值。下面是通过内存映射获得OEP值的方法: l //
通过文件内存映射读取OEP值 BOOL ReadOEPbyMemory(LPCSTR szFileName) { struct PE_HEADER_MAP {
DWORD signature; IMAGE_FILE_HEADER _head; IMAGE_OPTIONAL_HEADER opt_head;
IMAGE_SECTION_HEADER section_header[6]; } *header; HANDLE hFile; HANDLE
hMapping; void *basepointer; // 打开文件 if ((hFile = CreateFile(szFileName,
GENERIC_READ, FILE_SHARE_READ,0,OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN,0)) ==
INVALID_HANDLE_VALUE) { printf("Can~t open file.\n"); return FALSE; } //
创建内存映射文件 if (!(hMapping = CreateFileMapping(hFile,0,PAGE_READONLY|SEC_COMMIT,
0,0,0))) { printf("Mapping failed.\n"); CloseHandle(hFile); return FALSE; } //
把文件头映象存入baseointer if (!(basepointer =
MapViewOfFile(hMapping,FILE_MAP_READ,0,0,0))) { printf("View failed.\n");
CloseHandle(hMapping); CloseHandle(hFile); return FALSE; } IMAGE_DOS_HEADER *
dos_head =(IMAGE_DOS_HEADER *)basepointer; // 得到PE文件头 header = (PE_HEADER_MAP
*)((char *)dos_head + dos_head->e_lfanew); // 得到OEP地址. DWORD
dwOEP=header->opt_head.AddressOfEntryPoint; // 清除内存映射和关闭文件
UnmapViewOfFile(basepointer); CloseHandle(hMapping); CloseHandle(hFile); //
显示OEP地址 printf("OEP by memory:%d\n",dwOEP); return TRUE; } 2.4.3
读取OEP值方法的测试为了检验以上两种获取OEP值方法的正确性和一致性,可以用以下的方法来测试: l // oep.cpp:读取OEP的实例 //
#include #include BOOL ReadOEPbyMemory(LPCSTR szFileName);
BOOL ReadOEPbyFile(LPCSTR szFileName); void main() {
ReadOEPbyFile("..\\calc.exe"); ReadOEPbyMemory("..\\calc.exe"); } l
运行以上代码后,可以得到如图2.3所示的结果。从图中可以看出,以上两种获取OEP值方法所得到的结果是一致的。
获取OEP值方法的测试结果
|
一些PE格式(Portable
Executable)的EXE文件常常存在很多资源,如图标、位图、对话框、声音等。若要把这些资源取出为自己所用,或修改这些文件中的资源,则需要对PE文件中资源数据结构有所了解。
2.5.1 查找资源在文件中的起始位置要找出一个PE文件中的某种资源,首先需要确定资源节在PE文件中的起始位置。有两种方法来确定资源在文件中的起始位置。 第一种方法,首先根据FileHeader中的成员NumberOfSections的值,确定文件中节的数目,再根据节的数目,遍历节表数组。也就是从0到(节表数–1)的每一个节表项。比较每一个节表项的Name字段,看看是否等于“.rsrc”,如果是,就找到了资源节的节表项。这个节表项的PointerToRawData
中的值,就是资源节在文件中的位置。 第二种方法,取得PE
Header中的IMAGE_OPTIONAL_HEADER中的DataDirectory数组中的第三项,也就是资源项。DataDirectory[]数组的每项都是IMAGE_DATA_
DIRECTORY结构,该结构定义如下: l typedef struct _IMAGE_DATA_DIRECTORY { DWORD VirtualAddress; DWORD Size; } IMAGE_DATA_DIRECTORY, *PIMAGE_DATA_DIRECTORY; l 从以上结构对象取得DataDirectory数组中的第三项中的成员VirtualAddress的值。这个值就是在内存中资源节的RVA。然后根据节的数目,遍历节表数组,也就是从0~(节表数–1)的每一个节表项。每个节在内存中的RVA的范围是从该节表项的成员VirtualAddress字段的值开始(包括这个值),到VirtualAddress+Misc.VirtualSize的值结束(不包括这个值)。遍历整个节表,看看所取得的资源节的RVA是否在那个节表项的RVA范围之内。如果在范围之内,就找到了资源节的节表项。这个节表项中的PointerToRawData
中的值,就是资源节在文件中的位置。如果这个PE文件没有资源
的话,DataDirectory数组中的第三项内容为0。这样也可以得到了资源在文件中开始的位置。 2.5.2 确定PE文件中的资源得到了资源节在文件中的位置后,就可以确定某个资源类型及其二进制数据在PE文件中的位置和数据块的大小。 资源节最开始是一个IMAGE_RESOURCE_DIRECTORY结构,在winnt.h文件中有这个结构的定义。这个结构长度为16字节,共有6个参数,其结构的原型如下: l typedef struct _IMAGE_RESOURCE_DIRECTORY
{ DWORD Characteristics; DWORD TimeDateStamp; WORD MajorVersion; WORD MinorVersion; WORD NumberOfNamedEntries; WORD NumberOfIdEntries; // IMAGE_RESOURCE_DIRECTORY_ENTRY
DirectoryEntries[]; } IMAGE_RESOURCE_DIRECTORY,
*PIMAGE_RESOURCE_DIRECTORY; l 其中各个参数的含义如下所述 Characteristics:
标识此资源的类型。 TimeDateStamp:资源编译器产生资源的时间。 MajorVersion:资源主版本号。 MinorVersion:资源次版本号。 NumberOfNamedEntries和NumberofIDEntries:分别为用字符串和整形数字来进行标识的IMAGE_RESOURCE_DIRECTORY_ENTRY项数组的成员个数。 紧跟着IMAGE_RESOURCE_DIRECTORY后面的是一个IMAGE_RESOURCE_
DIRECTORY_ENTRY数组。这个结构长度为8个字节,共有两个字段,每个字段4个字节。其结构原型如下: l typedef struct
_IMAGE_RESOURCE_DIRECTORY_ENTRY { union { struct { DWORD NameOffset:31; DWORD NameIsString:1; }; DWORD Name; WORD Id; }; union { DWORD OffsetToData; struct { DWORD
OffsetToDirectory:31; DWORD
DataIsDirectory:1; }; }; } IMAGE_RESOURCE_DIRECTORY_ENTRY,
*PIMAGE_RESOURCE_DIRECTORY_ENTRY; l 其中,对于第一个字段,当其最高位为1(0x80000000)时,这个DWORD剩下的31位表明相对于资源开始位置的偏移,偏移的内容是一个IMAGE_RESOURCE_DIR_
STRING_U,用其中的字符串来标明这个资源类型;当第一个字段的最高位为0时,表示这个DWORD的低WORD中的值作为Id标明这个资源类型。 对于第二个字段,当第二个字段的最高位为1时,表示还有下一层的结构。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容将是一个下一层的IMAGE_RESOURCE_DIRECTORY结构;当第二个字段的最高位为0时,表示已经没有下一层的结构了。这个DWORD的剩下31位表明一个相对于资源开始位置的偏移,这个偏移的内容会是一个IMAGE_RESOURCE_DATA
_ENTRY结构,此结构会说明资源的位置。对于资源标示号Id,当Id等于1时,表示资源为光标,等于2时表示资源为位图等,等于3时表示资源为图标等。在winuser.h文件中有定义。 标识一个IMAGE_RESOURCE_DIRECTORY_ENTRY一般都是使用Id,就是一个整数。但是也有少数使用IMAGE_RESOURCE_DIR_STRING_U来标识一个资源类型。这个结构定义如下: l typedef struct
_IMAGE_RESOURCE_DIR_STRING_U { WORD Length; WCHAR NameString[1]; } IMAGE_RESOURCE_DIR_STRING_U,
*PIMAGE_RESOURCE_DIR_STRING_U; l 这个结构中将有一个Unicode的字符串,是字对齐的。这个结构的长度可变,由第一个字段Length指明后面的Unicode字符串的长度。 经过3层IMAGE_RESOURCE_DIRECTORY_ENTRY(一般是3层,也有可能更少些)最终可以找到一个IMAGE_RESOURCE_DATA_ENTRY结构,这个结构中存有相应资源的位置和大小。这个结构长16个字节,有4个参数,其原型如下: l typedef struct
_IMAGE_RESOURCE_DATA_ENTRY { DWORD OffsetToData; DWORD Size; DWORD CodePage; DWORD Reserved; } IMAGE_RESOURCE_DATA_ENTRY,
*PIMAGE_RESOURCE_DATA_ENTRY; l 其中各个参数的含义如下所述。 OffsetToData:这是一个内存中的RVA,可以用来转化成文件中的位置。用这个值减去资源节的开始RVA,就可以得到相对于资源节开始的偏移。再加上资源节在文件中的开始位置,即节表中资源节中PointerToRawData的值,就是资源在文件中的位置。注意,资源节的开始RVA可以由Optional
Header中的DataDirectory数组中的第三项中的VirtualAddress的值得到,或者节表中资源节那项中的VirtualAddress的值得到。 Size:资源的大小,以字节为单位。 CodePage:代码页。 Reserved:保留项。 总之,资源一般使用树来保存,通常包含3层,最高层是类型,其次是名字,最后是语言。在资源节开始的位置,首先是一个IMAGE_RESOURCE_DIRECTORY结构,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组,这个数组的每个元素代表的资源类型不同;通过每个元素,可以找到第二层另一个IMAGE_RESOURCE_
DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组。这一层的数组的每个元素代表的资源名字不同;然后可以找到第三层的每个IMAGE_
RESOURCE_DIRECTORY,后面紧跟着IMAGE_RESOURCE_DIRECTORY_ENTRY数组。这一层的数组的每个元素代表的资源语言不同;最后通过每个IMAGE_RESOURCE_
DIRECTORY_ENTRY可以找到每个IMAGE_RESOURCE_DATA_ENTRY。通过每个IMAGE_RESOURCE_DATA_ENTRY,就可以找到每个真正的资源。
|
在下面的实例中,将把一段MessageBoxA()的计算机代码根据PE文件的格式注入到一个PE程序中。有关把代码注入到一个应用程序的技术将在后面的章节专门介绍。
2.6.1 如何获得MessageBoxA代码
要实现代码注入PE程序且能够运行,首先要做的是如何得到这段代码。为了得到这种代码,作者编写了一段汇编源程序
msgbx.asm,然后用RadASM编译器进行编译,当然也可以使用其他的方法来实现代码的注入。编写这段代码最关键的问题是如何把对话框标题字符串和显示字符串一起存放在代码段,以便提取,否则无法提取。下面是生成MessageBoxA()的源代码:
l
;msgbx.asm 文件.
;
.386p
.model flat, stdcall
option casemap:none
include
\masm32\include\windows.inc
include
\masm32\include\user32.inc
includelib \masm32\lib\user32.lib
.code
start:
push MB_ICONINFORMATION or MB_OK
call Func1
db "Test",0
Func1:
call Func2
db "Hello",0
Func2:
push NULL
call MessageBoxA
; ret
end start
l
其中"Test"是MessageBoxA()对话框的标题,"Hello"是要显示的字符串。Message-
BoxA()所用的Windows句柄为NULL。
用RadASM编译器对以上代码编译后,可以生成一个msgbx.obj文件,用VC++
编辑器打开后,如图2.4所示,可以查看这个文件的机器代码。
Msgbx.obj文件的机器代码
把图2.4中所选择的计算机机器代码取出变成一个命令字符串,即:
l
unsigned char cmdline[35]={
0x6a, // (1) push 命令
0x40, // (1)
MB_ICONINFORMATION|MB_OK
0xe8, // (1) call命令
0x05,0x00,0x00,0x00, // (4) 标题字符串字节个数,包括结束位
(DWORD)
0x54,0x65,0x73,0x74, 0x00, // (5) "Test",0(标题)
0xe8, // (1) call命令
0x06,0x00,0x00,0x00, // (4) 标题字符串字节个数,包括结束位
(DWORD)
0x48,0x65,0x6c,0x6c,0x6f,0x00, // (6) "Hello",0(显示字符串)
0x6a, // (1) push 命令
0x00, // (1) 窗口句柄hWnd,NULL
0xe8, // (1) call命令
0x00,0x00,0x00,0x00, // (4) MessageBoxA的地址
(DWORD)
0x1a, // (1) 第26位,校验和
0x00,0x00,0x00,0x0b // (4) 返回地址 (DWORD)
};
l
其中()中的数值表示这一行上代码的字节个数。0x6a是汇编语言中的push命令,0xe8是汇编语言中的call命令,而jmp命令为0xe9。“校验和”是从第一个push命令开始计算所得到的字节总数和(包括校验计数位),从以上代码第一个字节开始计数起到“校验和”位正好是第26位字节个数。字符串字节个数位为一个DWORD型,占4个字节,它是按Little-endian的方式存放的,要把这4个字节位的顺序颠倒才能得到实际数值,即把高位字节变成低位,把低位变换到高位。
要把以上代码注入到一个PE文件中,需要修改4个地方:(1)修改PE文件的入口地址,使PE装载器首先装载以上代码;(2)修改以上代码MessageBoxA()的地址,使以上的代码能够显示出一个对话框;(3)把“校验和”位变成跳转位,即变成jmp
(0xe9);(4)修改返回地址,把程序引入到原来的装载点上。
|
2.6.2 把MessageBoxA()代码写入PE文件的完整实例
根据以上的对MessageBoxA()的分析,可以直接把以上代码注入到一个PE可执行
文件中。为了使程序有通用性,这里编写了一个产生显示任意长度字符的对话框的函数WriteMessageBox()。
下面是用于注入MessageBoxA()代码的头文件,取名为Pe.h,其中用
#include包含了相关的文件头,定义了peHeader结构,且定义了CPe类,其源代码如下:
l
// Pe.h: 定义CPe类
//
#ifndef _PE_H__INCLUDED
#define _PE_H__INCLUDED
#include <io.h>
#include <fcntl.h>
#include <sys\stat.h>
typedef struct PE_HEADER_MAP
{
DWORD signature;
IMAGE_FILE_HEADER _head;
IMAGE_OPTIONAL_HEADER opt_head;
IMAGE_SECTION_HEADER section_header[6];
} peHeader;
class CPe
{
public:
CPe();
virtual ~CPe();
public:
void CalcAddress(const void *base);
void ModifyPe(CString strFileName,CString strMsg);
void WriteFile(CString strFileName,CString strMsg);
BOOL WriteNewEntry(int ret,long offset,DWORD dwAddress);
BOOL WriteMessageBox(int ret,long offset,CString strCap,CString
strTxt);
CString StrOfDWord(DWORD dwAddress);
public:
DWORD dwSpace;
DWORD dwEntryAddress;
DWORD dwEntryWrite;
DWORD dwProgRAV;
DWORD dwOldEntryAddress;
DWORD dwNewEntryAddress;
DWORD dwCodeOffset;
DWORD dwPeAddress;
DWORD dwFlagAddress;
DWORD dwVirtSize;
DWORD dwPhysAddress;
DWORD dwPhysSize;
DWORD dwMessageBoxAadaddress;
};
#endif
l
其中peHeader结构是前面所讲的PE
Header结构与节表(Section
Table)头结构(6个表头成员)的总结构。因为它们在PE文件中是紧凑排列的,所以可以这样写。其实只用一个节表头就可以。
下面分别介绍CPe类成员函数的定义,它们包含在Pe.cpp文件中。在这个文件开始用#include包含了stdafx.h和Pe.h文件。用MFC
VC++编译器编译时,必须包括stdafx.h文件,即使这个文件是空的,也需要包括它,这是编译器设置所致,除非修改MFC的编译器的默认设置。CPe类的构造和析构函数这里没有用上,对系统内存的访问和其他操作主要是通过主成员函数ModifyPe()来进行。它们的源代码如下:
l
// Pe.cpp: 实现 CPe类
//
#include "stdafx.h"
#include "Pe.h"
CPe::CPe()
{
}
CPe::~CPe()
{
}
void CPe::ModifyPe(CString strFileName,CString strMsg)
{
CString strErrMsg;
HANDLE hFile, hMapping;
void *basepointer;
// 打开要修改的文件
if ((hFile = CreateFile(strFileName,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
{
AfxMessageBox("Could not open file.");
return;
}
// 创建一个映射文件
if
(!(hMapping = CreateFileMapping(hFile, 0, PAGE_READONLY | SEC_ COMMIT, 0, 0,
0)))
{
AfxMessageBox("Mapping failed.");
CloseHandle(hFile);
return;
}
// 把文件头映象存入baseointer
if (!(basepointer = MapViewOfFile(hMapping, FILE_MAP_READ, 0,
0, 0)))
{
AfxMessageBox("View failed.");
CloseHandle(hMapping);
CloseHandle(hFile);
return;
}
CloseHandle(hMapping);
CloseHandle(hFile);
CalcAddress(basepointer); // 得到相关地址
UnmapViewOfFile(basepointer);
if(dwSpace<50)
{
AfxMessageBox("No room to write the data!");
}
else
{
WriteFile(strFileName,strMsg); // 写文件
}
if ((hFile = CreateFile(strFileName,
GENERIC_READ|GENERIC_WRITE,
FILE_SHARE_READ|FILE_SHARE_WRITE, 0,
OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, 0)) == INVALID_HANDLE_ VALUE)
{
AfxMessageBox("Could not open file.");
return;
}
CloseHandle(hFile);
}
其中对一个PE文件进行MessageBoxA()代码的注入是通过ModifyPe()函数进行,它的入口参数是要被修改的PE可执行文件名。在这个函数中,首先创建所修改文件的句柄,然后创建映射文件,再通过映射文件的句柄获得这个PE文件的文件头指针,最后把这个指针传给函数CalcAddress()。通过CalcAddress()函数来计算PE
Header的开始偏移、保存旧的程序入口地址、计算新的程序入口地址和计算PE文件的空隙空间等。
CalcAddress()函数的源代码如下:
l
void CPe::CalcAddress(const void *base)
{
IMAGE_DOS_HEADER * dos_head =(IMAGE_DOS_HEADER *)base;
if (dos_head->e_magic != IMAGE_DOS_SIGNATURE)
{
AfxMessageBox("Unknown type of file.");
return;
}
peHeader * header;
// 得到PE文件头
header = (peHeader *)((char *)dos_head +
dos_head->e_lfanew);
if(IsBadReadPtr(header, sizeof(*header)))
{
AfxMessageBox("No PE header, probably DOS
executable.");
return;
}
DWORD mods;
char tmpstr[4]={0};
if(strstr((const char
*)header->section_header[0].Name,".text")!=
NULL)
{
// 此段的真实长度
dwVirtSize=header->section_header[0].Misc.VirtualSize;
// 此段的物理偏移
dwPhysAddress=header->section_header[0].PointerToRawData;
// 此段的物理长度
dwPhysSize=header->section_header[0].SizeOfRawData;
// 得到PE文件头的开始偏移
dwPeAddress=dos_head->e_lfanew;
//
得到代码段的可用空间,用以判断可不可以写入我们的代码
// 用此段的物理长度减去此段的真实长度就可以得到
dwSpace=dwPhysSize-dwVirtSize;
// 得到程序的装载地址,一般为0x400000
dwProgRAV=header->opt_head.ImageBase;
//
得到代码偏移,用代码段起始RVA减去此段的物理偏移
//
应为程序的入口计算公式是一个相对的偏移地址,计算公式为:
// 代码的写入地址+dwCodeOffset
dwCodeOffset=header->opt_head.BaseOfCode-dwPhysAddress;
// 代码写入的物理偏移
dwEntryWrite=header->section_header[0].PointerToRawData+header->
section_header[0].Misc.VirtualSize;
//对齐边界
mods=dwEntryWrite%16;
if(mods!=0)
{
dwEntryWrite+=(16-mods);
}
// 保存旧的程序入口地址
dwOldEntryAddress=header->opt_head.AddressOfEntryPoint;
// 计算新的程序入口地址
dwNewEntryAddress=dwEntryWrite+dwCodeOffset;
return;
}
}
l
下面的StrOfDWord()函数是把一个DWORD值转换成一个字符串,因为一个DWORD值占有4个字节,因此把一个DWORD值变成一个字符串,若保持数值不变,就变成了一个4个字节的字符串。同时把这个值的位置顺序颠倒,这是为了把一个实际的值变成按Little-endian的方式写入PE文件中,其转换方法如下:
l
CString CPe::StrOfDWord(DWORD dwAddress)
{
unsigned char waddress[4]={0};
waddress[3]=(char)(dwAddress>>24)&0xFF;
waddress[2]=(char)(dwAddress>>16)&0xFF;
waddress[1]=(char)(dwAddress>>8)&0xFF;
waddress[0]=(char)(dwAddress)&0xFF;
return waddress;
}
l
下面的WriteNewEntry()函数把新的入口点写入PE程序原来的入口点处,使PE装载器在载入程序时,直接跳入到MessageBoxA()的入口处,该函数的源代码如下:
l
BOOL CPe::WriteNewEntry(int ret,long offset, DWORD dwAddress)
{
CString strErrMsg;
long retf;
unsigned char waddress[4]={0};
retf=_lseek(ret,offset,SEEK_SET);
if(retf==-1)
{
AfxMessageBox("Error seek.");
return FALSE;
}
memcpy(waddress,StrOfDWord(dwAddress),4);
retf=_write(ret,waddress,4);
if(retf==-1)
{
strErrMsg.Format("Error write: %d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
return TRUE;
}
l
下面的WriteMessageBox()函数是把MessageBoxA()的机器代码写入到PE文件中。这个函数显示的对话框标题和显示的字符串内容和长度不是固定的。在这个函数中,首先就计算MessageBoxA()函数的地址和函数的返回地址,然后把重新生成的对话框代码写入到程序中。WriteMessageBox()函数的源代码如下:
l
BOOL CPe::WriteMessageBox(int ret,long offset,CString
strCap,CString
strTxt)
{
CString strAddress1,strAddress2;
unsigned char waddress[4]={0};
DWORD dwAddress;
// 获取MessageBox在内存中的地址
HINSTANCE gLibMsg=LoadLibrary("user32.dll");
dwMessageBoxAadaddress=(DWORD)GetProcAddress(gLibMsg,"MessageBoxA");
// 计算校验位
int nLenCap1 =strCap.GetLength()+1; // 加上字符串后面的结束位
int nLenTxt1 =strTxt.GetLength()+1; // 加上字符串后面的结束位
int nTotLen=nLenCap1+nLenTxt1+24;
// 重新计算MessageBox函数的地址
dwAddress=dwMessageBoxAadaddress-(dwProgRAV+dwNewEntryAddress+nTot
Len-5);
strAddress1=StrOfDWord(dwAddress);
// 计算返回地址
dwAddress=0-(dwNewEntryAddress-dwOldEntryAddress+nTotLen);
strAddress2=StrOfDWord(dwAddress);
// 对话框头代码(固定)
unsigned char
cHeader[2]={0x6a,0x40};
// 标题定义
unsigned char
cDesCap[5]={0xe8,nLenCap1,0x00,0x00,0x00};
// 内容定义
unsigned char
cDesTxt[5]={0xe8,nLenTxt1,0x00,0x00,0x00};
// 对话框后部分的代码段
unsigned char cFix[12]
={0x6a,0x00,0xe8,0x00,0x00,0x00,0x00,0xe9,0x00,0x00,0x00,0x00};
// 修改对话框后部分的代码段
for(int i=0;i<4;i++)
cFix[3+i]=strAddress1.GetAt(i);
for(i=0;i<4;i++)
cFix[8+i]=strAddress2.GetAt(i);
char* cMessageBox=new
char[nTotLen];
char* cMsg;
// 生成对话框命令字符串
memcpy((cMsg =
cMessageBox),(char*)cHeader,2);
memcpy((cMsg += 2),cDesCap,5);
memcpy((cMsg +=
5),strCap,nLenCap1);
memcpy((cMsg +=
nLenCap1),cDesTxt,5);
memcpy((cMsg +=
5),strTxt,nLenTxt1);
memcpy((cMsg +=
nLenTxt1),cFix,12);
// 向应用程序写入对话框代码
CString strErrMsg;
long retf;
retf=_lseek(ret,(long)dwEntryWrite,SEEK_SET);
if(retf==-1)
{
delete[] cMessageBox;
AfxMessageBox("Error seek.");
return FALSE;
}
retf=_write(ret,cMessageBox,nTotLen);
if(retf==-1)
{
delete[] cMessageBox;
strErrMsg.Format("Error write:
%d",GetLastError());
AfxMessageBox(strErrMsg);
return FALSE;
}
delete[] cMessageBox;
return TRUE;
}
l
下面的WriteFile()函数是总的写入函数。在这个函数中,先打开被修改的PE文件,然后调用WriteNewEntry()和WriteMessageBox()函数。WriteFile()函数的源代码如下:
l
void CPe::WriteFile(CString
strFileName)
{
CString
strAddress1,strAddress2;
int ret;
unsigned char waddress[4]={0};
ret=_open(strFileName,_O_RDWR |
_O_CREAT | _O_BINARY,_S_IREAD | _S_
IWRITE);
if(!ret)
{
AfxMessageBox("Error
open");
return;
}
// 把新的入口地址写入文件,程序的入口地址在偏移PE文件头开始第40位
if(!WriteNewEntry(ret,(long)(dwPeAddress+40),dwNewEntryAddress))
return;
// 把对话框代码写入到应用程序中
if(!WriteMessageBox(ret,(long)dwEntryWrite,"Test","We are the world!"))
return;
_close(ret);
}
l
仅仅利用以上CPe类还是不能对一个PE文件进行注入MessageBoxA()代码的修改,还必须要一个“载体程序”。例如:
l
// Pefile.cpp:修改PE文件实例
//
#include "stdafx.h"
#include "Pe.h"
void main()
{
CopyFile("..\\calc.exe","..\\calc_shell.exe",FALSE);
CPe a;
a.ModifyPe("..\\calc_shell.exe","We are the world!");
}
l
这个修改后的PE文件运行时,就会先显示对话框,单击“确定”按钮后又继续执行。总之,在了解了PE文件格式后,就可以对某一个PE文件进行修改。本实例只是对PE文件处理的一种应用,在实际中还有更多的其他方面的应用。
|
2.7 本章小结
( 本章字数:344 更新时间:2007-4-26
19:08:26)
|
本章首先介绍了PE文件的基本结构,对一些容易混淆的名词进行了解释。通过介绍一个对PE文件注入对话框代码的实例,加强了对PE文件结构的认识。
本章所介绍的向PE文件注入代码的实例只是用来说明如何修改PE文件,有关如何向一个应用程序中注入代码的技术还要在以后的章节专门介绍。此外,还有其他的技术没有介绍,例如如何提取程序中的代码,在以后的章节中对此也还要专门介绍。总之,了解了PE文件结构,就可以很容易地对某个应用程序进行加壳、挂钩或捆绑。
|
参考文献
( 本章字数:459 更新时间:2007-4-26
19:08:26)
|
[1]
看雪学院编著. 软件加密技术内幕. 电子工业出版社,2004
[2]
Prince. PE文件之旅. www.pediy.com,2005
[3]
Ilsy. 关于PE可执行文件的修改.
www.xfocus.net,2001
|
1.1 引言
( 本章字数:892 更新时间:2007-4-26
19:08:26)
|
要成为一个程序编写高手,不仅需要熟悉各种计算机语言,而且还需要懂得计算机是如何工作的。虽然不必对计算机的各个部件了解得十分清楚,但至少需要懂得计算机操作系统对内存是如何管理的。只有这样,才能编写出计算机内核级的程序来。本书的以后章节会涉及到内存管理的一些操作函数,因此,有必要先介绍这方面的内容。其实,有关计算机内存管理的书籍已经很多了,为了加深读者对这些内存管理函数使用方法的进一步认识,这里重复谈论此话题。
计算机是由各种电子器件组成的,其核心部分是中央处理器,它的英文名字叫CPU,也常被称为微处理器。微处理器有各种各样的型号,如80386、80486、80586(Pentium,
这个词是“第五代”的意思)和80686(Pentium 2)等。通常可以通过向CPU送出指令对计算机内存进行访问。
在计算机硬件发展的同时,PC计算机的操作系统也在不断更新。20世纪90年代以前,个人电脑的操作系统是DOS,此后Windows操作系统逐步成为主导。DOS和Windows操作系统对计算机内存管理的方式不同,前者主要采用实模式管理,而后者主要采用保护模式管理。本章首先介绍保护模式下的分页机制,然后介绍如何进行内存访问的操作。
|
内存管理是操作系统最重要的一部分,它决定了操作系统的性能。为了说明如何进行内存访问的操作,有必要先介绍有关内存管理的一些术语及背景。
1.2.1 虚拟内存
所谓虚拟内存就是用硬盘空间来弥补计算机物理内存不足的技术。Windows操作系统用虚拟内存来动态管理运行时的交换文件。为了提供比实际物理内存还多的内存容量,Windows操作系统占用了硬盘上的一部分空间作为虚拟内存。当CPU有要求时,首先会读取内存中的资料。当内存容量不够用时,Windows就会将需要暂时存储的数据写入硬盘。所以,计算机的内存大小等于实际物理内存容量加上“分页文件”(就是交换文件)的大小。Windows
98中分页文件名采用Win386.swp形式,而Windows
2K/XP/2003中采用pagefile.sys,默认位于系统分区的根目录下,具有隐藏属性。如果需要的话,“分页文件”会动用硬盘上所有可以使用的空间。
安装好Windows以后,系统采用默认的设置自动处理虚拟内存,为了优化系统的
工作性能,根据Windows操作系统中虚拟内存的设置方法,可以自己动手设置内存管理参数。
1.2.2 CPU工作模式
计算机系统有不同的工作模式,在不同的模式下,CPU的寻址方式是不一样的,通常见到的CPU工作模式如下所述。
1.实模式
实模式是为了Pentium处理器与8086/8088兼容而设置的。8086和8088只能工作于实模式,而80286及以上的处理器可工作于实模式或者保护模式下。实模式操作方式只允许微处理器寻址第一个1MB的存储空间,从0x00000~0xFFFFF。在实模式下的存储器寻址是段地址+偏移地址。例如段寄存器的内容是0x1000,则它寻址开始于0x10000的段,偏移量大小从0x0000~0xFFFF,即偏移量的空间大小是216=64KB。
2.保护地址模式
保护地址模式又称为虚拟地址存储管理方式。保护模式下主要有两种特征。
(1)内存分段管理
在保护模式下,各个16位的段寄存器里面放置的是选择符。各项任务共享的内存空间由全局选择符来索引;而某个任务独立使用的内存空间由局部选择符来索引。由选择符的高13位作为偏移量,再以CPU内部事先初始化好的GDTR(全局描述符表寄存器)中的32位基地址为基,可以获得相应的描述符。由描述符中的线性地址决定段的基地址。再利用指令(或其他方式)给出的偏移量,便可以得到线性地址,即
线性地址=段线性基地址+偏移量
保护模式采用上面介绍的分段管理,可以实现的存储器寻址范围为4GB,通常把通过段变换获得的地址称为线性地址。这种线性地址是同32位物理地址对应的,为了获得更大的寻址范围,还可以对线性地址实行分页管理。在保护模式下,处理器通过CRO控制寄存器的PG(page)位进行管理,当PG=0时,由段变换获得的线性地址可直接作为物理地址使用;若PG=1,则进一步进行页变换。
(2)内存分页管理
分页管理的基本思想是将内存分为大小固定为4KB或者1MB的若干页,通过一定机制对内存进行管理。与前面的分段管理类似,程序或数据将根据其长度分配若干页。为了进行页面管理,在分页管理机制中采用了页表、页目录对线性地址作页变换。
1.2.3 逻辑、线性和物理地址
在保护地址模式下,经常遇到三种地址:逻辑地址(Logical
Address)、线性地址(Linear Address)和物理地址(Physical
Address)。CPU通过分段机制将逻辑地址转换为线性地址,再通过分页机制将线性地址转换为物理地址。
(1)逻辑地址
这是内存地址的精确描述,通常表示为十六进制:xxxx:YYYYYYYY,这里xxxx为selector(选择器),而YYYYYYYY是针对selector所选择的段地址的线性偏移量。除了指定xxxx的具体数值外,还可使用具体的段寄存器的名字来替代,如CS(代码段),DS(数据段),ES(扩展段),FS(附加数据段#1),GS(附加数据段#2)和SS(堆栈段)。这些符号都来自旧的“段:偏移量”风格,在
8086 实模式下使用此种方式来指定“far pointers”(远指针)。
(2)线性地址
线性地址是逻辑地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
如果启用了分页机制,那么线性地址可以再经变换以产生一个物理地址。若没有启用分页机制,那么线性地址直接就是物理地址。不过,在开启分页功能之后,一个线性地址可能没有相对映的物理地址,因为它所对应的内存可能被交换到硬盘中。32位线性地址可用于定位4GB存储单元。
(3)物理地址
所谓物理地址,就是指系统内存的真正地址。对于32
位的操作系统,它的范围为0x00000000~0xFFFFFFFF,共有4GB。只有当CPU工作于分页模式时,此种类型的地址才会变得非常“有趣”。本质上,一个物理地址是CPU插脚上可测量的电压。操作系统通过设立页表将线性地址映射为物理地址。Windows
2K/XP所用页表布局的某些属性对于调试软件开发人员非常有用。
1.2.4 存储器分页管理机制
程序代码和数据必须驻留在内存中才能得以运行,然而系统内存量很有限,往往不能容纳一个完整程序的所有代码和数据,特别是在多任务系统中,如Windows,可能需要同时打开多个执行程序,如画图程序,浏览器等,想让内存驻留所有这些程序显然不大可能,因此首先能想到的就是将程序分割成小部分,只让当前系统运行它所有需要的那部分留在内存,其他部分都留在硬盘(虚拟内存)。当系统处理完当前任务片段后,再从外存中调入下一个待运行的任务片段。于是,存储器分页管理机制随之而被发明。
如前所述,在保护模式下,控制寄存器CR0中的最高位PG位控制分页管理机制是否生效。如果PG=1,分页机制生效,把线性地址转换为物理地址。如果PG=0,分页机制无效,线性地址就直接作为物理地址。必须注意,只有在保护方式下分页机制才可能生效。只有在保证使PE位为1的前提下,才能够使PG位为1,否则将引起通用保护
故障。
分页机制把线性地址空间和物理地址空间分别划分为大小相同的块。这样的块称为页。通过在线性地址空间的页与物理地址空间的页之间建立映射,分页机制可以实现线性地址到物理地址的转换。线性地址空间的页与物理地址空间的页之间的映射可根据需要来确定。线性地址空间的任何一页,可以映射为物理地址空间中的任何一页。
1.2.5 线性地址到物理地址的转换
线性地址空间的页到物理地址空间的页之间的映射用表来描述。目前所见到的有4KB和1MB大小的物理分页,对于4KB页面的分页,线性地址到物理地址的转换过程如图1.1所示。对于1MB页面分页,线性地址到物理地址的转换与4KB的基本相似,不同的是线性地址的低22位对应一个物理页面。
对于4KB页面的线性地址到物理地址的转换示意图
对于4KB页面分页,页映射表的第一级称为页目录表,存储在一个物理页中。页目录表共有1024个页目录项(PDE,page
directory
entry),其中,每个PDE为4字节长,包含对应第二级表所在物理地址空间页的页码。页映射表的第二级称为页表,每张页表也被存储在一个物理页中。每张页表有1024个页表项(PTE,page
table entry),每个PTE为4字节长,其中PTE的低12位用来存放诸如“页是否存在于内存”或“页的权限”等信息。
一个线性地址大小为4个字节(32bit),包含着找到物理地址的信息,分为3个部分:第22位到第31位这10位(最高10位)是页目录中的索引,第12位到第21位这10位是页表中的索引,第0位到第11位这12位(低12位)是页内偏移。在把一个线性地址转换成物理地址时,CPU首先根据CR3中的值,找到页目录所在的物理页。然后根据线性地址的第22位到第31位这10位(最高的10bit)的值作为索引,找到相应的PDE,其中含有这个虚拟地址所对应页表的物理地址。有了页表的物理地址,再把虚拟地址的第12位到第21位这10位的值作为索引,找到该页表中相应的PTE,其中就有这个虚拟地址所对应物理页的物理地址。最后用线性地址的最低12位,也就是页内偏移,加上这个物理页的物理地址,就得到了该线性地址所对应的物理地址。
|
每个进程都拥有自己的虚拟地址空间,那么怎样才能访问这个空间呢?这就需要用到Windows
API函数。这些函数直接与编写程序相关,因而更受软件工程师的关注。有关这方面的函数较多,这里介绍几个重要的函数。 1.3.1
获取系统信息在一个程序中不能直接应用某个系统的设备参数,否则将不利于程序的移植。因此,如果确实需要用到这样的设备参数,则需要一个系统信息函数来获得。VC++
编译器所提供这样的函数为GetSystemInfo()。该函数需要一个指向SYSTEM_INFO结构的指针作为参数。其原型表示为: l void
GetSystemInfo(LPSYSTEM_INFO lpSystemInfo); l
其中lpSystemInfo返回LPSYSTEM_INFO结构的地址,用于装载适当的系统信息,这个结构体定义为: l typedef struct
_SYSTEM_INFO { union { DWORD dwOemId; struct { WORD wProcessorArchitecture; WORD
wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID
lpMaximumApplicationAddress; DWORD_PTR dwActiveProcessorMask; DWORD
dwNumberOfProcessors; DWORD dwProcessorType; DWORD dwAllocationGranularity; WORD
wProcessorLevel; WORD wProcessorRevision; } SYSTEM_INFO; l 其中参数含义如下所述。
dwOemId:是一个过时选项,用于与Windows NT 3.5以及以前的版本兼容。
wProcessorArchitecture:指明处理的结构,如Intel、Alpha、Intel 64位或Alpha 64位。
dwPageSize:用于显示CPU的页面大小。在x86 CPU上,这个值是4096字节。在Alpha
CPU上,这个值是8192字节。在IA-64上,这个值是8192字节。
lpMinimumApplicationAddress:用于给出每个进程可用地址空间的最小内存地址。在Windows
98上,这个值是0x400000,因为每个进程的地址空间中下面的4MB是不能使用的。在Windows
2K/XP上,这个值是0x10000,因为每个进程的地址空间中开头的64KB总是空闲的。
lpMaximumApplicationAddress:用于给出每个进程可用地址空间的最大内存地址。在Windows
98上,这个地址是0x7FFFFFFF,因为共享内存映射文件区域和共享操作系统代码包含在上面的2GB分区中。在Windows
XP上,这个地址是0x7FFEFFFF。 dwActiveProcessorMask:位屏蔽,指明哪个CPU是活动的。
dwNumberOfProcessors:计算机中CPU的数目。 dwProcessorType:处理器类型。
dwAllocationGranularity:保留的地址空间区域的分配粒度。 wProcessorLevel:进一步细分处理器的结构。
wProcessorRevision:用于进一步细分处理器的级别。 wReserved:保留供将来使用。
在以上参数中只有lpMinimumApplicationAddress、lpMaximumApplicationAddress、dwPageSize和dwAllocationGranularity与内存有关。
1.3.2 在应用程序中使用虚拟内存对内存分配可以采用不同的方法,常用的方法有:用C/C++语言的内存分配函数,例如,用malloc() 和
free()、new 和 delete
函数分配和释放堆内存;用Windows传统的全局或者局部内存分配函数,如GlobalAlloc()和GlobalFree();用Win32的堆分配函数,如HeapAlloc()和HeapFree();用Win32的虚拟内存分配函数,如VirtualAlloc()和VirtualFree()。注意,用不同的方法分配内存后,要用相对应的函数来释放所占用的内存。这里只介绍Win32的虚拟内存分配函数。
在进程创建之初并被赋予地址空间时,其虚拟地址空间尚未分配,处于空闲状态。这时地址空间内的内存是不能使用的,必须通过VirtualAlloc()函数来分配其中的各个区域,对其进行保留。VirtualAlloc()函数原型为:
l LPVOID VirtualAlloc( LPVOID lpAddress, DWORD dwSize, DWORD flAllocationType,
DWORD flProtect ); l
该函数用来分配一定范围的虚拟页。参数1指定起始地址;参数2指定分配内存的长度;参数3指定分配方式,取值MEM_COMMINT或者MEM_RESERVE;参数4指定控制访问本次分配的内存的标识,取值为PAGE_READONLY、PAGE_READWRITE或者PAGE_NOACCESS。
分配完成后,即在进程的虚拟地址空间中保留了一个区域,可以对此区域中的内存进行保护权限许可范围内的访问。当不再需要访问此地址空间区域时,应释放此区域,由VirtualFree()负责完成。其函数原型为:
l BOOL VirtualFree( LPVOID lpAddress, DWORD dwSize, DWORD dwFreeType ); l
其中参数含义如下所述。
lpAddress:指向待释放页面区域的指针。如果参数dwFreeType指定了MEM_RELEASE,则lpAddress必须为页面区域保留由VirtualAlloc()所返回的基地址。
dwSize:指定了要释放的地址空间区域的大小,如果参数dwFreeType指定了MEM_RELEASE标志,则将dwSize设置为0,由系统计算在特定内存地址上的待释放区域的大小。
dwFreeType:为所执行的释放操作的类型,其可能的取值为MEM_RELEASE和MEM_DECOMMIT,其中MEM_RELEASE标志指明要释放指定的保留页面区域,MEM_DECOMMIT标志则对指定的占用页面区域进行占用的解除。
如果VirtualFree()执行完成,将回收全部范围的已分配页面,此后如再对这些已释
放页面区域内存进行访问将引发内存访问异常。释放后的页面区域可供系统继续分配 使用。 1.3.3 获取虚存状态 Windows
API函数GlobalMemoryStatus()可用于检索关于当前内存状态的动态信息。在软件的About对话框中,通常用这个函数来获取系统内存的使用情况。其函数原型为:
l void GlobalMemoryStatus(LPMEMORYSTATUS lpmstMemStat); l
其中lpmstMemStat返回MEMORYSTATUS结构的地址,这个结构体的定义为: l typedef struct MEMORYSTATUS{
DWORD dwLength; DWORD dwMemoryLoad; DWORD dwTotalPhys; DWORD dwAvailPhys; DWORD
dwTotalPageFile; DWORD dwAvailPageFile; DWORD dwTotalVirtual; DWORD
dwAvailVirtual; } MEMORYSTATUS ,* LPMEMORYSTATUS; l 其中参数含义如下所述。
dwLength:MEMORYSTATUS结构大小。 dwMemoryLoad:已使用内存所占的百分比。 dwTotalPhys:物理存储器的总字节数。
dwAvailPhys:空闲物理存储器的字节数。 dwTotalPageFile:页文件包含的最大字节数。
dwAvailPageFile:用户模式分区中空闲内存大小。 dwTotalVirtual:用户模式分区大小。
dwAvailVirtual:表示当前进程中还剩下的自由区域的总和。
在调用GlobalMemoryStatus()之前,必须将dwLength成员初始化为用字节表示的结构的大小,即一个MEMORYSTATUS结构的大小。这个初始化操作使得Microsoft能够在新版本Windows系统中将新成员添加到这个结构中,而不会破坏现有的应用程序。当调用GlobalMemoryStatus()时,它将对该结构的其余成员进行初始化并返回。
如果某个应用程序在内存大于4GB的计算机上运行,或者合计交换文件的大小大于4GB,那么可以使用新的GlobalMemoryStatusEx()函数。其函数的原型为:
l BOOL GlobalMemoryStatusEx(MEMORYSTATUSEX &mst); l
其中mst返回MEMORYSTATUSEX结构的填充信息,该结构体与原先的MEMORYSTATUS结构基本相同,差别在于新结构的所有成员的大小都是64位宽,因此它的值可以大于4
GB。 1.3.4
确定虚拟地址空间的状态对内存的管理除了对当前内存的使用状态信息进行获取外,还经常需要获取有关进程的虚拟地址空间的状态信息。例如,如何得到一个进程已提交的页面范围?这就要用到两个
API函数VirtualQuery()或VirtualQueryEx()来进行查询。这两个函数的功能相似,不同就是VirtualQuery()只是查询本进程内存空间信息,而VirtualQueryEx()可以查询指定进程的内存空间信息。VirtualQuery()函数原型如下:
l DWORD VirtualQuery( LPVOID lpAddress, PMEMORY_BASIC_INFORMATION lpBuffer,
DWORD dwLength ); l VirtualQueryEx()函数原型如下: l DWORD VirtualQueryEx( HANDLE
hProcess , LPCVOID lpAddress , PMEMORY_BASIC_INFORMATION lpBuffer , DWORD
dwLength ); l 其中参数含义如下所述。 hProcess:进程的句柄。 lpAddress:想要了解其信息的虚存地址。
lpBuffer:返回MEMORY_ BASIC_INFORMATION结构的地址。 dwLength:返回的字节数。
PWEMORY_BASIC_INFORMATION的定义如下: l typedef struct _MEMORY_BASIC_INFORMATION{
PVOID BaseAddress; PVOID AllocationBase; DWORD AllocationProtect; DWORD
RegionSize; DWORD State; DWORD Protect; DWORD Type; } MEMORY_BASIC_INFORMATION,
* PMEMORY_BASIC_INFORMATION; l 其中参数含义如下所述。 BaseAddress:被查询内存块的基地址。
AllocationBase:用VirtualAlloc()分配该内存时实际分配的基地址。
AllocationProtect:分配该页面时,页面的一些属性,如PAGE_READWRITE、PAGE_EXECUTE等(其他属性可参考 Platform
SDK)。 RegionSize:从BaseAddress开始,具有相同属性的页面的大小。
State:页面的状态,有3种可能值:MEM_COMMIT、MEM_FREE和MEM_ RESERVE,这个参数是最重要的,从中可知指定内存页面的状态。
Protect:页面的属性,它可能的取值与 AllocationProtect 相同。
Type:指明了该内存块的类型,有3种可能值:MEM_IMAGE、MEM_MAPPED和MEM_PRIVATE。 1.3.5
改变内存页面保护属性在进行进程挂钩时,经常要向内存页中写入部分代码,这就需要改变内存页的保护属性。有幸的是Win32提供了两个API函数VirtualProtect()和VirtualProtectEx(),它们可以对改变内存页保护。例如,在使用这两个函数时,可以先按PAGE_READWRITE属性来提交一个页的地址,并且立即将数据填写到该页中,然后再把该页的属性改变为PAGE_READONLY,这样可以有效地保护数据不被该进程中的任何其他线程重写。在调用这两个函数之前最好先了解有关页面的信息,可以通过VirtualQuery()来实现。
VirtualProtect()与VirtualProtectEx()函数的区别在于VirtualProtect()只适用于本进程,而VirtualProtectEx()可以适用于其他进程。VirtualProtect()函数原型如下:
BOOL VirtualProtect( PVOID pvAddress, DWORD dwSize, DWORD flNewProtect, PDWORD
pflOldProtect ); l VirtualProtectEx()函数原型如下: l BOOL VirtualProtectEx( HANDLE
hProcess, PVOID pvAddress, DWORD dwSize, DWORD flNewProtect, PDWORD
pflOldProtect ); l 其中参数的含义如下所述。 hProcess:要修改内存的进程句柄。
pvAddress:指向内存的基地址(它必须位于进程的用户方式分区中)。 dwSize:用于指明想要改变保护属性的字节数。
flNewProtect:代表PAGE_*保护属性标志中的任何一个标志,但PAGE_
WRITECOPY和PAGE_EXECUTE_WRITECOPY这两个标志除外。
pflOldProtect:是DWORD大小的地址,VirtualProtect()和VirtualProtectEx()将用原先与pvAddress位置上的字节相关的保护属性填入该地址。尽管许多应用程序并不需要该信息,但是必须为该参数传递一个有效地址,否则该函数的运行将会失败。
1.3.6
进行一个进程的内存读写前面已经说明了如何获得一个进程的内存属性、如何分配内存和如何改变内存页的保护属性,其最终的目的是要对一个进程中内存内容进行读写。要完成此工作,需要用到两个函数:ReadProcessMemory()
和WriteProcessMemory(),这两个函数非常有用。如果知道了一个进程的句柄和内存地址,就可以用ReadProcessMemory()函数来得到该进程和该地址中的内容,此函数的原型为:
l BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID
lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead ); l
其中hProcess为要读入的进程句柄,lpBaseAddress为读内存的起始地址,lpBuffer为读入数据的地址,nSize为要读入的字节数,lpNumberOfBytesRead为实际读入的字
节数。 同样,如果知道了一个进程的句柄和内存地址,可以用WriteProcessMemory()函数向该进程和该地址中写入新的内容,这个函数的原型为: l
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer,
DWORD nSize, LPDWORD lpNumberOfBytesWritten ); l
其中参数hProcess为要写入的进程句柄,lpBaseAddress为写内存的起始地址,lpBuffer为写入数据的地址,nSize为要写入的字节数,lpNumberOfBytesWritten为实际写入的字节数。
|
文件的内存映射的主要用途有两方面,第一是用来在多个进程之间共享数据,第二是直接用内存映射文件来访问磁盘上的数据文件,无需再进行文件的I/0操作。进程间共享数据有很多种方法,稍后将对这些技术进行介绍。内存映射文件的使用可以分为以下三步:
(1)用CreateFileMapping()创建一个文件映射内核对象; (2)用MapViewOfFile()将文件数据映射到进程的地址空间;
(3)用UnmapViewOfFile()从进程地址空间解除这个映射。 1.4.1
内存映射API函数在进行内存映射文件时,首先要用到的是CreateFileMapping()函数,其原型为: l HANDLE
CreateFileMapping( HANDLE hFile, LPSECURITY_ATTRIBUTES lpFileMappingAttributes,
DWORD flProtect, DWORD dwMaximumSizeHigh, DWORD dwMaximumSizeLow, LPCTSTR lpName
); l 其中参数含义如下所述。
hFile:指定待映射到进程地址空间的文件句柄,例如,可以由CreateFile()函数的返回值获取该句柄。如果需要创建一个与文件无关的内存映射,
可以将它设置成为0xFFFFFFFF(INVALID_HANDLE_VALUE)或者取为–1。
lpFileMappingAttributes:一个指向SECURITY_ATTRIBUTES结构的指针,它指明返回的句柄是否可以被子进程所继承。另外,在SECURITY_ATTRIBUTES结构中,也包括一个安全性描述的子指针。
flProtect:允许指定内存块的访问权限,权限值有PAGE_READONLY、PAGE_
READWRITE和PAGE_WRITECOPY,PAGE_WRITECOPY。 dwMaximumSizeHigh和dwMaximumSizeLow
指定了文件的最大字节数,由于这两个参数共64位,因此所支持的最大文件长度为16EB,几乎可以满足任何大数据量文件处理的要求。
lpName:内存映射对象指定名字,通过调用CreateFileMapping()函数和Open-
FileMapping()函数,其他进程可用这个名字来访问相同的文件映射。
在调用CreateFileMapping()时,可以用GetLastError()来检查其返回的错误信息。如果返回值为ERROR_ALREADY_EXISTS,则表示内存映射对象指定名字已经存在。有关其他返回值的意义见MSDN的详细说明。
一旦某个内存映射对象由CreateFileMapping()创建成功,就可以调用MapView-
OfFile()函数,把文件视图映射到进程地址空间上,这个函数需要使用一个由CreateFileMapping()函数或OpenFileMapping()函数返回的句柄,并允许指定访问模式和映射的字节数,以及文件映射对象中的偏移量。MapViewOfFile()函数的原型为:
l LPVOID MapViewOfFile( HANDLE hFileMappingObject, DWORD dwDesiredAccess, DWORD
dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap ); l
其中参数含义如下所述。 hFileMappingObject:为CreateFileMapping()返回的文件映射对象句柄。
dwDesiredAccess:再次指定了对文件数据的访问方式,而且同样要与CreateFile- Mapping()函数所设置的保护属性相匹配。
dwFileOffsetHigh和dwFileOffsetLow:分别为文件偏移的高32位和低32位。
dwNumberOfBytesToMap:为映射视图的大小。
另外,还可以使用MapViewOfFileEx()函数来实现同样的功能,此函数还允许调用进程为映射视图指定特殊的内存地址,但是如果指定的内存地址空间大小不够,则函数执行失败。MapViewOfFileEx()函数的原型为:
l LPVOID MapViewOfFileEx( HANDLE hFileMappingObject, DWORD dwDesiredAccess,
DWORD dwFileOffsetHigh, DWORD dwFileOffsetLow, DWORD dwNumberOfBytesToMap,
LPVOID lpBaseAddress ); l
其中参数lpBaseAddress指定映射视图的实际内存地址。其他参数与MapViewOfFile()函数中的相同。
在完成对映射到进程地址空间区域的文件处理后,需要通过函数UnmapViewOfFile()完成对文件数据映射的释放,该函数原型为: l BOOL
UnmapViewOfFile(LPCVOID lpBaseAddress); l
其中参数lpBaseAddress为MapViewOfFile()函数的返回值。在使用了MapViewOfFile()函数之后,必须要有对应的UnmapViewOfFile()函数调用,否则在进程终止之前,保留的区域将无法释放。除此之外,在进程终止之前还必须要用CloseHandle()将文件句柄释放,否则将会出现资源泄漏的问题。
|
内存映射文件是在多个进程之间共享一个数据块的重要方法。通过调用API函数CreateFileMapping()可以像对待一个文件那样对待一个内存区。
本节编写了两个程序:一个传输数据(Send.exe),另一个接收数据(Recv.exe)。本例中共享的数据很简单,只是一个字符串。在Send程序的对话框上输入一个字符串,然后单击【发送】命令,把所输入的字符串放入内存文件里。Recv程序中设定一个定时器用于监测内存映射文件中的字符串,当然可以用一个工作线程来完成此操作,只是用Timer时间控制器较方便。当内存映射文件改变时,在时间控制器的作用下,Recv程序对话框上也将更新数据。图1.2显示了两个程序之间的通信。
在进程之间共享数据的Send和Recv应用程序
这两程序是在Visual
C++的MFC下编写,创建它们的步骤如下所述。
(1)创建Send应用程序的步骤
①
用MFC的AppWizard(exe)创建新项目Send
取Project
name 为Send,单击“确定”按钮后进入创建应用程序类型,选择Dialog based类型并按下Finish按钮。
②
增加CSendDlg中的成员变量
通过ClassWizard创建变量m_strSend用于传递字符串,增加成员变量m_hMapObject和m_pszMapView以记录内存映射文件的句柄和映射。
l
// SendDlg.h : 头文件
//
#if
!defined(AFX_SENDDLG_H__INCLUDED_)
#define AFX_SENDDLG_H__INCLUDED_
//////////////////////////////////////////////////////////////////////
// CSendDlg dialog
class CSendDlg : public CDialog
{
// Construction
public:
CSendDlg(CWnd* pParent = NULL); //
standard constructor
~CSendDlg();
// Dialog Data
//{{AFX_DATA(CSendDlg)
enum { IDD = IDD_SEND_DIALOG };
CString m_strSend;
//}}AFX_DATA
// ClassWizard generated virtual
function overrides
//{{AFX_VIRTUAL(CSendDlg)
protected:
virtual void
DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
HANDLE m_hMapObject;
LPSTR m_pszMapView;
// Generated message map
functions
//{{AFX_MSG(CSendDlg)
virtual BOOL OnInitDialog();
afx_msg void OnSend();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif //
!defined(AFX_SENDDLG_H__INCLUDED_)
③
编辑CSendDlg::OnInitDialog()
编辑OnInitDialog()以创建该内存映射文件,并将文件的视图映射到进程的地址空间。如果内存映射文件创建成功,通过MapViewOfFile()即得到在内存中起始地址的指针,该指针保存在成员变量m_pszMapView中。以下代码中所创建的映射文件大小为4096
(0x1000)个字节。
l
BOOL CSendDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog.
The framework does this automatically
// when the application~s main
window is not a dialog
SetIcon(m_hIcon, TRUE); //
Set big icon
SetIcon(m_hIcon, FALSE); //
Set small icon
m_hMapObject=CreateFileMapping((HANDLE)0xFFFFFFFF,NULL,
PAGE_READWRITE, 0, 0x1000,
_TEXT("shared_memory"));
if(!m_hMapObject){
AfxMessageBox("Unable to create
shared memory file.");
return FALSE;
}
// Map view of file into our
address space
m_pszMapView=(LPSTR)MapViewOfFile
(m_hMapObject,FILE_MAP_WRITE,
0, 0,0);
if(!m_pszMapView){
AfxMessageBox(_T("Unable to map
shared memory file."));
return FALSE;
}
return TRUE; // return TRUE
unless you set the focus to a control
}
l
④
用MFC ClassWizard 添加CSendDlg::OnSend()函数
当在输入框里输入了一个字符串,例如“Share
Memory
File”,单击【发送】按钮后,将m_strSend字符串赋值给m_pszMapView,以便能够让Recv.exe程序调用。将如下的代码添加到OnSend()函数中。
l
void CSendDlg::OnSend()
{
UpdateData();
strcpy(m_pszMapView,m_strSend);
}
l
(2)创建Recv应用程序的步骤
①
用MFC的AppWizard(exe)创建新项目Recv
取Project
name 为Recv,单击“确定”按钮后进入创建应用程序类型,选择Dialog based类型并单击Finish按钮。
②
修改CRecvDlg类文件头
通过ClassWizard创建变量m_strText用于显示接收到的字符串,与Send.exe一样,增加成员变量m_hMapObject和m_pszMapView以记录内存映射文件的句柄和映射,并且增加析构函数
~CRecvDlg(),用于释放内存映射文件的句柄和文件映射。增加时间控制器函数OnTimer(),它相当于一个服务程序。
l
// RecvDlg.h : 头文件
//
#if !defined(AFX_RECVDLG_H__INCLUDED_)
#define AFX_RECVDLG_H__INCLUDED_
//////////////////////////////////////////////////////////////////////
// CRecvDlg dialog
class CRecvDlg : public CDialog
{
// Construction
public:
CRecvDlg(CWnd* pParent = NULL); // standard constructor
~CRecvDlg();
// Dialog Data
//{{AFX_DATA(CRecvDlg)
enum { IDD = IDD_RECV_DIALOG };
CString m_strText;
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CRecvDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
HANDLE m_hMapObject;
LPSTR m_pszMapView;
// Generated message map functions
//{{AFX_MSG(CRecvDlg)
virtual BOOL OnInitDialog();
afx_msg void OnTimer(UINT nIDEvent);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif // !defined(AFX_RECVDLG_H__INCLUDED_)
l
③
增加析构函数CRecvDlg::~CRecvDlg()
在析构函数中释放内存映射文件的句柄和映射。在程序关闭时,把所使用内存交还给系统,以保证系统有充分的内存资源。
l
CRecvDlg::~CRecvDlg()
{
// Unmap view of file
if (!UnmapViewOfFile(m_pszMapView)){
AfxMessageBox("Unable to unmap view of file.");
}
// Close the shared memory file
CloseHandle(m_hMapObject);
}
l
④
编辑CRecvDlg::OnInitDialog()
在此函数中,首先使用名字“shared_memory”打开内存映射文件。如果Open-
FileMapping()成功,即得到内存映射文件的第一个字节的指针。然后,用SetTimer()函数设定时间控制器。
l
BOOL CRecvDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog. The framework does this
automatically
// when the application~s main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
// Open memory mapped file.
m_hMapObject=OpenFileMapping(FILE_MAP_READ,FALSE,_TEXT("shared_mem
ory"));
if(!m_hMapObject){
AfxMessageBox("Can~t open shared memory file.");
return FALSE;
}
// Get pointer to shared data.
m_pszMapView=(LPSTR)MapViewOfFile
(m_hMapObject,FILE_MAP_READ, 0, 0,0);
if(!m_pszMapView){
AfxMessageBox("Can~t map view of shared memory file.");
return FALSE;
}
SetTimer(0x10,900,NULL);
return TRUE; // return TRUE unless you set the focus to a
control.
}
l
⑤
编辑CRecvDlg::OnTimer()
先用ClassWizard添加这个时间控制器函数,然后将如下的代码添加到OnTimer()函数中。这个函数只有在调用SetTimer()函数后才起作用。
l
void CRecvDlg::OnTimer(UINT nIDEvent)
{
m_strText=m_pszMapView;
UpdateData(FALSE);
CDialog::OnTimer(nIDEvent);
}
1.4.3 用内存映射文件读取大型文件
通常情况下,用文件读写函数对文件进行处理,如Win32
API的CreateFile()、WriteFile()、ReadFile()等。一般来说,以上这些函数可以满足大多数场合的要求,但是对于某些特殊应用领域需要几十GB、几百GB的海量存储,用通常的处理方法进行文件处理显然是行不通的。目前,对于这种大文件的操作一般是以内存映射的方式来加以处理的。下面以实例说明如何使用内存映射来处理文件。
(1)创建MemFile应用程序的步骤
在Visual
C++的MFC下编写这个程序的步骤如下所述。
①
用MFC的AppWizard(exe)创建新项目MemFile
取Project
name 为MemFile,单击“确定”按钮后进入创建应用程序类型,选择Dialog based类型并单击Finish按钮。
②
CMemFileDlg类文件头
用ClassWizard添加成员变量m_strText以显示一个文件内容,增加一个成员函数LoadFile()以实现用内存映射处理文件的操作。
l
// MemFileDlg.h: 头文件
//
#if !defined(AFX_MEMFILEDLG_H__INCLUDED_)
#define AFX_MEMFILEDLG_H__INCLUDED_
//////////////////////////////////////////////////////////////////////
// CMemFileDlg dialog
class CMemFileDlg : public CDialog
{
// Construction
public:
CMemFileDlg(CWnd* pParent = NULL); // standard constructor
~CMemFileDlg();
// Dialog Data
//{{AFX_DATA(CMemFileDlg)
enum { IDD = IDD_MEMFILE_DIALOG };
CString m_strText;
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CMemFileDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
BOOL LoadFile(CString strFileName);
// Generated message map functions
//{{AFX_MSG(CMemFileDlg)
virtual BOOL OnInitDialog();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif // !defined(AFX_MEMFILEDLG_H__INCLUDED_)
③
编辑CMemFileDlg::OnInitDialog()
增加LoadFile()函数的调用,再增加UpdateData()函数以便使来自映射文件中的内容得到显示。文件test_memfile.txt为事先准备好的,其内容任意给定。
l
BOOL CMemFileDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog. The framework does this
automatically
// when the application~s main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
LoadFile("c:\\test_memfile.txt");
UpdateData(FALSE);
return TRUE; // return TRUE unless you set the focus to a
control
}
l
④
增加CMemFileDlg::LoadFile()函数
首先用CreateFile()来创建一个读文件的句柄,如果创建失败,函数返回FALSE。接着用CreateFileMapping()创建一个文件映射对象。同样,如果创建失败,函数返回FALSE。然后用MapViewOfFile()获得文件第一个字节的指针。通过该指针可以得到文件中的全部内容。最后使用CloseHandle()关闭文件对象和文件映射对象,并且使用UnmapViewOfFile()来释放文件数据映射。
l
BOOL CMemFileDlg::LoadFile(CString strFileName)
{
HANDLE hFile, hMapping;
void *basepointer;
// Create file object.
if ((hFile = CreateFile(strFileName, GENERIC_READ,
FILE_SHARE_READ, 0, OPEN_EXISTING,
FILE_FLAG_SEQUENTIAL_SCAN, 0))== INVALID_HANDLE_VALUE)
{
AfxMessageBox("Could not open file.");
return FALSE;
}
// Creates a named file mapping object.
if (!(hMapping
= CreateFileMapping(hFile,0,PAGE_READONLY|SEC_COMMIT, 0,0,0)))
{
AfxMessageBox("Mapping failed.");
CloseHandle(hFile);
return FALSE;
}
// Map view of file into baseointer.
if (!(basepointer = MapViewOfFile(hMapping,
FILE_MAP_READ,0,0,0)))
{
AfxMessageBox("View failed.");
CloseHandle(hMapping);
CloseHandle(hFile);
return FALSE;
}
// Close file mapping object.
CloseHandle(hMapping);
// Close file object.
CloseHandle(hFile);
// Get the context of the file.
m_strText=(LPSTR)basepointer;
// Unmap view of file.
UnmapViewOfFile(basepointer);
return TRUE;
}
l
(2)用内存映射文件处理文件的实验
首先用notepad
创建一个文件,其文件名为test_memfile.txt,文件的内容设为“北京是中国的首都”,并把这个文件存放在C:\目录下。运行应用程序MemFile.exe后,则在其窗口中显示“北京是中国的首都”这样的字符串,如图1.3所示。
用内存映射文件处理文件的MemFile应用程序
从上面的实例可以看出,用内存映射文件处理文件时不需要用文件数据的读取函数,如ReadFile()等,这给文件的处理,特别是大文件的处理,带来极大的方便。
|
要学好C语言,就必须要学好指针,而对于初学者来说,很多人对指针的含义很模糊,以至越学越糊涂。这里将从内存的角度分析指针的真正含义。
1.5.1 指针的真正本质
什么是指针?所谓指针其实就是一个变量或一个函数在计算机内存中的地址。也许把指针直接称为“地址”更为确切,更容易被人理解。
在C语言中,任何变量在使用前都必须先要进行定义,否则就会编译出错,这是因为需要给这个变量在内存中申请一个地址空间。有了一个内存地址空间后,这个变量值就可以存放在这个地址上。比如说一间房子,那么这个房子的地址就是“指针”,其名称则是“指针”值,而房子里面的东西就是“变量值”。
对于一个变量,如何才能获得它的地址呢?C语言中提供了一种能够获取变量地址的方法,也就是获得某个变量指针的方法。例如:
l
int x
int *y
x=10;
*y=20;
l
在上面的代码中,变量x和*y在定义时就在内存中申请到一个地址。在C编译时,编译器将给这两个变量各分配一个地址代码;在Windows系统中,它为一个虚地址。用操作&x可以得到变量x的虚地址代码值,而y就是变量*y的虚地址代码值。也就是,&x和y分别是变量x和*y变量的指针。
当这两个变量分别申请到一个内存地址空间后,就可以分别给它们赋值了,即上面代码x=10和
*y=20的语句。
在申请变量地址时,所申请到的变量地址代码值与该变量在程序代码中的相对位置有关。有兴趣的话可以做实验来认识这一点。如果能够获得一个应用程序中某个变量的地址代码值,就可以在另外一个应用程序中通过内存访问操作来改变这个变量值。
1.5.2 用指针进行应用程序之间的通信
以下通过一个实例来说明用指针如何进行应用程序间的通信。这里将给出两个应用程序,其中一个为服务程序,另一个为控制程序。先在服务程序中获得某个变量的指针,然后在控制程序中通过ReadProcessMemory()和WriteProcessMemory()这两个函数来对这个指针地址的内容进行读和写。当然这个指针值也可以通过一个DEBUG工具,如
Ollydbg等软件来获得。
两个应用程序之间用指针进行通信的方法如图1.4所示。
对话框TestServer由CTestServerDlg类建立,它为一个服务程序,而对话框TestCtrl由CTestCtrlDlg类建立,它是一个控制程序。
用指针进行通信的TestServer和TestCtrl应用程序
在实际操作时,先在CTestServerDlg对象中获得m_dwValue的指针(如本实例0x12fee8),并且给m_dwValue赋值。在CTestServerDlg类中,使用Timer时间控制器更新窗口界面。
(1)创建TestServer应用程序
同样用MFC
AppWizard来创建应用程序TestServer,其窗口类型为对话框。在CTestServerDlg类的头文件中,字符串m_strPointer用来存放变量m_dwValue的指针,即内存地址值。当m_dwValue的值发生改变时,用OnTimer函数来更新它在TestServer对话框上的显示。在应用程序TestServer编译后,m_strValue的地址是固定的。CTestServerDlg类的头文件代码如下:
l
// TestServerDlg.h : 头文件
//
#if
!defined(AFX_TESTSERVERDLG_H__INCLUDED_)
#define
AFX_TESTSERVERDLG_H__INCLUDED_
//////////////////////////////////////////////////////////////////////
// CTestServerDlg dialog
class CTestServerDlg : public
CDialog
{
// Construction
public:
CTestServerDlg(CWnd* pParent =
NULL); // standard constructor
// Dialog Data
//{{AFX_DATA(CTestServerDlg)
enum { IDD = IDD_TESTSERVER_DIALOG
};
CString m_strPointer;
CString m_strValue;
//}}AFX_DATA
// ClassWizard generated virtual
function overrides
//{{AFX_VIRTUAL(CTestServerDlg)
protected:
virtual void
DoDataExchange(CDataExchange* pDX); // DDX/DDV support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
DWORD m_dwValue;
// Generated message map
functions
//{{AFX_MSG(CTestServerDlg)
virtual BOOL OnInitDialog();
afx_msg void OnTimer(UINT
nIDEvent);
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif // !defined(AFX_TESTSERVERDLG_H__INCLUDED_)
l
在下面的CTestServerDlg::OnInitDialog函数中,调用SetTimer()以使OnTimer()起作用,其中给变量m_dwValue赋一个初值;在CTestServerDlg::OnTimer()中,先计算变量m_dwValue的指针,并把m_dwValue类型变成字符串m_strValue。最后,调用UpdateData()使字符串能在窗口界面上显示。这些成员函数的源代码如下:
l
BOOL CTestServerDlg::OnInitDialog()
{
CDialog::OnInitDialog();
// Set the icon for this dialog. The framework does this
automatically
// when the application~s main window is not a dialog
SetIcon(m_hIcon, TRUE); // Set big icon
SetIcon(m_hIcon, FALSE); // Set small icon
SetTimer(1,900,NULL);
m_dwValue=20060901;
return TRUE; // return TRUE unless you set the focus to a
control.
}
void CTestServerDlg::OnTimer(UINT nIDEvent)
{
m_strPointer.Format("0x%x",&m_dwValue);
m_strValue.Format("%d",m_dwValue);
UpdateData(FALSE);
CDialog::OnTimer(nIDEvent);
}
l
(2)创建TestCtrl应用程序
同样,程序TestCtrl用MFC
AppWizard
来创建,其窗口类型也为对话框。要对TestServer程序地址0x12fee8上的内容进行读或取,TestCtrl程序需要获得TestServer程序的进程句柄。这就要求TestServer程序在TestCtrl程序之前先运行起来,否则寻找TestServer程序进程操作将会失败。在CTestCtrlDlg类头文件中增加了读和写字符串,相应地定义了读和写函数。CTestCtrlDlg类头文件的代码如下:
l
// TestCtrlDlg.h: 头文件
//
#if
!defined(AFX_TESTCTRLDLG_H__INCLUDED_)
#define
AFX_TESTCTRLDLG_H__INCLUDED_
//////////////////////////////////////////////////////////////////////
// CTestCtrlDlg dialog
class CTestCtrlDlg : public CDialog
{
// Construction
public:
CTestCtrlDlg(CWnd* pParent = NULL); // standard constructor
// Dialog Data
//{{AFX_DATA(CTestCtrlDlg)
enum { IDD = IDD_TESTCTRL_DIALOG };
CString m_strRead;
CString m_strWrite;
//}}AFX_DATA
// ClassWizard generated virtual function overrides
//{{AFX_VIRTUAL(CTestCtrlDlg)
protected:
virtual void DoDataExchange(CDataExchange* pDX); // DDX/DDV
support
//}}AFX_VIRTUAL
// Implementation
protected:
HICON m_hIcon;
// Generated message map functions
//{{AFX_MSG(CTestCtrlDlg)
virtual BOOL OnInitDialog();
afx_msg void OnRead();
afx_msg void OnWrite();
//}}AFX_MSG
DECLARE_MESSAGE_MAP()
};
#endif // !defined(AFX_TESTCTRLDLG_H__INCLUDED_)
l
在下面函数中,用FindWindow()函数查找到TestServer窗口后,得到其进程的句柄,然后用ReadProcessMemory()或WriteProcessMemory()函数来读取或改变TestServer程序地址0x12fee8上的内容。
l
void CTestCtrlDlg::OnRead()
{
DWORD pid;
HWND hWnd = ::FindWindow
(NULL,TEXT("TestServer"));
if(!hWnd)
{
AfxMessageBox("没有找到TestServer进程.");
return;
}
::GetWindowThreadProcessId(hWnd,
&pid );
HANDLE hProcess = ::OpenProcess
(PROCESS_ALL_ACCESS,FALSE,pid);
LPVOID
lpBaseAddress=(LPVOID)0x12fee8;
DWORD dwValue;
if(!::ReadProcessMemory(hProcess
,
lpBaseAddress,(void*)&dwValue,sizeof(DWORD),0)) return;
m_strRead.Format("%d",dwValue);
UpdateData(FALSE);
}
void CTestCtrlDlg::OnWrite()
{
DWORD pid;
HWND hWnd = ::FindWindow
(NULL,TEXT("TestServer"));
if(!hWnd)
{
AfxMessageBox("没有找到TestServer进程.");
return;
}
::GetWindowThreadProcessId(hWnd,
&pid );
HANDLE hProcess = ::OpenProcess
(PROCESS_ALL_ACCESS,FALSE,pid);
LPVOID
lpBaseAddress=(LPVOID)0x12fee8;
UpdateData();
DWORD dwValue=atoi(m_strWrite);
if(!::WriteProcessMemory(hProcess
,
lpBaseAddress,(void*)&dwValue,sizeof(DWORD),0)) return;
}
|
1.6 本章小结
( 本章字数:482 更新时间:2007-4-26
19:08:26)
|
本章介绍了内存管理的一些函数及其应用,介绍了什么是虚拟内存,并说明了逻辑地址、线性地址和物理地址之间的区别。还介绍了存储器分页管理机制,说明了如何实现线性地址到物理地址的转换。
本章介绍了一些虚拟内存访问函数,用这些函数可以获得有关内存管理的一些信息,同时可以用这些函数来改变内存地址中的内容。文件的内存映射可以用来访问磁盘上的数据文件,还可以用来在多个进程之间共享数据。本章最后深入分析了C语言指针的真正含义,会对指针有较深刻的认识。
|
参考文献
( 本章字数:640 更新时间:2007-4-26
19:08:26)
|
[1]
潘峰. 微型计算机原理汇编语言. 北京:电子工业出版社,1997
[2]
毛德操、胡希明著. LINUX内核源代码情景分析(上、下册). 浙江:浙江大学出版社,2001
[3]
Scott Stanfield,Ralph Arvesen著. Visual C++ 4 开发人员指南. 华译工作室译.
北京:机械工业出版社,西蒙与舒斯特国际出版公司,1997
|
|