当前位置: 首页 > 科技观察

比GDB更方便的代码调试工具:CGDB

时间:2023-03-19 12:23:05 科技观察

CGDB是GDB的前端。在终端窗口(基于ncurse)以图形形式调试代码非常方便。与GDB相比,可以大大提高效率。在这篇文章中,我将分享CGDB最基本的使用方法。如果是第一次听说,强烈推荐你去体验一下,你一定会爱上它的!带有bug的示例代码无符号短数据长度;unsignedintflag;}__attribute((packed))__;constunsignedchar*g_data="hello";/*功能:加载一段数据参数一:data[OUT]:加载数据的缓冲区参数二:len[OUT]:实际加载的数据长度返回值:0-成功,否则-失败*/staticintget_data(unsignedchar*data,unsignedint*len){assert(data&&len);memcpy((void*)data,(void*)g_data,strlen(g_data));*len=strlen(g_data);return0;}intmain(intargc,char*argv[]){//创建结构变量structUSER_DATAuser_data;user_data.flag=0xA5;//将数据加载到结构变量中if(0==get_data(user_data.data,&user_data.data_len)){printf("get_dataok!\n");printf("data_len=%d,data=%s\n",user_data.data_len,user_data.data);printf("user_data.flag=0x%x\n",user_data.flag);//期望值:0xA5}else{printf("get_datafailed!\n");}return0;}编译前看代码,可以发现是是bug吗?当然在编译的时候,编译器会以Warning的形式给出风险提示,因为示例代码很简单,所以很容易找到。但是在一个项目中,如果不喜欢消除compilationWarning警告,这个bug还是比较隐蔽的,编译测试代码:gcc-gtest.c-otest因为要用gdb调试,别忘了加上-g选项。GDB调试操作$gdb./test(gdb)r//全速执行一次(gdb)rStartingprogram:/home/captain/demos_2022/cgdb/testteststart...get_dataok!data_len=5,data=hellouser_data.flag=0x0【劣1(进程9933)正常退出】发现user_data.flag的值不对,决定在调用get_data之前的那行打个断点,然后从开始:检查代码行号:(gdb)lmain18*len=strlen(g_data);19return0;20}2122intmain(intargc,char*argv[])23{24structUSER_DATAuser_data;25user_data.flag=0xA5;26if(0==get_data(user_data.data,&user_data.data_len))27{断点在第25行:(gdb)b25Breakpoint1at0x400771:filetest.c,line25.开始运行:(gdb)rStartingprogram:/home/captain/demos_2022/cgdb/testBreakpoint1,main(argc=1,argv=0x7fffffffdc58)attest.c:2525user_data.flag=0xA5;停在断点处,赋值语句还没有执行,所以第一步执行一次:(gdb)step26if(0==get_data(user_data.data,&user_data.data_len))此时,打印出的值和地址变量user_data.flag:因为后面会进入调用的函数,这个变量是不可见的,所以需要按地址打印。(gdb)print&user_data.flag$1=(unsignedint*)0x7fffffffdb62(gdb)print/xuser_data.flag$2=0xa5此时赋值正确,然后继续执行,进入调用函数get_data(),(gdb)stepget_data(data=0x7fffffffdb40"n\333\377\377\377\177",len=0x7fffffffdb60)attest.c:1616assert(data&&len);这个函数一共有4行代码,我们每一步执行一句话,只是打印user_data.flag变量的内容。单步执行下一行memcpy,看user_data.flag变量地址处的内容是否还是:0xa5:(gdb)step17memcpy((void*)data,(void*)g_data,strlen(g_data));(gdb)print/x*0x7fffffffdb62$3=0xa5继续单步执行(因为没有必要跟进memcpy和strlen的内部,所以使用next命令),打印:(gdb)next18*len=strlen(g_data);//thisAsentenceisabouttobeexecuted(gdb)print/x*0x7fffffffdb62$4=0xa5(gdb)next19return0;(gdb)print/x*0x7fffffffdb62$5=0x0发现一个问题:执行*len=strlen(g_data)语句后,变量user_data.flag地址中的内容发生了变化。再次仔细查看代码,可以诊断是数据类型使用错误。解决bug:get_data()函数的最后一个参数应该是一个无符号的短指针。问题解决了,但是回过头来看gdb的调试过程,还是比较繁琐:调试命令和代码显示混在一起,需要敲很多命令。CGDB调试操作启动CGDB后,终端窗口分为上下两部分:上半部分是代码窗口,下半部分是调试窗口。按ESC键进入代码窗口。这时候可以上下浏览代码,进行一系列操作:空格键:设置或取消断点;o:查看代码所在文件;/或?:在代码中搜索字符串;。..还有很多方便的快捷键:-:缩小代码窗口;+:展开代码窗口;gg:将光标移动到文件开头;GG:移动光标到文件末尾;ctrl+b:将代码向上翻一页;ctrl+u:代码向上翻半页;ctrl+f:代码向下翻一页;ctrl+d:代码向下翻半页;按i键返回调试窗口,进入调试模式。使用的调试指令和GDB几乎一样!也就是说:可以在实时查看代码的同时进行调试操作,大大提高了效率。我们先回顾一下上面GDB的调试过程:按ESC键进入代码窗口,如果代码前面的行号是白色的,就是当前行。按j键将突出显示的当前行向下移动。移动到第25行时如下:按空格键在该行设置断点,此时行号变为红色:并在调试窗口打印一行信息:(gdb)Breakpoint1at0x400771:filetest.c,line25.按i键回到debug运行窗口,再输入运行命令r,会停在25行,如下图绿色箭头所示:当然,debugwindow也会打印出相关信息:(gdb)rStartingprogram:/home/captain/demos_2022/cgdb/testBreakpoint1,main(argc=1,argv=0x7fffffffdc58)attest.c:25单步执行此赋值语句,然后打印user_data.flag的值和地址:(gdb)print/xuser_data.flag1:/xuser_data.flag=0xa5(gdb)print&user_data.flag2:&user_data.flag=(unsignedint*)0x7fffffffdb62At至此,赋值语句执行正确,打印出来的值也符合预期。然后执行单步指令,进入函数get_data():(gdb)stepget_data(data=0x7fffffffdb40"n\333\377\377\377\177",len=0x7fffffffdb60)attest.c:16此时,上面的代码窗口自动进入了get_data()相关的代码,如下图:继续单步执行,在执行赋值语句*len=strlen(g_data)之前打印变量user_data.flag地址的内容;:(gdb)print/x*0x7fffffffdb62$2=0xa5正确!然后执行赋值语句后,再次打印:(gdb)next(gdb)print/x*0x7fffffffdb62$3=0x0发现问题:执行*len=strlen(g_data)语句后,变量user_data.flag地址的内容被改变了。总结:虽然我把CGDB的运行过程写得比较啰嗦,但是实际使用起来真的是丝般顺滑,就像巧克力一样!