8.2 高级调试技巧
8.2.1 远程调试
远程调试是Visual Studio的一个强大功能,当你的程序在其他计算机上运行时,可以通过远程调试来正常地调试程序。Xcode 4.2版本也有一个WiFi调试功能,类似于远程调试,但由于不稳定,在之后的版本中被移除了。
远程调试功能强大,但操作起来并不麻烦,首先需要在目标机器以管理员身份运行远程调试监视器,可以在Visual Studio的安装目录下找到,如图8-15所示。
图8-15 监视器目录
x64和x86分别对应64位和32位的操作系统,将目录复制到目标机器,然后运行目录下的msvsmon.exe,启动监视器后,可以在工具→选项中设置调试的端口、身份验证模式、空闲时间等属性,如图8-16所示。如果不在同一个内网,需要选择“无身份验证”模式,使用该模式是存在一定风险的。
图8-16 设置远程调试监视器
服务器就绪后,可以在Visual Studio中选择“调试”→“附加到进程”命令,在弹出的“附加到进程”对话框中,选择“传输”下拉列表框中的“远程(无身份验证)”选项,然后在“限定符”中输入远程的IP和端口,按Enter键之后会刷新出可以挂载的进程列表,再选择目标计算机中要调试的程序,即可进行调试,如图8-17所示。
图8-17 附加到远程进程
需要注意的是本地的代码和pdb文件需要与我们要调试的目标程序匹配,否则无法进行断点调试。所以在发布一个调试版本时,最好打一个分支或者将项目保存一份,然后不再修改。当不需要远程调试时,应该将远程调试监视器及时关闭,在需要调试时再开启。
8.2.2 coredump调试
如果不方便进行远程调试,而又希望获得程序崩溃时的堆栈等信息,那么可以使用Windows系统的coredump进行调试,Linux系统也有该机制。
在程序崩溃时会自动生成coredump, Windows系统下需要设置一下,在“系统属性”对话框中单击“启动和故障恢复”栏的“设置”按钮,在弹出对话框中选择“核心内存转储”选项,如图8-18所示。而Linux系统下则只需要执行一条ulimit -c unlimited命令即可。
图8-18 生成coredump
单击“写入调试信息”下的下拉按钮,可以选择“核心内存转储”,选择完之后还可以设置coredump文件存放的路径。也可以在Windows的任务管理器中,右击选择崩溃的进程,然后选择创建转储文件,如图8-19所示。
图8-19 生成转储文件
找到生成的DMP文件并双击,会启动Visual Studio,选择右侧的仅限本机进行调试按钮,Visual Studio会自动加载项目和pdb文件(项目不能修改或移动),然后就可以定位到程序崩溃时的堆栈了,读者可以自己写一段崩溃的代码测试一下。
8.2.3 使用Bugly捕获崩溃堆栈
前面介绍了Windows下崩溃堆栈的获取方法,但是Cocos2d-x做的并不是Windows游戏,而是手机游戏,所以这里介绍一个专门监控Android和iOS平台下崩溃信息的库Bugly。这是腾讯提供的一个第三方库,在Cocos2d-x下可以很方便地使用,网址为https://bugly.qq.com/cocossdk。官方文档很清楚地介绍了如何接入,以及如何使用。
登录Bugly可以看到所有的崩溃上报,有哪些地方崩溃了,崩溃了多少次,影响了多少个用户,用户的设备是什么型号,崩溃的时间,剩余的内存和磁盘空间等。在首页会有BUG列表,可以进行版本的筛选,只查看最新版本的崩溃信息,也可以只查看某渠道的崩溃信息,以及指定时间内的崩溃信息。
如图8-20是BUG的详情页面,左侧的列表为同样的BUG的多次上报记录,下方的“出错线程”面板显示了崩溃堆栈,除了C++的崩溃,Lua的崩溃也会有详细的堆栈,“系统日志”面板则上报了崩溃时输出的日志,使用CCLOG打印出的信息会被上报到这里(但日志条数有数量限制)。
图8-20 BUG详情页面
8.2.4 命中断点
断点是调试程序最核心的功能之一,当我们命中了一个断点之后,程序会中断,然而断点只能中断吗?当然不是,我们可以让断点在命中之后不中断,而是执行某些操作,如打印出当前的某些变量或者堆栈等。Visual Studio中可以在断点上右击选择命中条件,会弹出如图8-21所示对话框,在其中选择“打印信息”后可以输入所要打印的信息。
图8-21 命中断点设置
我们可以输入$开头的特殊关键字,如$PID、$CALLSTACK等,也可以输入一个变量或表达式,用{}包裹住,命中断点时会打印出变量或表达式的值。选择“继续执行”可以让断点命中之后继续执行,而不是中断。
需要注意的是设置了命中断点之后,程序运行的效率会降低不少,而且前面设置的条件断点会失效,但可以在代码中添加一个条件判断,在条件判断成功后执行一行代码,在这行代码中设置命中条件,也可以起到过滤的作用。
Xcode在命中断点之后可以执行的处理更为丰富,而且与命中条件并不冲突,在命中断点之后,可以执行以下动作。
❑ AppleScript执行一段Apple脚本,AppleScript是Apple推出的一门强大的脚本语言。
❑ Capture GPU Frame捕获当前GPU所绘制的帧,用于辅助图形调试。
❑ Debugger Command可以执行lldb的调试命令,如使用bt命令输出当前堆栈。
❑ Log Message可以在调试窗口输出一个消息,可以用@var@来打印表达式。
❑ Shell Command可以执行一个shell指令。
❑ Sound可以让Xcode播放一个系统声音。
如图8-22所示,通过单击右侧的“+”按钮,可以添加多个Action。选中最下方Options后的复选框,还可以让断点命中之后继续执行,而不是中断。
图8-22 Xcode编辑断点
8.2.5 数据断点
Visual Studio的数据断点可以设置一个地址,当这个地址对应的内存被修改时断住。当我们发现某个地址被莫名其妙地修改时,就可以借助数据断点来定位问题,选择“调试”→“新建断点”→新建数据断点”命令,可以打开数据断点的设置对话框,如图8-23所示。
图8-23 数据断点
有时候我们会碰到一种越界访问的BUG,当出现了这样的BUG,并且程序没有立刻崩溃时,问题就变得很隐蔽了,可能会在任何正常的地方崩溃,如一个vector的push_back()方法,并且同一个问题导致的崩溃可能每一次都不一样,如果没有经验,那么这种BUG解决起来就非常痛苦了。
由于C/C++操作指针或者数组时很容易越界,越界之后访问的可能是某个类的内部结构,当对越界的内存进行写入操作时,就会破坏这些类的内部结构,从而导致崩溃。由于崩溃的地方与崩溃的原因毫无关系,所以这类问题比较难以解决,当发现不应该崩溃的代码莫名其妙地崩溃时,就要看看是否出现了越界操作。下面是常见的越界操作。
//首先定义了数组,然后对数组进行初始化,实际上这个初始化是对a的第65个元素赋值,很有 隐蔽性 char a[64]; a[64] = { 0 }; //内存复制,dst没有足够的内存空间或复制的长度错误,会覆盖dst后面的内存 memcpy(dst, src, sizeof(src)); //还是内存复制,原本应该复制到buffer->data的,但是直接复制到了buffer buffer->data = new char[len]; memcpy(buffer, src, len); //字符串格式化,参数传漏或缓冲区不够大,都可能导致溢出,应该使用snprintf sprintf(buf, "%s, %d, %s", str1, num1);
除了使用数据断点监视指定的内存是否被修改,还可以在崩溃处逆推,检查前方的代码是否存在类似上述的问题,特别是指针相关的操作。
另外还可以用排除法,屏蔽掉部分代码来分析问题,还可以通过svn或git来分析是哪个版本提交的代码之后导致的问题,缩小查找范围。利用这些经验,以后再碰到了莫名其妙的问题之后就不至于手足无措了。
8.2.6 即时窗口
即时窗口是Visual Studio提供的一个实时调试窗口,可以在即时窗口输入指令来打印变量、执行语句以及计算表达式等,如图8-24所示。
图8-24 即时窗口
之所以提供这么一个即时窗口,是因为其真的非常灵活,例如,当我们希望分析一块内存,这块内存是由各种数据结构组成的,通过即时窗口可以很方便地检查这些结构的赋值是否正确,如服务器下发了一块内存数据,对应的一系列结构体。此外,在分析的时候还可以执行各种表达式和语句,包括直接执行一些成员函数,用于对代码进行单元测试也是非常不错的。
在Xcode下的输出窗口,也属于可以实时进行调试的窗口,它是一个lldb命令行窗口,lldb类似GDB,是一个强大的命令行调试工具,详情读者可以查阅官方的这篇lldb初学者教程,网址为http://lldb.llvm.org/tutorial.html。
8.2.7 多线程调试
当调试多线程程序时,它们执行的同一段代码可能会被多次中断,如果希望只针对某一个线程进行调试,则可以在断点上右击,在快捷菜单中选择“添加断点筛选器”命令,在弹出的对话框中设置线程ID的筛选条件,如图8-25所示。
图8-25 设置断点筛选器
8.2.8 性能调试
Visual Studio和Xcode都提供了强大的性能分析工具,帮助解决性能问题,Visual Studio中可以使用菜单上的“分析”→“性能与诊断”命令,运行性能向导,如图8-26所示。
图8-26 性能与诊断
单击“开始”按钮,然后一直选择下一步,就会将程序运行起来,运行一段时间结束程序后,Visual Studio会自动生成分析报告。
我们可以选择CPU采样,也可以选择检测函数执行的耗时,单击生成的分析报告,可以查看耗时最多的函数,并标识出函数对应的代码视图。通过这个视图,可以轻易地分析出哪些方法的性能消耗比较大,从而有针对性地进行优化。双击左右两侧的蓝色函数窗口,可以在函数堆栈中上下切换,观察消耗,如图8-27所示。
图8-27 性能分析详情
Xcode的性能分析工具更加强大,Xcode的Instruments提供了一系列的分析工具,除了性能分析之外,还有内存泄漏、GPU、动画、网络、系统I/O等一系列的分析工具,在Xcode中选择Product→Profile命令,会弹出如图8-28所示的界面,在界面中选择Time Profiler。
图8-28 Instruments
在Xcode菜单中选择Open Developer Tools→Instruments命令,也可以打开Instruments界面,但在这里打开只能附加在已运行的进程中,而通过Product→Profile这种方式打开则是调试当前程序。
选择Time Profile之后,会弹出Time Profile的信息界面,如图8-29所示。单击左上角的红色圆圈按钮(不是关闭),之后会开始性能分析,然后改按钮变为黑色的方块形状,其旁边的按钮可以暂停程序。
图8-29 CPU性能分析
在CPU Usage的右侧会出现CPU使用的曲线图,曲线图直观地反映了程序运行时CPU的占用情况。曲线图的下方罗列了性能消耗最多的函数,单击这些函数我们可以一点一点打开,观察该函数执行的消耗点,也可以用鼠标在曲线图上单击,框选某一段时间,观察指定时间内的性能消耗,默认统计的是整个程序运行周期中的性能消耗。