据传,世界上最早一批程序员中的一位,葛丽丝·霍普,在调试程序时出现故障,拆开继电器后,发现有只飞蛾被夹扁在触点中间,从而“卡”住了机器的运行。于是,霍波诙谐的把程序故障统称为“臭虫(BUG)”,把排除程序故障叫DeBug。后来,Debug 就成为了一个专业的计算机术语。
Debug 是一个程序员的基本功。它有很多层含义,比如源码级Debug,汇编级Debug,系统级Debug等等。Fortran 程序员,通常只会接触到源码级Debug。本文也只描述源码级Debug,如您对汇编和反汇编有一定的了解,还可尝试汇编级Debug。
源码级 Debug,意思是,让程序逐行逐行的运行,运行在中途时,可暂停运行,并将此时的若干状态呈现给程序员查看,以便程序员分析,此时各变量各过程及程序流程,是否符合自己的预期。同时,调试者可随时改变这些状态,例如变量的值,然后继续运行,以便测试在不同情况下程序的反应。
目前的计算机运行速度已经很快了,往往很长的代码运行完毕也就一眨眼的功夫。如果运行过程中出现错误,或者计算结果不正确。往往程序员不知道问题发生在哪里。而 Debug 则可以让程序在可疑的程序段暂停下来,程序员可以查看此时是否符合自己的预期?从而为查找问题提供更多的依据。
源码级 Debug,可便于程序员查找运行时错误(Run-time Error),计算结果不正确,这两种问题。(编译链接错误不能通过Debug查找)
第二,Debug 的前提
Debug 实际上是一种运行程序的方式。所以,首要前提是,程序已经可以正常链接,获得可执行文件。对于不单独运行的程序,例如 DLL,LIB 等,需要额外的 Loader 来加载它,并进行调试(本文暂不涉及,如您有疑问,可于本站论坛提出)。如果您的代码编译链接还有错误,那么请先解决编译链接问题。
其次,Debug 需要调试器(Debugger),这是一种软件。一般商业编译器都会附带调试器。免费开源编译器也会有附属的开源调试器。如果你的编译器产品需要选择组件安装,请确保自己勾选了相应的调试器并进行了安装。
最后,很多编译器允许两种编译链接方式:Debug模式 和 Release模式。
这两种链接方式的区别主要是:
1.Debug 模式:程序几乎不进行优化。产生的可执行程序具有调试信息,执行效率低,文件尺寸大。
2.Release 模式:程序进行合理优化。产生的可执行程序不具有调试信息,执行效率高,文件尺寸小。
(实际上,Debug模式 和 Release模式只是编译器预设的两种方式,我们可以通过调节编译链接参数来获得更自由的搭配,产生介于Debug和Release之间的编译方式)
想要进行 Debug 调试,我们需要程序中存在调试信息,需采用 Debug 模式 编译链接程序才可以。
第三,Debug 的操作
调试器的操作,因不同调试器而不同,这里以集成在 Visual Studio 中的 Intel 调试器为例。Intel Visual Fortran 会默认安装这个调试器。
其他的调试器,如果也是集成在IDE中的,则操作方式大同小异。如果是单独的命令行程序,则需要通过命令来进行调试(本文暂不涉及)
首先我们来看一个示范代码:
Program www_fcode_cn !// 本程序用于演示 Intel 调试器的使用 Implicit None Integer , parameter :: N = 10 Real :: a( N ) , b( N ) integer :: i Do i = 1 , N a(i) = i b(i) = 100 - i End Do !a(5) = 0.0 Do i = 1 , N write( * , * ) b(i) / a(i) End Do End Program www_fcode_cn
程序第11行,是故意将a(5) 设定为 0.0 的。如果不执行它,则程序应该输出:
99.00000
49.00000
32.33333
24.00000
19.00000
15.66667
13.28571
11.50000
10.11111
9.000000
如果执行了11行,则可能会输出 Infinity,也可能会出现运行时错误forrtl: error (73): floating divide by zero
(根据不同编译器或编译参数中,浮点数的设置而不同)
假设我们程序执行时,遇到了错误,分母为零了。我们来看看如何通过 Debug 发现这个错误。
首先第一步,我们要在可疑的位置插入断点(Insert breakpoint),调试器会让程序运行到断点的位置并暂停下来。如果可疑的位置有多处,可以分别插入断点。
集成开发环境里,在编辑代码时,右键既可插入断点。(在某些环境下,直接点击某行最前方也可插入断点)
请注意,如果你修改了代码,则需要保存,然后编译链接,才能够插入断点。另外,断点不可位于注释语句上,比如上例,想在11行插入断点,就需要取消11行的注释感叹号(然后保存重新编译链接)
需要注意的是,断点需要执行到它的位置,只能位于执行语句(定义语句并不执行)。
如上图,我们插入了两个断点。存在断点的行,一般会有红色的实心圆。
第二步,启动调试程序。可通过菜单栏,工具栏上的按钮进行。在不同的环境下,具体菜单栏和按钮位置不同。你或许需要自定义一下工具栏。
之后,我们可以看到,程序执行到它遇到的第一个断点处。
此时,黄色的箭头指向的行,即为当前程序执行到的位置。(一般来说,一开始都在断点上,所以是红色圆内一个黄色箭头)
此时,程序就在一开始的位置等待我们。我们可以查看到各种状态,例如变量的值。在局部变量窗口我们会看到 I B A 三个变量(或数组),他们的值很乱。因为此时还没有对他们进行初始化。
如果你无法看到局部变量窗口,可能是被隐藏起来了,你或许需要在角落里找一下它,或者通过某些菜单把他“召唤”出来。(由于VS版本的不同,具体位置和图标外观,菜单名称可能稍有不同)
在局部变量里,我们已经可以修改 i 或者 a b 数组的值了,但是目前修改,还没有太大的意义。
我们按下逐语句按钮:
黄色箭头就会下移到下一条语句,这表示程序又向前执行了一行。
同时,局部变量里,i 的值变成了 1 ,这表示循环变量 i 有了值,并且当前是 1。红色,表示此时与上一步相比值发生了改变。相比而言,B 和 A 数组未改变,因此不是红色。
再次点击逐语句按钮(或快捷键F11),黄色箭头继续下移,且 A(1) 变为红色。这表示数组 a 的第一个元素发生了改变。
多次点击逐语句按钮。会发现,黄色箭头开始在循环体内来回移动,i 的值从1开始变大,a 数组慢慢变为 1-10 之间的数。这是完全符合我们的代码预期的。
如果循环较多,比如1000次循环,点击逐语句就需要点击几千次。此时,我们可以在循环外插入第二个断点,例如在本例的第11行。然后点击继续按钮(表示直接运行到下一个断点处,而不是一步一步执行了)。
此时,可发现黄色箭头已到了下一个断点的位置,而且 A 数组和 B 数组已经赋值完毕。均符合我们的预期。
(假设我们此时无法确定第11行导致了 a(5) = 0.0)
再次点击“继续”按钮。编译器一直运行,直到发生了错误为止,会弹出错误:
我们点击“中断”,会发现程序虽然没有碰到断点,但依然暂停了。黄色箭头停留在第13行。我们把鼠标移动到 i 变量上,看到他的值是 5,说明第5次循环出现了错误。
分别移动鼠标到 b 和 a 上,查看 b(5) 和 a(5) 的值。会发现 a(5) 的值为 0.000,这就是导致错误的原因。
在某些情况下,某些编译器,或者因为设置原因,并不会抛出错误,也无法中断,编译器会让 b(5) / 0.0 = Infinity。
此时,就需要更细致的断点,跟踪,检查各变量的值是否符合预期,然后再来确定原因了。(当然也可以通过设置让编译器抛出浮点数错误),例如 IVF 如此设置:
关于调试,还有很多问题。下面简单陈述,具体效果请读者自行测试,很容易理解。
上述的逐语句按钮,在遇到函数调用时,会进入函数内部。如果不希望进入函数内部,可使用逐过程按钮(或跟踪步过),如果已进入函数内,可使用跳出当前函数执行到返回。
读者可以自行拿出自己以前的100行以内有函数调用的代码来测试一下,点击这些按钮,观察黄色箭头的位置,这些概念并不难理解。
最后,有一个比较常用的东西,叫做“条件断点”,它会在满足某些条件时才会触发这个断点。例如,一个10000次的循环,循环到 1234 次出现了错误。我们不能点1234次逐语句吧?此时,我们可设置断点的触发条件为 i>=1234。
或者说,循环到不知道多少次的时候,某个数 ff 变为 0 了,我们可设置断点的触发条件为 ff < 0.00001。
首先插入常规断点,然后在断点处右键:
调试还有很多可利用的工具,例如调用堆栈可查看程序调用函数的全部路线,对某些变量数组添加监视,查看内存中的数据,寄存器的值,windows返回的错误,调试字符串等等。
请读者朋友们根据现有的提示自行琢磨。祝大家的代码都调通!