Visual C++ 之 调试程序
程序员文章站
2022-05-25 17:06:26
一、debug调试器
发布版本没有调试信息,不能进行程序调试,但程序进行了优化,debug版本则包含调试信息(在debug文件夹),但没有优化。所以程序有时候能够在调试版本运行,但不能运行于发布版本...
一、debug调试器
发布版本没有调试信息,不能进行程序调试,但程序进行了优化,debug版本则包含调试信息(在debug文件夹),但没有优化。所以程序有时候能够在调试版本运行,但不能运行于发布版本。时一般先生成一个debug版本程序,程序在经过调试确认无误后,再编译链接生成一个release版本程序。
调试工具:
1. 调试窗口
用于显示程序的调试信息。
output窗口:显示有关build和debug操作的信息,包括编译链接错误信息和调试时一些宏的输出信息
variable窗口:观察和修改某个作用域内所有变量的当前值,调试器可根据当前程序运行过程中变量的变化情况自动选择应显示的变量。可以在context下拉列表选择要查看的函数,然后会显示函数内部的局部变量的值。auto页面显示当前语句或者前一条语句(当前语句没有相关变量值);locals显示当前函数局部变量的值;this页面以树形方式显示当前类的对象的所有成员。
watch窗口:观察和修改变量或表达式的值,但需要程序员在窗口设置要观察的变量或表达式。
调试窗口用红色表示变量的值在程序当前的执行过程中发生了变化。可以在调试窗口手工改变变量的值,程序采用新的变量继续向后执行。另外,工具栏有工具按钮可以是用户在调试过程中改变的语句生效。
如果变量是一个对象、对象引用、指针,调试窗口将自动展开变量,显示其成员信息。
2. 其他窗口
register窗口:显示通用寄存器和cpu状态寄存器的内容(标志值、浮点堆栈)
memory窗口:显示当前内存的内容;address框允许你指定从哪个虚拟内存地址开始显示。
call stack窗口:列出所有调用未结束的函数,当前函数在堆栈的顶端。
disassembly窗口:列出反汇编后得到的由编译器生成的对应于源代码的汇编指令
3. 设置断点:
在源程序的某语句设置一个暂停点,在调试器中运行程序时可以强制程序执行到断点暂时停止运行。
单步执行:
(1)step into:遇见函数调用语句,进入函数内部
(2)step over:遇见函数调用语句,但不进入函数内部,跳过该函数。调试时,如果不能确定这个函数是否有错,一般先跳过函数而不进入。
(3)step out:从当前的函数中跳出,程序流程执行函数调用语句的下一步。
4.调试符号
程序文件(.pdb)包含了visual c++调试器所需的调试信息和程序信息。调试信息包含了变量的名字和类型、函数原型、源代码行号、类和结构的布局、fpo调试信息(重建堆栈帧)以及进行增量链接所需的信息。
二、编译选项
1.针对调试版本的编译选项:
a) /mdd,/mld或者/mtd
调试版本的运行时刻库(runtime library)有调试符号,使用了调试堆,调试堆的目的是发现内存破坏和内存泄漏,并且向用户报告源代码的哪个地方出了问题。
特性:
(1)调试版本的运行时刻库(runtime library)对内存的分配做了追踪,允许用户检查内存泄漏;
(2)在刚分配的内存里写上0xcd的字节模式,用0xcd来填充刚分配的内存,有助于发现数据未被初始化的错误;
(3)在被释放的内存写上0xdd的字节模式,有助于发现已被释放的内存。;
(4)在缓冲区的两头边界分配了四字节的保护数据,并用0xfd的字节模式作初始化,来检查写内存的上溢出和下溢出;
(5)在每个内存分配的地方对源代码文件名和行号作了记录,有助于用户在源代码中对内存分配进行定位
b)/od
用来关闭优化开关。
好处:
(1)容易理解:未被优化的代码直接对应于源代码,所以比优化后的代码更容易读懂;
(2)节省调试时间:未被优化的代码编译和链接会更快,会有更短的调试周期;
(3)优化可能带来潜在错误:优化代码要求编译器做一些假设,去除冗余,但有时这个假设是错误的,并且去掉的冗余也有可能隐藏错误。所以优化不一定比调试版本更好。
c)/d “_debug”
打开条件编译调试代码开关。
实际上这也是一个条件编译:只有这个符号被定义,调试代码才会被编译。mfc使用_debug符号来确定到底链接的是哪个版本的mfc类库。参考之前写过的条件编译。
d)/zi
创建编辑继续(edit and continue)的程序数据库。这个选项会打开/gf编译选项,/gf编译选项会消除重复字符串,并将字符串放到只读内存。
e)/gz
在调试版本中用来发现那些在发布版本里才发现的错误。
(1)用0xcc模式初始化自动(本地)变量;不是0xcd
(2)在通过函数指针调用函数时,检查栈指针,确认是否有调用规则不匹配;
(3)在函数最后检查栈指针是否被改变。
f)/gm
a)/md,/ml或者/mt
i.使用发布版本的运行时刻库(runtime library)。
b)/o1或者/o2
i.打开优化开关,使得程序会最小或者速度会最快;
ii.优化器还可能发现代码中潜在的错误,而这些错误可能会被调试版本掩盖。(调试版本的gz)
c)/d “ndebug”
i.关闭条件编译调试代码开关。
d)/gf
i.消除重复字符串并将它们放到只读内存中以避免被错误地修改。
e)/zi
i.创建包含调试符号的程序数据库。 (如果一个错误只发生在发布版本里,除非你是个汇编高手,否则你需要调试符号来提示你到底程序出现了什么问题。)
三、提高调试器的差错能力
1. 尽量采用编译时刻检查而不是运行时刻检查。
/gz选项用来发现那些在发布版本里才发现的错误,包括未被初始化的自动(局部)变量、堆栈错误、不正确的函数原型等。
4. 使用#pragma warning编译器指示
可以使用#pragma warning编译器指示来禁止整个程序、特定的头文件、特定的代码文件或是特定的某一行代码的特定警告
5. 使用没有警告的编译法则/wx
这个编译选项把所有的警告当成错误来对待,只有在假警告被消除之后才能应用。
四、内存虚拟地址空间
windows使用一组固定的范围来分割进程的4gb虚拟地址空间,因此有时可通过查看指针的返回值来判断指针是否有效。
(1)windows2000虚拟地址空间划分
0~0xffff(64kb):不能用来检测空指针赋值(访问冲突)
0x10000(64kb)~0x7ffeffff(2gb-64kb):win32进程私有的(非保留的),用于程序代码和数据
0x7fff0000(2gb-64kb)~0x7fffffff(2gb):不能用来防止覆盖os分区(访问冲突)
0x800000000(2gb)~0xffffffff(4gb):为操作保留,不可访问(访问冲突)
(2)windows2000虚拟地址空间使用
0x00030000~0x0012ffff:线程栈
0x00130000~0x003fffff:堆(有时堆位于此处)
0x00400000~0x005fffff:可执行代码
0x00600000~0x0fffffff:堆(有时堆位于此处)
0x10000000~0x5fffffff:app dlls、msrt.dll、mfc42.dll
0x77000000~0xffffffff:advapi32.dll、comctl32.dll、gdi32.dll、kernel32.dll、ntdll.dll、rpcrt4.dll、shell32.dll、user32.dll
1、内存分配错误:动态内存分配错误有两种基本类型:内存错误和内存泄漏。
a)内存错误
当一个指针或者该指针所指向的内存单元成为无效单元,或者内存中的数据结构被破坏时,再使用原来的指针(悬垂指针/迷途指针,指针的值还是原来指向的内存的地址,但是该内存上的内容已经被释放)去访问对象就会造成内存错误。
无效指针的原因:指针未被初始化,指针被初始化为一个无效地址,指针被不小心错误地修改,在与指针相关联的内存区域被释放后使用该指针(悬垂指针/迷途指针),这些都会使指针成为无效指针。
内存分配错误:当通过一个错误的指针或者悬垂指针对内存进行写入,或者将指针强制转换为不匹配的数据结构,又或者是写数据越界,内存自身都会遭到破坏。删除未被初始化的指针(释放还没被分配的内存)、删除非堆指针(指向的不是new出来的对象所占的内存,即不在堆区)、多次删除同一指针或者覆盖一个指针的内部数据结构,都会造成内存分配系统错误。
b)内存泄漏
内存泄漏在动态分配的内存没有被释放时产生,产生原因:(1)没有在程序的全部执行路径中释放内存;(2)没有在析构函数中释放所有的内存;等等
一个程序在崩溃之前可运行的时间越长,导致崩溃的原因与内存泄漏关系越大(比如循环次数非常大的函数)
系统会在程序结束的时候将泄漏的内存收回(回收整个应用程序的所有内存),因此内存泄漏是个暂时性的问题。但消除内存泄漏仍然尤其必要性:(1)内存泄漏往往导致系统资源的泄漏,动态分配的内存常不仅仅代表一块存储区域,还代表某些类型的系统资源(如文件、窗口、设备等);(2)高质量的程序和特定的服务器程序必须能够无限地运行下去;(3)往往是其他程序错误或者不良编程习惯的征兆。
2、内存的初始化
在调试版本,堆里面未被初始化的内存被0xcd字节填充,堆里释放的内存用0xdd字节填充。堆栈里面被初始化的内存用0xcc字节填充,调试版本和发布版本中,未被初始化的全局内存都被初始化为0;
1.调试死循环
使用debug菜单下的break命令,如果程序有输入请求,可以使用f12中断程序,然后检查窗口的调用栈,或单步跟踪代码找到死循环的发生原因。
2.用spy++调试与消息有关的问题
调试消息的最好方案是使用visual c++提供的spy++工具。spy++允许程序员查看窗口、消息、进程和线程。spy++默认的消息输出:第一栏显示行号。第二栏显示接受消息的句柄。第三栏中的“s”表示消息是用sendmessage发出的,“p”代表消息是由postmessage发出的,“r”是消息句柄的返回值。第四栏给出解码后的消息名,消息参数或返回值。
3.非常规方法
(1)重新编连你的应用程序 rebuild all
当你的程序表现出异常的或意外的行为,或者visual c++编译器因为一个内部编译器错误而失败时,最好删除工程中的debug或release文件夹,从头开始重新进行编连。
(2)重新启动visual c++
visual c++有超强的能力,但编译器的某些特性也会引起奇怪的错误。如果你的程序表现得很奇怪,你可是试着清除所有的断点,关闭或隐藏观察窗口,检查工程设置对话框看最近做了什么修改,直至重新启动visual c++以便消除由于visual c++环境引起的异常行为。
(3)重新启动windows
发布版本没有调试信息,不能进行程序调试,但程序进行了优化,debug版本则包含调试信息(在debug文件夹),但没有优化。所以程序有时候能够在调试版本运行,但不能运行于发布版本。时一般先生成一个debug版本程序,程序在经过调试确认无误后,再编译链接生成一个release版本程序。
调试工具:
1. 调试窗口
用于显示程序的调试信息。
output窗口:显示有关build和debug操作的信息,包括编译链接错误信息和调试时一些宏的输出信息
variable窗口:观察和修改某个作用域内所有变量的当前值,调试器可根据当前程序运行过程中变量的变化情况自动选择应显示的变量。可以在context下拉列表选择要查看的函数,然后会显示函数内部的局部变量的值。auto页面显示当前语句或者前一条语句(当前语句没有相关变量值);locals显示当前函数局部变量的值;this页面以树形方式显示当前类的对象的所有成员。
watch窗口:观察和修改变量或表达式的值,但需要程序员在窗口设置要观察的变量或表达式。
调试窗口用红色表示变量的值在程序当前的执行过程中发生了变化。可以在调试窗口手工改变变量的值,程序采用新的变量继续向后执行。另外,工具栏有工具按钮可以是用户在调试过程中改变的语句生效。
如果变量是一个对象、对象引用、指针,调试窗口将自动展开变量,显示其成员信息。
2. 其他窗口
register窗口:显示通用寄存器和cpu状态寄存器的内容(标志值、浮点堆栈)
memory窗口:显示当前内存的内容;address框允许你指定从哪个虚拟内存地址开始显示。
call stack窗口:列出所有调用未结束的函数,当前函数在堆栈的顶端。
disassembly窗口:列出反汇编后得到的由编译器生成的对应于源代码的汇编指令
3. 设置断点:
在源程序的某语句设置一个暂停点,在调试器中运行程序时可以强制程序执行到断点暂时停止运行。
单步执行:
(1)step into:遇见函数调用语句,进入函数内部
(2)step over:遇见函数调用语句,但不进入函数内部,跳过该函数。调试时,如果不能确定这个函数是否有错,一般先跳过函数而不进入。
(3)step out:从当前的函数中跳出,程序流程执行函数调用语句的下一步。
4.调试符号
程序文件(.pdb)包含了visual c++调试器所需的调试信息和程序信息。调试信息包含了变量的名字和类型、函数原型、源代码行号、类和结构的布局、fpo调试信息(重建堆栈帧)以及进行增量链接所需的信息。
二、编译选项
1.针对调试版本的编译选项:
a) /mdd,/mld或者/mtd
调试版本的运行时刻库(runtime library)有调试符号,使用了调试堆,调试堆的目的是发现内存破坏和内存泄漏,并且向用户报告源代码的哪个地方出了问题。
特性:
(1)调试版本的运行时刻库(runtime library)对内存的分配做了追踪,允许用户检查内存泄漏;
(2)在刚分配的内存里写上0xcd的字节模式,用0xcd来填充刚分配的内存,有助于发现数据未被初始化的错误;
(3)在被释放的内存写上0xdd的字节模式,有助于发现已被释放的内存。;
(4)在缓冲区的两头边界分配了四字节的保护数据,并用0xfd的字节模式作初始化,来检查写内存的上溢出和下溢出;
(5)在每个内存分配的地方对源代码文件名和行号作了记录,有助于用户在源代码中对内存分配进行定位
b)/od
用来关闭优化开关。
好处:
(1)容易理解:未被优化的代码直接对应于源代码,所以比优化后的代码更容易读懂;
(2)节省调试时间:未被优化的代码编译和链接会更快,会有更短的调试周期;
(3)优化可能带来潜在错误:优化代码要求编译器做一些假设,去除冗余,但有时这个假设是错误的,并且去掉的冗余也有可能隐藏错误。所以优化不一定比调试版本更好。
c)/d “_debug”
打开条件编译调试代码开关。
实际上这也是一个条件编译:只有这个符号被定义,调试代码才会被编译。mfc使用_debug符号来确定到底链接的是哪个版本的mfc类库。参考之前写过的条件编译。
d)/zi
创建编辑继续(edit and continue)的程序数据库。这个选项会打开/gf编译选项,/gf编译选项会消除重复字符串,并将字符串放到只读内存。
e)/gz
在调试版本中用来发现那些在发布版本里才发现的错误。
(1)用0xcc模式初始化自动(本地)变量;不是0xcd
(2)在通过函数指针调用函数时,检查栈指针,确认是否有调用规则不匹配;
(3)在函数最后检查栈指针是否被改变。
f)/gm
打开最小化重新链接开关,减少链接时间。
2.针对release版本的编译选项a)/md,/ml或者/mt
i.使用发布版本的运行时刻库(runtime library)。
b)/o1或者/o2
i.打开优化开关,使得程序会最小或者速度会最快;
ii.优化器还可能发现代码中潜在的错误,而这些错误可能会被调试版本掩盖。(调试版本的gz)
c)/d “ndebug”
i.关闭条件编译调试代码开关。
d)/gf
i.消除重复字符串并将它们放到只读内存中以避免被错误地修改。
e)/zi
i.创建包含调试符号的程序数据库。 (如果一个错误只发生在发布版本里,除非你是个汇编高手,否则你需要调试符号来提示你到底程序出现了什么问题。)
三、提高调试器的差错能力
1. 尽量采用编译时刻检查而不是运行时刻检查。
2. 使用最高的编译警告级别/w4
象if(x=2)这样的语句,默认的警告级别为/w3时不显示任何信息,但改成最高警告级别/w4时则会出现“waning c4706:assignment within conditional expression”的警告。/w4能给出一些/w3所不能给的警告。
3. 在调试版本中使用/gz编译选项/gz选项用来发现那些在发布版本里才发现的错误,包括未被初始化的自动(局部)变量、堆栈错误、不正确的函数原型等。
4. 使用#pragma warning编译器指示
可以使用#pragma warning编译器指示来禁止整个程序、特定的头文件、特定的代码文件或是特定的某一行代码的特定警告
5. 使用没有警告的编译法则/wx
这个编译选项把所有的警告当成错误来对待,只有在假警告被消除之后才能应用。
四、内存虚拟地址空间
windows使用一组固定的范围来分割进程的4gb虚拟地址空间,因此有时可通过查看指针的返回值来判断指针是否有效。
(1)windows2000虚拟地址空间划分
0~0xffff(64kb):不能用来检测空指针赋值(访问冲突)
0x10000(64kb)~0x7ffeffff(2gb-64kb):win32进程私有的(非保留的),用于程序代码和数据
0x7fff0000(2gb-64kb)~0x7fffffff(2gb):不能用来防止覆盖os分区(访问冲突)
0x800000000(2gb)~0xffffffff(4gb):为操作保留,不可访问(访问冲突)
(2)windows2000虚拟地址空间使用
0x00030000~0x0012ffff:线程栈
0x00130000~0x003fffff:堆(有时堆位于此处)
0x00400000~0x005fffff:可执行代码
0x00600000~0x0fffffff:堆(有时堆位于此处)
0x10000000~0x5fffffff:app dlls、msrt.dll、mfc42.dll
0x77000000~0xffffffff:advapi32.dll、comctl32.dll、gdi32.dll、kernel32.dll、ntdll.dll、rpcrt4.dll、shell32.dll、user32.dll
其中,0x00400000是所有版本的windows能使用的最低基地址。
五、常见内存错误1、内存分配错误:动态内存分配错误有两种基本类型:内存错误和内存泄漏。
a)内存错误
当一个指针或者该指针所指向的内存单元成为无效单元,或者内存中的数据结构被破坏时,再使用原来的指针(悬垂指针/迷途指针,指针的值还是原来指向的内存的地址,但是该内存上的内容已经被释放)去访问对象就会造成内存错误。
无效指针的原因:指针未被初始化,指针被初始化为一个无效地址,指针被不小心错误地修改,在与指针相关联的内存区域被释放后使用该指针(悬垂指针/迷途指针),这些都会使指针成为无效指针。
内存分配错误:当通过一个错误的指针或者悬垂指针对内存进行写入,或者将指针强制转换为不匹配的数据结构,又或者是写数据越界,内存自身都会遭到破坏。删除未被初始化的指针(释放还没被分配的内存)、删除非堆指针(指向的不是new出来的对象所占的内存,即不在堆区)、多次删除同一指针或者覆盖一个指针的内部数据结构,都会造成内存分配系统错误。
b)内存泄漏
内存泄漏在动态分配的内存没有被释放时产生,产生原因:(1)没有在程序的全部执行路径中释放内存;(2)没有在析构函数中释放所有的内存;等等
一个程序在崩溃之前可运行的时间越长,导致崩溃的原因与内存泄漏关系越大(比如循环次数非常大的函数)
系统会在程序结束的时候将泄漏的内存收回(回收整个应用程序的所有内存),因此内存泄漏是个暂时性的问题。但消除内存泄漏仍然尤其必要性:(1)内存泄漏往往导致系统资源的泄漏,动态分配的内存常不仅仅代表一块存储区域,还代表某些类型的系统资源(如文件、窗口、设备等);(2)高质量的程序和特定的服务器程序必须能够无限地运行下去;(3)往往是其他程序错误或者不良编程习惯的征兆。
2、内存的初始化
在调试版本,堆里面未被初始化的内存被0xcd字节填充,堆里释放的内存用0xdd字节填充。堆栈里面被初始化的内存用0xcc字节填充,调试版本和发布版本中,未被初始化的全局内存都被初始化为0;
消除错误:使用调试版本的/mdd,/mld或者/mtd 编译选项。利用断点调试等在调试窗口观察内存的相应值。
六、一些调试方法1.调试死循环
使用debug菜单下的break命令,如果程序有输入请求,可以使用f12中断程序,然后检查窗口的调用栈,或单步跟踪代码找到死循环的发生原因。
2.用spy++调试与消息有关的问题
调试消息的最好方案是使用visual c++提供的spy++工具。spy++允许程序员查看窗口、消息、进程和线程。spy++默认的消息输出:第一栏显示行号。第二栏显示接受消息的句柄。第三栏中的“s”表示消息是用sendmessage发出的,“p”代表消息是由postmessage发出的,“r”是消息句柄的返回值。第四栏给出解码后的消息名,消息参数或返回值。
3.非常规方法
(1)重新编连你的应用程序 rebuild all
当你的程序表现出异常的或意外的行为,或者visual c++编译器因为一个内部编译器错误而失败时,最好删除工程中的debug或release文件夹,从头开始重新进行编连。
(2)重新启动visual c++
visual c++有超强的能力,但编译器的某些特性也会引起奇怪的错误。如果你的程序表现得很奇怪,你可是试着清除所有的断点,关闭或隐藏观察窗口,检查工程设置对话框看最近做了什么修改,直至重新启动visual c++以便消除由于visual c++环境引起的异常行为。
(3)重新启动windows
当你发现windows或者其他程序表现出异常的或出人意料的行为时,就应该重新启动windows,以消除操作系统给调试带来的干扰。
上一篇: 养生从心开始,论情绪管理的重要性
推荐阅读
-
Visual Studio怎么新建最小的c++工程项目?
-
数据结构之链表中倒数第k个结点(C++/Java语言实现)
-
Android开发笔记之:一分钟学会使用Logcat调试程序的详解
-
win10用visual studio进行c++程序编写的方法
-
Visual C++ 中的ODBC编程的介绍
-
在Visual C++编程中如何取得CPU的信息
-
Microsoft Visual C++ runtime error解决步骤图解
-
使用Visual Studio 2017作为Linux C++开发工具
-
数据结构之链表中倒数第k个结点(C++/Java语言实现)
-
在Visual C++编程中如何取得CPU的信息