什么是调试?怎么调试?
所有发生过的事情都一定有迹可循,如果问心无愧,就不需要就掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越同意顺藤而上,这就是推理的途径。一名优秀的程序员,同时也是一名优秀的侦探
文章目录
本文中所有调试技巧,都是基于Visual Studio 2019 版本。
本文所使用IDE为Visual Studio 2019 版本,程序所展现的情况仅为此IDE环境导致,不引伸其它IDE或者编译器。
调试
所有发生过的事情都一定有迹可循,如果问心无愧,就不需要就掩盖也就没有迹象了,如果问心有愧,就必然需要掩盖,那就一定会有迹象,迹象越多就越同意顺藤而上,这就是推理的途径。一名优秀的程序员,同时也是一名优秀的侦探
一、什么是BUG?
我想许多人都听过bug这个词,不管是否是软件相关专业的,但是不甚了解这个词究竟是什么含义。
软件的bug,狭义概念是指软件程序的漏洞或者缺陷,广义概念除此之外还包括测试工程师或者用户发现和提出的软件可以改进的细节,或与需求文档存在差异的功能实现等。
BUG 百度翻译为:
(机器,尤指计算机的)故障,程序错误,缺陷 ;虫子;小昆虫;轻微的传染病;小病;热衷;着迷;窃听器
第一个BUG
BUG,一词的原意是“臭虫”或者“虫子”。
1945年,计算机还是由机械师继电器和真空管驱动的,机器有房间那么大。提现当时技术水平的MarkII,是由哈佛大学制造的一个庞然大物。当技术人员正在进行不整机运行时,它突然停止工作。技术人员爬上去找原因,发现这台巨大的计算机内部的一组继电器的触点之间有一只飞蛾,这显然是由于飞蛾受到光和热的吸引,飞到了触点上,然后被高电压击打而死。
所以后来Bug这个名词就被沿用下来,用来表示电脑系统或者程序中隐藏的错误、缺陷、漏洞等问题。
二、调试
什么是调试?
所谓调试(Debug),就是跟踪程序的运行过程,从而发现程序的逻辑错误(思路错误),或者隐藏的缺陷(Bug)。简单来说,就是找到程序错误,并且改正错误。
在调试的过程中,我们可以监控程序的每一个细节,包括变量的值、函数的调用过程、内存中数据、县城的调度等,从而发现隐藏的错误,或者低效的代码。
每个人都会遇到逻辑错误,而且会经常遇到,初学者更是会,额~,例如我这种,经常会错的离谱,所以,必须要掌握调试技能,没有选择的余地,没有学会调试就是没有学会编程。
放两个搞笑的图,供大家娱乐:
这就是我!!!
调试的基本步骤
- 发现程序错误的存在
- 以隔离、消除等但是对错误进行定位
- 确定错误产生的原因
- 提出纠正错误的解决办法
- 对程序错误予以改正,重新测试
如何发现bug的存在?
能够发现bug的存在,有三方人,第一是研发人员,第二是测试人员,第三是用户。而越往后发现bug产生的后果相对越严重。
-
开发人员
开发人员研发出产品的同时,也是制造BUG的过程,在开发过程中不可避免的会出现这样或者那样的bug。如果在开发过程中就发现bug的存在,这个怎么说呢?发现就发现呗,悄咪咪的改掉就好了啦。 -
测试人员
开发人员不可避免的生产出bug,而开发人员又不可能找出全部的bug,毕竟这就是开发自己写的嘛(我写的代码怎么会有bug),所以测试人员的存在是必须的,以便在产品交付之前尽可能多的找出bug,保证交给客户的软件质量要更好一些。
而开发和测试,一个是造bug的一个是找bug的,这个看起来就不怎么和谐。我们可以看一个段子,仅仅只是调侃:
绝对不可能……
测试MM对阿猿说发现了一个Bug。
阿猿矢口否认:不可能,绝对不可能!
MM:真的有Bug,你过来看一下!
阿猿:我都不用看,在我这儿好好儿的。
MM:你来看一下嘛……
阿猿:看什么看,我都测过的,绝对没问题。
伴随着凳子响,阿猿不情愿的起身,脸上不耐烦的表情,心理却嘀咕着怎么回事儿呢这是……
过了一阵儿,阿猿不好意思的声音响起:真没想到是这儿的问题……你怎么测的……一般用户都不会这么用的……
- 用户
产品研发出来,就是为了给用户使用,如果是免费开源的产品,用户使用的时候发现bug,那么用户报给开发人员,进行修复就好了。但是有些软件的之所以存在,就是因为客户出钱,让开发人员进行研发,这种如果用户在使用期间出现了bug,在没有对客户造成损失的情况下,和气生财;如果让客户产生了损失,那就是“对簿公堂”喽~。(夸张了夸张了)
Debug和Release
上述两图中,箭头所指的地方,我想对于使用VS这款IDE的初学者而言,是很陌生的,并且疑惑,这个是什么?有什么用?
通常我们在使用VS这款IDE时,箭头所指向的位置为Debug,而点一下方框内的小三角就可以改为Release。
Debug称为调试版本,它包含了调试信息,并且不做任何的优化,便于开发人员调试程序。
Release称为发布版本,它往往进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好的使用。测试人员所拿到的版本也是此版本。
我们可以通过查看同一个程序分别使用Debug和Release所生成的可执行文件来观看其差别:
Debug版本:
Release
通过上述两个图片,我们可以直观的看出,两个不同版本的可执行文件的大小差异很大,这正是因为Debug版本的文件里面包含了各种调试信息,并且编译器没有对程序进行优化。
所以我们说调试就是在Debug版本的环境当中,寻找代码中潜伏的问题的一个过程。
优化
在介绍Debug和Release时,我们提到了编译器在Release版本的环境中在编译时会对程序进行优化,那么这个优化是怎么回事呢?
我们看到下面一段代码:
再次声明:本文所使用IDE为Visual Studio 2019 版本,程序所展现的情况仅为此IDE环境导致,不引伸其它IDE或者编译器。
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 0 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
分别在Debug
和Release
环境下执行程序,你会发现结果却是大不相同。
在
Debug
环境下,程序的执行结果是死循环。
在Release
环境下,程序的执行结果是打印了十二次hehe
,并没有死循环。
而这就是因为优化导致的。
至于是如何对这个程序进行优化的,我会在下文中的调试实例中,进行分析。
三、常见错误
编译型错误
编译型错误,即在编写源代码时留下的错误,然后在执行编译时因为源代码中存在问题而出现编译错误,例如语法错误,关键字拼写错误,中英文括号等等问题。
这类问题只需要直接查看错误提示信息,或者凭借经验即可解决。
链接型错误
链接型错误,即调用函数或者别的文件中的函数时,找不到目标。
- 标识符名不存在(拼写错误)。
- 项目工程与第三方库版本不一致,比如工程师64位的,而库是32位的;
- 找不到相应的库文件。
这类问题,主要是看错误提示信息,在代码中找到错误信息的标识符,然后定位问题所在。
运行时错误
运行时错误,即代码语法没有问题,编译器可以编译通过。但是程序执行的结果不是预期的结果。
例如: 1+1 预期为 2 但是没有输出结果或者输出结果为 1。
这类问题只能通过调试来进行定位问题。
四、Visual Studio 2019 调试环境介绍
调试环境
在进行调试之前,应该把环境设置为Debug选项,才能使代码正常进行调试。
常用快捷键
快捷键的使用可以提高我们的开发效率,使用快捷键是一个好的习惯。
而从哪里可以找到什么快捷键对应什么功能呢?你可以在VS编译器的工具栏中,依次点开,你就会看到功能描述的后面对应着的就是其快捷键,下面,我们来讲一些经常用的快捷键。当然这只是我目前所经常用到的,如果你还有别的需求可以自行查找。
注:笔记本电脑在使用键盘的功能键区,也就是F1 ~ F12时,在开启功能键的情况下,需要按住Fn建然后在按F1 ~ F12,否则所对应的功能就是加减声音等标注在按键上的图案。
-
开始调试(F5)
启动调试,经常用来直接跳到下一个断点处,所以通常与切换断点(F9),配合使用。 -
切换断点(F9)
创建断点和取消断点。
断点的重要作用:可以在程序的任意位置设置断点。这样就可以在调试时使得程序在想要的位置随意的停止执行,继而一步步执行下去。 -
逐过程(F10)
通常用来处理一个过程。一个过程可以是一次函数调用,也可以只是一条语句。 -
逐语句(F11)
每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数的内部(这是最长用的)。 -
开始执行-不调试(Ctrl+F5)
开始执行,并且不进行调试。如果你想让程序直接运行起来而不进行调试就可以使用此快捷键。
开始调试与断点
这里我们将开始调试快捷键和断点快捷键一起讲解。
- 设置断点(F9)
首先是设置断点,设置断点的方式有3种
-
方法一:把光标点在需要设置断点的那一行代码上,然后右击鼠标,在出现的一级菜单中选择断点,然后在二级菜单中选择插入断点,之后就会看到行号的前面出现一个红色圆点。
-
方法二:把光标点在需要设置断点的那一行代码上,然后在键盘上敲击
F9
键,就可以设置断点(如果是笔记本用户,可能是Fn
+F9
),如果需要删除断点只需要再敲击一次F9键即可。 -
方法三:鼠标点击行号前面的空白区域,也可以进行设置断点或者删除断点的操作。
- 开始调试(F5)
当设置好断点之后,敲击键盘F5
键,开始调试。调试开始之后程序会自动执行到断点处,然后暂停程序,进入等待。等候你的下一步操作。
如果设置多个断点,并且断点不在循环内部,那么每敲击一次F5
键,程序就执行到下一个断点处。
如果你设置的断点在循环内部,那么你第一次敲击F5
,程序会执行到设置的断点处;第二次敲击程序会执行第二次循环,一直到这个循环执行结束。
逐过程与逐语句
-
逐过程(F10)
调试时,每一次敲击F10键,都只会执行一条语句,也就是一行代码,如果遇到函数,不会进入函数内部,会直接完成函数调用,然后执行下一条语句。 -
逐语句(F11)
在进行调试时,每一次敲击F11键,也会执行一条语句,但是遇到函数时,会进入到函数内部,然后按F11键,会执行函数内部的第一条语句,直到函数执行完成,再执行函数的下一条语句。
开始执行-不调试
与F5
键(开始执行)不同的是,F5+Ctrl
键(开始执行-不调试),在执行程序时遇到断点不会暂停,会一直把这一个程序执行完毕。
调试时查看程序当前信息
在调试时,如果只是执行程序,然后逐语句执行程序,这样来进行排查bug,也太low了吧。什么都看不到,这样的调试是很没有意义的,如果能看到程序执行过程中,每一次执行一条语句发生了什么,有没有达到你的预期,这种才是好的调试过程。
在VS IDE中可以在调试过程中查看这几种信息:
其中常用到的:
- 监视
- 内存
- 调用堆栈
- 反汇编
- 寄存器
重要的一点是,这些窗口,一定是你在调试开始之后才能看到,平时是不显示的。
监视
在上图中,我们可以看到监视的下面还有自动窗口和局部变量。
自动窗口和局部变量
这两个在使用时其实不是那么方便,使用时不可以由使用者添加或者删除监视的变量。其会根据程序所执行到的语句或者程序执行到的局部范围内,自动删减监视的变量。
自动窗口,如下图,当程序在主函数内执行时,会自动增加你所执行语句中的变量,但是当你进入函数内部时,主函数中的变量就不再可以观察。只有当你出了函数,回到主函数才会再次出现主函数内部的变量。
局部变量,与自动窗口不同的是,在使用局部变量时,即使程序并没有执行到局部变量所在的那一条语句,局部变量窗口也会直接显示出这个变量。也就是说局部变量窗口会直接显示程序执行所在的局部内的所有可监视的变量。当然也是不可以由操作者自行添加和删除,并且程序执行到别的函数内,就只显示当前局部的变量。
监视
既然说了自动窗口和局部变量的缺点,并且说其不方便,那么监视中肯定不会出现这些不方便的点。
监视,顾名思义监视你想要看到的值,或者值的变化。
在使用监视窗口的过程中,你可以随意的增加和删除你想监视的量,例如变量、地址、数组等等等。当然相对于另外两位,它也有不方便的点,就是不自动还要咱们手动输入。
小技巧,当你监视一个数组时,如果只想看到这个数组的前面的某些元素,你可以在监视中添加名称时,在数组名后面加上一个英文的逗号和一个你想要看的元素的范围,例如你想看到三个元素,
str,3
,即可。
内存
为了方便显示与查看,当你打开内存窗口时你看到的是十六进制的数。
在内存窗口,你可以查看你想要查看的变量的地址,以及这个变量在内存中是怎么存储的。
如果是一个变量只需要在内存窗口中输入&a,如果是数组名直接输入数组名即可。
例如:创建一个整型变量a,查看a的内存地址,以及a的值在内存中是怎么存储的。
a的值为12,转换为16进制是0x0000000c
内存窗口显示的列数,如果你没有更改过,最初为自动显示,如果有要求,可以随意更改为你想要的列数。
调用堆栈
调用堆栈窗口可以清晰的反应函数的调用关系以及当前调用所处的位置。
查看汇编信息
在调试开始之后有两种方式打开反汇编窗口,查看反汇编信息
- 方法一:调试->窗口->反汇编
- 方法二:右击鼠标->转到反汇编
查看寄存器
- 打开方式:
什么是寄存器
寄存器是一块速度非常快的计算机内存,相比于其余存储设备,计算机的速度是最快的,同时也是造价最昂贵的。
在 8086 架构中,所有的内部寄存器、内部以及外部总线都是 16 位宽,可以存储两个字节,因为是完全的 16 位微处理器。8086 处理器有 14 个寄存器,每个寄存器都有一个特有的名称。
- 通用寄存器
通用寄存器主要有四种,这四个寄存器一般用来存放数组,也被称为数据寄存器。
AX:累加寄存器,它主要用于输入/输出和大规模的指令运算。
BX:基址寄存器,用来存储基础访问地址
CX:计数寄存器,CX 寄存器在迭代的操作中会循环计数
DX:数据寄存器,它也用于输入/输出操作。它还与 AX 寄存器以及 DX 一起使用,用于涉及大数值的乘法和除法运算。
- 段寄存器
CPU中包含四个段寄存器,用作程序指令,数据或栈的基础位置。
CS:代码寄存器,程序代码的基础位置
DS:数据寄存器,变量的基本位置
SS:栈寄存器,栈的基础位置
ES:其他寄存器,内存中变量的其他基本位置。
- 牵引寄存器
牵引寄存器主要包含段地址的偏移量。
BP:基础指针,它是栈寄存器上的偏移量,用来定位栈上变量
SP: 栈指针,它是栈寄存器上的偏移量,用来定位栈顶
SI : 变址寄存器,用来拷贝源字符串
DI : 目标变址寄存器,用来复制到目标字符串
借鉴于:https://www.cnblogs.com/mq0036/p/15667691.html
四、调试实例
下面这段程序,在debug
环境中,会死循环打印“hehe”,而在Release
环境中只会打印12次“hehe”。
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe\n");
}
return 0;
}
- 监视
- 通过监视i的循环一次加一,以及数组的相应变化进行调试
- 当我们进行了十三次循环之后发现程序中的
i
和数组越界后的arr[12]
同时变成了0此时,我们有理由怀疑它们两个在内存中的地址可能是相同的。
- 内存
- 通过监视内存也证实了这一想法
- 画图解释
-
Debug 环境
在创建局部变量时,局部变量分配的内存空间在栈区,当创建好一个局部变量之后,再创建其它的局部变量,同时也在内存空间的栈区分配空间,此时就会进行压栈。在栈区创建变量时,先使用内存高地址,再使用内存低地址。
从图中可以看出,当数组越界访问2个4四个字节的内存空间之后,再越界访问第三个,也就是arr[12],正好是i
在内存中分配的内存空间,也就是&arr[12] = &i
。所以当i = 12
时进入循环,执行arr[12] = 0
,也就使得了他俩相同的那块内存地址中的值变为了0,使得i = 0
,之后循环再进入判断,执行。此时程序死循环。 -
Release
当一个程序在生成Release版本的可执行文件时,编译器会对程序进行各种优化,使得程序在代码大小和运行速度上都是最优的。
在这个程序中,编译器对程序的优化,我们可以通过简单修改代码,然后在执行程序,就可以发现。
#include<stdio.h>
int main()
{
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("&i = %p\n", &i);
printf("&arr[%d] = %p\n", i,&arr[i]);
}
return 0;
}
截取程序输出结果的一部分:
&i = 0137F860
&arr[12] = 0137F894
从输出结果可以看出,此时i
在内存空间的地址比数组arr
在内存空间中的地址要低,从图像上看:
所以Release版本下,不会出现把i的值赋值为0的情况,及不会出现死循环。
4,5,6,7,8,9,10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf(“&i = %p\n”, &i);
printf(“&arr[%d] = %p\n”, i,&arr[i]);
}
return 0;
}
截取程序输出结果的一部分:
```C
&i = 0137F860
&arr[12] = 0137F894
从输出结果可以看出,此时i
在内存空间的地址比数组arr
在内存空间中的地址要低,从图像上看:
[外链图片转存中…(img-7UOa3QSQ-1669863697546)]
所以Release版本下,不会出现把i的值赋值为0的情况,及不会出现死循环。
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)