Linux GDB 调试

1  调试信息和调试原理

  一般要调试某个程序,为了能清晰地看到调试的每一行代码、调用的堆栈信息、变量名和函数名等信息,需要调试程序含有调试符号信息。使用 gcc 编译程序时,如果加上 -g 选项即可在编译后的程序中保留调试符号信息。以下命令将生成一个带调试信息的程序 hello_world。

gdb -g -o hello_world hello_world.c

当然我们可以通过gdb来判断程序是否带有调试信息:

gdb hello_world 

如果gdb 加载成功以后,会显示如下信息:

Reading symbols from /root/testclient/hello_server...done

我们也可以使用 Linux 的 strip 命令移除掉某个程序中的调试信息。

strip hello_world

调试时建议关闭编译器的程序优化选项,因为程序优化后调试显示的代码和实际代码可能就会有差异了,这会给排查问题带来困难。

gdb -g -O0 hello_world world_world.c

  


2  启动GDB调试

2.1  直接调试目标程序

gdb hello_world   // gdb + 程序名

2.2  附加进程

当一个程序已经启动,我们想调试这个程序,但又不想重启这个程序时,可以通过使用 gdb attach 进程ID 来将gdb调试器附加到想要调试的程序上。

gdb attach 进程ID

当⽤ gdb attach 上⽬标进程后,调试器会暂停下来,此时可以使⽤ continue 命令让程序继续运⾏,或者加上相应的断点再继续运⾏程序。当调试完程序想结束此次调试时,⽽且不对当前进程有任何影响,可以在 GDB 的命令⾏界⾯输⼊ detach 命令 让程序与 GDB 调试器分离。

(gdb)  detach

2.3  调试core文件

Linux 系统默认是不开启程序崩溃产⽣ core ⽂件这⼀机制的,我们可以使⽤ ulimit -c 命令来查看系统是否开启了这⼀机制。使用ulimit -c unlimited 直接将core文件的大小修改成不限制大小。然后就可以通过以下命令调试core文件:

gdb filename corename

通过调试core文件可以看到程序崩溃的地方,使用bt命令查看崩溃时的调用堆栈,进一步分析找到崩溃的原因。当有多个程序崩溃时,有时很难通过core文件的名称来判断对应的core文件。我们可以自己修改core文件的名称来解决该问题。通过修改/proc/sys/kernel/core_uses_pid 可以控制产生的 core 文件的文件名,修改方式如下:

echo "/corefile/core-%e-%p-%t" > /proc/sys/kernel/core_pattern

 文件名各个参数的说明如下:

参数名称 参数含义(中文)
%p 添加 pid 到 core 文件名中
%u 添加当前 uid 到 core 文件名中
%g 添加当前 gid 到 core 文件名中
%s 添加导致产生 core 的信号到 core 文件名中
%t 添加 core 文件生成时间(UNIX)到 core 文件名中
%h 添加主机名到 core 文件名中
%e 添加程序名到 core 文件名中

假设现在的程序叫 test,我们设置该程序崩溃时的 core 文件名如下:

echo "/root/testcore/core-%e-%p-%t" > /proc/sys/kernel/core_pattern 

那么最终会在 /root/testcore/ 目录下生成的 test 的 core 文件名格式如下:

-rw-------. 1 root root 409600 Jan 14 13:54 core-test-13154-1547445291  

 

3  GDB常用调试命令

命令名称 命令缩写 命令说明
run r 运行一个程序
continue c 让暂停的程序继续运行
next n 运行到下一行
step s 如果有调用函数,进入调用的函数内部,相当于 step into
until u 运行到指定行停下来
finish fi 结束当前调用函数,到上一层函数调用处
return return 结束当前调用函数并返回指定值,到上一层函数调用处
jump j 将当前程序执行流跳转到指定行或地址
print p 打印变量或寄存器值
backtrace bt 查看当前线程的调用堆栈
frame f 切换到当前调用线程的指定堆栈,具体堆栈通过堆栈序号指定
thread thread 切换到指定线程
break b 添加断点
tbreak tb 添加临时断点
delete del 删除断点
enable enable 启用某个断点
disable disable 禁用某个断点
watch watch 监视某一个变量或内存地址的值是否发生变化
list l 显示源码
info info 查看断点 / 线程等信息
ptype ptype 查看变量类型
disassemble dis 查看汇编代码
set args   设置程序启动命令行参数
show args   查看设置的命令行参数

3.1  run 命令

前面说的 gdb filename 命令只是附加的一个调试文件,并没有启动这个程序,需要输⼊ run 命令(简写为 r)启动这个程序。

3.2  continue 命令

当 GDB 触发断点或者使⽤ Ctrl + C 命令中断下来后,想让程序继续运⾏,只要输⼊ continue 命令即可(简写为 c)。

3.3  break 命令

break 命令(简写为 b)即我们添加断点的命令,可以使⽤以下⽅式添加断点:

  • break functionname,在函数名为 functionname 的⼊⼝处添加⼀个断点;
  • break LineNo,在当前⽂件⾏号为 LineNo 处添加⼀个断点;
  • break filename:LineNo,在 filename ⽂件⾏号为 LineNo 处添加⼀个断点。

3.4  backtrace 与 frame 命令

backtrace 命令(简写为 bt)⽤来查看当前调⽤堆栈。查看调用的堆栈信息后可以使⽤ frame + 堆栈编号 命令(简写为 f),切换⾄指定堆栈顶部。

3.5  info break、 enable、 disable 和 delete 命令

在程序中加了很多断点,⽽我们想查看加了哪些断点时,可以使⽤ info break 命令(简写为 info b)。

(gdb) info b
Num Type Disp Enb Address What
1 breakpoint keep y 0x0000000000423450 in main at server.c:3709 breakpoint already hit 1 time
2 breakpoint keep y 0x000000000049c1f0 in _redisContextConnectTcp at net.c:267

  由上面的内容片段可以知道,目前一共增加了2个断点,断点1触发1次,断点2未触发过。我们想禁⽤某个断点时,使⽤“ disable 断点编号 ”就可以禁⽤这个断点了,同理,被禁⽤的断点也可以使⽤“ enable 断点编号 ”重新启⽤。使⽤“delete 编号”可以删除某个断点,如果输⼊ delete 不加命令号,则表示删除所有断点。

3.6  list命令

  第⼀次输⼊ list 命令会显示断点处前后的代码,继续输⼊ list 指令会以递增⾏号的形式继续显示剩下的代码⾏,⼀直到⽂件结束为⽌。当然 list 指令还可以往前和往后显示代码,命令分别是“list + (加号) ”和“list - (减号) ”。

3.7  print 和 ptype 命令

  通过 print + 变量名 可以打印出指定变量的值,print 命令也可以显示进⾏⼀定运算的表达式计算结果值,甚⾄可以显示⼀些函数的执⾏结果值。举个例子,我们可以使用 p a+b+c 来打印这三个变量的结果值;也可以使用 p func() 命令输出一个可执行函数 func() 的执行结果。

  print 命令不仅可以输出表达式结果,同时也可以修改变量的值,我们尝试将端⼝号从 6379 改成 6400 试试:

(gdb) p server.port=6400
$24 = 6400
(gdb) p server.port
$25 = 6400
(gdb)

  ptype 命令,其含义是“print type”,就是输出⼀个变量的类型。

3.8  info thread 和 info args命令

⽤ info thread命令来查看当前进程有哪些线程,分别中断在何处。

(gdb) info thread
Id Target Id Frame
4 Thread 0x7fffef7fd700 (LWP 53065) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
3 Thread 0x7fffefffe700 (LWP 53064) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
2 Thread 0x7ffff07ff700 (LWP 53063) "redis-server" 0x00007ffff76c4945 in pthread_cond_wait@@GLIBC_2.3.2 () from /lib64/libpthread.so.0
* 1 Thread 0x7ffff7fec780 (LWP 53062) "redis-server" 0x00007ffff73ee923 in epoll_wait () from /lib64/libc.so.6

   通过 info thread 的输出可以知道 redis-server 正常启动后,⼀共产⽣了 4 个线程,包括⼀个主线程和三个⼯作线程,线程编号(Id 那⼀列)分别是 4、 3、 2、 1。三个⼯作线程(2、 3、 4)分别阻塞在 Linux API pthread_cond_wait 处,⽽主线程(1)阻塞在 epoll_wait 处。当有多个线程时,我们可以使用 backtrace 命令查看调用堆栈,通过过堆栈判断 GDB 作用在哪个线程上面。如何切换到其他线程呢?可以通过“thread 线程编号”切换到具体的线程上去。例如,想切换到线程 2 上去,只要输⼊ thread 2 即可。

   info 命令还可以⽤来查看当前函数的参数值,组合命令是 info args

3.9  next、 step、 util、 finish 和 return 命令

  next 命令(简写为n)是让 GDB 调到下⼀条命令去执⾏,这⾥的下⼀条命令不⼀定是代码的下⼀⾏,⽽是根据程序逻辑跳转到相应的位置。这⾥有⼀个⼩技巧,在 GDB 命令⾏界⾯如果直接按下回⻋键,默认是将最近⼀条命令重新执⾏⼀遍,因此,当使⽤ next 命令单步调试时,不必反复输⼊ n 命令,直接回⻋就可以了。

  step 命令(简写为 s)就是“单步步⼊”(step into),顾名思义,就是遇到函数调⽤,进⼊函数内部。

  finish 命令会执⾏函数到正常退出该函数;⽽ return 命令是⽴即结束执⾏当前函数并返回,也就是说,如果当前函数还有剩余的代码未执⾏完毕,也不会执⾏了。

  until 命令(简写为 u)可以指定程序运⾏到某⼀⾏停下来。比如直接输入 u 1888,就可以快速执行完中间的内容,直接跳到1888行。当然也可以使用断点的方式,但是使用until命令会更便捷。

3.10  set args 和 show args 命令

  很多程序需要我们传递命令⾏参数。在 GDB 调试中,很多⼈会觉得可以使⽤ gdb filename args 这种形式来给 GDB 调试的程序传递命令⾏参数,这样是不⾏的。正确的做法是在⽤ GDB 附加程序后,在使⽤ run 命令之前,使⽤“ set args 参数内容 ”来设置命令⾏参数

  如果单个命令⾏参数之间含有空格,可以使⽤引号将参数包裹起来。

(gdb) set args "999 xx" "hu jj"
(gdb) show args
Argument list to give program being debugged when it is started is ""999 xx" "hu j
j"".
(gdb) 

  如果想清除掉已经设置好的命令⾏参数,使⽤ set args 不加任何参数即可。

(gdb) set args
(gdb) show args
Argument list to give program being debugged when it is started is "".
(gdb)

3.11  tbreak 命令

  tbreak 命令也是添加⼀个断点,第⼀个字⺟“t”的意思是 temporarily(临时的),也就是说这个命令加的断点是临时的,所谓临时断点,就是⼀旦该断点触发⼀次后就会⾃动删除。添加断点的⽅法与上⾯介绍的 break命令⼀模⼀样,这⾥不再赘述。

3.12  watch 命令

  watch 命令是⼀个强⼤的命令,它可以⽤来监视⼀个变量或者⼀段内存,当这个变量或者该内存处的值发⽣变化时, GDB 就会中断下来。被监视的某个变量或者某个内存地址会产⽣⼀个 watch point(观察点)。

 

3.13  display 命令

  display 命令监视的变量或者内存地址,每次程序中断下来都会⾃动输出这些变量或内存的值。例如,假设程序有⼀些全局变量,每次断点停下来我都希望 GDB 可以⾃动输出这些变量的最新值,那么使⽤“ display变量名 ”设置即可。


4  GDB 调试技巧

4.1  将 print 打印结果显示完整

  当使⽤ print 命令打印⼀个字符串或者字符数组时,如果该字符串太⻓, print 命令默认显示不全的,我们可以通过在 GDB 中输⼊ set print element 0 命令设置⼀下,这样再次使⽤ print 命令就能完整地显示该变量的所有字符串了。

4.2  让被 GDB 调试的程序接收信号

void prog_exit(int signo)
{
    std::cout << "program recv signal [" << signo << "] to exit." << std::endl;
}
int main(int argc, char* argv[])
{
    //设置信号处理
    signal(SIGCHLD, SIG_DFL);
    signal(SIGPIPE, SIG_IGN);
    signal(SIGINT, prog_exit);
    signal(SIGTERM, prog_exit);
    int ch;
    bool bdaemon = false;
    while ((ch = getopt(argc, argv, "d")) != -1)
  {
    switch (ch)
    {
      case 'd':
      bdaemon = true;
      break;
    }
  }
  if (bdaemon)
  daemon_run();
  //省略⽆关代码...
}

  在这个程序中,我们接收到 Ctrl + C 信号(对应信号 SIGINT)时会简单打印⼀⾏信息,⽽当⽤ GDB 调试这个程序时,由于 Ctrl + C 默认会被 GDB 接收到(让调试器中断下来),导致⽆法模拟程序接收这⼀信号。解决这个问题有两种⽅式:在 GDB 中使⽤ signal 函数⼿动给程序发送信号,这⾥就是 signal SIGINT;改变 GDB 信号处理的设置,通过 handle SIGINT nostop print 告诉 GDB 在接收到 SIGINT 时不要停⽌,并把该信号传递给调试⽬标程序 。

(gdb) handle SIGINT nostop print pass
SIGINT is used by the debugger.
Are you sure you want to change it? (y or n) y
Signal Stop Print Pass to program Description
SIGINT No Yes Yes Interrupt
(gdb)

4.3  多线程下禁⽌线程切换

假设现在有 5 个线程,除了主线程,⼯作线程都是下⾯这样的⼀个函数:

void thread_proc(void* arg)
{
  //代码⾏1
  //代码⾏2
  //代码⾏3
  //代码⾏4
  //代码⾏5
  //代码⾏6
  //代码⾏7
  //代码⾏8
  //代码⾏9
  //代码⾏10
  //代码⾏11
  //代码⾏12
  //代码⾏13
  //代码⾏14
  //代码⾏15
}

  为了能说清楚这个问题,我们把四个⼯作线程分别叫做 A、 B、 C、 D。假设 GDB 当前正在处于线程 A 的代码⾏ 3 处,此时输⼊ next 命令,我们期望的是调试器跳到代码⾏ 4 处;或者使⽤“u 代码⾏10”,那么我们期望输⼊ u 命令后调试器可以跳转到代码⾏ 10 处。但是在实际情况下, GDB 可能会跳转到代码⾏ 1 或者代码⾏ 2 处,甚⾄代码⾏ 13、代码⾏ 14 这样的地⽅也是有可能的,这不是调试器 bug,这是多线程程序的特点,当我们从代码⾏ 4 处让程序 continue 时,线程A 虽然会继续往下执⾏,但是如果此时系统的线程调度将 CPU 时间⽚切换到线程 B、 C 或者 D 呢?那么程序最终停下来的时候,处于代码⾏ 1 或者代码⾏ 2 或者其他地⽅就不奇怪了,⽽此时打印相关的变量值,可能就不是我们需要的线程 A 的相关值。

   为了解决调试多线程程序时出现的这种问题, GDB 提供了⼀个在调试时将程序执⾏流锁定在当前调试线程的命令: set scheduler-locking on。当然也可以关闭这⼀选项,使⽤ set scheduler-locking off。

 

4.4  条件断点

所谓条件断点,就是满⾜某个条件才会触发的断点,这⾥先举⼀个直观的例⼦

void do_something_func(int i)
{
  i ++;
  i = 100 * i;
}
int main()
{
  for(int i = 0; i < 10000; ++i)
  {
    do_something_func(i);
  }
  return 0;
}

  在上述代码中,假如我们希望当变量 i=5000 时,进⼊ do_something_func() 函数追踪⼀下这个函数的执⾏细节。添加条件断点的命令是 break [lineNo] if [condition],其中 lineNo 是程序触发断点后需要停下的位置, condition 是断点触发的条件。这⾥可以写成 break 11 if i==5000,其中, 11 就是调⽤ do_something_fun() 函数所在的⾏号。当然这⾥的⾏号必须是合理⾏号,如果⾏号⾮法或者⾏号位置不合理也不会触发这个断点。

4.5  使⽤ GDB 调试多进程程序

  在实际的应⽤中,如有这样⼀类程序,如 Nginx,对于客户端的连接是采⽤多进程模型,当 Nginx 接受客户端连接后,创建⼀个新的进程来处理这⼀路连接上的信息来往,新产⽣的进程与原进程互为⽗⼦关系,那么如何⽤ GDB 调试这样的⽗⼦进程呢?⼀般有两种⽅法:⽤ GDB 先调试⽗进程,等⼦进程 fork 出来后,使⽤ gdb attach 到⼦进程上去,当然这需要重新开启⼀个 session 窗⼝⽤于调试, gdb attach 的⽤法在前⾯已经介绍过了;GDB 调试器提供了⼀个选项叫 follow-fork,可以使⽤ show follow-fork mode 查看当前值,也可以通过set follow-fork mode 来设置是当⼀个进程 fork 出新的⼦进程时, GDB 是继续调试⽗进程还是⼦进程取值是 child),默认是⽗进程( 取值是 parent)。

猜你喜欢

转载自www.cnblogs.com/lizhimin123/p/10416975.html