是否可以将 gdb 附加到崩溃的进程(又名“即时”调试)

Posted

技术标签:

【中文标题】是否可以将 gdb 附加到崩溃的进程(又名“即时”调试)【英文标题】:Is it possible to attach gdb to a crashed process (a.k.a "just-in-time" debugging) 【发布时间】:2014-04-25 21:19:17 【问题描述】:

当一个进程崩溃时,我希望能够在崩溃但未清理的状态下针对它调用 gdb(或类似的调试器)。通常对核心转储进行事后分析会提供足够的信息,但有时我想进一步探索运行状态,可能会抑制直接故障并进一步运行。从一开始就在 gdb 下运行进程并不总是合适的(例如,调用很复杂或错误对时间非常敏感)

我所描述的基本上是通过“AEDebug”注册表项在 MS Windows 上公开的即时调试工具:在进行某些诊断时让故障线程挂起。在非开发人员 Windows PC 上,这通常设置为崩溃诊断机制(以前称为“Dr Watson”),Ubuntu 等效项似乎是 "apport"。

我确实找到了一个old mail thread (2007),它指的是“不时弹出”这个问题,所以它可能存在,但以一种我无法搜索的方式描述?

【问题讨论】:

对于您的应用程序,您始终可以在 SIGSEGV 上添加信号处理程序。 @StephaneChazelas - 是的,但不幸的是,我继承了一个测试可执行文件的动物园,这些可执行文件通过一个有点难以理解的 perl 脚本(它喜欢创建符号链接,有时是递归的:-))。总的来说,我认为这将是一个有用的工具,可以添加到我的工具箱中。 您可能想看看valgrind 做了什么,因为我相信它可以在某些事件上调用 gdb。 @StephaneChazelas 感谢 valgrind 指导;我认为该协会提供了internal gdb server to make it convenient to inspect the traps it raises。所以这不是我正在寻找的具体灵丹妙药,但实际上是一种更好的子弹。 【参考方案1】:

我不知道这样的功能是否存在,但作为一个 hack,你可以 LD_PRELOAD 一些东西,在 SIGSEGV 上添加一个调用 gdb 的处理程序:

cat >> handler.c << 'EOF'
#include <stdlib.h>
#include <signal.h>
void gdb(int sig) 
  system("exec xterm -e gdb -p \"$PPID\"");
  abort();


void _init() 
  signal(SIGSEGV, gdb);

EOF
gcc -g -fpic -shared -o handler.so -nostartfiles handler.c

然后运行您的应用程序:

LD_PRELOAD=/path/to/handler.so your-application

然后,在 SEGV 上,它将在 xterm 中运行 gdb。如果你在那里执行bt,你会看到类似:

(gdb) bt
#0  0x00007f8c58152cac in __libc_waitpid (pid=8294,
    stat_loc=stat_loc@entry=0x7fffd6170e40, options=options@entry=0)
    at ../sysdeps/unix/sysv/linux/waitpid.c:31
#1  0x00007f8c580df01b in do_system (line=<optimized out>)
    at ../sysdeps/posix/system.c:148
#2  0x00007f8c58445427 in gdb (sig=11) at ld.c:4
#3  <signal handler called>
#4  strlen () at ../sysdeps/x86_64/strlen.S:106
#5  0x00007f8c5810761c in _IO_puts (str=0x0) at ioputs.c:36
#6  0x000000000040051f in main (argc=1, argv=0x7fffd6171598) at a.c:2

除了运行 gdb,您还可以暂停自己 (kill(getpid(), SIGSTOP) 或致电 pause() 以在闲暇时自行启动 gdb

如果应用程序自己安装了 SEGV 处理程序或者是 setuid/setgid...

这是@yugr 为他的libdebugme tool 使用的方法,您可以在这里使用:

DEBUGME_OPTIONS='xterm:handle_signals=1' \
  LD_PRELOAD=/path/to/libdebugme.so your-application

【讨论】:

这当然非常接近,因为(我认为!)它应该继承任何涉及的子进程树。一个补充问题:似乎 system() 在信号处理程序 (man7.org/linux/man-pages/man7/signal.7.html) 中并不一定是安全的 - 无论如何它通常是安全的并且被广泛使用的习语,或者如果它发出等待线程的信号是为了偏执的安全。 还要感谢 SIGSTOP/pause 建议 - 让故障安静地休眠可能是浸泡测试的一个很好的默认设置(让它整夜敲打,早上对一小部分重复进行验尸)跨度> @TomGoodfellow 您可以直接执行 fork/exec — 这两个都在列表中。我建议 fork/exec 在孩子中启动 gdb,并让父母自己 SIGSTOP。顺便说一句,即使是那个列表也不能保证在捕获 SIGSEGV 之后是安全的——嗯,这取决于产生段错误的原因。 (例如,程序可能已经覆盖了内存的随机字节,并命中了由动态链接器设置的跳转表。) 这种方法是libdebugme 工具的基础。 @yugr,您可能仍想为kernel.yama.ptrace_scope 是否大于一添加检查。【参考方案2】:

回答我自己的问题,包括我从真实答案中得出的充实代码(上面的@Stephane Chazelas)。对原始答案的真正更改是:

    设置 PR_SET_PTRACER_ANY 以允许 gdb 附加 多一点(徒劳?)试图避免使用 libc 代码以希望仍然可以工作 对于(一些)堆损坏 包括 SIGABRT,因为某些崩溃是 assert()s

我一直在使用它与 Linux Mint 16(内核 3.11.0-12-generic)

/* LD_PRELOAD library which launches gdb "just-in-time" in response to a process SIGSEGV-ing
 * Compile with:
 *
 * gcc -g -fpic -shared -nostartfiles -o jitdbg.so jitdbg.c
 * 
 * then put in LD_PRELOAD before running process, e.g.:
 * 
 * LD_PRELOAD=~/scripts/jitdbg.so defective_executable
 */

#include <unistd.h>
#include <signal.h>
#include <sys/prctl.h>


void gdb(int sig) 
  if(sig == SIGSEGV || sig == SIGABRT)
    
      pid_t cpid = fork();
      if(cpid == -1)
        return;   // fork failed, we can't help, hope core dumps are enabled...
      else if(cpid != 0)
        
          // Parent
          prctl(PR_SET_PTRACER, PR_SET_PTRACER_ANY, 0, 0, 0);  // allow any process to ptrace us
          raise(SIGSTOP);  // wait for child's gdb invocation to pick us up
        
      else
        
          // Child - now try to exec gdb in our place attached to the parent

          // Avoiding using libc since that may already have been stomped, so building the
          // gdb args the hard way ("gdb dummy PID"), first copy
          char cmd[100];
          const char* stem = "gdb _dummy_process_name_                   ";  // 18 trailing spaces to allow for a 64 bit proc id
          const char*s = stem;
          char* d = cmd; 
          while(*s)
            
            *d++ = *s++;
            
          *d-- = '\0';
          char* hexppid = d;

          // now backfill the trailing space with the hex parent PID - not
          // using decimal for fear of libc maths helper functions being dragged in
          pid_t ppid = getppid();
          while(ppid)
            
              *hexppid = ((ppid & 0xF) + '0');
              if(*hexppid > '9')
                *hexppid += 'a' - '0' - 10;
              --hexppid;
              ppid >>= 4;
            
          *hexppid-- = 'x';   // prefix with 0x
          *hexppid = '0';
          // system() isn't listed as safe under async signals, nor is execlp, 
          // or getenv. So ideally we'd already have cached the gdb location, or we
          // hardcode the gdb path, or we accept the risk of re-entrancy/library woes
          // around the environment fetch...
          execlp("mate-terminal", "mate-terminal", "-e", cmd, (char*) NULL);
        
    


void _init() 
  signal(SIGSEGV, gdb);
  signal(SIGABRT, gdb);

【讨论】:

【参考方案3】:

如果您能够预料到某个特定程序会崩溃,您可以在 gdb 下启动它。

gdb /usr/local/bin/foo
> run

如果程序崩溃,gdb 会捕获它并让您继续调查。

如果您无法预测何时以及哪个程序会崩溃,那么您可以在系统范围内启用核心转储。

ulimit -c unlimited

强制 foo 进程的核心转储

/usr/local/sbin/foo
kill -11 `pidof foo` #kill -3 likely will also work

应该生成一个核心文件,您可以将 gdb 附加到该文件

gdb attach `which foo` -c some.core

RedHat 系统有时需要除了 ulimit 之外的其他配置来启用核心转储。

http://www.akadia.com/services/ora_enable_core.html

【讨论】:

以上是关于是否可以将 gdb 附加到崩溃的进程(又名“即时”调试)的主要内容,如果未能解决你的问题,请参考以下文章

如何在 gdb 中附加进程

GDB:列出崩溃进程的所有映射内存区域

从附加到 ddd/dbx 的崩溃进程生成核心转储

gdb - 多线程和共享库

Python Gzip - 即时附加到文件

如何在 GDB 中打开源文件