热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

VisualStudio高效调试手段与技巧总结(经验分享)

本文详细讲述VisualStudio常用的

目录

1、对0xCCCCCCCC、0xCDCDCDCD和0xFEEEFEEE等常见异常值的辨识度

2、在Debug下遇到报错弹框,点击重试,查看函数调用堆栈

3、调试时程序和调试器都发生了闪退,可以尝试到Output窗口中找线索

4、调用OutputDebugString接口,将打印日志输出到调试器输出窗口中

5、调用被废弃的API函数IsBadReadPtr和IsBadWritePtr,可能会导致VS调试时产生异常 

6、VS自带的异常中断选项

7、条件断点与临时过滤条件

8、巧用数据断点

9、将VS附加到目标上调试

10、使用VS和IDA查看汇编代码

11、最后


       用了多年的Visual Studio,很多人是不是还没完全掌握其有效的调试方法和技巧呢?是不是还在为无法快速定位问题而发愁呢?本文将根据多年使用Visual Studio的经验,带着大伙逐一认识和掌握Visual Studio多种实用的调试方法和技巧,帮助大家去高效地解决开发过程中遇到的多种难题。

1、对0xCCCCCCCC、0xCDCDCDCD和0xFEEEFEEE等常见异常值的辨识度

       我们在调试程序的过程中,当遇到变量的值为0xCCCCCCCC0xCDCDCDCD0xFEEEFEEE等特殊的异常值时,我们应立即察觉可能是程序出问题了。常见的异常值如下所示:

这些值是微软调试器故意在某些场景下设置的特殊值,便于程序员快速地察觉到可能存在的问题。其中,0xCCCCCCC是微软调试器在Debug下填充到未初始化的栈内存中的值,当字符串看就是 “烫烫烫烫……”;0xCDCDCDCD是微软调试器在Debug下填充到未初始化的堆内存中的值,当字符串看就是 “屯屯屯屯……”;0xFEEEFEEE是填充到已经释放的堆内存中的值。

       所以遇到值为0xCCCCCCC或者0xCDCDCDCD的变量时,可能是这些变量没有初始化就被访问或使用了,这是非法的,所以大家平时要养成初始化变量的习惯。当遇到值为0xFEEEFEEE的变量时,有可能是该变量占用的内存已经被释放了,不能再使用了,再使用就会导致内存访问违例。

2、在Debug下遇到报错弹框,点击重试,查看函数调用堆栈

       我们在Debug下调试代码的过程中,会时常遇到这样的报错弹框:

其实很简单,只要点击重试按钮,调试器就会中断下来,然后打开调用堆栈(call stack)窗口去查看此时的函数调用堆栈,通过函数调用堆栈就能大概看出是执行了什么操作引发了异常。

       但有时光看函数调用堆栈,可能并不能看出问题所在,我们还可以尝试去看Output输出窗口中输出的信息,编译器和程序可能会将一些调试打印信息输出到Output窗口中,从打印信息中可能能找到线索。比如我们在使用duilib界面框架编写窗口的xml文件时,写了非法的xml节点:

从上图打印的信息可以看出,是duilib库在解析窗口的xml内容时产生了异常,duilib将有异常的xml节点附近的xml上下文打印到Output窗口中,通过这个打印我们就能找到xml中出问题的节点了。此问题是问题节点的开始符号“<”和结尾符“/>”不匹配导致的,具体地是我们在手动编写xml文件时手误写错导致的。

3、调试时程序和调试器都发生了闪退,可以尝试到Output窗口中找线索

       比如程序发生“stack overflow”线程栈溢出,有可能是函数递归调用触发的,也有可能是函数之间发生死循环调用触发的,也有可能是函数中的case语句分支过多导致的,只要发生了线程栈溢出的异常,程序就会直接闪退,调试器也会直接退出调试状态,此时无法查看异常时的详细信息,查看不到异常时的函数调用堆栈,更查看不到发生异常时的变量的值。

        不过此时可以尝试到Output窗口中看看,看看能否找到一些线索,对于线程栈溢出时的闪退,会在Output窗口中输出“Stack overflow”异常提示信息,如下所示:

这样我们就知道发生线程栈溢出了。

       那我们如何才能找到发生线程栈溢出时的函数调用堆栈呢?下面就轮到强大的Windows调试器windbg上场了,可以重新运行程序,然后将windbg附加到程序的进程上,复现线程栈溢出的问题,一旦问题复现,windbg会立即检测到该stack overflow的异常,中断下来,在windbg输入kn、kv或kp命令,就可以查看此时的函数调用堆栈了。查看到函数的调用堆栈,就知道是什么操作引发的异常了。线程栈溢出我们已经遇到过多次了,在这方面比较有经验。

4、调用OutputDebugString接口,将打印日志输出到调试器输出窗口中

       我们可以在代码中调用Windows API函数OutputDebugString将打印日志输出到调试器的输出窗口中。VS中自带的TRACE宏,也可以将日志输出到调试窗口中,但TRACE宏只在Debug下才有用,Relase下是无效的。其实TRACE宏内部在Debug下也是调用API函数OutputDebugString的,在realse下该宏定义为空,TRACE宏的内部实现如下:

#define TRACE                       CONCRT_TRACE // Enable tracing mechanisms #if defined(_DEBUG) && defined(CONCRT_TRACING) # define CONCRT_TRACE(...)  ::Concurrency::details::_ConcRT_Trace(__VA_ARGS__) #else # define CONCRT_TRACE(...)  ((void)0) #endif // Trace -- Used for tracing and debugging void _ConcRT_Trace(     int trace_level,     const wchar_t * format,     ...     ) {     InitializeUtilityRoutines();     // Check if tracing is disabled     if ((g_DesiredTraceLevel & trace_level) == 0) {         return;     }     wchar_t buffer[1024+1];     va_list args;     va_start(args, format);     ConcRT_FillBuffer(buffer, format, args);     va_end(args);     buffer[1024] = 0;     if (g_DebugOutFilePtr != NULL)     {         fwprintf(g_DebugOutFilePtr, buffer);         if (g_CommitFrequency > 0 && (g_TraceCount++ % g_CommitFrequency) == 0)             fflush(g_DebugOutFilePtr);     }     else     {         OutputDebugStringW(buffer);     } }

系统API函数OutputDebugString无论是在Debug下还是在Release下都会生效,我们在使用VS调试时打印信息会输出到VS的输出窗口中,我们在使用windbg附加到进程上调试时打印信息会输出到Windbg的输出窗口中

       那OutputDebugString是如何将打印日志(字符串)输出到调试窗口中的呢?我们可以到reactos操作系统的开源代码中查看OutputDebugString的内部实现:(和Windows接口的内部实现基本一致的)

VOID WINAPI OutputDebugStringA(IN LPCSTR _OutputString) {     _SEH2_TRY     {         ULONG_PTR a_nArgs[2];         a_nArgs[0] = (ULONG_PTR)(strlen(_OutputString) + 1);         a_nArgs[1] = (ULONG_PTR)_OutputString;         /* send the string to the user-mode debugger */         RaiseException(DBG_PRINTEXCEPTION_C, 0, 2, a_nArgs);     }     _SEH2_EXCEPT(EXCEPTION_EXECUTE_HANDLER)     {         // 此处代码省略         // ......     } }

从上述代码可以看出,OutputDebugString内部调用了RaiseException接口产生了一个DBG_PRINTEXCEPTION_C异常(会产生了一个标准的EXCEPTION_RECORD异常结构),该异常会被发到内核的异常处理模块,然后由内核异常处理模块将该异常分发给用户态的调试器,调试器最终将打印日志显示到调试器窗口上。

       比如我们使用的webrtc开源库会将运行日志输出到调试窗口中,比如我们在使用windbg附加到进程中调试时,在windbg的输出窗口中可以看到webrtc库输出的运行日之后。后来查看webrtc库的代码得知,webrtc库内部也是调用OutputDebugString将打印日志输出到调试器的输出窗口中的,代码如下:

       有人可能会说,他们有完善的日志管理系统,不需要使用OutputDebugString函数。我们这个地方主要介绍这种输出日志的手段,大家可以根据自己的需要去选择。

5、调用被废弃的API函数IsBadReadPtr和IsBadWritePtr,可能会导致VS调试时产生异常 

       有人经常会问,有没有系统API能判断当前内存地址是否可读可写,这样我们在读写内存之前可以使用这样的API函数判断一下。Windows之前是提供过IsBadReadPtrIsBadWritePtr这样的API接口,但接口的返回值并不可信,而且这两个API函数现在已经废弃了,不用了。

MSDN上对该函数的说明
Important:This function is obsolete(废弃的) and should not be used. Despite its name, it does not guarantee that the pointer is valid or that the memory pointed to is safe to use. .

       有的第三方老的库可能还调用了这两个接口,在VS调试时可能会触发一个异常,但该异常不是致命的,可以直接跳过去的,程序可以继续详细运行。这种异常在使用VS或Windbg调试时都会引发中断,我们在日常工作中都遇到过。VS中只需要取消勾选“遇到此异常不再中断”即可跳过

windbg动态调试时只需要输入并执行g命令即可跳过去:(有时可能要go多次)

       所以,在用windbg附加调试运行的过程中,如果当前的中断是IsBadWritePtr、 IsBadReadPtr或者IsBadCodePtr接口触发的,那这个异常不是致命的,可以直接go跳过去,程序还能继续运行的。

6、VS自带的异常中断选项

       VS中可以去指定想要中断的异常类型或者特殊异常,当发生这些异常时调试器就会中断下来,此时就可以去查看函数调用堆栈,去分析是什么原因触发的异常了。点击VS菜单栏中的Debug->Exceptions,弹出异常选项勾选对话框:

VS默认只勾选了一部分选项,你可以勾选你想用到的选项。

       比如我们可以勾选c000008c Array bounds exceededc00000fd Stack overflow,这样VS在捕捉到这些异常时VS就会中断下来。上面讲到程序发生stack overflow异常时,程序会闪退,调试器也会直接退出调试状态,如果此处勾选了“c00000fd Stack overflow”选项,估计调试器会立即中断下来,这样我们就可以看到函数调用堆栈了,这个大家可以尝试一下。

7、条件断点与临时过滤条件

       VS中可以给断点设置过滤条件,满足条件后断点就会中断下来。添加条件断点的方法是,先添加一般断点,然后右键点击该断点,给断点设置条件,如下所示:

 一般我们主要使用“条件”、“命中次数”和“筛选器”三种类型。可以设置命中断点的表达式条件,也可以设置断点的命中次数(包含大于、等于和小于),也可以给断点设置筛选器。

       设置筛选器的界面如下:

在设置筛选条件时,可以设置线程id,这在多线程调试时比较有用。当然要事先打断点,将目标线程的id记录下来,然后再来设置过滤线程的条件。

       但条件断点中设置条件表达式比较苛刻,只能设置简单的表达式,很多复杂点的表达式都不支持,设置后会提示不能设置这样的条件。个人觉得还是人为添加临时的if过滤条件代码最灵活(断点设置在if语句的body体中),我们平时经常使用这种方法,比如我们想在duilib库的CEditUI编辑框控件的DoEvent接口中添加断点进行调试,这个控件很多窗口都会使用,但我们只想调试某个窗口中的CEditUI控件对象,如果在DoEvent接口中直接打断点,会中断很多次,所以我们在该接口中人为添加if过滤条件的代码,通过控件名称(每个窗口中的控件名称是不同的)将目标对象过滤出来:

然后将断点设置在if语句的body中。这种添加临时if过滤条件代码的方式很灵活,可以设置各种复杂的过滤条件。我们在日常工作中主要还是使用临时添加if过滤条件的方式。

8、巧用数据断点

       我们有时会遇到变量值无故被篡改,导致程序逻辑出现异常或产生崩溃,这类问题我们遇到好几次了。在最初排查问题时,搜索了操作变量的所有代码,仔细排查了都没问题,并添加了日志打印,运行程序后打印出来的日志显示也没问题,但变量还是被无缘无故地被篡改了!

       这种情况一般都是内存越界导致的,内存越界有全局内存越界、栈内存越界和堆内存越界,直接排查代码是很难找到问题的。遇到这种情况,应该第一时间想到数据断点,第一时间请数据断点上场。

       设置数据断点其实就是监控目标变量的内存,一旦内存被修改,就会命中该数据断点,调试器就会中断下来。设置数据断点是有技巧的,被监控的目标变量需要在被分配内存后才能被监控,一般在类的构造函数中先设置一般断点,命中断点后,目标监控变量就分配了内存,就可以通过表达式“&变量名”去设置数据断点了:(可以设置监控的内存长度)

一旦被监控的内存被修改,就会命中对应的数据断点,此时去查看函数调用堆栈就可以看到是什么代码篡改了目标变量的内存了。

       这个地方需要留意一下,正常的赋值也是修改内存中的内容,也会命中数据断点,所以要将正常的赋值和内存越界篡改内存的情况区别开来。

9、将VS附加到目标上调试

       很多时候直接调试源代码是最直接最有效的,除了使用VS直接调试代码之外,还支持将VS附加到目标进程上调试。这特别适用底层库(比如网络库、协议库、组件库等)的调试。

       一般情况下,当程序发生崩溃时,我们会取来程序异常捕获模块捕获到的保存异常上下文信息的dump文件,先用windbg对异常进行分析,在排查不出问题时,如果问题好复现,我们会建议发生崩溃的dll模块的维护组尝试去使用VS手动附加调试一下,让他们去看看到底是什么问题。

       附加调试之前,先将编译出来的dll库拷贝到exe程序的目录中,然后重新启动程序。再将打开待调试库代码的VS附加到目标进程上调试。如果要调试release版本(有的问题可能在debug下没问题,只在release下有问题,所以需要进行release版本的调试),则需要事先在工程属性中将优化关闭掉。

       VS附加调试的入口:点击VS菜单栏中的“调试->附件到进程”,打开附加到进程的窗口,如下:

然后选择要附加到的目标进程,点击确定就可以了。

       有人可能会问,直接将编译出来的dll库拷贝到目标exe程序的路径中,为啥能调试呢?其实本质上,函数变量符号及调试信息都是放置在pdb文件中的,拷贝过去的库文件中会默认写入其使用的pdb文件的绝对路径,不管你将dll库文件拷贝到本机的哪个位置,都能根据绝对路径找到pdb文件,都能加载到库的调试信息,所以都能进行调试的。如果手动将对应的pdb文件删除掉,就没法进行调试的。

       这个附加调试一般在目标进程启动后去附加,但有时我们可能需要调试程序启动时的初始化代码,但等程序启动起来后,初始化的代码都执行完了,都没有机会调试了。这里有个技巧,可以在被调试进程的main函数中添加一个messagebox,先将目标进程阻塞住,等待VS附加到进程中,然后再点击messagebox中的OK按钮继续运行程序,这样就能调试初始化的代码了。

10、使用VS和IDA查看汇编代码

       一般情况下,在分析软件异常时,我们会用windbg打开包含异常信息的dump文件或者将windbg附加到目标进程上去调试。关于如何使用windbg,可以参见之前写的这篇文章:

Windbg调试工具使用详解https://blog.csdn.net/chenlycly/article/details/120631007

       软件是崩溃在某一条汇编指令上,windbg中可以看到这条崩溃的汇编指令,但有时很难通过C++代码看出为啥会导致崩溃,这时就需要去查看对应模块的二进制文件(dll或exe)的汇编代码的上下文,看看到底为啥会导致那句汇编代码崩溃了。

       我们平时一般会使用反汇编工具IDA Pro去打开二进制文件,去查看二进制文件对应的汇编代码。关于如何使用IDA工具,可以参见之前写的这篇文章:

IDA反汇编工具使用详解https://blog.csdn.net/chenlycly/article/details/120635120

       其实我们也可以在VS中查看C++代码对应的汇编代码,先在要查看汇编代码的C++代码处添加断点,当命中断点时,点击右键,在弹出的右键菜单中点击“转到反汇编”菜单项:


即可看到C++代码对应的汇编代码了,这对学习C++语句的汇编代码也是很有好处的。切换到汇编代码页面后,我们也可以直接调试汇编代码

       在查看汇编代码时,可以将C++代码与汇编代码一起对照看,从而搞清楚很多场景下的汇编代码是怎么编写的,这对我们走读汇编代码的上下文很有帮助。当我们遇到读不懂的汇编代码时,我们会在VS中写一些C++测试代码,然后这些C++代码的汇编代码是什么样子的,通过类比就能搞清楚了。

       对于分析C++软件异常需要掌握哪些基本的汇编知识,可以查看我之前写的一篇文章:

分析C++软件异常需要掌握的汇编知识汇总(实战经验分享)https://blog.csdn.net/chenlycly/article/details/124758670

11、最后

       作为使用VS开发调试的开发人员,很有必要去掌握上述调试技巧的。在掌握这些调试手段与技巧之后,能让我们在排查问题时更加的高效。大家如果有什么好的调试方法和技巧,可以在评论区一起讨论。


推荐阅读
  • [转]doc,ppt,xls文件格式转PDF格式http:blog.csdn.netlee353086articledetails7920355确实好用。需要注意的是#import ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
  • 本文详细介绍了在 CentOS 7 系统中配置 fstab 文件以实现开机自动挂载 NFS 共享目录的方法,并解决了常见的配置失败问题。 ... [详细]
  • 本文回顾了作者初次接触Unicode编码时的经历,并详细探讨了ASCII、ANSI、GB2312、UNICODE以及UTF-8和UTF-16编码的区别和应用场景。通过实例分析,帮助读者更好地理解和使用这些编码。 ... [详细]
  • MicrosoftDeploymentToolkit2010部署培训实验手册V1.0目录实验环境说明3实验环境虚拟机使用信息3注意:4实验手册正文说 ... [详细]
  • 在分析Android的Audio系统时,我们对mpAudioPolicy->get_input进行了详细探讨,发现其背后涉及的机制相当复杂。本文将详细介绍这一过程及其背后的实现细节。 ... [详细]
  • 本文详细介绍了MySQL数据库的基础语法与核心操作,涵盖从基础概念到具体应用的多个方面。首先,文章从基础知识入手,逐步深入到创建和修改数据表的操作。接着,详细讲解了如何进行数据的插入、更新与删除。在查询部分,不仅介绍了DISTINCT和LIMIT的使用方法,还探讨了排序、过滤和通配符的应用。此外,文章还涵盖了计算字段以及多种函数的使用,包括文本处理、日期和时间处理及数值处理等。通过这些内容,读者可以全面掌握MySQL数据库的核心操作技巧。 ... [详细]
  • 在使用Eclipse进行调试时,如果遇到未解析的断点(unresolved breakpoint)并显示“未加载符号表,请使用‘file’命令加载目标文件以进行调试”的错误提示,这通常是因为调试器未能正确加载符号表。解决此问题的方法是通过GDB的`file`命令手动加载目标文件,以便调试器能够识别和解析断点。具体操作为在GDB命令行中输入 `(gdb) file `。这一步骤确保了调试环境能够正确访问和解析程序中的符号信息,从而实现有效的调试。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • 零拷贝技术是提高I/O性能的重要手段,常用于Java NIO、Netty、Kafka等框架中。本文将详细解析零拷贝技术的原理及其应用。 ... [详细]
  • 在软件开发过程中,经常需要将多个项目或模块进行集成和调试,尤其是当项目依赖于第三方开源库(如Cordova、CocoaPods)时。本文介绍了如何在Xcode中高效地进行多项目联合调试,分享了一些实用的技巧和最佳实践,帮助开发者解决常见的调试难题,提高开发效率。 ... [详细]
  • 在《Cocos2d-x学习笔记:基础概念解析与内存管理机制深入探讨》中,详细介绍了Cocos2d-x的基础概念,并深入分析了其内存管理机制。特别是针对Boost库引入的智能指针管理方法进行了详细的讲解,例如在处理鱼的运动过程中,可以通过编写自定义函数来动态计算角度变化,利用CallFunc回调机制实现高效的游戏逻辑控制。此外,文章还探讨了如何通过智能指针优化资源管理和避免内存泄漏,为开发者提供了实用的编程技巧和最佳实践。 ... [详细]
  • 在Linux系统中避免安装MySQL的简易指南
    在Linux系统中避免安装MySQL的简易指南 ... [详细]
  • 类加载机制是Java虚拟机运行时的重要组成部分。本文深入解析了类加载过程的第二阶段,详细阐述了从类被加载到虚拟机内存开始,直至其从内存中卸载的整个生命周期。这一过程中,类经历了加载(Loading)、验证(Verification)等多个关键步骤。通过具体的实例和代码示例,本文探讨了每个阶段的具体操作和潜在问题,帮助读者全面理解类加载机制的内部运作。 ... [详细]
  • 本文详细解析了Java类加载系统的父子委托机制。在Java程序中,.java源代码文件编译后会生成对应的.class字节码文件,这些字节码文件需要通过类加载器(ClassLoader)进行加载。ClassLoader采用双亲委派模型,确保类的加载过程既高效又安全,避免了类的重复加载和潜在的安全风险。该机制在Java虚拟机中扮演着至关重要的角色,确保了类加载的一致性和可靠性。 ... [详细]
author-avatar
猥琐叔装嫩小孩
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有