【游戏编程扯淡精粹】调试方法论
文章目录
【游戏编程扯淡精粹】调试方法论故障树分析法Bug跟踪系统 & Bug交流分享面对Bug的心态如何在工作中正确高效地修bug?找到原作者和模块相关负责人了解需求 异常弹窗只在Release版本产生的问题调试Release版本的高级技巧二分法assert 多线程问题堆栈损坏理解断点Visual Studio高级调试技巧watchparallel watch数据断点条件断点使用@err.hr获取错误码 map文件 & Windows PDB内存转储(core dump) 调试游戏程序的学问五步调试法高级调试技巧困难的情形和模式理解底层错误调试的基础设施预防bug Udacity调试课程如何避免bug:写出高质量的程序调试是监控代码的行为是否和你预期的一致不要让自己的栈太深 游戏日志系统功能需求性能调试本质是一种实验
/wiki/Experiment
实验的基本方法论,初中某本课本上就有:假设=》验证的循环
另一种角度是,调试是解决问题,而解决问题的本质是搜索
所以调试的快慢,和方法,习惯,经验很有关系
习惯和经验,影响执行实验的正确性,熟练度,速度
经验,还有启发和缓存的作用,加速搜索,直达结果
总的来讲,调试应该有严谨的方法论,按步骤进行,而不是无脑猜想和实验,那样只会浪费时间
好的方法论的差别,主要体现在解决复杂问题上,因为复杂的问题对应复杂的图,需要更多内存来搜索
比如你只靠脑子去记,每次尝试的小结果,那你肯定会在大迷宫里迷失,因为不断会有小的新想法会冒出来,挤掉你的旧记忆
复杂的问题应该有一张表来记录
故障树分析法
几个关键:
能反复复现的Bug是最容易的大部分情况是手误等低级错误召集相关工程师讨论可能性使用一个表格记录分析可能导致该问题的区域和原因 | 验证该假设的方法 | 结论(打勾,假设是否成立)控制变量,每次只改一个内容
Bug跟踪系统 & Bug交流分享
Bug跟踪系统,就是Bug缓存,缓存解决方案和结果,需要的时候谷歌
嗯,就是StackOverflow,但是我自己也会维护一个缓存
建立Bug跟踪系统 简单描述Bug,或者有程序报错信息,方便以后搜索 和别人交流Bug,重点,严重,有趣,比较骚的Bug可以拿出来分享
面对Bug的心态
当一切方法都失败时
保持冷静,不要对同事发火找到另一个程序员解释你的问题,可以是新人休息,洗澡,睡觉
如何在工作中正确高效地修bug?
7月22日 19:36
我的第一份实习工作的内容就是修复BUG,两个月修了100+个
对于线上项目来说,发版本前会预留一些时间来集中修复BUG
比如QA发现了100个BUG,主程序会过一遍BUG清单,对BUG模块和难度,严重程度进行分级,分配给对应的人,每个人分到一些开始修复
这是一份非常枯燥的工作,大部分问题是逻辑错误,或者是非程序岗位操作错误(填表错误),我发现,老手程序员修BUG的效率非常高,主要还是经验积累,比如界面模块,ngui的源码是必须看完的
下面只讲基本的方法和注意点
找到原作者和模块相关负责人
svn的show log和blame
或者问同事
总之找到负责人,就可以问他了
了解需求
要了解这个bug本来的预期行为是怎么样的
确保做好需求分析,需求定义
知道完整的玩法和相关内容
保证自己能判断bug修好了,且是正确的,不会引起相关问题,自测是有质量的,避免测试测出问题返工
异常弹窗
此处应有图片
Visual Studio有异常管理机制,可以过滤异常
弹窗的正确使用方法:
错误字符串,函数名/traceback,文件名,行号中止(abort):进入调试器重试(retry):继续运行忽略:继续运行并禁用错误标志(m_enabled)
再次命中该行将不会触发错误
对于循环中的错误语句很重要
只在Release版本产生的问题
常见问题:
变量未初始化编译器优化Debug和Release下代码不同
变量初始化,可以用静态分析解决
C++有这个问题,但是C#没有,会自动做这件事,所以是编译器可以解决的
调试Release版本的高级技巧
在debugger中阅读,单步执行反汇编(disassembly):使用反汇编检视模式(避免乱序执行)使用寄存器推理变量的值,地址:如果没执行地太远,寄存器没有被换出使用地址:比如你拿到地址,在watch里直接写一个cast解释成某种类型使用全局变量:略二分法
在最麻烦的情况下,只能二分定位
二分的目标是让release版尽可能接近debug版
比如对编译target进行二分,开关debug/release,来确定在哪一个编译target
assert
以下代码在Debug和Release不一样
原因是,assert,crtdbg,debugbreak仅在debug版本下有效
所以release下有专门的调试方法,crash dump等
Debug
Release
多线程问题
比如音频是在音频线程跑的
一个典型的BUG是,提前释放了音频内存,破坏了音频缓冲
调试方法是,关掉音频系统,看看还有无问题
堆栈损坏
堆栈的结构,是栈帧的stack,每个栈帧,开头一般是返回地址,然后是局部变量
堆栈损坏,可能是因为返回地址坏掉了,看一下崩溃附近的内存操作
理解断点
普通的断点是指令断点,运行到特定指令时停下
还有数据断点,数据变化时停下
指令断点通过int 3触发,这是通过硬件中断实现的
数据断点,则是监控内存变化,然后停下
Visual Studio高级调试技巧
watch
键入函数会触发断点,这很有用,比如你可以在一次debug会话中不断调整f(x)的x来看情况已有watch,双击修改表达式,添加逗号,d/x/n 可以指定显示格式:十进制,十六进制,展开指针数组 比如 “a,5”,显示a[5] NOTE 大型struct和数组会让VS很慢parallel watch
在已有的watch上右键添加parallel watch
并行观察的区别在于,会自动更新求值结果
数据断点
由数据写入(内存)触发;又称为硬件断点(是由cpu中断实现的)变量改变时就会触发debug》windows》breakpoint;左上角new下拉;new data breakpoint;键入“&val”ByteCount=4;其他值可能会让VS很慢条件断点
在数据断点和普通断点上都可以设置条件断点:条件和命中次数
这是一个必要的功能,比如你要访问for loop的第300次断点,手工点是不可能的
使用@err.hr获取错误码
windows下对应GetLastError的错误码调试
map文件 & Windows PDB
map文件
将内存地址映射到符号符号是通过XXX规则生成的崩溃产生时,看到EIP,然后找到EIP所在的函数体
安装Windows PDB文件
Windows PDB会更新,有一个服务器
// 安装前ntdll.dll!77f60b6f()ntdll.dll!77fb4dbd()ntdll.dll!77f79b78()ntdll.dll!77fb4dbd()// 安装后ntdll.dll!_RtlDispatchException@8() + 0x6ntdll.dll!_KiUserExceptionDispatcher@8() + 0xe00031328()ntdll.dll!ExecuteHandler@20() + 0x24ntdll.dll!_KiUserExceptionDispatcher@8() + 0xe000316f4()ntdll.dll!ExecuteHandler@20() + 0x24ntdll.dll!_KiUserExceptionDispatcher@8() + 0xe00031ac0()
内存转储(core dump)
WIP
调试游戏程序的学问
7月28日18:32《游戏编程精粹》4-1-1
五步调试法
重现:需要QA提供准确的重现步骤,并说明可能导致bug的事件(这更加精准,直接指向bug根源)收集线索,这点模糊定位问题 基本方法:假设与验证分治法,这里也模糊 解决问题:尤其到项目后期,解决方案不能修改底层代码,作为一个普通程序员,不要不经过考虑就自行修改测试,除了解决问题之外,要避免引入新的问题,这需要测试,也就是让游戏被QA玩一会高级调试技巧
保持思路开阔,避免做出过多假设,尤其是假设一些貌似简单的东西是正确的,这样就会错过答案,朝着错误的方向搜索控制变量,减少干扰,关闭一些无关系统,比如声音减少随机性,比如关闭随机数,使用常量检查边界条件减少并行,比如 将并行改为串行增加线程延迟 调试工具 条件断点内存watch寄存器watch栈混合汇编调试 vcs 检查最近的改动,恢复到可以运行的状态运行新旧两个版本进行调试,比较差异 找同事帮助,找其他人帮助暂时放下问题,开阔思路困难的情形和模式
仅在发布版出现,调试版正常 通常是数据未初始化一般来说,调试版自动清零,而发布版不会
代码优化相关bug
逐渐打开优化开关进行比较使用符号表,可能能调试,可能能拿到stack trace
做了一个无害改动后,bug消失了 时序问题内存覆盖问题
bug还在,请在这次解决它
间歇性问题 捕捉到问题时要尽可能记录多的信息,因为机会不多比较程序正确与错误时数据的差异 无法解释的行为 单步时变量自动修改了,说明debugger失去同步,清除缓存使debugger重新同步重试(retry):清除游戏状态重建(rebuild):重新构建游戏,最为重要重启(reboot):重启电脑,清空内存重装(reinstall):重装电脑,恢复os和app的设置 编译器错误 有一些是编译器本身的问题内存爆了无法理解复杂的c++模板
解决
完整地重新构建游戏重启电脑,重新构建游戏升级编译器升级库检查其他电脑是否能正确编译分治法,隔离一些代码来定位错误
理解底层错误
这对console开发尤为重要
csapp中有“有缺陷的抽象”
编译器实现原理 继承虚函数调用调用约定异常分配内存处理内存对齐
这些是如何实现的
硬件细节 cache内存对齐大小端栈的大小类型的大小 汇编语言 工作原理会阅读汇编语言
调试的基础设施
运行时修改变量的值,通过cmd gm可视化ai,gizmos,结合文字和3d线框记录和回放 函数式(玩家输入序列得到确定结果)这个太难了,做不到的但是记录输入序列是比较有用的,但是也存在困难 跟踪内存分配崩溃时打印尽可能多的信息,stack trace,等其他培训整个团队 非程序员,非测试(美术和设计)会忽略错误,没有记录错误,基本的,遇到msgbox应该记录下来预防bug
编译器警告级别调到最高 warning也要当做error处理pragma可以关掉一些警告 使游戏可以在多个平台多个编译器上构建,可以生成一个功能稍弱的版本编写自定义的内存管理器,保护内存溢出错误,在调试版本中预留出上溢和下溢内存块(我的理解是,保留这个内存块,使得分配在os层面是ok,不会crash,然后被我们的分配器捕获到错误,对于pc而言,不需要,因为vc++的内存系统很强大,还有SmartHeap之类的工具,易于定位内存错误Udacity调试课程
我观察到了一个bug,然后往前推,推到infection(感染的地方),接着找到一些可能是源头的地方a. 程序的执行是一颗树,可能是多个源头导致一个bug现象
b. 但是我们显然要从概率最高的地方开始利用科学方法:假设=》实验=》得出结论
如何避免bug:写出高质量的程序
明确需求,自己的期望是什么自动化test early,test oftenreview:让别人看你的代码错误历史(problem history):以前出错的更容易出错a. 怎么记录,放在哪里,怎么查找学习错误,很难说,没有标准
调试是监控代码的行为是否和你预期的一致
不要让自己的栈太深
游戏日志系统
现在养成打日志的习惯,增加一个途径去理解程序,减少对debugger的依赖
不是说debugger不好,而是debugger不适用于真实复杂的程序,你总是去开debugger,如果日志一眼能解决的话,相比之下,调试效率增加了
再来看游戏程序的特点:
复杂(废话)大量,重量的数学计算实时计算,有些bug是与时间有关系的(timing-dependent)
比如bug仅在程序全速运行时出现,因此不能通过debugger暂停来调试,观察比如bug由大的复杂的事件序列造成
我们的目标是:让程序不容易出错;出错时容易解决错误
功能需求
分频: 一般按模块划分(动画,物理)实际上,可以进一步允许一条日志附加多个tag,那么模块就是”主要tag“ 输出流: console文件网络 清晰漂亮的显示输出到哪里问题一般不大,但是趋势是网络,浏览器提供日志查看工具
但是现在简单的情况下可以console-log,比较轻量
好看的显示其实很重要,plain-text是很让人没有欲望的
HINT 崩溃日志,email给全体程序员