完善资料让更多小伙伴认识你,还能领取20积分哦, 立即完善>
本帖最后由 friend0720 于 2016-3-23 09:54 编辑
目录 调试过程 编写便于调试的 C++ 代码 编写便于调试的 C++ 代码(2) 使用断言 使用跟踪语句 使用异常 在 Windows 中调试 使用 Visual C++ 调试器调试 调试版本与发布版本 调试发布版本 使用断点 VS2010 断点设置技巧 普通调试技术 内存调试 Visual C++ 运行时刻函数库内存调试 MFC 内存调试 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 目录 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-3 02:37 编辑
调试的过程 首先尽可能阻止错误发生! 巧妙地而不是艰苦地调试! 1.1 正确调试的五步曲 1. 确定错误的存在。有人(程序员、测试员、用户)确定存在某个错误。错误可能是通过测试、运行调试代码、使用开发工具或者检查源代码等等而发现的。 2. 收集错误信息。报告错误存在的人员和程序员一起,收集一些附加信息,有助于分析错误。可以通过运行程序、检查源代码或使用调试工具来收集这些信息。附加信息包括:错误是否可以重现,重现错误需要怎样做,重现错误需要的数据和程序设置,重现错误需要的系统和软件配置。 3. 分析错误信息。 通过分析错误信息,程序员可以确定代码中的问题和问题的根源,以及要消除错误应该对源代码进行什么修改。 4. 消除错误。 程序员通过修改源代码来消除错误。 5. 修改的验证。 程序员需要验证新的代码消除了错误并且没有产生新的错误,并需要检查那些可能产生类似错误的相关代码。 1.2 确定错误的存在 除测试外,错误还可以直接通过调试过程检测到,调试过程包括通过代码来预防和揭示错误。你可以充分利用 C++ 编译器的优点,避免那些常常是错误来源的语言陷阱,这样可以预防错误。最普通的技术是使用断言语句、跟踪语句、异常、检测资源泄漏的方法揭示错误。当然,你也可以直接通过检查源代码来发现错误。 1.3 收集错误信息 . 发现错误的日期。 . 测试者的名字、公司、和联系信息 . 程序名和版本号。 . 系统配置信息。 包括 Windows 版本号和 service pack 的版本 . 错误类型。 包括系统崩溃、程序失效、可用性问题、安装错误、文档和帮助方面的问题、产品问题以及建议。 . 问题描述。 . 重现错误的步骤。 如果问题是不可重现的或反复无常的,应该有注释说明。 . 添加附加的详细说明。 包括:屏幕快照、 Dr.Watson 文件、测试数据文件等等。 1.4 分析错误信息 . 错误代码。 . 症兆代码。 . 原因代码。 . 解决代码。 1.5 消除错误 不要处于危险中,在冒险验证修改时,一定要备份。 1.6 修改的验证 . 源问题消除了。 . 没有引入新的问题。 . 程序中类似的问题不存在了。 使用文件比较工具找出源文件修改的地方,再看一遍。然后使用调试工具,在代码修改的地方设置断点,观察执行情况。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-7-7 16:36 编辑
编写便于调试的 C++ 代码 C++ 是一种非同寻常的编程语言,有惊人的产生错误和避免错误的能力。 2.1 设计简单性(simplicity) 大多数常见的设计错误都来源于程序设计中不必要的复杂成分。一个好的设计应该反映问题本身的要求,也就是说,解决方案应该与问题相一致,而不要添加不必要的特性。 耦合性(coupling) 耦合性用来衡量不同对象之间的依赖程度。由于依赖性越少越好,因此,程序中能独立的对象要尽量独立出来。这种去耦合的程序易于理解,易于实现,易于测试和维护。 2.2 C++ 编程风格 编写结构良好的代码 当你的程序崩溃时,你得到的最基本的调试信息是源代码文件、问题所在的行号和一个调用栈(call static)。调用栈是调试程序时最有帮助的部分,因为它提供给你了错误出现的上下文,也就是带参数的函数调用序列。你书写的代码结构越好,调用栈就能给你越多信息。 使用良好的标识名字 . 简单的描述性的名字 . 避免简写 . 避免相似的名字 . 避免采用一般的名字 . 避免随机产生的名字 . 避免开玩笑的名字 用简单的语句 C++ 语言中,可以在一行里面写好几个语句。当你在选择一行中要安排哪些语句的时候,要时刻牢记调试的时候是面向行的,除非你在汇编代码一级进行调试。 使用统一的排列 一些程序员十分仔细地排列他们的代码,然而有一些程序员让代码处于“天然”的状态。毫不奇怪,统一的代码更可取。不仅仅使得代码更好看,而且排列使模式更好识别。 用括号使书写清晰 尽管编译器已经牢记了优先级和结合率,大多数程序员却不一定做到这一点。所以如果你不能确定是否需要用括号,你就需要用括号。使用多余的括号并不影响编译后的代码,因此不会带来性能的损失。 2.3 C++语言 这里有几种采用C++语言本身和它的编译器来防止错误和避免隐含的失误的方法。 用C++而不是C 使用以下的技术来充分利用C++的编译器 1. 用 const 代替 #define 来创建常量。 2. 用 enum 代替 #define 来创建常量集合。 3. 用内联 (inline) 函数代替 #define 宏。 4. 用 new 和 delete 代替 malloc 和 free。 5. 用输入输出流(iostreams)代替 stdio。 使用头文件 把不同文件之间共享的各种定义写在头文件里面。使用头文件保证所有的定义对于各个编译单元来说都是相同的。 初始化变量 使用变量之前一定要记住把它们初始化。 使用位掩码(Bit Masking) 如果位掩码不是 0 或 2 的幂次方,那么位掩码就容易出错。如果每个位都是 2 的幂次方,在整数的表示法里面,一位就代表一个状态。 不幸的是,Windows 程序中使用的位掩码不都是 2 的非零次方,这能使一个整数中包含更多的信息。考虑下面这种用法: #define SS_LEFT 0x00000000L #define SS_CENTER 0x00000001L #define SS_RIGHT 0x00000002L #define SS_ICON 0x00000003L #define SS_BLACKRECT 0x00000004L if((contrilStyle & SS_LEFT) == SS_LEFT) ......; if((contrilStyle & SS_CENTER)==SS_CENTER) ......; 很明显,这两条条语句是不正确的。这种类型的错误很难被追踪到,因为代码看上去很正确。有可能在大多数情况下,代码都会正常工作。 使用布尔变量 尽管 C++ 现在有一个内嵌的布尔类型(即 bool ,值为 true 和 false,大小为一个字节),Windows 程序通常还是使用 BOOL 类型。定义如下: typedef int BOOL; #define FALSE 0 #define TRUE 1 在C++中,一个布尔表达式如果值为0则为假,如果有其它的值则为真。这就意味着下面这段看起来正确的语句实际上有错误: if(booleanValue == TRUE) ......; if(booleanValue != TRUE) ......; 当然了,问题就出在当 booleanValue 的值多于 0 和 1的时候,这些布尔值的检查就会出现错误。 使用整型、字符型和浮点变量 C++ 中使用整型变量通常比较直接,但是有几个典型的错误应该注意: . 减一错误。一个常见的错误发生在一个整型变量做减一运算的时候,尤其是在循环中,整型变量作为循环控制变量时。一个简单的检查方法就是做一个测试,尤其是要覆盖第一个和最后一个索引值。 . 除零。除法运算中用零做除数会导致除零异常。如果你不能保证除数不为0,那就要处理可能出现的异常。 . 溢出。整型变量大小是有限的,因此如果它们的值过大就会最终导致溢出。 . 用无符号数来检查有符号数。显然无符号数和有符号数有不同的最大值和最小值。比如一个无符号数永远都不会等于-1。因此下面的语句不会像预期的那样起作用: if(nusignedInt == -1) ......; . 字符类型的数据是有符号的。这个意料不到的细节经常被忽略。由于一个字符类型变量的取值范围在 -128~127之间,如果你不仔细考虑的话,很容易发生溢出。下面这段代码是一个无限循环的例子: for(char ch =0;ch<200;ch++) ...... . 上溢出和下溢出。很难想象 double 类型的变量会发生溢出,但 float 类型就相对容易发生。如果你不能保证不发生上溢出或下溢出,那就要处理可能出现的异常。 . 检测浮点指针的值。浮点指针没有精确的二进制表示法,所以不要期望它们会有精确的值。因此,也不应该用比较两个浮点指针的值的方法来判断二者是否相等。比如,要检测一个变量是否为 42.0,应该用下面的代码: if(fabs(floatValue - 42.0) 这里 FLT_EPSILON 是浮点值的最大表示误差,而 DBL_EPSILON 是双浮点值的最大表示误差。 使用指针和句柄 回收指针的对象的时候要重新初始化这个指针,并且要在指针被释放为空之前就对其进行处理。 使用数据结构 Windows 总是把结构的大小(以字节为单位)作为结构中的第一个数据成员。这样做有两个好处。第一个,这个值起到了版本标识的作用。第二个好处,这个值可以作为结构中的一个信号,这样就很容易判断出结构是否出现了问题。 如果你使用这种技术的话,要保证这个值的合理性,使之能够在出现问题的时候,明显地表示出来。不幸的是,使用这种结构的 Windosw API 函数要求这个值必须正确,否则就不能正常工作,并且没有任何声明。忘记初始化这些值是很容易的,这种毫无声息的失败使得我们需要大量时间来调试程序。 用引用做参数而不是指针 引用是对象的别名,因此,它必须和有效的对象相关联。不存在空的引用和没有初始化的引用。当你在函数中收到一个引用参数时,你可以肯定这是一个有效的对象。 使用强制类型转换 不要匆忙使用强制类型转换。为了保证安全,每一个强制类型转换都需要你手工进行类型检查,从而增加了你的负担。强制类型转换会在维护时引发问题。请尽量选择C++强制类型转换而不是C强制类型转换。 使用const 在程序中使用 const 是一种好方法,能帮助编译器在编译时刻帮你发现错误。 正确使用循环语句 任何人都知道什么是 for 语句,什么是 while 语句,以及它们之间的区别。尽管如此,还是能够经常发现在应该使用 for 语句的时候使用了 while 语句。问题在于虽然 for(int i=0;i { ...... } 和 while(i { i++; } 在计算上面是相同的,但是如果在语句后面再加一句 continue 后,这种相等关系就会被破坏。因为 for 语句可以保证加一操作能够被执行,但 while语句就不能了。因此在 while 循环后面加上 continue 语句是一件很危险的事情,因为人们总是忘记执行变量增加的操作。 使用构造和析构函数 保证析构函数中的异常在析构函数内部得到处理 使用拷贝构造函数和赋值运算符 如果你的类需要虚析构函数,要么提供拷贝构造函数和赋值运算符,要么避免它们被自动加上。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-3 20:22 编辑
编写便于调试的 C++ 代码(2) 2.4 Visual C++ 编译器总是使用 /W4警告级别 考虑下面的语句: if(x=2) ......; Visual C++ 高高兴兴地编译这条语句,不给你任何警告。如果不知道上下文,最好假设程序员实际意思是: if(x==2)......; 要避免这个问题,很多程序员都采用下面的风格: if(2==x)......; 这种看起来笨拙的风格的好处在于:下面的代码会引起“error C2106:'=':left operand must be l_value”编译错误。 if(2=x)......; 当然,如果左边的操作数不是常量,这种风格就没有作用了。另一种更加优秀的解决方法,就是使用最高的编译警告级别(/W4)。如果你用 /W4 ,那么语句 if(x=2)......; 就会导致“warning C4706:assignment within conditional expression”(警告 C4706:在条件表达式中出现了赋值)警告。如果你真的想写这样的语句,你可以改写成下面这样来避免警告: if((x=2)!=0)......; 当两个操作数都是变量的时候你也会收到警告: if(x=y)......; /W4 警告级别能给你下面这些 /W3警告级别不能给你的警告: warning C4100:'id':unreferenced formal parameter (警告 C4100:未被引用的正式参数) warning C4127:conditional expression is constant(警告 C4127:条件表达式是常量) warning C4189:'id':local variable is initialized but not referenced(警告 C4189:'id':局部变量被初始化了,但没被引用) warning C4245:'conversion':conversion from 'type1' to 'type2',signed/unsigned mismatch(警告 C4245:‘conversion':从'type1'到'type2'的转换,有符号的/无符号的不匹配) warning C4701:local variable 'name'may be used without having being initialized warning C4705:statement has no effect warning C4706:assignment within conditional expression warning C4710:'function':function not inlined 在调试版本里总是使用 /GZ 编译选项这个编译选项的作用如下: . 用0xCC模式初始化自动(本地)变量。 . 在通过函数指针调用函数时,检查栈指针确认是否有调用规则不匹配。 . 在函数最后检查栈指针是否被改变。 抑制假的警告消息 使用 /W4 层的警告有一个缺点,你会收到大量的假警告。第一个处理的警告消息是: warning C4100:'id':unreferenced formal parameter。在 Windows 程序中,不用到所有的函数参数是一件很普通的事情。 #pragma warning 编译器指示 你可以使用 #pragma warning 编译器指示来禁止整个程序、特定的头文件、特定的代码文件或特定的某一行代码的特定警告,这就看你把 #pragma 指示放在什么地方了。 “没有警告的编译”法则 1. 消除编译错误和消除代码中的问题并不是完全相同的。 2. 过早地消除合理的编译警告并不是一件好事。 在消除编译警告之前要对它们进行仔细检查。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-4 10:56 编辑
使用断言 编译器能通过静态分析源代码来查错,但是仍然存在大量只有在动态分析的时候才能发现的错误。要找出这些错误,你必须运行程序。你可以在自己的代码中添加信息,让程序自己自动检测运行时刻错误。要达到这个目的,就要使用断言。 断言是能让错误在运行时刻暴露在程序员面前的布尔调试语句。使用断言来验证你的程序是否有效,但是记住,有效和正确并不相等。断言不能代替细致的测试,保持断言的简单性。 3.2 断言类型 ANSI C 断言 ANSI C 中的一个断言就是 assert 函数,使用这个函数要求你包含 assert.h 头文件,并且链接 C 运行时刻库函数: void assert(int expression); C 运行时刻函数库断言 你可以使用 _ASSERT 宏或者 _ASSERTE 宏。这两个宏都需要你包含 crtdbg.h 头文件并且与C运行时刻函数库链接。 _ASSERT(booleanExpression); _ASSERTE(booleanExpression); MFC 库中的断言 ASSERT(booleanExpression); ATL 断言 ATLASSERT 3.3 更多的MFC断言宏 MFC 为 ASSERT 宏提供了几个变种,同时还有和断言布尔表达式一样有用的调试函数。 VERIFY(booleanExpression); VERIFY 可以让你把程序代码(不是调试代码)放入布尔表达式里面。它和 ASSERT 的区别在于 VERIFY 中的布尔表达式在发布版本里被保留下来,只有 ASSERT 本身被删除。使用 VERIFY 宏比其他断言更危险。 另一个 ASSERT 宏的变种是 MFC ASSERT_VALID 宏,它被用来决定一个指向 CObject 派生类的对象的指针是否有效。 ASSERT_VALID(pObjectDerivedFromCObject); 在使用 CObject 派生类的对象之前都要调用 ASSERT_VALID 宏。 ASSERT 宏的另一个变种是 MFC ASSERT_KINDOF 宏。 ASSERT_KINDOF(className,pObjectDervedFromCObject);同样,这个宏也只是用于 CObject 派生类的对象。 3.4 自定义断言 有的情况下没有一个标准 Visual C++ 断言合适。这时你可以创建自定义断言. 3.5 可移植的断言 3.6 使用断言的策略 断言的正确使用需要一定的策略。不要随意地把断言分布到你的代码里。 什么需要断言 . 检查函数的输入。 . 检查函数的输出。 . 检查对象的当前状态。 . 检查逻辑变量的合理性和一致性。 . 检查类中的不变量。 什么不需要断言 . 断言不能检查哪些可能正确也可能错误的情况。 . 断言不是防御性编程的替代品。 . 断言不能检查哪些不是实现方面错误的错误。 . 断言不能用来向用户报告错误。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-4 13:32 编辑
使用跟踪语句 在 Visual C++中,跟踪消息通常输出到输出窗口(output window)中的 Debug 标签。 跟踪语句的特性如下: . 跟踪语句用于报告代码中重要的运行事件。 . 跟踪语句的编译通常是有条件的,并只存在于调试版本中,而在发布版本中不被编译。 . 跟踪语句不能包含程序代码或对程序代码有间接的影响作用。 . 跟踪语句的目的是向程序员提供信息,而不是用户。 4.1 跟踪语句的类型 Windows 的跟踪语句 void OutputDebugString(LPCTSTR traceText); ANSI C++运行时刻函数库跟踪 Visual C++ 的 C 运行时刻函数库跟踪语句 MFC 的跟踪语句 如果你使用 MFC ,你可以使用 TRACE 和 AfxOutputDebugString 宏、CObject::Dump 虚拟函数和 AfxDumpStack。AfxOutputDebugString 宏和 AfxDumpStack 函数可以在所有的版本中编译,而其它的函数则只能在调式版本中编译。 TRACE 宏有以下形式: _TRACE(reportType,format); _TRACE0(reportType,format,arg1); _TRACE1(reportType,format,arg1,arg2); _TRACE2(reportType,format,arg1,arg2,arg3); _TRACE3(reporttype,format,arg1,arg2,arg3,arg4); TRACE 宏的参数不能使用大于512字节的文本缓冲区,因为那会导致一个断言失败。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-10 23:03 编辑
使用异常 C++ 异常的基本特性: . 异常是基于每个线程而提出并处理的。 . 异常不能被线程忽略,必须被处理。 . 未处理的异常会使进程结束,而不仅仅是线程结束。 . 异常处理在释放时会释放所有的栈对象,因此避免了资源的漏洞。 . 异常处理需要大量的额外操作,使得它并不适于经常运行的代码。 . 你可以抛出任何类型的异常对象,但不包括整数。 异常是用于处理错误的: . 访问违例:线程不能继续执行,因为有一个虚拟内存的地址是无效的。 . 栈溢出: 线程不能继续执行,因为没有多余的栈空间。 . 非法指令:线程不能继续执行,因为当前 CPU 执行是无效的。 . 整数被0除:线程不能继续执行,因为一个整数值是无意义的。 . 浮点数溢出:线程不能继续执行,因为一个浮点数是无意义的。 . 文件系统错误:例如找不到磁盘驱动器、文件夹、文件等。 . 内存错误:例如超出了堆空间或虚拟内存空间。 . 网络错误:例如不能进行网络连接。 . 外围设备错误:例如打印机脱机或没有纸。 . 数据错误:例如输入数据无效或没有输入数据。 . 用户错误:例如用户通过无效的输入进入一个对话框。 将 Windows 结构化异常转化为 C++ 异常 Visual C++ 允许你通过使用 _set_se_translator 函数将结构异常转化为 C++ 异常。 #include class CSEHException{ public: CSEHException(UINT code,PEXCEPTION_POINTERS pep){ m_exceptionCode = code; m_exceptionRecord = *pep->ExceptionRecord; m_context = *pep->ContextRecord; _ASSERTE(m_exceptionCode == m_exceptionRecord.ExceptionCode); } operator unsigned int () {return m_exceptionCode;} UINT m_exceptionCode; EXCEPTION_RECORD m_exceptionRecord; CONTEXT m_context; }; void _cdecl TranslateSEHtoCE(UINT code,PEXCEPTION_POINTERS pep){ throw CSEHException(code,pep); }; int APIENTRY WinMain(HINSTANCE hInstance,HINSTANCE hprevInstance,LPSTR lpCmdLine,int nCmdShow) { _set_se_translator(TranslateSEHtoCE); …… } 有了上面的代码,你就可以像捕获 C++ 异常一样捕获结构异常: void TestBugException2(){ try{ int *pInt=0; *pInt=42; } catch(CSEHException &bug){ switch(bug) { case EXCEPTION_ACCESS_VIOLATION: MessageBox(0,_T(""),_T(""),MB_OK); break; case EXCEPTION_INT_DIVIDE_BY_ZERO: break; case EXCEPTION_STACK_OVERFLOW: throw; } } } 在处理转化后的结构异常时,你应该只从下面的几个异常中恢复: . EXCEPTION_ACCESS_VIOLATION; . EXCEPTION_INT_DIVIDE_BY_ZERO; . EXCEPTION_FLT_DIVIDE_BY_ZERO; . EXCEPTION_FLT_OVERFLOW; . EXCEPTION_FLT_UNDERFLOW; |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-13 21:09 编辑
在 Windows 中调试 也许你以前还没有遇到过下面所述的事情,但迟早有一天它会发生在你身上。你把你的程序给某个重要人物使用,不幸的是程序在他们那里运行时崩溃了。他们记不起他们执行了些什么操作,但有一点是肯定的,这个错误很严重,而且,程序的崩溃导致了很大的麻烦。 使用映射文件调试 在进行事后调试时,要找到正确的解决办法,你需要一个映射文件,准确地说,你需要一个对应于程序创建的所有模块的映射文件。映射文件包含对应模块的最佳装载地址、段表、输出符号地址、静态符号地址以及程序代码地址和源程序行号的映射。 映射文件记录的信息比较原始,读懂它需要花费一定的精力,但它有两个非常重要的优势:它是可读性很好的文本文件;它不依赖于任何版本的 Visual C ++。 VS2010 中输出 .map 文件的方法如下: 使用 PDB 文件调试 我们用 Visual C++ 进行事后调试,使用调试器找到源代码中的崩溃地址。这个方法要求你有相应的源文件和崩溃程序的 PDB 文件。此外,你还必须使用和 PDB 文件格式版本兼容的 Visual C++。如果崩溃地址所在模块已经在虚拟内存中作了重定位,这个技术可能并不能解决问题。 默认情况下 VS2010 会在发布版本和调试版本中自动生成工程的 PDB 文件。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-14 10:47 编辑
使用 Visual C++ 调试器调试 . Visual C++调试器完全嵌入到了 Visual C++ 开发环境,允许用户直接从源代码窗口使用调试器设置断点和控制程序的指向。 . 在进行调试时,源代码窗口通过数据标签显示变量值,这使得用户只需要在源代码周围移动鼠标就可以得到大量的信息,而不必使用对话框查看变量。 . 调试器有一个功能强大的观察窗口,不仅显示了变量和结构,还允许用户计算表达式、执行函数、选择显示格式。 . 调试器有一个非常有用的变量窗口,能方便地显示当前语句或前面语句的变量。当你跳过一个函数时,它还显示返回值、当前函数的局部变量或者 this 指针指向的对象。变量窗口不像观察窗口一样,需要输入查看的变量。 . 支持多种高级断点,例如,条件代码定位断点、数据断点以及消息断点。 . 允许用户不需要额外的工作就可以调试 DLL 。 . 能让用户在不得不进行调试远程程序或用户不能安装 VC 时,进行远程调试。 . 支持及时(JIT)调试,为事后调试提供了方便。 . 支持编辑继续功能,减少了调试周期的时间和所花费的精力。 编译与链接选项 使用 Visual C++ 调试器的第一步就是选择合适的编译与链接选项。 编译选项 /W4 在最高的警告层次作编译(所有的版本都使用) /D"_DEBUG" 打开条件编译调试代码开关,例如,断言语句与跟踪语句(仅在调试版本中使用) /GZ 有助于在调试版本中找到在发布版本中经常会出现的错误,包括未被初始化的自动(局部)变量、堆栈错误、不正确的函数原型(仅在调试版本中使用) /Od 关闭优化开关,使代码在调试器下更容易读懂(仅在调试版本中使用) /GF 消除重复字符串,并将字符串放到只读内存中,从而避免它们被错误地修改(明确地只能被发布版本使用)。 /ZI 用调试符号和编辑继续信息创建程序数据库,从而减少调试周期的时间和所花费的精力(仅在调试版本中使用) /Zi 创建调试符号的程序数据库(仅在发布版本中使用) 链接选项 /MAP: "Debug/ProgramName.map" 创建一个映射文件 /MAPINFO:LINES 在映射文件中添加行号信息 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-15 14:07 编辑
使用 Visual C++ 调试器调试 调试版本与发布版本 程序员经常会对调试版本与发布版本的区别很迷惑,特别是在调试版本可以运行、而发布版本不能运行时。让我们首先从调试版本与发布版本不同的编译选项开始,来看看这些不同到底是什么含义。 调试版本的编译选项 /MDd,/MLd,或者 /MTd 使用调试版本的运行时刻函数库 /Od 关闭优化开关 /D"_DEBUG" 打开条件编译调试代码开关 /ZI 创建编辑继续的程序数据库 /GZ 在调试版本中捕获发行版本的错误 /Gm 打开最小化重新链接开关,减少链接时间 发布版本的编译选项 /MD,/ML或者/MT 使用发布版本的运行时刻库函数 /O1 或 /O2 打开优化开关,使得程序会最小或者说速度会最快 /D"NDEBUG" 关闭条件编译调试代码开关(具体说就是 ANSI C 的 assert 函数) /GF 消除重复字符串,并将字符串放到只读内存中,从而避免它们被错误地修改。 实际上,调试版本使用了一组选项来帮助你进行调试,而发布版本也使用了一组选项来产生高效的代码。在任何版本中,你可以选择任何一组选择(仅有的限制是一些选项与另一些选项不相容),所以你可以有一个带有调试符号、跟踪语句、断言语句的发布版本,只要你选择了这些选项。编译器并不关心这些,因为它丝毫也意识不到这两个版本之间的差异。通常,一个发布版本一般意味着有些类型的优化,然而一个调试版本意味着没有优化。 下面让我们来更具体地看看每一个调试编译链接选项,并和发布版本相对比。 使用调试版本的运行时刻函数库 与C的运行时刻函数库链接的调试版本能帮助你进行调试。除了有调试符号,最重要的区别在于调试版本的运行时刻函数库使用了调试堆(heap)。 . 调试版本的运行时刻函数库对内存的分配作了跟踪,并允许用户检查内存泄漏。 . 在刚分配的内存里写上 OxCD 的字节模式,这有助于发现使用未被初始化数据的错误。 . 在被释放的内存里写上 0xCD 的字节模式,这有助于发现使用已被释放的内存。 . 在缓冲区的两边分配了四字节的保护数据,并用 0xFC 的字节模式作初始化,来检查写内存的上溢出和下溢出。 . 在每个内存分配的地方对源代码文件名和行号作了记录,这有助于用户在源代码中对内存分配进行定位。 在这些特性中,两种版本之间的一个明显不同处在于调试版本能发现多种内存错误,而发布版本不能。另一个重要的不同点是调试版本允许(并且报告出)对内存的写操作有四个字节的上溢出和四个字节的下溢出,对程序不会有任何影响,然而同样的错误在发布版本中就会导致内存破坏。 关闭优化开关 因为未被优化的汇编代码直接对应于源代码,所以比优化后的代码更容易读懂。所以当你调试代码时,调试器会以你预想的方式工作,而优化后的代码却可能会到处跳转,和你的预想很不一样,而且一些变量也因为优化去掉了。此外,未被优化的代码编译与链接会更快,从而会有更短的调试周期。 理论上,调试版本不会比发布版本更完善,所以一旦你的调试版本被调试通过,你可以确信你的发布版本至少可以运行得同样好。不幸的是,这个完美的理论因为优化而被打破了。优化代码要求编译器作一些假设,并去除冗余;但有时这些假设是错误的,并且去掉的冗余也有可能隐藏错误。下面介绍与优化有关的一些问题。 帧指针省略 在调试版本中,堆栈基址指针(EBP 寄存器)在函数内部是一个常数,所以它可以用来寻找堆栈里的内容,具体地说,便是函数返回地址。然而,在发布版本中,堆栈基址指针也许被优化掉了。这种类型的优化被成为帧指针省略(FPO),堆栈基址寄存器被用作通用寄存器,而且省去了函数调用时对 EBP 寄存器的压入、弹出操作。 异常优化 如果你的程序编译时采用了同步异常模式,异常处理程序可能会被优化掉,同步异常模式通过 /GX 或者 /EH 编译选项来设置,并且成为默认模式。使用异常模式时,Visual C++ 假设 C++ 异常仅仅会被含有 throw 语句或者调用了一个函数(函数可能有一个 throw 语句)的代码产生。任何其它的代码不可能接收到 C++ 异常,所以编译器在发布版本中将不必要的异常处理代码优化掉了,而在调试版本中没有这样做。因此,同步异常模式会阻止程序中的 C++ 异常处理代码安全地捕获结构化的异常(由非法访问、被零除、堆栈溢出等待错误引起)。要捕获结构化的异常,你必须使用异步异常模式,这可以使用 /Eha 编译选项来设置。 volatile 关键字 volatile 关键字告诉编译器一个特定的变量可能以一种编译器不知道的方式改变值,例如***作系统修改,被硬件修改或者被一个并发运行的线程修改,所以编译器一定不能对这个变量作任何优化。结果是,一个 volatile 变量总是从它被存储的地方读进和写入,而不是为了优化而保存在寄存器中。调试版本没有作优化,所以事实上调试版本中的所有变量都是 volatile 的。相反,如果一个变量被错误地未设置成 volatile,发布版本中便会有一个与优化相关的错误。如果你的程序是多线程的,那么程序出问题的可能性非常大。 变量优化 优化会去掉不必要的变量以及重复使用的变量,这可能会隐藏错误。看看下面的代码。 void StackAttack() { int optimizedOut1,optimizedOut2; TCHAR bugsText[16],*bugs=_T("This function has bugs!"); _tcscpy(bugsText,bugs); } 在这个函数中,bugsText 缓冲区只有16个字节长,对于接收 bugs 字符串来说太短了。不必要的变量 optimizedOut1 和 optimizedOut2 在调试版本中会保护堆栈内容不被破坏,但这些变量在发布版本中会被去掉。导致的结果是,缓冲区的溢出会破坏堆栈里的函数返回地址,从而在发布版本中会导致崩溃,而在调试版本中不会。 优化错误 发布版本也许真的有一个优化错误,这是由于过度优化或者优化器本身的错误引起的。你可以通过以下方式来确定一个错误是否与优化相关。 . 完全关掉优化 . 使用更安全的优化形式,例如对代码的大小作优化而不是对速度作优化。 . 选定某些文件关掉优化或者作更安全的优化 . 使用 #pragma optimize 对选定的代码关掉优化。 如果错误在这些措施下会消失,那么这个错误就是与优化相关的。 打开条件编译调试代码 如果 _DEBUG 符号被定义了, Visual C++ 的 C 运行时刻函数库和 MFC 的调试代码才会被编译。 使用编辑继续程序数据库 打开编辑继续选项有一定的副作用,他会打开 /GF 编译选项, /GF 编译选项会消除重复字符串,并将字符串放到只读内存。当发布版本也打开了 /GF 编译选项时,调试版本和发布版本应该有同样的执行效果。 在调试版本中捕获发布版本中的错误 /GZ 编译选项会做下面的一些事情: . 用 0xCC 字节模式初始化所有的自动变量。 . 当通过函数指针调用函数时,会通过检查堆栈指针来验证函数调用的匹配性。 . 在函数末尾检查堆栈指针,确认它没有被修改。 许多程序员认为 Visual C++在调试版本中将自动变量初始化为0,而在发布版本中不是这样。这个信条可以理解成一种市井传说,因为它从来就不是正确的。编译器是意识不到一个版本是不是调试版本,所以编译器根本就没有办法知道是否要进行这样的初始化。但是,如果 /GZ 编译选项(它与优化版本不相容),编译器会将所有的自动变量初始化为 0xCC 字节模式。 打开最小化重新编连 如果代码已经成功地编译了,打开最小化重新编连不应该对程序的调试版本的执行有影响。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-16 09:06 编辑
使用 Visual C++ 调试器调试 调试发布版本 假设你的程序有一个只在发布版本中才出现的错误。除非你是一个汇编语言高手,否则,你需要调试符号来提示你到底发生了什么。 Visual C++ 调试的黄金定律:最好为你的可执行程序创建调试符号,并将得到的 PDB 文件存档,即使程序属于发布版本。幸运的是 VS2010 已经自动为我们做了这方面的工作,但是如果你还在使用古老的 Visual C++ 6.0 版本,你需要自己做这方面的工作。 在 VC6.0 中你需要做如下工作: 1. 打开工程设置对话框,在 Setting for 对话框中选择所需的版本。 2. 在工程控制树里,通过单击根节点选择整个工程。 3. 在 C/C++ 标签里选择 Common 类。在调试信息里,如果是发布版本则选择 Program Database,如果是调试版本则选择 Program Database for Edit and Continue。 4. 在 Link 标签里选择 Debug 类。然后选择 Debug info 和 Microsoft format 选项。记住不要选择 Separate types 选项,这样所有的调试信息才会合并到单独的一个 PDB 文件中。另外,如果你需要做事后调试的映射文件时,记住要选择产生 Generate Mapfile 选项。 5. 对于发布版本,选择 Link 标签,在 Project Option 对话框的最后加上 "/OPT:REF"。这个选项使得不被引用的函数和数据不会出现在可执行文件中,从而避免了文件无谓的增大。 6. 使用 Rebuild All 命令重新编译整个工程。 如果你要调试带有符号的程序,你必须将 PDB 文件和你发布的任何版本的源文件、可执行文件一起存档。如果不这样做,调试符号就没有任何价值了。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-17 12:37 编辑
使用 Visual C++ 调试器调试 使用断点调试 有效地使用断点对调试的高效性有很大的作用。断点实际上是允许你向调试器描述环境,并让调试器设置好程序状态的一个机制。理想状态下,处于断点时,你要做的只是检查调用堆栈,再跟踪少数几条代码,检查少数几个变量,解决问题。你可以设置下列类型的断点: . 代码定位断点:你可以设置无条件代码定位断点,也可以设置基于表达式或者基于代码执行次数的条件代码定位断点。 . 数据断点:你可以设置基于表达式的条件数据断点。对于数组和结构,你还可以指定查看的数组元素或者结构元素数。 . 消息断点:你可以设置消息断点,该断点在某个特定的窗口函数接收到某个特定的消息时有效。 断点表达式 断点条件表达式 断点条件表达式用来确定调试器是否要暂停在该断点。表达式可以是一个布尔表达式,当表达式成真时,调试器会暂停;表达式也可以是非布尔表达式,从上次断点到达时起,如果表达式的值变了,调试器也会暂停。 断点条件表达式能包含下列的操作数: . 常数(但不能是程序里的常数) . 程序变量 . 寄存器和伪寄存器 它还包含以下的操作符: . 算术运算(+、-、*、/、%) . 关系运算符(== != < <= > >=) . 布尔运算(! || &&) . 位运算(~ >> << & | ^) . 地址运算(& *) . 数组运算 ([]) . 类、结构、联合运算 (. ->) . sizeof . C 强制类型转换 (只能有一层间接转换) . 内存内容 (DW() WO() BY()) 你还可以使用括号,但它不会被算作操作符。和观察窗口表达式不同的是,断点条件表达式不支持赋值操作和函数调用。而且,对于结构和数组的比较,一次只能比较一个元素。例如,当字符串 s 含有 “bugs” 时程序暂停,你应该使用下面的表达式: s[0]=='b' && s[1]=='u' && s[2]=='g' && s[3]=='s' 高级断点表达式 Visual C++ 使用高级断点表达式来表示内部断点。在断点对话框的底部你可以看到 Breakpoints 列表中的表达式。你也可以自己在 Location标签的 Break at 框里、或者在 Data 标签的 Enter the expression to be evaluated 框里自己输入表达式。 这些表达式和观察窗口表达式使用相同的上下文操作符,还带有源代码行号、变量名,调用函数名,虚拟内存地址或者语句标签。下面是一些例子: a line number: {[function],[source],[executable]}.100 a variable name: {[function],[source],[executable]}MyVariable a function name: {[function],[source],[executable]}MyFunction a function name: {[function],[source],[executable]}CMyClass::MyFunction a memory address: {[function],[source],[executable]}0x00401234 a statement label: {[function],[source],[executable]}Barney 如果你在 Break at 框或者 Enter the expression to be evaluated 框里输入的表达式使用了程序当前执行范围里的变量,调试器会自动添加正确的上下文操作符。否则,你必须自己添加上下文操作符。 代码定位断点 代码定位断点是最常使用的断点。你可以设置下面几种类型的代码断点。 . 源代码行数断点 . 代码虚内存地址断点 . 函数名断点 . 语句标签断点 上下文操作符允许用户在函数名处设置断点,但有时这不能提供足够的信息,因为 C++ 函数的重载功能允许多个函数有同样的名字。 数据断点 . 一次要尽可能少地使用数据断点。 . 关掉观察窗口,或者删除所有没必要的观察窗口表达式,特别是那些有函数调用的表达式。 . 一般情况下不用使用数据断点,除非你必须要使用它们。 . 基于变量的数据断点很慢,而基于数据成员的数据断点更慢。 . 避免在调用了调试堆诊断函数的代码里使用数据断点,例如 _CrCheckMemory 和 _CrtDumpMemoryLeaks,将这两者结合在一起会使得你的程序就像蠕虫在爬动一般。如果有必要的话,可以暂时删除这些诊断函数。 消息断点 消息断点使得当某个特定窗口函数接收到某个特定消息时程序暂停。如果要在一个窗口里检视多个消息,使用多个消息断点就可以了。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-17 13:02 编辑
VS2010断点设置技巧(转) 许多Visual Studio下的程序员,甚至一些很有经验的开发人员,都不知道这些技巧。希望这篇文章能帮你掌握这些技巧。它们都很简单,却能帮你节约大量的时间。 一、跳到当前光标处(Ctrl+F10) 我经常看到人们为了到达目标代码位置,而在程序中早早设定了断点,然后反复地按F10/F11,一步步走到目标代码处。当程序员的确需要仔细观察每一步的状态变化时, F10/F11是合理的选择。然而多数情况下,人们只想快速到达他们真正关心的代码处,这时候F10/F11就不是最佳选择了。 这时,你应该利用“跳到当前光标处”这个功能。先把光标定位在要测的目标代码行上,再同时按Ctrl和F10,被测程序将直接跳到该行停下。你再也不用按许多次F10/F11了。即使目标代码位于独立的类或方法中,你仍然可以从当前正在检查的地方跳过去。 二、条件中断 另一种常见的情况是:开发人员设置断点,运行程序,利用不同的输入触发断点,然后在断点处手工检查是否满足某些特定的条件,从而决定是否继续调查。如果当前场景不是他们想要的,按F5继续运行程序,尝试别的输入,手动重复刚才的过程。 针对上述情况,Visual Studio提供了一个方便得多的功能——“条件中断”。只有当程序满足了开发人员预设的条件后,条件断点才会被触发,调试器中断。这将避免频繁地手工检查/恢复程序运行,大量减少调试过程中的手工和烦琐工作。 如何设置条件断点 设置条件断点非常容易。在特定的行上,按F9设置断点。 然后右击断点–编辑窗口左侧的红点,在上下文菜单上选择“Condition…”。 这时弹出一个对话框供你设置激活该断点所需的条件。比如:我们希望只有当局部变量paginatedDinners的尺寸小于10时,调试才中断。我们可以写出如下的表达式: 现在我再运行这个程序,实现搜索,只有返回值小于10时,程序运行才会被中断。对于大于10的值,该断点将被跳过。 三、记录到达断点次数 有时你希望,只有当第N次满足条件的运行到达断点时,才中断程序运行。例如:当第五次返回少于10份晚餐的查询结果时,中断程序运行。 可以通过右击断点,然后在弹出菜单上选择“Hit count…”菜单命令实现。 这时系统弹出一个对话框,它允许你指定:(1)当满足条件,而且进入断点的累计次数等于N时,断点命中一次。(2)当满足条件,而且进入断点的累计次数是N的倍数时,断点命中一次。(3)当满足条件,而且进入断点的累计次数大于N时,每次命中断点。 四、机器/线程/进程过滤 设置如下:右击断点;在弹出菜单上选择“Filter…”菜单命令;然后指定命中断点的特定条件:在指定的机器上、或指定的进程中、或指定的线程中。 跟踪点—进入断点时的自定义操作 许多人不知道“跟踪点(TrackPoints)”这个调试功能。“跟踪点“是种特殊的断点,当它被命中时,它会触发一系列自定义操作。如果你想观察程序的行为,而又不想中断调试的时候,这个功能尤其有用。 我将用一个简单的控制台程序来演示如何使用“跟踪点”。如下是Fibonacci数列的一个递归实现: 以上程序中,我们使用Console.WriteLine() 输出针对特定输入值生成的最终斐波那契数列。如果希望在调试器里观察操作中每一次递归运算后的数列而又不实际中断程序运行,该怎么办呢?“跟踪点”可以轻松实现。 设置跟踪点 你可以在特定的行上,按F9加跟踪点。然后 右击断点,在上下文菜单中选择“When Hit…”: 在弹出对话框上,你可以设置命中该断点时,所触发的事件。 在上面例子中,我们设定一旦命中断点时就打印追踪信息。注意,我们已经把局部变量“x”的值,作为追踪信息的一部分输出。局部变量可以通过{变量名}语法输出。你还可以利用系统内置的命令($CALLER, $CALLSTACK, $FUNCTION等等),在追踪信息中输出常用的调试值。 在上例中,我们同时选中了底端的“continue execution“选项,这说明我们不希望程序中断调试状态,而是继续运行。唯一的不同是:每次断点条件满足时,我们的自定义追踪信息都将被输出。 现在当我们运行程序时,会发现自定义追踪信息自动显示在Visual Studio的“输出“窗口里。这让我们很容易看到程序的递归调用过程: 你也可以选择往应用程序中添加一个自定义追踪信息的监听器。这时追踪点的输出信息将通过它输出,而不是Visual Studio的“输出“窗口。 五、跟踪点—运行自定义的宏 当命中跟踪点时,能否自动输出所有的局部变量? Visual Studio中并没有这样的内置功能,但我们可以写一个自定义宏来实现,然后在命中跟踪点时调用该宏。这个的实现需要先打开Visual Studio的宏编辑器(工具->宏->宏IDE菜单命令),然后在项目资源管理器的MyMacros节点下选择一个模块或创建新模块(如:加个名为“UsefulThings”的模块),再把下面的VB宏代码贴到模块中并保存。 Sub DumpLocals() Dim outputWindow As EnvDTE.OutputWindow outputWindow = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput).Object Dim currentStackFrame As EnvDTE.StackFrame currentStackFrame = DTE.Debugger.CurrentStackFrame outputWindow.ActivePane.OutputString(“*Dumping Local Variables*” + vbCrLf) For Each exp As EnvDTE.Expression In currentStackFrame.Locals outputWindow.ActivePane.OutputString(exp.Name + ” = ” + exp.Value.ToString() + vbCrLf) Next End Sub 上述宏代码将循环当前的堆栈,把所有的局部变量输出到“输出”窗口。 使用自定义的“DumpLocals”宏 然后,我们可以在如下的一个简单程序中使用刚定制的“DumpLocals”宏了: 上述代码中,我们用F9在“Add”方法的返回值处加了个断点,然后右击断点,在弹出菜单上选择“When hit”。 将显示如下对话框。和之前不一样, 我们不选“Print a message”选项,也不手工设定需要输出的变量;而是选择“Run a marco”复选框,并指定到我们上面创建的UsefulThings.DumpLocals宏上: 为了使程序能在命中跟踪点后仍继续运行,我们将继续选中“continue execution”复选框。 运行程序 现在按F5运行程序,当“Add”方法被调用时,我们会在Visual Studio的“输出”窗口中看到如下结果。注意命中跟踪点时,宏会自动列出每个局部变量的名称和值: |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-17 15:23 编辑
普通调试技术 我们应该采取什么步骤使得我调试代码的能力最大? . 重定位你的程序的可执行代码以防止虚拟地址空间冲突。 . 为程序的可执行代码创建 MAP 文件,并存档。 . 在程序的可执行文件中创建调试符号(即使是在发布版本中),并存档 PDB 文件。 . 将程序的工程、源代码和可执行文件存档。 . 为用户创建一个提交错误报告的机制,包括一个错误报告表单和指示。 我希望在发布版本中包含一些调试代码,但不希望它影响程序的运行性能,应当怎么做? 你可以使用 API 函数 IsDebuggerPresent 将发布版本中的调试代码关闭。例如,下面这段代码仅当程序在调试器中运行才会影响性能: if(IsDebuggerPresent()) PerformTimeConsumingDebugCheck(); 用户一般都不会启动调试器,因此一个更好的方法是通过 INI 文件或注册表设置将发布版本中的调试代码关闭。如下: BOOLO ReleaseDebug=GetPrivateProfileInt(_T("My App")),_T("Debug"),FALSE,_T("MyApp.ini")); ... if (ReleaseDebug) PerformTimeConsumingDebugCheck(); 我无法对某个问题进行调试,因为在调试器中有太多的数据,无法将问题隔离出来。应该怎么办? 你可以使用以下几种方法之一: . 创建调试数据。创建特殊的调试数据,消除所有与问题无关的冗余数据。 . 从程序中临时过滤数据。如果创建调试数据不太现实,你可以在程序中过滤掉所有无关的数据从而隔离问题。 . 使用一个全局变量动态地激活调试代码。 . 使用键盘动态地激活调试代码。有的时候我们希望用键盘动态地控制调试代码,例如某几个建的组合。例如,你可以借助 API 函数 GetAsyncKeyState 检查键盘的状态,是程序仅当 Control 建按下的时候激活调试代码。 #ifdef _DEBUG if(GetAsynKeyState(VK_CONTROL)<0 && data.x!=42) continue; #endif 我觉得C预处理器产生的代码不是我所期望的,如何进行调试? 你可以使用以下几种技术检查预处理器的输出: 在 Project Setting 对话框里选择整个工程,然后单击 c/c++ 标签。在 Project Options 输入框中,在设置的最后添加编译选项 /P 。然后重新 build 你觉得有问题的文件,例如 bogus.cpp , 你会发现 bogus.i 出现在工程文件夹里。然后你就可以用任何一种文本编辑器查看这个文件。预处理器文件可能很大,因为文件的开始部分都是预处理器产生的垃圾,所以你可以直接跳到文件的最后,这才是你的程序代码所在的地方。然后你就可以搜索这个文件,看有关的预处理器符号到底是怎么定义的。 看完后,记得必须把 /P 编译选项从工程设置中去掉。 我优化了程序的速度,但似乎存在优化的错误,而且无法绕过去。我该怎么办? 你可以尝试优化程序的代码大小,这种方法安全的多。 忽然之间,所有的东西都乱套了。我该怎么办? 重启 Windows,把工程中的 Debug 和 Release 文件夹都删掉,从头编连程序。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-20 12:48 编辑
内存调试 动态内存分配错误有两种级别类型:内存错误和内存泄漏。 Windows 内存调试 Windows 中进行动态内存分配的基本方式是 VirtualAlloc 和 VirtualFree 应用程序接口函数。Windows 为所有的进程提供了 4G 字节的虚拟地址空间。这样看起来就像是有很多地址空间,但是要记住这是虚拟内存,因此它仅仅是一段内存区域,除此之外什么都不是。在物理内存同虚拟内存的某一页对应起来以前,用户不可能使用虚拟内存完成任何有用的工作。所有被程序使用的内存都会被映射到虚拟内存中。但是这不一定意味着 Windows 程序需要直接对虚拟内存应用程序接口函数进行处理,除非该程序需要管理大规模数据(例如三维图形程序)或者完成系统功能(例如 Windows 自身)。 大多数 C++ 程序使用 C 运行时刻函数库中提供的 new 和 delete,而不是直接对虚拟内存进行处理。new 函数和 delete 函数工作时,会分配一大块虚拟内存(Windows 默认分配 1M 字节,但是可以通过修改连接器的 /HEAP 选项来修改分配字节数)并将这块虚拟内存安装程序需求再划分为小块(这被称为在分配)。C 运行时刻函数库是使用 HeapAlloc 和 HeapFree 实现这种再分配的。当一个进程被装载时,windows 会默认在该进程的虚拟地址空间中创建一个堆,这也就是我们所知道的默认堆空间。大多数程序有一个堆就足够了。但是有时候用户可以通过创建而外的堆来优化内存管理的性能。每一个进程中的 C 运行时刻函数库的实例都有其自己独立的堆。用户程序在一种情况下会具有多个堆(而用户也许毫不知情),那就是程序使用了动态链接库,而这个动态链接库具有其自己独立的运行时刻函数库的实例。我们常说的本地堆是指由运行时刻函数库的一个特殊实例来进行管理的堆。由此,所有 C 运行时刻函数库中内存调试函数的行为都将本地堆为基础来进行定义。 GlobalAlloc 和 GlobalFree 函数是从 16位 Windows中继承下来的。它们基本上是对 HeapAlloc 和 HeapFree 函数做了向后兼容的包装,因为它们除了从默认堆中进行再分配以外,计划没有多做任何其它的事情。 Windows 对内存调试提供的支持是十分简单的。Windows 提供了 IsBadCodePtr、IsBadReadPtr、IsBadStringPtr 和 IsBadWritePrt 应用程序接口函数来帮助用户判断各种各样的类型指针是否合法。在用户觉得十分艰难的时候,可以使用 HeapWalk应用程序接口函数来检查一个堆中的内容。出去这些函数,如果要调试从 Windows 那里直接获取的内存,用户在相当多的时候需要自己独立完成。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-22 23:07 编辑
Visual C++ 运行时刻函数库内存调试 Visual C++ 的 C 运行时刻函数库提供了广泛的功能,帮助用户检测动态内存分配的内存错误和内存泄漏。这些功能仅仅在调试版本中提供,因此她对发布版本没有影响,这样也就不会造成性能上的损失。 VC 的 C 运行时刻函数库可以帮助用户使用许多方法堆内存错误进行调试。最主要的一种帮助就是对已经分配或者释放的内存写入确定的字节作为标识,以帮助暴露程序中的错误: 0xCD 已经分配的数据 0xDD 已经释放的数据 0xFD 被保护的数据 0xCD 标识被用来填充那些最近分配的内存区域,而 0xDD 被用来填充已经释放的内存,保护字节被写入在被保护内存区域的开始和结束的四个字节,以帮助检测上溢出和下溢出。举例来说,在下面所列出的代码执行以后,pData 被设为堆中的一个地址,*pData 被设为 0xCDCDCDCD,fence1和fence2被设为 0xFDFDFDFD。 float *pData=new float; int fence1=*((int*)pData)-1); int fence2=*(int*)(((char*)pData)+sizeof(float)); 而当下面的代码被执行以后, delete pData; *pData 接着被设定为 0xDDDDDDDD。 如果你没有选中调试堆选项中的 _GRTDBG_DELAY_FREE_MEM_DF,而且重新计算 fence1 和 fence2 的值,就会发现它们也被设成了 0xDDDDDDDD。这是因为在内部数据结构中也使用了释放数据的字节标识。 另一种帮助调试内存错误的方法就是 C 运行时刻函数库使用内存块类型标识符,将内存划分为五种块类型,这确定了内存信息是如何被跟踪和报告的。 _NORMAL_BLOCK 由程序直接分配的内存 _CLIENT_BLOCK 由程序直接分配的内存,可以通过内存调试函数对其拥有特殊控制权。 _CRT_BLOCK 由运行时刻函数库内部分配的内存 _FREE_BLOCK 已经被释放,但是跟踪仍然被保留下来的内存。 _IGNORE_BLOCK 当使用 _CrtDbgFlag 关闭内存调试操作以后分配的内存 还需要注意的一点是,所有这些函数的使用范围仅仅是在调试版本中,因此不要期望一个象 _CrtIsValidPointer 这样的函数能够在发布版本中工作。对于发布版本,可以使用应用程序接口函数 IsBadReadPtr 和 IsBadWritePtr 作为替代。与之类似,在发布版本中,使用 _heapchk 函数代替了 _CrtCheckMemory。下面列出了一些对调试内存泄漏有用的运行时刻函数库函数。 Visual C++ 的C运行时刻函数库提供的帮助调试内存错误的函数 _CrtCheckMemory 检查每一个内存块的内部数据结构和守护字节,以测试其完整性。 _CrtIsValidHeapPointer 检验指定指针是否存在于本地堆中 _CrtIsValidPonter 检验给定内存范围对读写操作是否合法 _CrtIsMemoryBlock 检验给定内存范围是否位于本地堆当中,是否拥有例如 _NORMAL_BLOCK 这样的内存块 类型标识符 拥有调试内存泄漏的 Visual C++ 的 C 运行时刻函数库中的函数 _CrtSetBreakAlloc 在给定的分配数目上分配断点,每一块被分配的内存都被指派一个连续 的分配号。 _CrtDumpMemoryLeaks 判断一个内存泄漏是否发生。 _CrtMemCheckPoint 在 _CrtMemState 结构中产生一个本地堆的当前状态快照 _CrtMemDifference 比较两个堆中的断点,并将不同之处保存在 _CrtMemState 结构中。 _CrtMemDumpallObjectsSince将从给定断点或者从程序头开始分配的内存的所有信息按照用户可以 阅读的方式进行内存映像转储 _CrtMemDumpStatistics 将信息按照用户可以阅读的方式进行内存映像转储到一个 _CrtmemState 结构中。 用于一般内存调试的Visual C++ 的C运行时刻函数库中的函数 _CrtSetDbgFlag 控制内存调试函数的行为 _CrtSetAllocHook 加载一个在内存分配过程中的钩子函数。 _CrtSetReportHook 加载一个进行定制报告处理的函数。 _CrtSetDumpClient 加载一个对用户进行内存映像转储的函数。 _CrtDoForAllClientObject 对于所有作为用户块进行分配的数据,调用指定的函数。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-23 10:20 编辑
MFC 内存调试 由MFC控制的内存有一个不同,那就是 new 函数被设定为默认在失败时抛出一个异常。在 MFC 中,用户必须通过使用 AfxSetNewHandler 而不是 _set_new_handler 改变分配失败时候的行为。默认处理异常的函数是 AfxNewHandler,它会抛出类型为 CMemoryException 的异常。在十分必要的情况下,用户可以通过调用 AfxSetNewHandler(0)来使new 在失败的时候返回一个空指针。同样,在 MFC 中,用户可以直接调用 AfxThrowMemoryException 函数来抛出一个内存异常。 由 MFC 控制的内存还有另外一个不同,那就是 MFC 重载了 CObject::operator new 和 CObject::operator delete,这样就给所有由 CObject 派生的对象加上了一个 _CLIENT_BLOCK 的内存表示符。这就可以允许 MFC 调用 _CrtSetDumpClient 来加载 _AfxCrtDumpClient 函数,这样使用 CObject::Dump 虚函数就可以对有效的从 CObject 派生的对象进行内存映像转储,用户可以由此得到更有用的内存错误分析信息。 然而,在默认情况下,_AfxCrtDumpclient 函数并不会调用虚拟函数 CObject::Dump,为了使用内存映像转储虚拟函数,用户必须向自己的代码中添加下列代码,而且最后是添加在初始化的地方 #ifdef _DEBUG afxDump.setDepth(1); #endif 很多内存映像转储函数还使用了深度值来决定是使用浅度策略还是深度策略。例如,类 CobList 采用下面的方法实现内存映像转储: void CObList::dump(Cdumpcontext& dc)const{ CObject::Dump(dc); dc<<"with"< if(dc.GetDepth()>0){ POSITION pos=getHeadPosition(); while (pos !=NULL) dc<<"nt"< } dc<<"n"; } _AfxCrtDumpClient 函数会将所有内容内存映像转储到 AfxDump 以及 MFC 与定义的全局变量 CDumpContext。这表明 MFC 的调试堆输出由 VC 跟踪工具进行控制。如果你没有得到预期的内存映像转储信息,那么运行跟踪程序,确信打开了 Enable tracing 选项。 MFC 到 C运行时刻函数库的内存调试函数转换手册 AfxCheckMemory _CrtCheckMemory AfxDoForAllObjects _CrtDoforAllClientObjects AfxDumpMemoryLeaks _CrtDumpMemoryLeaks AfxIsMemoryBlock _CrtIsMemoryBlock CMemoryState::Checkpoint _CrtMemCheckpoint CMemoryState::Difference _CrtMemdifference CMemoryState::DumpAllObjectsSince _CrtMemDumpAllObjectsSince CMemoryState::DumpStatistics _CrtMemDumpstatistics AfxSetAllocStop _CrtSetbreakAlloc AfxenableMemorytracking _CrtSetDbgFlag afxMemDF _CrtSetDbgFlag AfxSetNewHandler _set_new_handler |
|
|
|
|
|
本帖最后由 friend0720 于 2016-7-7 10:52 编辑
VC++内存泄漏定位 [转]转:http://hi.baidu.com/yongyongjijip/blog/item/7d93b34d5f18d3f7d72afcf6.html 第一种:通过"OutPut窗口"定位引发内存泄漏的代码 我们知道,MFC程序如果检测到存在内存泄漏,退出程序的时候会在调试窗口提醒内存泄漏。例如: class CMyApp : public CWinApp { public : BOOL InitApplication() { int * leak = new int [ 10 ]; return TRUE; } }; 产生的内存泄漏报告大体如下: Detected memory leaks ! Dumping objects -> c:/work/test.cpp( 186 ) : { 52 } normal block at 0x003C4410 , 40 bytes long . Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 这挺好。问题是,如果我们不喜欢MFC,那么难道就没有办法?或者自己做? 呵呵,这不需要。其实,MFC也没有自己做。内存泄漏检测的工作是VC++的C运行库做的。也就是说,只要你是VC++程序员,都可以很方便地检测内存泄漏。我们还是给个样例: #include < crtdbg.h > inline void EnableMemLeakCheck() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); } void main() { EnableMemLeakCheck(); int * leak = new int [ 10 ]; } 运行(提醒:不要按Ctrl+F5,按F5),你将发现,产生的内存泄漏报告与MFC类似,但有细节不同,如下: Detected memory leaks ! Dumping objects -> { 52 } normal block at 0x003C4410 , 40 bytes long . Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 为什么呢?看下面。 定位内存泄漏由于哪一句话引起的你已经发现程序存在内存泄漏。现在的问题是,我们要找泄漏的根源。 一般我们首先确定内存泄漏是由于哪一句引起。在MFC中,这一点很容易。你双击内存泄漏报告的文字,或者在Debug窗口中按F4,IDE就帮你定位到申请该内存块的地方。对于上例,也就是这一句: int* leak = new int[10]; 这多多少少对你分析内存泄漏有点帮助。特别地,如果这个new仅对应一条delete(或者你把delete漏写),这将很快可以确认问题的症结。 我们前面已经看到,不使用MFC的时候,生成的内存泄漏报告与MFC不同,而且你立刻发现按F4不灵。那么难道MFC做了什么手脚? 其实不是,我们来模拟下MFC做的事情。看下例: inline void EnableMemLeakCheck() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); } #ifdef _DEBUG #define new new(_NORMAL_BLOCK, __FILE__, __LINE__) #endif void main() { EnableMemLeakCheck(); int * leak = new int [ 10 ]; } 再运行这个样例,你惊喜地发现,现在内存泄漏报告和MFC没有任何分别了。 第二种方法:直接定位指定内存块错误的代码行(下面转)。 单确定了内存泄漏发生在哪一行,有时候并不足够。特别是同一个new对应有多处释放的情形。在实际的工程中,以下两种情况很典型: 创建对象的地方是一个类工厂(ClassFactory)模式。很多甚至全部类实例由同一个new创建。对于此,定位到了new出对象的所在行基本没有多大帮助。 COM对象。我们知道COM对象采用Reference Count维护生命周期。也就是说,对象new的地方只有一个,但是Release的地方很多,你要一个个排除。 那么,有什么好办法,可以迅速定位内存泄漏? 答:有。 在内存泄漏情况复杂的时候,你可以用以下方法定位内存泄漏。这是我个人认为通用的内存泄漏追踪方法中最有效的手段。 我们再回头看看crtdbg生成的内存泄漏报告: Detected memory leaks ! Dumping objects -> c:/work/test.cpp( 186 ) : { 52 } normal block at 0x003C4410< 内存泄漏定位 今天调试程序,发现有内存泄漏但是没有提示具体是哪一行,搞得我很头疼。结果在网上搜索了一些资料,经自己实践后整理如下: 第一种:通过"OutPut窗口"定位引发内存泄漏的代码(下面转,我写的没原文好,也懒得写)。 我们知道,MFC程序如果检测到存在内存泄漏,退出程序的时候会在调试窗口提醒内存泄漏。例如:class CMyApp : public CWinApp{public: BOOL InitApplication() { int* leak = new int[10]; return TRUE; }}; 产生的内存泄漏报告大体如下:Detected memory leaks!Dumping objects ->c:/work/test.cpp(186) : {52} normal block at 0x003C4410, 40 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 这挺好。问题是,如果我们不喜欢MFC,那么难道就没有办法?或者自己做? 呵呵,这不需要。其实,MFC也没有自己做。内存泄漏检测的工作是VC++的C运行库做的。也就是说,只要你是VC++程序员,都可以很方便地检测内存泄漏。我们还是给个样例:#include inline void EnableMemLeakCheck(){ _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);}void main(){ EnableMemLeakCheck(); int* leak = new int[10];} 运行(提醒:不要按Ctrl+F5,按F5),你将发现,产生的内存泄漏报告与MFC类似,但有细节不同,如下:Detected memory leaks!Dumping objects ->{52} normal block at 0x003C4410, 40 bytes long. Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 为什么呢?看下面。 定位内存泄漏由于哪一句话引起的你已经发现程序存在内存泄漏。现在的问题是,我们要找泄漏的根源。一般我们首先确定内存泄漏是由于哪一句引起。在MFC 中,这一点很容易。你双击内存泄漏报告的文字,或者在Debug窗口中按F4,IDE就帮你定位到申请该内存块的地方。对于上例,也就是这一句: int* leak = new int[10]; 这多多少少对你分析内存泄漏有点帮助。特别地,如果这个new仅对应一条delete(或者你把delete漏写),这将很快可以确认问题的症结。 我们前面已经看到,不使用MFC的时候,生成的内存泄漏报告与MFC不同,而且你立刻发现按F4不灵。那么难道MFC做了什么手脚? 其实不是,我们来模拟下MFC做的事情。看下例: inline void EnableMemLeakCheck(){ _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF);}#ifdef _DEBUG#define new new(_NORMAL_BLOCK, __FILE__, __LINE__)#endifvoid main(){ EnableMemLeakCheck(); int* leak = new int[10];} 再运行这个样例,你惊喜地发现,现在内存泄漏报告和MFC没有任何分别了。 第二种方法:直接定位指定内存块错误的代码行(下面转)。 单确定了内存泄漏发生在哪一行,有时候并不足够。特别是同一个new对应有多处释放的情形。在实际的工程中,以下两种情况很典型: 创建对象的地方是一个类工厂(ClassFactory)模式。很多甚至全部类实例由同一个new创建。对于此,定位到了new出对象的所在行基本没有多大帮助。 COM对象。我们知道COM对象采用Reference Count维护生命周期。也就是说,对象new的地方只有一个,但是Release的地方很多,你要一个个排除。 那么,有什么好办法,可以迅速定位内存泄漏?答:有。在内存泄漏情况复杂的时候,你可以用以下方法定位内存泄漏。这是我个人认为通用的内存泄漏追踪方法中最有效的手段。我们再回头看看crtdbg生成的内存泄漏报告: Detected memory leaks!Dumping objects ->c:/work/test.cpp(186) : {52} normal block at 0x003C4410< Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD Object dump complete. 除了产生该内存泄漏的内存分配语句所在的文件名、行号为,我们注意到有一个比较陌生的信息:{52}。这个整数值代表了什么意思呢? 其实,它代表了第几次内存分配操作。象这个例子,{52}代表了第52次内存分配操作发生了泄漏。你可能要说,我只new过一次,怎么会是第52次?这很容易理解,其他的内存申请操作在C的初始化过程调用的呗。:) 有没有可能,我们让程序运行到第52次内存分配操作的时候,自动停下来,进入调试状态?所幸,crtdbg确实提供了这样的函数:即 long _CrtSetBreakAlloc(long nAllocID)。我们加上它: inline void EnableMemLeakCheck() { _CrtSetDbgFlag(_CrtSetDbgFlag(_CRTDBG_REPORT_FLAG) | _CRTDBG_LEAK_CHECK_DF); } #ifdef _DEBUG #define new new(_NORMAL_BLOCK, __FILE__, __LINE__) #endif void main() { EnableMemLeakCheck(); _CrtSetBreakAlloc( 52 ); int * leak = new int [ 10 ]; } 你发现,程序运行到 int * leak = new int [ 10 ]; 一句时,自动停下来进入调试状态。细细体会一下,你可以发现,这种方式你获得的信息远比在程序退出时获得文件名及行号有价值得多。因为报告泄漏文件名及行号,你获得的只是静态的信息,然而_CrtSetBreakAlloc则是把整个现场恢复,你可以通过对函数调用栈分析(我发现很多人不习惯看函数调用栈,如果你属于这种情况,我强烈推荐你去补上这一课,因为它太重要了)以及其他在线调试技巧,来分析产生内存泄漏的原因。通常情况下,这种分析方法可以在 5分钟内找到肇事者。 当然,_CrtSetBreakAlloc要求你的程序执行过程是可还原的(多次执行过程的内存分配顺序不会发生变化)。这个假设在多数情况下成立。不过,在多线程的情况下,这一点有时难以保证。 个人心得:我在用这种方法时开始没看懂,后来在MSDN中也找到了这方面相关的信息,后来才会用。我感觉在这方面网上介绍的不够详细,下面我就相对详细地解释一下(为什么用“相对详细”?本人比较懒)。首先说明一下,下面的函数不需要上面所添加的宏定义和"crtdbg.h" 头文件,也不需要EnableMemLeakCheck()函数。只需在main函数一开始运行 _CrtSetBreakAlloc(long (4459))函数。其中4459是申请内存的序号(上面有说明),然后F5运行(不需要设断点),然后会出现“Find Source”这个对话框,点击“取消”。然后会出现“User breakpoint called from code at xxxx”的对话框,点击“确定”,会看到一些汇编的代码(不要怕,其实我也看不懂,算然原来学过点汇编),调出堆栈窗口(call stack),在其中的“main() line xxx + xxx bytes”上双击(或它的上一行双击,我的上一行是一个自定义函数,双击后直接定位到我new的地方,定位还是很准的,开始我怀疑,但最后检查果然是这地方没释放)会定位到错误行。 第三种:用Ctrl+B来设定,不过现在好像忘了。效果根第二种方法基本一样。 有人会问,既然第一种方法定位没问题,为什么还要介绍第二种?其实在实际应用中,某些内存泄漏它没有定位到哪一行的,只有内存块的序号(有可能我用的不太会用),这个时候就需要用第二种方法。 |
|
|
|
|
|
本帖最后由 friend0720 于 2016-3-13 21:41 编辑
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|
|
|
|
|
STC32G8K64 单片机 的P00(ADC8)脚短路到GND 会死机,怎么解决?
377 浏览 1 评论
嵌入式学习-飞凌嵌入式ElfBoard ELF 1板卡-Linux C接口编程入门之ioctl操作
782 浏览 0 评论
《DNK210使用指南 -CanMV版 V1.0》第十七章 machine.WDT类实验
525 浏览 0 评论
1134 浏览 0 评论
嵌入式学习-飞凌嵌入式ElfBoard ELF 1板卡-通用文件I/O模型之close
1291 浏览 0 评论
【youyeetoo X1 windows 开发板体验】少儿AI智能STEAM积木平台
11486 浏览 31 评论
小黑屋| 手机版| Archiver| 电子发烧友 ( 湘ICP备2023018690号 )
GMT+8, 2024-10-19 12:46 , Processed in 0.904305 second(s), Total 74, Slave 65 queries .
Powered by 电子发烧友网
© 2015 bbs.elecfans.com
关注我们的微信
下载发烧友APP
电子发烧友观察
版权所有 © 湖南华秋数字科技有限公司
电子发烧友 (电路图) 湘公网安备 43011202000918 号 电信与信息服务业务经营许可证:合字B2-20210191 工商网监 湘ICP备2023018690号