一、插装——开发人员编写的额外代码,来提高程序的可观察性和可控制性。
二、发现bug的机会:
(1)可调试的源代码(2)插装(3)宏定义(4)编译器标志(5)静态检查器
(6)选择的库(7)链接器选项(8)代码插装工具(9)测试用例/输入数据
(10)调试器:源代码、剖析、内存读取、操作系统调用跟踪器(如truss和strace)
三、用户态调试器——查看调试目标状态
1、运行中的线程、内存、寄存器以及在进程空间中打开的内核对象
2、修改调试目标状态:改变线程的执行顺序,修改寄存器的内容,修改内存的内容,接收在目标进程中发生的特定事件。
3、分析包含进程快照的转储文件
4、cdb.exe——CDB,基于字符界面的控制台程序
5、ntsd.exe——一个GUI程序并可创建自己的控制台(NTSD)
6、windbg.exe——图形界面调试器
四、调试规则
1、理解需求2、制造失败3、简化测试用例4、读取恰当的错误信息5、检查易见的问题
6、从解释中分离出事实7、分而治之8、工具要与bug匹配9、一次只做一项更改
10、保持审计跟踪11、获得全新观点12、bug不会自动修复13、回归测试来检查bug修复
回归测试——通常是一组脚本的集合,它执行稳步增加的测试集,结果是两个清单,一个列出通过的测试,一个失败的测试。每次添加新功能或修复了一个bug时,都应该增加测试。
分而治之——(1)整理一份清单,列出潜在问题以及如何调试他们。
(2)将环境更改和源代码更改区分开。
(3)放大并治之。(同步比较两个版本的数据、日志文件和控制流)
黑盒——验证组件。预期功能。忽略实际实现,很好的移植性。
白盒——测试实际边界情况。
1、自己尝试再现相同的bug——分析错误描述和日志文件。
2、现场调试——源代码文件,在函数中设置断点,走查程序的运行,获取栈的跟踪信息,获取或设置变量。
3、远程连接——WebEx,VNC,客户机运行可调试的软件,源代码在调试者电脑上。
符号信息——包含函数和变量的名称以及CPU指令,源文件和行号之间的关系。
栈溢出——栈:内存片段,存储每个活动函数调用的栈帧。
栈帧:由返回地址、函数参数和局部变量组成。
栈跟踪:一个实际的栈帧链,从调试器当前停止或暂停的最顶部函数开始,向下一 直到main函数。当嵌套函数调用的链过长,造成栈没有足够的内存来存储当前栈 帧时,就发生了栈溢出。
栈帧号:当前为0,main最高。
调试——行断点:源代码指定行。
函数断点:函数第一行。
条件断点:条件保持为真。
事件断点:发生特定事件,signals和C++异常。
step-into:移动到下一个可执行的代码行。
step-over:在同一个调用栈层中移动到下一个可执行的代码行。
step-out:在栈中前进到下一层,并在调用函数的下一行停止。
1、内存泄漏,访问已释放的内存,多次释放同一个内存位置,释放从未分配的内存,读写空指针。
2、混用C中malloc、free和C++中new、delete。
3、对数组使用delete而非delete[ ]。
4、数组越界。
5、访问从未分配的内存。
6、读取未初始化的内存。
使用源代码调试器来检查变量值和调用调试函数。过滤方式:
(1)错误类型:如对未初始化内存的访问。
(2)函数调用链。
(3)对象或源文件名称。
不要自动排查所有与第三方库有关的错误。
测试用例应该很很好的代码覆盖率。
内存剖析——检查是否有大的内存泄漏。
估计预期的内存使用。
用多个输入来测量内存使用随时间的变化。
查找使用内存的数据结构。
内存剖析工具——记录动态内存分配的时间,由谁(调用栈)分配的,大小以及何时,由谁释放的。
为数据结构编写插装代码——例如,为C++类添加一个方法,它给出类对象内存使用的大概值。(包括成员的内存使用)又如,计算活动的类对象数,定义类的所有delete和new操作符,并用静态计数器来计算增减。
影响运行时测量的一些因素:运行时间过短。系统调用。CPU时钟频率不稳定。文件I/O。没有足够的内存。其他进程。
剖析工具——采集。插装。
采样:在固定时间间隔拦截程序,观察调用栈,并跟踪每个函数在调用栈顶端出现的频率。
插装:在程序中插入用于跟踪函数调用频率及调用者的语句。可在编译期,链接期,模拟程序期,也可在函数内部。优点:可以精确到CPU周期。
呈现数据:平面剖析。调用图。
C/C++并行程序:多路通信。多线程。信号或中断处理。
TBB——一个C++模版库,用于编写独立与操作系统的多线程代码。
数据竞争:首先识别共享的内存变量,在访问其代码中找缺陷。
模拟线程的调度:从一个线程任意切换到另一个线程。
插装代码原则:
(1)一定要使用线程安全的原子函数和命令,否则最后调试的就是插装代码,而不是实际的程序。
(2)在每条I/O语句后面立即调用fflush( ),错误流stderr是未被缓冲的,故适合用来做记录。C++流最不合适,因为它们可能产生来自不同线程的完全交叉的文本输出。
(3)当转储大型程序中的线程信息时,应使用时间戳。可用挂钟式CPU时间,分别为time函数和clock函数。
(4)两台不同机器上的挂钟时间永远不会同步。
(5)创建辅助工具,跟踪函数,跟踪缓冲区变量。
(6)使用断言。
调试死锁:
(1)循环互斥锁定,协议不匹配。
(2)Thread Checker工具。
(3)信号处理函数由操作系统调用,操作系统在将一个信号传递给进程时调用此函数,信号是异步的。
(4)使用跟踪文件来调试竞争条件,确保跟踪机制是线程安全的。
(5)在使用时间戳对事件进行排序之前,检查并行处理库是否有时钟同步机制。
查找环境和编译器问题
1、UINX——PATH、LD_LIBDARY_PATH(用于定位动态载入程序共享库)
Windows——PATH、LIB、INCLUDE(PATH用来查找程序和DLL)
2、程序的行为可能依赖于当前工作目录,如进程ID依赖。
3、Strace——对没有源代码的程序或链入的库进行调试的工具。
(1)文件I/O
(2)在操作系统例程中未捕获的错误或中断
(3)操作系统调用的频率
(4)内存分配、释放、映射
4、编译器也有bug。
5、错误的动态库搜索路径可能是与环境有关的程序崩溃的原因。
处理链接问题
1、链接——基于对象文件和文件库构件可执行文件。编译器或汇编程序将源代码转换为机器代码,并输出对象文件。对象中包含符号,符号表示源代码中定义的函数或变量。
已定义/已解析符号:一个符号被关联到相同文件中的一个地址和代码段,链接器将已解析和未解析的符号保存在两个列表中。
2、大型项目中,以模块化方式构建可执行文件。(将每个对象文件及随后的可执行文件或库的构建阶段与对象文件明确分开)
3、若项目的对象文件或库中均找不到丢失的符号,搜索:
(1)源代码中
(2)系统库中
(3)第三方软件库中
(4)计算机或网络外部
4、链接顺序——(1)载入对象文件(初始化代码)(至少以未定义的main符号开始)
(2)按照链接行参数的顺序走查指定的对象文件和库
(3)若在链接行的结尾,所有符号均已解析,则链接成功。
5、链接C和C++代码——严格限定文件扩展名。
6、Extern C——明确通知C++编译器不对那些需要由C对象文件中的符号来解析的未定义符号进行名称改编。
7、具有多个定义的符号——一般链接器只选择它在命令行所指定的对象文件和库文件找到的第一个定义。
8、信号冲突——相同符号存在多个定义,而在链接时并未检测到。调用错误的函数可能会导致内存破坏。
(1)重新命名导致冲突的变量和函数。
(2)引入C++命名空间
(3)在源代码中使用static限制特定符号的导出——符号的本地化
(4)使用工具EDITBIN或LIB
9、系统不匹配——用同一个编译器编译所有的对象文件,用另一个不同编译器(版本)来链接。如果链接器报错,指出丢失了内部使用的函数和方法(如cout或_throw)的符号,那么很可能是因为对象文件与系统库之间不匹配。
C++编译器内部使用的函数通常以双下划线为前缀,如__dynamic_cast。
10、对象文件不匹配——链接用不同编译器(版本)生成的对象文件时,常见为必须使用那些无法获得源代码的对象文件或库,如第三方库文件。
编译器不匹配的症状是链接器报错(有符号丢失),但这些符号在对象文件或库中都存在,而且链接顺序看起来也是正确的。
11、运行时崩溃——核心转储通常是由于用了特定的C++函数引起的,以下划线为函数前缀,栈跟踪中的特定C++函数并不总是表明编译器或链接器不匹配。
方法——用同一个编译器重新编译所有源文件,链接时也使用这个编译器。
12、链接或载入DLL——(1)直接链接;(2)运行时显式调用载入。
VS中不使用实际的DLL来链接,用一个专门的导入库,其与静态库具有相同的扩展名(.lib)
(1)LoadLibrary( )——控制载入DLL或何时载入。
(2)GetProcAddress( )——通过系统调用访问DLL中的符号。
13、无法找到DLL文件——PATH变量,在载入器的搜索路径中添加目录。
14、在DLL中设置断点——当调试器被链接到程序时,将为此时已打开的所有DLL读取调试信息。(将断点设置为挂起状态)
15、GetLastError和FormatMessage提供有用的错误信息。
在C++函数方法和操作符中设置断点
1、函数签名——包括名称、类名称、所有参数的类型和命名空间。在函数模版特化中,签名还包括模版参数。
签名:使调试器能够区分函数的多个版本,链接程序时也会用到。
2、VS调试器——Debug——BreakPoint——Break at Function——输入函数名
对模版化的函数或成员时,GDB断点只能用于模版的一个实例,而并非所有。
3、调试静态构造/析构函数
(1)静态或全局类对象的构造/析构函数是在main之前/之后进行的。
(2)由静态初始化程序的顺序依赖性引起的bug。初始化顺序依赖编译器,链接器,链接顺序。
(3)栈跟踪——其中没有main,因为它尚未被调用。
(4)在静态初始化之前连接调试器。
4、使用观察点(数据断点)
(1)在表达式值发生变化时停止程序的执行
(2)VS中无法为局部变量设置观察点,只能观察地址。
5、捕捉信号——信号是异步的,进程间通过信号通信
提高可见性,禁用干扰性信号,引发bug。
6、捕获异常——异常在同一个进程中被抛出(生成)和捕获(使用)
VS——Debug——Exceptions——C++ EXceptions(使用程序断点)
7、读取栈跟踪——栈跟踪是一个帧列表,每个帧对应一个被调用的函数。
帧报告:(1)程序被删除,调试信息被删除
(2)调试信息在共享库中,而调试器尚未载入它。
(3)调试器无法理解调试信息。(混用)
(4)存储栈帧的内存被破坏了。(如越界写数组)
8、核心转储——程序遇到bug并触发一个分段错误,则操作系统创建一个文件。
(1)包含程序已分配的所有数据的一个副本,也有栈数据。
(2)Debug——Save Dump As ...(事后调试)
(3)上下移动栈帧,查询变量和内存的值。
调试正在运行的程序
1、操纵数据:
(1)修改变量值或函数的实际参数
(2)修改函数的返回参数值
(3)修改堆内存的内容
(4)修改环境变量
2、操纵流的控制:
(1)调用函数(即时窗口)
(2)从函数退出,跳过剩下的语句
(3)跳过或重新执行当前函数中的语句
注释的重要性
1、函数签名的注释——函数要做什么;说明函数参数,以及在异常下如何处理这些参数;接口使用上的假设;内存分配;副作用;记录所有已知的陷阱和临时折中办法。
2、对折中办法的注释
3、对不确定的代码注释
4、仔细选择名称(C++标准库规则)
5、不要压缩代码
6、为复杂表达式使用临时变量
7、避免使用预处理宏
8、使用常量或枚举替代宏
9、使用函数来替代预处理器宏
10、调试预处理器输出(—E标志)
1、C++支持将特定操作符映射到用户自定义的代码。
2、静态检查的作用——现代编译器报告:
(1)在枚举类型的分支语句中丢失了case
(2)未使用的函数、函数参数或便签
(3)对register变量进行了取址操作
(4)整数被零除
(5)死代码(即不可达代码)
(6)丢失了函数声明和return语句
(7)内存的错误使用:未使用的或未初始化的变量
(8)未来将与C++标准不兼容的地方
(9)与64位CPU不兼容
3、建议不要忽略编译器警告,若警告表明有一个实际或潜在的bug,则应该修复它。若bug是无害的或无法修复,则应在代码中用注释进行解释。
4、静态检查器——在源代码上执行规则检查,不必运行程序,因此无需提供测试用例和输入数据。
5、静态分析的高级应用——可移植性;反向工程;代码统计;安全性。