《VC调试基础知识.ppt》由会员分享,可在线阅读,更多相关《VC调试基础知识.ppt(63页珍藏版)》请在taowenge.com淘文阁网|工程机械CAD图纸|机械工程制图|CAD装配图下载|SolidWorks_CaTia_CAD_UG_PROE_设计图分享下载上搜索。
1、VC调试基础知识目录复制构造函数和赋值操作符new,new和delete,delete解析堆栈初步认识内存管理基础(物理内存,虚拟内存,堆的关系)内存异常定位的不确定性实时调试之看懂内存实时调试之内存异常定位华生医生日志查看VC调试环境介绍转储文件(*.dmp)调试诡异现象之谜复制构造函数和赋值操作符现有网管代码中,有不少类或结构定义了复制构造函数和赋值操作符。不适当的自定义的弊端1.可维护性差,易出错,特别是当改变了成员结构,却忘记更新代码2.性能损失3.制造垃圾代码。我们是否需要自定义复制行为?复制构造函数和赋值操作符对象默认的复制行为1.简单类型,直接复制。2.对象类型,调用赋值操作符,
2、若无,采用默认复制。3.若编译器判断该对象内存结构可直接整块复制,将优化为二进制copy。类似memcpy().须自定义的场景1.存在指针,且指针是对象自身管理,对象间不共享。2.复制时要做其他特殊操作。复制构造函数和赋值操作符须自定义复制构造和赋值的场景示例:class Apublic:A():m_pBuffer(NULL)A()if(m_pBuffer)delete m_pBuffer;m_pBuffer=NULL;private:char*m_pBuffer;A a1;A a2=a1;new,new和delete,delete解析从几行代码开始:1)char*p=new charn;de
3、lete p;/or delete p;2)void*p=NULL;if(xxx)new An;/class Aelse new Bn;/class Bdelete p;/ordelete p;以上两段代码,两种delete方式,有何区别,哪种正确?你认为不正确的用法会引起什么问题?或者,两种都有问题?new,new和delete,delete解析对于delete对象指针问题,教科书一般这样说,delete将指针当单个对象来删,而delete将指针当成对象数组来删(大意)。所以,我们得出结论,删除对象数组时若不加,将会导致仅删除数组第一个元素,从而导致内存泄露。对吗?new,new和delet
4、e,delete解析为什么要设计两种delete语法?根本原因在于,C+规定删除一个对象时,必须自动调用析构函数(若有),这个责任给了编译器,而非程序员。所以删除一个对象数组,编译器必须保证调用数组每个元素的析构函数。但一个对象指针是不是数组,编译器无法知晓,必须借助于不同的语法来区分。可以看出,两种语法本质是为了“析构函数”,而不是释放内存。遗漏并不必然导致内存泄露。new,new和delete,delete解析编译器如何识别数组个数?无标准规定,编译器自行实现。VC编译器的实现:在数组前部添加一个整型长度(4字节),存放数组长度。即,我们在分配对象数组时,返回的地址其实是向后偏移了4字节的
5、。用delete时,编译器会以头部的整型为数组长度,循环调用每个元素的析构函数,然后将指针前移4字节,调用free()释放数组。由此大家应该可以看出问题,若遗漏了,某些情况下不是内存泄露那么简单,将引起内存定位错误,程序崩溃。但事情并非绝对new,new和delete,delete解析size对象对象对象p:返回指针。对象数组内存结构new,new和delete,delete解析既然两种delete只是为了析构函数,当一个类没有析构函数时,还要使用上述的数组处理方式吗。没错,编译器是聪明的,它不会做不必要的无用功。当对象不存在析构函数时,delete,delete都等价于free(),事实上编
6、译时就是直接转化为free()了。是否存在析构函数,并不是完全由程序员决定的。同时满足:1)程序员没有显式定义析构函数2)类中成员若有对象类型,这些成员也不存在析构函数。满足这两个条件,编译器将不为此类生成析构函数,delete时也不经过析构函数这一步骤。否则,即使未显式定义,编译器也会自动生成一个。顺便提一下,构造函数也类似,若编译器判断不需要时,将不生成构造函数。new等同于malloc.new,new和delete,delete解析回头看第一页的代码,结论是不是清楚了?堆栈初步认识进程初始化时创建,大小固定默认1M,但可以用编译选项修改自底向上增长线程具有独立的堆栈,默认1M,也可以在创
7、建线程时指定堆栈作用:分配局部变量,保存现场,传递参数堆栈初步认识猜猜看,以下代码执行结果是什么?void main()int i;int ar10;for(i=0;i Stack Back Trace-*ChildEBP RetAddr Args to Child 0d8ff7f0 0c6f6e8a 00000016 0cd6ba80 0d8ff810 SDHNetworkCross!CArray:ElementAt+0 x320d8ff800 0c6f5f8d 00000016 0d8ff874 0d8ffe4c SDHNetworkCross!CArray:operator+0 x1a0
8、d8ff810 0cace114 00000016 0dea0d5f 0d8fff20 SDHNetworkCross!CDLGChoosePathgroupCols:GetIndex+0 x1d0d8ffe4c 0cacd8a5 08000116 1f003485 00000000 SDHNetworkCross!CNotifyMsgMgr:DisplayPathGrid+0 x5d40d8fff2c 0cacb6d4 0d8fff74 0ba5ebd8 cccccccc SDHNetworkCross!CNotifyMsgMgr:ProcessPathSwitch+0 x9b50d8fff
9、58 0cae9da5 0d8fff74 00000000 00000000 SDHNetworkCross!CNotifyMsgMgr:ProcessMsg+0 xe40d8fffb8 77e64829 0b779148 00000000 00000000 SDHNetworkCross!CMsgQueueMgr:ThreadProc+0 x135华生医生日志查看华生医生日志查看以下是演示时间打开一个工程中的日志查看VC调试环境介绍调试工具窗口VC调试环境介绍-准备工作安装源文件。安装时选择安装mfc源代码,crt源代码。这样就可以跟踪进库文件代码中。让调试信息明明白白,不再雾里看花。你是不
10、是经常在堆栈中看到ntdll!7c819020这种让人心碎的东西?其实M$已经提供了更贴心的服务符号服务器,可以提供所有版本系统文件的调试信息(pdb文件),只是我们一般没去用它。VC调试环境介绍-准备工作程序数据库(PDB)应用程序的符号信息存储在pdb文件中,链接器将创建 程序名.PDB,它包含项目的 EXE 文件的调试信息。PDB 文件包含完整的调试信息(包括函数原型)。设置pdb文件路径:打开菜单工具-选项,定位到“调试”VC调试环境介绍-准备工作VC调试环境介绍-准备工作让调试器自动从微软符号服务器上下载匹配的系统pdb文件。方法:设置环境变量_NT_SYMBOL_PATH=SRV*
11、D:Symbols*http:/ _tmain(int argc,_TCHAR*argv)int*p=NULL;memset(p,0,10);VC调试环境介绍-有符号转储文件(*.dmp)调试转储文件生成win2008之前使用华生医生,2008采用了新的内置工具WER,需要手工配置:HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsWindows Error ReportingLocalDumpsHKEY_LOCAL_MACHINESOFTWAREWow6432NodeMicrosoftWindowsWindows Error ReportingLocalDu
12、mps转储文件(*.dmp)调试在菜单文件-打开-项目/解决方案,选择dmp文件。启动调试,将中断在异常处。dmp调试并非真正的运行,只是出错时的一个快照,调试器模拟了当时的现场状况。因此不能像正常运行那样调试,只能做静态的分析。可以查看堆栈,寄存器,线程,模块,局部变量等数据。完全转储时可以看到完整内存,但少量转储只能看到一部分堆栈的内存。若有和dmp相应的pdb和源代码,调试器可以定位到源代码。否则,只能看到汇编代码。源代码路径保存在pdb文件中,若源文件已移动,第一次调试器会弹出界面让重新指定。点取消则不使用源代码。转储文件(*.dmp)调试class Apublic:A()p=NULL
13、;char*p;int _tmain(int argc,_TCHAR*argv)int i=1;A*a=new A;memset(a-p,0,10);printf(%d,i);当少量转储时,可以看到i的值,但看不到a-p的值。完全转储则可以。这段代码少量转储是6K,而完全转储要9.9M.转储文件(*.dmp)调试少量转储:完全转储:转储文件(*.dmp)调试无源代码的情况:eax=aecx=*(a+0)即 a-p转储文件(*.dmp)调试有源代码的情况:诡异现象之谜在pdb文件缺失的情况下,将可能产生一些奇怪的现象。接上一节的源代码。class Apublic:A()p=NULL;char*p
14、;int _tmain(int argc,_TCHAR*argv)int i=1;A*a=new A;memset(a-p,0,10);printf(%d,i);诡异现象之谜单步执行到箭头位置,停在memset()处,此时的变量信息及堆栈如下图:诡异现象之谜执行memset(),程序异常,此时在堆栈中切换到main()函数中,再一次查看变量值:诡异现象之谜前后的变量地址对比:原来堆栈也变了:诡异现象之谜原因分析:在没有pdb信息的时候,调试器可能会无法正确解析出外层ebp的值,而ebp非常重要,局部变量、参数和返回值都是通过ebp+偏移得到的,ebp错了的话,就全错了。下一步:调试器解析得到e
15、bp值,用ebp+4作为地址,得到外层返回地址。用ebp作为地址,定位到外层的堆栈底,而此处保存的也正是更外层的ebp。堆栈中wmain()那一行看起来似乎是对的,代码定位没错,但实践中也可能出现堆栈完全错误的情况。堆栈显示错误不可怕,只是显示而已,等函数返回后会恢复成正确的结果。在调试时,可能遇到这种堆栈,且中断是处在系统库中。极端情况下,可能完全看不到应用程序的痕迹,只有系统库。可以尝可以尝试着跳过错误代码,让调用返回,这样局面有可能试着跳过错误代码,让调用返回,这样局面有可能会豁然开朗会豁然开朗。跳过错误方法:旧ebp返回地址旧ebp返回地址旧ebp返回地址栈顶参考39页的代码:int
16、test()004115A0 55 push ebp 004115A1 8B EC mov ebp,esp 004115A3 81 EC C0 00 00 00 sub esp,0C0h 004115A9 53 push ebx 004115AA 56 push esi 004115AB 57 push edi 004115BE B8 01 00 00 00 mov eax,1 004115C3 5F pop edi 004115C4 5E pop esi 004115C5 5B pop ebx 004115C6 8B E5 mov esp,ebp 004115C8 5D pop ebp 004115C9 C3 ret 转到正确地址后,单步执行返回,如果堆栈层次较深,一直处在系统库中,可能要多返回几层,直到到达我们的程序所在空间中,此时就可以看到正常的堆栈,正常的代码了。诡异现象之谜在这一行上点右键,选择“设置下一语句”,将执行强制跳转到此。