gdb基本命令以及gdb解决Segmentation fault范例

在linux系统下,对程序的调试一般使用gdb。这里简单介绍一下gdb的基本交互命令以及利用gdb解决程序中Segmentation fault报错的例子。

添加core文件

启动程序之前输入命令:

ulimit -S -c unlimited
sudo sysctl -w kernel.core_pattern=/home/name/core-%e-%s-%u-%g-%p-%t

其中第二个式子的含义如下

%e  The process or thread's comm value, which typically is the
    same as the executable filename (without path prefix, and
    truncated to a maximum of 15 characters)
%i  TID of thread that triggered core dump, as seen in the PID
    namespace in which the thread resides.
%p  PID of dumped process, as seen in the PID namespace in which
    the process resides.
%s  Number of signal causing dump.

启动gdb

对c/c++程序的调试,需要在编译前加上-g选项。

$g++ -g helloWorld.cpp -o  helloworld

若程序编译通过,会产生一个名为 helloworld 的可执行文件,此时只需要输入如下命令就可以进入gdb调试模式:

$gdb helloworld

若程序编译有问题(一会将会介绍一个例子讲解),会产生一个core文件,一般在当前目录下,此时输入如下命令就会进入到调试模式(core是程序非法执行后core dump后产生的文件):

$gdb helloworld core

若看不到core文件,可使用ulimit -c 命令可查看core文件的生成开关。若结果为0,则表示关闭了此功能,不会生成core文件。

gdb常用交互命令

启动gdb后,进入到交互模式,通过以下命令完成对程序的调试;注意高频使用的命令一般都会有缩写,熟练使用这些缩写命令能提高调试的效率;
运行

  1. run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令;
  2. continue (简写c ):继续执行,到下一个断点处(或运行结束);
  3. next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内;
  4. step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的;
  5. until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体;
  6. until+行号: 运行至某行,不仅仅用来跳出循环;
  7. finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息;
  8. call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55);
  9. quit:简记为 q ,退出gdb。

设置断点

  1. break n (简写b n):在第n行处设置断点(可以带上代码路径和代码名称: b OAGUPDATE.cpp:578);
  2. b fn1 if a>b:条件断点设置
  3. break func(break缩写为b):在函数func()的入口处设置断点,如:break cb_button
  4. delete 断点号n:删除第n个断点
  5. disable 断点号n:暂停第n个断点
  6. enable 断点号n:开启第n个断点
  7. clear 行号n:清除第n行的断点
  8. info b (info breakpoints) :显示当前程序的断点设置情况
  9. delete breakpoints:清除所有断点:

查看源代码

  1. list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
  2. list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
  3. list 函数名:将显示“函数名”所在函数的源代码,如:list main
  4. list :不带参数,将接着上一次 list 命令的,输出下边的内容。

打印表达式
5. print 表达式:简记为 p ,其中“表达式”可以是任何当前正在被测试程序的有效表达式,比如当前正在调试C语言的程序,那么“表达式”可以是任何C语言的有效表达式,包括数字,变量甚至是函数调用。
6. print a:将显示 a 的值
7. print gdb_test(a):将以变量 a 作为参数调用 gdb_test() 函数
8. display 表达式:在单步运行时将非常有用,使用display命令设置一个表达式后,它将在每次单步进行指令后,紧接着输出被设置的表达式及值。如: display a
9. watch 表达式:设置一个监视点,一旦被监视的“表达式”的值改变,gdb将强行终止正在被调试的程序。如: watch a
10. whatis :查询变量或函数
11. info function: 查询函数
12. 扩展info locals: 显示当前堆栈页的所有变量

查询运行信息

  1. where/bt :当前运行的堆栈列表;
  2. bt backtrace 显示当前调用堆栈
  3. up/down 改变堆栈显示的深度
  4. set args 参数:指定运行时的参数
  5. show args:查看设置好的参数
  6. info program: 来查看程序的是否在运行,进程号,被暂停的原因。

分割窗口

  1. layout:用于分割窗口,可以一边查看代码,一边测试:
  2. layout src:显示源代码窗口
  3. layout asm:显示反汇编窗口
  4. layout regs:显示源代码/反汇编和CPU寄存器窗口
  5. layout split:显示源代码和反汇编窗口
  6. Ctrl + L:刷新窗口

gdb解决Segmentation fault范例

我们打算使用gdb去解决为什么下面的程序(文件为segfault.c)引起了段错误的问题。

#include <stdio.h>
#include <stdlib.h>
 
int main(int argc, char **argv)
{
   char *buf;
 
   buf = malloc(1<<31);
 
   fgets(buf, 1024, stdin);
   printf("%s\n", buf);
 
   return 1;
}

第一步是使用带有调试标志(debugging flags)的方式编译这段代码,如下:

~# gcc -g segfault.c

然后运行:

~# a.out
Hello World!
Segmentation fault

这并不是我们所期待的。是时候启动强大的gdb了。

~# gdb a.out
GNU gdb 5.0
Copyright 2000 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu"...
(gdb) 

我们直接运行就来看看到底发生了什么:

(gdb) run
Starting program: /home/dgawd/cpsc/363/a.out 
test string

Program received signal SIGSEGV, Segmentation fault.
0x4007fc13 in _IO_getline_info () from /lib/libc.so.6

我们收到了来自操作系统的SIGSEGV信号。这就意味着我们试图去访问一段非法的内存,让我们试试backtrace(= bt)命令:

(gdb) backtrace
#0  0x4007fc13 in _IO_getline_info () from /lib/libc.so.6
#1  0x4007fb6c in _IO_getline () from /lib/libc.so.6
#2  0x4007ef51 in fgets () from /lib/libc.so.6
#3  0x80484b2 in main (argc=1, argv=0xbffffaf4) at segfault.c:10
#4  0x40037f5c in __libc_start_main () from /lib/libc.so.6

这里我们只关心我们自己的代码,因此我们就切换到3号堆栈帧(stack frame3)来看看程序在哪里崩溃的:

(gdb) frame 3
#3  0x80484b2 in main (argc=1, argv=0xbffffaf4) at segfault.c:10
10        fgets(buf, 1024, stdin)

哦,原来是调用fgets引起的崩溃。一般的,我们都假设库函数比如fgets都可以正确地工作。因此这个问题的原因就一定是其中我们的一个参数的问题。你也许不知道‘stdin’是一个全局的变量,它是被stdio 库创建的。因此我们假定这个参数是正确的。那么剩下的就只能是‘buf’了,然后查看buf当前的值:

(gdb) print buf
$1 = 0x0

buf的值是0x0,也就是NULL指针。这并不是我们锁期待的 —— buf应该指向第8行代码分配到的内存。因此我们需要返回到第8行并看看在哪里发生了什么。首先kill掉我们程序当前运行的调用:

(gdb) kill
Kill the program being debugged? (y or n) y

(注意:不用使用quit直接退出gdb,这样比较麻烦。直接kill掉当前的程序调用即可)

然后在第8行设置一个断点:

(gdb) break segfault.c:8
Breakpoint 1 at 0x8048486: file segfault.c, line 8.

再次运行程序:

(gdb) run
Starting program: /home/dgawd/cpsc/363/a.out 

Breakpoint 1, main (argc=1, argv=0xbffffaf4) at segfault.c:8
8         buf = malloc(1<<31);

我们检查malloc调用前后buf值的变化。初始化buf以前,其值应该是一个随机杂乱值(garbage),就像这里的:

(gdb) print buf
$2 = 0xbffffaa8 "鳃?\177\003@t`\001@\001"

我们step over(单步执行)malloc调用然后再次检查buf的值:

(gdb) next
10        fgets(buf, 1024, stdin);
(gdb) print buf
$3 = 0x0

可见调用了malloc之后,buf是NULL。如果你查看malloc的手册页(man page),你就会发现malloc在不能分配够所需的内存的时候就会返回NULL。因此确定是我们的malloc失败了。让我们返回到代码再次看看:

7 :   buf = malloc(1<<31);

哦,表达式1<<31(整型1左移31次,原文错写为右移)是429497295, 或4GB (gigabytes).很少有机器会有这样的内存——大多数只有256MB(显然这篇文章有年头了,都什么年代了,这点内存操作系统估计启动一半就挂了)。因此malloc必然会失败。此外,在fgets中我们只读入1024字节。所有的额外空间都会白白浪费掉,尽管我们可以分配到。这里我们将1<<31改为1024(或者1<<9),这样程序就会按照我们的期望运行了:

~# a.out
Hello World!
Hello World!

这样你就可以知道怎样使用gdb来调试段错误了,这是非常有用的。这个例子同时也说明了一个非常重要的准则:总是检查malloc的返回值!

gdb启动以及core文件主要参考:https://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html
https://blog.csdn.net/cozhzh/article/details/88105478
gdb调试主要参考 https://blog.csdn.net/Deutschester/article/details/6739861

  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值