是否可以将 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 附加到崩溃的进程(又名“即时”调试)的主要内容,如果未能解决你的问题,请参考以下文章