热门标签 | 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开发调试的开发人员,很有必要去掌握上述调试技巧的。在掌握这些调试手段与技巧之后,能让我们在排查问题时更加的高效。大家如果有什么好的调试方法和技巧,可以在评论区一起讨论。


推荐阅读
  • Go Cobra命令行工具入门教程
    本文介绍了Go语言实现的命令行工具Cobra的基本概念、安装方法和入门实践。Cobra被广泛应用于各种项目中,如Kubernetes、Hugo和Github CLI等。通过使用Cobra,我们可以快速创建命令行工具,适用于写测试脚本和各种服务的Admin CLI。文章还通过一个简单的demo演示了Cobra的使用方法。 ... [详细]
  • 本文讨论了如何优化解决hdu 1003 java题目的动态规划方法,通过分析加法规则和最大和的性质,提出了一种优化的思路。具体方法是,当从1加到n为负时,即sum(1,n)sum(n,s),可以继续加法计算。同时,还考虑了两种特殊情况:都是负数的情况和有0的情况。最后,通过使用Scanner类来获取输入数据。 ... [详细]
  • 本文讨论了使用差分约束系统求解House Man跳跃问题的思路与方法。给定一组不同高度,要求从最低点跳跃到最高点,每次跳跃的距离不超过D,并且不能改变给定的顺序。通过建立差分约束系统,将问题转化为图的建立和查询距离的问题。文章详细介绍了建立约束条件的方法,并使用SPFA算法判环并输出结果。同时还讨论了建边方向和跳跃顺序的关系。 ... [详细]
  • C语言注释工具及快捷键,删除C语言注释工具的实现思路
    本文介绍了C语言中注释的两种方式以及注释的作用,提供了删除C语言注释的工具实现思路,并分享了C语言中注释的快捷键操作方法。 ... [详细]
  • 本文介绍了Oracle数据库中tnsnames.ora文件的作用和配置方法。tnsnames.ora文件在数据库启动过程中会被读取,用于解析LOCAL_LISTENER,并且与侦听无关。文章还提供了配置LOCAL_LISTENER和1522端口的示例,并展示了listener.ora文件的内容。 ... [详细]
  • Python正则表达式学习记录及常用方法
    本文记录了学习Python正则表达式的过程,介绍了re模块的常用方法re.search,并解释了rawstring的作用。正则表达式是一种方便检查字符串匹配模式的工具,通过本文的学习可以掌握Python中使用正则表达式的基本方法。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • mysql-cluster集群sql节点高可用keepalived的故障处理过程
    本文描述了mysql-cluster集群sql节点高可用keepalived的故障处理过程,包括故障发生时间、故障描述、故障分析等内容。根据keepalived的日志分析,发现bogus VRRP packet received on eth0 !!!等错误信息,进而导致vip地址失效,使得mysql-cluster的api无法访问。针对这个问题,本文提供了相应的解决方案。 ... [详细]
  • 开发笔记:实验7的文件读写操作
    本文介绍了使用C++的ofstream和ifstream类进行文件读写操作的方法,包括创建文件、写入文件和读取文件的过程。同时还介绍了如何判断文件是否成功打开和关闭文件的方法。通过本文的学习,读者可以了解如何在C++中进行文件读写操作。 ... [详细]
  • IOS开发之短信发送与拨打电话的方法详解
    本文详细介绍了在IOS开发中实现短信发送和拨打电话的两种方式,一种是使用系统底层发送,虽然无法自定义短信内容和返回原应用,但是简单方便;另一种是使用第三方框架发送,需要导入MessageUI头文件,并遵守MFMessageComposeViewControllerDelegate协议,可以实现自定义短信内容和返回原应用的功能。 ... [详细]
  • Postgresql备份和恢复的方法及命令行操作步骤
    本文介绍了使用Postgresql进行备份和恢复的方法及命令行操作步骤。通过使用pg_dump命令进行备份,pg_restore命令进行恢复,并设置-h localhost选项,可以完成数据的备份和恢复操作。此外,本文还提供了参考链接以获取更多详细信息。 ... [详细]
  • 本文介绍了使用Spark实现低配版高斯朴素贝叶斯模型的原因和原理。随着数据量的增大,单机上运行高斯朴素贝叶斯模型会变得很慢,因此考虑使用Spark来加速运行。然而,Spark的MLlib并没有实现高斯朴素贝叶斯模型,因此需要自己动手实现。文章还介绍了朴素贝叶斯的原理和公式,并对具有多个特征和类别的模型进行了讨论。最后,作者总结了实现低配版高斯朴素贝叶斯模型的步骤。 ... [详细]
  • 微信官方授权及获取OpenId的方法,服务器通过SpringBoot实现
    主要步骤:前端获取到code(wx.login),传入服务器服务器通过参数AppID和AppSecret访问官方接口,获取到OpenId ... [详细]
  • 展开全部下面的代码是创建一个立方体Thisexamplescreatesanddisplaysasimplebox.#Thefirstlineloadstheinit_disp ... [详细]
  • SpringBoot整合SpringSecurity+JWT实现单点登录
    SpringBoot整合SpringSecurity+JWT实现单点登录,Go语言社区,Golang程序员人脉社 ... [详细]
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社区 版权所有