收藏 分享(赏)

VC调试基础知识.ppt

上传人:yjrm16270 文档编号:7437189 上传时间:2019-05-17 格式:PPT 页数:63 大小:716KB
下载 相关 举报
VC调试基础知识.ppt_第1页
第1页 / 共63页
VC调试基础知识.ppt_第2页
第2页 / 共63页
VC调试基础知识.ppt_第3页
第3页 / 共63页
VC调试基础知识.ppt_第4页
第4页 / 共63页
VC调试基础知识.ppt_第5页
第5页 / 共63页
点击查看更多>>
资源描述

1、VC调试基础知识,目录,复制构造函数和赋值操作符 new, new和delete,delete解析 堆栈初步认识 内存管理基础(物理内存,虚拟内存,堆的关系) 内存异常定位的不确定性 实时调试之看懂内存 实时调试之内存异常定位 华生医生日志查看 VC调试环境介绍 转储文件(*.dmp)调试 诡异现象之谜,复制构造函数和赋值操作符,现有网管代码中,有不少类或结构定义了复制构造函数和赋值操作符。 不适当的自定义的弊端 1.可维护性差,易出错,特别是当改变了成员结构,却忘记更新代码 2.性能损失 3.制造垃圾代码。 我们是否需要自定义复制行为?,复制构造函数和赋值操作符,对象默认的复制行为 1.简单

2、类型,直接复制。 2.对象类型,调用赋值操作符,若无,采用默认复制。 3.若编译器判断该对象内存结构可直接整块复制,将优化为二进制copy。类似memcpy(). 须自定义的场景 1.存在指针,且指针是对象自身管理,对象间不共享。 2.复制时要做其他特殊操作。,复制构造函数和赋值操作符,须自定义复制构造和赋值的场景示例:class A public: A():m_pBuffer(NULL)A() if (m_pBuffer) delete m_pBuffer; m_pBuffer = NULL; private:char *m_pBuffer; ; A a1; A a2 = a1;,new,

3、new和delete,delete解析,从几行代码开始: 1) char *p = new charn; delete p; /or delete p; 2) void *p = NULL; if (xxx) new An; /class A else new Bn; /class B delete p; /or delete p; 以上两段代码,两种delete方式,有何区别,哪种正确? 你认为不正确的用法会引起什么问题? 或者,两种都有问题?,new, new和delete,delete解析,对于delete对象指针问题,教科书一般这样说,delete将指针当单个对象来删,而delete将

4、指针当成对象数组来删(大意)。 所以,我们得出结论,删除对象数组时若不加 ,将会导致仅删除数组第一个元素,从而导致内存泄露。 对吗?,new, new和delete,delete解析,为什么要设计两种delete语法? 根本原因在于,C+规定删除一个对象时,必须自动调用析构函数(若有),这个责任给了编译器,而非程序员。所以删除一个对象数组,编译器必须保证调用数组每个元素的析构函数。但一个对象指针是不是数组,编译器无法知晓,必须借助于不同的语法来区分。 可以看出,两种语法本质是为了“析构函数”,而不是释放内存。遗漏并不必然导致内存泄露。,new, new和delete,delete解析,编译器如

5、何识别数组个数?无标准规定,编译器自行实现。VC编译器的实现: 在数组前部添加一个整型长度(4字节),存放数组长度。即,我们在分配对象数组时,返回的地址其实是向后偏移了4字节的。 用delete时,编译器会以头部的整型为数组长度,循环调用每个元素的析构函数,然后将指针前移4字节,调用free()释放数组。 由此大家应该可以看出问题,若遗漏了,某些情况下不是内存泄露那么简单,将引起内存定位错误,程序崩溃。 但事情并非绝对,new, new和delete,delete解析,size,对象,对象,对象,p:返回指针,。,对象数组内存结构,new, new和delete,delete解析,既然两种de

6、lete只是为了析构函数,当一个类没有析构函数时,还要使用上述的数组处理方式吗。没错,编译器是聪明的,它不会做不必要的无用功。 当对象不存在析构函数时,delete,delete都等价于free(),事实上编译时就是直接转化为free()了。 是否存在析构函数,并不是完全由程序员决定的。同时满足:1)程序员没有显式定义析构函数2)类中成员若有对象类型,这些成员也不存在析构函数。满足这两个条件,编译器将不为此类生成析构函数,delete时也不经过析构函数这一步骤。否则,即使未显式定义,编译器也会自动生成一个。 顺便提一下,构造函数也类似,若编译器判断不需要时,将不生成构造函数。new等同于mal

7、loc.,new, new和delete,delete解析,回头看第一页的代码,结论是不是清楚了?,堆栈初步认识,进程初始化时创建,大小固定 默认1M,但可以用编译选项修改 自底向上增长 线程具有独立的堆栈,默认1M,也可以在创建线程时指定 堆栈作用:分配局部变量,保存现场,传递参数,堆栈初步认识,猜猜看,以下代码执行结果是什么?void main()int i;int ar10;for (i=0; i=10; i+)ari = 0;printf(“i = %dn”, i); ,堆栈初步认识,堆栈初步认识,上例中,堆栈的布局:,低地址,高地址,堆栈初步认识,*(,低地址,高地址,传说中的缓冲区

8、溢出,黑客的最爱.,内存管理基础,物理内存: 真实的内存RAM 虚拟内存: 1).逻辑内存,具有线性地址,32位cpu下从0到0xFFFFFFFF共4G。和物理内存大小无关。 2).每个进程具有4G逻辑内存空间,缺省高2G为操作系统核心代码所用,进程只能使用低2G的空间。 3).虚拟内存使用页式管理(段页式),即将4G空间按4K大小划分为220个页面,物理内存也同样分页,虚拟内存的分配以及和物理内存的映射,都以页面为单位。即分配虚拟内存的大小必须是页面的整数倍。 4)已映射的虚拟内存页面可能被从物理内存交换到页面文件。活动进程在访问某地址时,系统根据页面状态分配物理页,或从页面文件中加载到物理

9、页,或异常。 5)windows提供了一组虚拟内存管理的API,应用程序可以直接使用虚拟内存。但更常见的是使用堆来分配内存。 6)虚拟内存页具有权限属性,常见的有读/写/执行(R,W,E)的组合,若非法访问了该页,将产生一个硬件异常(保护错误)。 堆: 1)为应用程序提供动态内存分配,堆管理器提供了比虚拟内存更简单的使用接口。 2)堆建立在虚拟内存层之上,对虚拟内存做了封装,处理任意大小的内存分配和释放,避免内存碎片等。C+内存分配函数new/delete/malloc/free调用堆管理器进行内存的分配和释放。,内存管理基础,物理内存,虚拟内存,CPU,OS+CPU,堆管理器,OS,C运行时

10、库,C Runtime,new, malloc,HeapAlloc,VirtualAlloc,应用程序,内存管理基础,启动过程:C Runtime 调用 HeapCreate() HeapCreate调用VirtualAlloc() 分配: new 调用malloc(), malloc调用HeapAlloc() 释放: delete 调用free(), free()调用HeapFree() 结束进程: C Runtime 调用 HeapDestroy(),删除堆. HeapDestroy调用VirtualFree()释放虚拟内存。,内存异常定位之不确定性,内存在堆上分配,而堆是可读写的,即使未

11、调用malloc分配。 指针非法修改了堆上未分配的内存,或已分配内存的边界,将造成堆破坏,但堆管理器无法察觉。 在某内存块被释放或新分配内存时,可以检测出内存块的损坏,但为时已晚。 出现异常时,一般人会试图在当前代码范围寻找线索,但真正的元凶可能和此模块风马牛不相及。,内存异常定位之不确定性,考虑以下代码: void *p = malloc(5); free(p); void *p2 = malloc(10); free(p2); void *p3 = malloc(2); memset(p, 0, 5); /(1) memset(p2, 0, 10); /(2) free(p3); /异常,

12、内存异常定位之不确定性,执行(1):,执行(2):,内存异常定位之不确定性,补充说明: 若上述语句修改内存字节数进一步加大,将彻底破坏堆的边界,导致后续分配内存失败。现象不能预料。 补充说明2: 若内存free后,又被修改,修改字节数少,不足以破坏堆结构的,将导致下次再分配时报错,弹出上页中下图错误信息。但可忽略,分配可成功。,实时调试之看懂内存,先来认识几个魔术数字: 0xCCCCCCCC,0xCDCDCDCD 0xDDDDDDDD,0xFEEEFEEE 0xFDFDFDFD,0xABABABAB,0xCCCCCCCC DEBUE版,静态/局部未初始化 0xCDCDCDCD DEBUG版,动

13、态未初始化 0xDDDDDDDD DEBUG版,标记为删除 0xFEEEFEEE 堆管理器已释放 0xFDFDFDFD DEBUG版,内存块边界 0xABABABAB 堆管理内存块尾部,实时调试之看懂内存,实时调试之看懂内存,malloc分配的内存块结构: _CrtMemBlockHeader - 可用数据块-0xFDFDFDFD - 0xABABABAB - 0xABABABAB 即,从malloc返回的地址,往前是一个32字节的结构体,往后是4字节CRuntime设置的边界标识,再是8字节堆管理器设置的边界标识。,实时调试之看懂内存,typedef struct _CrtMemBlockH

14、eader struct _CrtMemBlockHeader * pBlockHeaderNext;struct _CrtMemBlockHeader * pBlockHeaderPrev;char * szFileName;int nLine;size_t nDataSize;int nBlockUse;long lRequest;unsigned char gap4; / = 0xfdfdfdfd _CrtMemBlockHeader;,实时调试之看懂内存,分配一个5字节的内存块,其内存结构如图:,实时调试之内存异常定位,前面讲过,内存异常定位的困难。很多时候要靠经验直觉来分析。 但我们

15、也并非完全束手无策。下面介绍两种实用技术来帮助我们分析问题。,实时调试之内存异常定位,第一种:使用CRuntime辅助函数_CrtSetDbgFlag 。 _CrtSetDbgFlag(_CRTDBG_DELAY_FREE_MEM_DF). 此函数设置了一个标志,告诉free函数不要真正释放内存。而仅仅设置释放标志,并置数据块为0xDDDDDDDD。说明:默认内存分配会优先分配刚释放的内存块,这样会出现一个内存块反复被多个模块使用,出错时很难确定哪部分代码引起。此选项意义在于,一块内存分配后,就不会再次被分配,避免交叉使用时互相干扰,定位问题可以在小范围内即可。而且,数据全为0xDD,很容易被

16、识别出来,也容易触发异常被及时发现。,实时调试之内存异常定位,这种方法只是浮云,下面才是真正的利器。第二种:gflags,实时调试之内存异常定位,gflags:微软工具,在windows调试工具包中附带。 gflags并不是一种调试工具,只是一个简单的设置工具。从win2000开始,系统就提供了改变内存分配策略的手段,在注册表中设置即可。gflags就是用来做设置的。 基本原理: 每次分配一块内存时,都分配到内存页尾部边界上。然后预留与此相邻的下一页,设为不可访问,作为保护区域。这样,一旦出现内存读写越界时,就会溢出到下一页,而此页不可访问,就会产生异常。从而帮助及时检查内存越界。 释放后,将

17、全页设为不可访问,后续分配也避免再分配此页,这样,如有指针试图访问这块内存,会立即被发现。,实时调试之内存异常定位,用法:gflags.exe /p /enable 程序名.exe /full /unaligned,实时调试之内存异常定位,不设置unaligned:沿8字节对齐,设置unaligned:,华生医生日志查看,工程上使用华生医生这个工具,在程序崩溃时生成调试日志。华生医生等转储工具生成的日志或转储文件,都是汇编级代码。另外用vc调试时,程序出现异常并不总是停留在源代码位置,却经常出现在系统库中,如ntdll.dll,mfc90d.dll等等。所以了解一点汇编基础是有必要的。 查看或

18、修改选项:运行drwtsn32.exe,可打开设置选项。可修改日志存储位置,设置转储的选项。 默认日志存放在C:Documents and SettingsAll UsersApplication DataMicrosoftDr Watson,华生医生日志查看,汇编基础: 类成员函数调用(_thiscall),需要隐含传递一个this指针,VC下都是放在ecx寄存器传给被调函数的。 返回值通常使用eax寄存器,所以函数返回后,eax寄存器值就是函数返回值。 函数开始处,会重新设置栈底寄存器ebp值,然后可以用 ebp-n访问局部变量,而ebp+n,(n为8,12,16等),指传入的参数。在函数

19、返回前,ebp是不变的。,华生医生日志查看,函数调用通常是以下两条指令开始: push ebp mov ebp,esp 这就是设置当前栈底,以便和外层函数隔离。结尾是: mov esp,ebp pop ebp ret 恢复原堆栈底和顶,并返回,华生医生日志查看,int 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 004115A

20、C 8D BD 40 FF FF FF lea edi,ebp-0C0h 004115B2 B9 30 00 00 00 mov ecx,30h 004115B7 B8 CC CC CC CC mov eax,0CCCCCCCCh 004115BC F3 AB rep stos dword ptr es:edi return 1; 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

21、pop ebp 004115C9 C3 ret,华生医生日志查看,*- Stack Back Trace :ElementAt+0x32 0d8ff800 0c6f5f8d 00000016 0d8ff874 0d8ffe4c SDHNetworkCross!CArray:operator+0x1a 0d8ff810 0cace114 00000016 0dea0d5f 0d8fff20 SDHNetworkCross!CDLGChoosePathgroupCols:GetIndex+0x1d 0d8ffe4c 0cacd8a5 08000116 1f003485 00000000 SDHNe

22、tworkCross!CNotifyMsgMgr:DisplayPathGrid+0x5d4 0d8fff2c 0cacb6d4 0d8fff74 0ba5ebd8 cccccccc SDHNetworkCross!CNotifyMsgMgr:ProcessPathSwitch+0x9b5 0d8fff58 0cae9da5 0d8fff74 00000000 00000000 SDHNetworkCross!CNotifyMsgMgr:ProcessMsg+0xe4 0d8fffb8 77e64829 0b779148 00000000 00000000 SDHNetworkCross!CM

23、sgQueueMgr:ThreadProc+0x135,华生医生日志查看,华生医生日志查看,以下是演示时间 打开一个工程中的日志查看,VC调试环境介绍,调试工具窗口,VC调试环境介绍-准备工作,安装源文件。安装时选择安装mfc源代码,crt源代码。这样就可以跟踪进库文件代码中。 让调试信息明明白白,不再雾里看花。 你是不是经常在堆栈中看到ntdll!7c819020这种让人心碎的东西?其实M$已经提供了更贴心的服务符号服务器,可以提供所有版本系统文件的调试信息(pdb文件),只是我们一般没去用它。,VC调试环境介绍-准备工作,程序数据库 (PDB) 应用程序的符号信息存储在pdb文件中,链接器

24、将创建 程序名.PDB,它包含项目的 EXE 文件的调试信息。PDB 文件包含完整的调试信息(包括函数原型)。 设置pdb文件路径: 打开菜单工具-选项,定位到“调试”,VC调试环境介绍-准备工作,VC调试环境介绍-准备工作,让调试器自动从微软符号服务器上下载匹配的系统pdb文件。方法:设置环境变量 _NT_SYMBOL_PATH=SRV*D:Symbols*http:/ 其中d:symbols是本地目录,随便改的。其他的是固定的。 设置方法:打开我的电脑-属性-高级-环境变量,VC调试环境介绍,运行时加载符号 若符号文件没有在指定的目录存放,也没有和exe放一起,调试器将无法加载。但你可以在

25、运行时手工加载。,点加载符号,将弹出 文件打开对话框,让你 指定符号文件,若文件 和exe匹配则加载成功, 否则不成功。,VC调试环境介绍-无符号,int _tmain(int argc, _TCHAR* argv) int *p = NULL; memset(p, 0, 10); ,VC调试环境介绍-有符号,转储文件(*.dmp)调试,转储文件生成 win2008之前使用华生医生, 2008采用了新的内置工具WER,需要手工配置: HKEY_LOCAL_MACHINESOFTWAREMicrosoftWindowsWindows Error ReportingLocalDumps HKEY_

26、LOCAL_MACHINESOFTWAREWow6432NodeMicrosoftWindowsWindows Error ReportingLocalDumps,转储文件(*.dmp)调试,在菜单文件-打开-项目/解决方案,选择dmp文件。 启动调试,将中断在异常处。 dmp调试并非真正的运行,只是出错时的一个快照,调试器模拟了当时的现场状况。因此不能像正常运行那样调试,只能做静态的分析。 可以查看堆栈,寄存器,线程,模块,局部变量等数据。完全转储时可以看到完整内存,但少量转储只能看到一部分堆栈的内存。 若有和dmp相应的pdb和源代码,调试器可以定位到源代码。否则,只能看到汇编代码。源代码

27、路径保存在pdb文件中,若源文件已移动,第一次调试器会弹出界面让重新指定。点取消则不使用源代码。,转储文件(*.dmp)调试,class A public:A() p = NULL;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)调试,无源代码的情

28、况:,eax = a,ecx = *(a+0) 即 a-p,转储文件(*.dmp)调试,有源代码的情况:,诡异现象之谜,在pdb文件缺失的情况下,将可能产生一些奇怪的现象。 接上一节的源代码。 class A public:A() p = NULL;char *p; ; int _tmain(int argc, _TCHAR* argv) int i = 1;A *a = new A;memset(a-p, 0, 10);printf(“%d“, i); ,诡异现象之谜,单步执行到箭头位置,停在memset()处, 此时的变量信息及堆栈如下图:,诡异现象之谜,执行memset(),程序异常,此

29、时在堆栈中切换到main()函数中,再一次查看变量值:,诡异现象之谜,前后的变量地址对比:,原来堆栈也变了:,诡异现象之谜,原因分析:在没有pdb信息的时候,调试器可能会无法正确解析出外层ebp的值,而ebp非常重要,局部变量、参数和返回值都是通过ebp+偏移得到的,ebp错了的话,就全错了。 下一步:调试器解析得到ebp值,用ebp+4作为地址,得到外层返回地址。用ebp作为地址,定位到外层的堆栈底,而此处保存的也正是更外层的ebp。 堆栈中wmain()那一行看起来似乎是对的,代码定位没错,但实践中也可能出现堆栈完全错误的情况。 堆栈显示错误不可怕,只是显示而已,等函数返回后会恢复成正确的

30、结果。在调试时,可能遇到这种堆栈,且中断是处在系统库中。极端情况下,可能完全看不到应用程序的痕迹,只有系统库。可以尝试着跳过错误代码,让调用返回,这样局面有可能会豁然开朗。跳过错误方法:,旧ebp,返回地址,旧ebp,返回地址,旧ebp,返回地址,栈顶,参考39页的代码:int 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 转到正确地址后,单步执行返回,如果堆栈层次较深,一直处在系统库中,可能要多返回几层,直到到达我们的程序所在空间中,此时就可以看到正常的堆栈,正常的代码了。,诡异现象之谜,在这一行上点右键,选择“设置下一语句”,将执行强制跳转到此。,谢谢观赏,

展开阅读全文
相关资源
猜你喜欢
相关搜索
资源标签

当前位置:首页 > 企业管理 > 管理学资料

本站链接:文库   一言   我酷   合作


客服QQ:2549714901微博号:道客多多官方知乎号:道客多多

经营许可证编号: 粤ICP备2021046453号世界地图

道客多多©版权所有2020-2025营业执照举报