最近需要在linux上使用c++开发后台服务器程序。原先使用Python很顺手,但是基于项目需求的原因需要转到c++开发,后者优点是效率高,缺点是技术难度大,最要命的是调试难度比python要大很多,于是我又不得不把GDB应用的一些知识点捡起来。
在linux上调试c++代码比windows痛苦的多,因为后者有visual studio这个宇宙第一好用的IDE存在,基于命令行的linux唯一的选择就是GDB,你需要记住很多命令,在用户界面下的很多简单调试功能,例如查看变量值等只不过挪动一下鼠标即可,但是在gdb下你需要输出不少命令,好在一旦这些命令熟悉了,使用GDB的效率就会大大提高。因此我借此把GDB的使用心得或学习内容记录下来,以后需要的时候还能查看。
我们先使用一段示例代码来发起GDB调试流程:
#include "stdio.h"
#include "stdlib.h"
//x数组用于将接收的字符型数字转换为整形
//y数组用于排序插入
int x[10], y[10], num_inputs, num_y = 0;
void get_args(int ac, char **av) { //该函数将命令行输入参数从字符转换为数字并存储在数字x中
int i;
num_inputs = ac - 1;
for (i = 0; i < num_inputs; i++) {
x[i] = atoi(av[i+1]);
}
}
void scoot_over(int jj) {//执行插入排序,将数组y中元素向右移动把位置空出来给新的元素
int k;
for (k = num_y - 1; k > jj; k++) {
y[k] = y[k-1];
}
}
void insert(int new_y) {//获得新的要插入的数值然后插入到数值y
int j;
if (num_y = 0) { //problem here
y[0] = new_y;
return;
}
for (j = 0; j < num_y; j++) {//如果要插入的数值比当前元素小,那么将当前及后续元素向右挪动
if (new_y < y[j]) {
scoot_over(j);
y[j] = new_y;
return;
}
}
}
void process_data() {//启动插入排序流程
for (num_y = 0; num_y < num_inputs; num_y++) {
insert(x[num_y]);
}
}
void print_results() {//输出排序结果
int i ;
for (i = 0; i < num_inputs; i++) {
printf("%d\n", y[i]);
}
}
int main(int argc, char **argv) {
get_args(argc, argv);
process_data();
print_results();
return 0;
}
接着我们使用如下GDB命令编译代码:
gcc -g -Wall -o insert_sort ins.c
编译命令中的-g很重要,它让编译器输出符号表,gdb必须依赖符号表才能有效进行调试过程,编译好后执行如下命令:
./insert_sort ins.c 12 5
执行起来程序会卡死,显然代码有bug!使用如下命令通过GDB将有问题的程序加载起来:
gdb insert_sort -tui
-tui是gdb提供的介于命令行和纯文本之间的一种中间形态,或者说是GDB想通过文本的方式提供某种类似于命令行的功能,其执行后效果如下:
可以看到它使文本模拟了一个调试”窗口“,这种模式使得GDB调试时能让用户获得某种窗口模式下的便利,同时又不缺乏命令行模式下的高效便捷。然后执行run 12 5启动程序运行,这时候他自然会卡死,此时执行ctrl+c中断程序运行,你会发现gdb显示图像如下:
可以看到gdb在上层“窗口”中以高亮的形式停留在38行,这感觉跟使用vs设置断点后,程序停留在断点对应行的感觉一样,这种显示让人感觉很温暖,比存命令行模式只输出一系列文本信息体验就好了很多。
根据GDB显示的情形,我们有理由怀疑for这行是造成卡死的根本原因,造成for不停止的主要原因可能是变量num_y没有正确增加,因此我们要检测一下它的数值,于是使用如下命令:
print num_y
执行后所得结果为 $1 = 1, 其中"$1"用于指代变量num_y,它表示num_y是print输出的第一个变量的数值,如果后面我们使用print输出其他变量值,那么对应的变量就对应$2,$3…依次类推。按理说我们让程序跑了一会才中断,如果循环执行的逻辑正确,num_y的值肯定是一个很大的数值,现在它依然是1,于是可以怀疑,它的值没有正常增加。
由于在for循环中,影响到变量num_y值的除了for自己,还有可能就是insert函数,因此我们在该函数的入口处插入一个断点跟踪一下:
break insert
condition 1 == num_y
break insert会让GDB每次运行进入函数insert时就会中断,同时我们加了第二行命令,它告诉GDB,在进入函数insert后,还需要判断num_y等于1时才要中断,这两句其实就对应所谓的条件断点,接下来执行run命令,GDB会问你是否要重新运行程序,我们输入y,让GDB重新运行程序,接着我们得到结果如下:
我们看到GDB正好高亮在有问题的那条语句,由于此时变量num_y的值为1,如果我们继续执行程序,因为它的值不等于0,它应该越过if这段,直接跳到下面的for循环进行执行,于是我们让GDB执行“运行下一条命令”的指令,该指令对应的命令就是next,于是我们输入两次next命令,此时发现箭头没有进入for(j=0…)这个循环体内,而是直接跑到了函数末尾,也就是说程序控制流直接越过了for(j…)这个循环,如果该循环没能执行,最有可能的就是j < num_y这句不成立,但是第一次循环时j的值是0,因此要让j < num_y不成立,它的值应该不大于0,我们使用print输出该变量的值看看:
print num_y
执行命令后,输出结果为$2=0,既然num_y在进入insert函数时还是取值1,因为这是条件断点的前提条件,但是继续执行后它的值变成了0,于是Bug就可能存在于insert入口到for(j…)这个循环之间,通过审查这两处之间的代码我们可以看到一个非常经典的错误if(num_y = 0),显然if的条件判断应该是num_y == 0,是两个等号,只有一个等号就会变成赋值,然后if再根据赋值结果来决定是否进入if里面的代码,于是我们将if(num_y=0)修改成if(num_y==0),然后再运行一次:
./insert_sort 12 5,输出结果是5 , 0由此可见还有其他错误存在,我们的调试需要再接再厉。
为了定位问题,我们先输入一些简单的数据,例如只输入一个12,运行起来后可以看到程序正确输出12,因此我们初步估计,应该是在输入第二个数值5时产生问题,于是我们再次在insert函数里设置断点,由于第二次输入时num_y应该等于1,于是上次我们设置的条件断点依然能用于这次调试,因此再次执行如下命令:
break insert if num_y == 1
注意看,这次我们把两条语句合在一起,这样看起来简练一些,然后输入run 12, 5,程序会在断点处停止,然后我们使用两次next 指令执行语句,这时断点停在语句 if (new_y < y[j]) { 这行,new_y对应的值应该是第二次输入的5,前面只有12输入数组,因此第一次运行到这里时,j 等于0,y[j]应该对应数值12,我们使用命令print y[j]看一下,确定它的值确实是12,再次执行next指令进入if 语句内部,此时要执行函数scoot_over,这时我们可以选择进入该函数,那么就要使用step命令,如果直接执行该函数,那么就运行next,通常情况下,我们先直接执行它,如果结果不对再进入该函数进行查看,于是我们再次执行指令next。
按理scoot_over函数要把数组y中的元素向右边移动,然后把新输入的元素放在空出来的位置上,也就是说它要把数值12向右移动一位,然后把5放在12原来的位置上,于是我们将y数值打印出来看看结果,于是执行命令print y,执行后输出结果是{12, 0, 0…},如此看来该函数有问题,它没有吧12往右挪一位。
这样我们确定问题出现在scoot_over函数的内部实现上,现在我们可以在它里面设置断点进行调试了,我们使用命令clear insert来清除掉原来的断点,然后执行命令break scoot_over if num_y == 1,接着使用run 12, 5让程序重新执行一次,我们来到如下情况:
按道理,我们执行next指令后,代码要进入循环体内部,执行y[k] = y[k-1]从而实现元素向右动,但是执行next指令时,它居然直接越过去,循环体内的语句没有执行。显然k > jj这条判断语句没能满足。分别使用指令print k, print jj查看这两个变量的值,可以发现这两个值都是0,于是要么k的值错了,要么jj的值错了。
点击向下箭头按钮,窗口里的代码就会往下拉,我们将代码拉到scoot_over被调用的地方,也就是insert函数里面看看可以确定,scoot_over(j)的作用是将包括j以及它右边的元素都向右挪动一位,然后把新的数值放到j对应的位置。由于一开始元素12的位置就在0,因此j对应的值就是12所在位置,也就是0,于是从逻辑上推断应该是k的值错了,而k的值在for循环中初始化,于是y = num_y - 1这句应该有问题。由于num_y对应当前y数组中元素的个数,从这里判断k = num_y -1 应该是 k = num_y。
改完之后我们认为这回应该对了吧,于是再次运行,结果出现如下情况:
"段错误“
我Cao,没完了,这是代码调试最令人讨厌的地方,改完逻辑bug结果出现段错误!好在GDB是调试段错误的最好工具,我们下次再说。