当一个程序带有命令行参数时,我该如何使用GDB分析它的核心转储文件?

216

我的程序运行方式如下:

exe -p param1 -i param2 -o param3

程序崩溃了并生成了一个核心转储文件,core.pid

我想通过分析这个核心转储文件来

gdb ./exe -p param1 -i param2 -o param3 core.pid

但是GDB将EXE文件的参数识别为GDB的输入。

在这种情况下,我该如何分析核心转储文件?


1
你确定你的 exe 不是一个像 Linux 上的 firefox 那样的 shell 脚本(用于设置一些变量等)吗? - Basile Starynkevitch
“file core.pid”会告诉你是哪个命令导致了核心转储,通常不需要添加命令行参数(因为它们是核心的一部分)。 - U. Windl
9个回答

262

您可以以许多方式在GDB中使用core文件,但是将要传递给可执行文件的参数传递给GDB不是使用core文件的方法。这也可能是您遇到错误的原因。您可以以下列方式使用core文件:

gdb <executable> <core-file>gdb <executable> -c <core-file>

gdb <executable>
...
(gdb) core <core-file>

使用核心文件时,无需传递参数。崩溃场景在GDB中显示(在Ubuntu上检查了GDB版本7.1)。

例如:

$ ./crash -p param1 -o param2
Segmentation fault (core dumped)
$ gdb ./crash core
GNU gdb (GDB) 7.1-ubuntu
...
Core was generated by `./crash -p param1 -o param2'. <<<<< See this line shows crash scenario
Program terminated with signal 11, Segmentation fault.
#0  __strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:99
99    ../sysdeps/i386/i686/multiarch/../../i586/strlen.S: No such file or directory.
    in ../sysdeps/i386/i686/multiarch/../../i586/strlen.S
(gdb)

如果你想将参数传递给GDB中要调试的可执行文件,使用 --args

例如:

$ gdb --args ./crash -p param1 -o param2
GNU gdb (GDB) 7.1-ubuntu
...
(gdb) r
Starting program: /home/@@@@/crash -p param1 -o param2

Program received signal SIGSEGV, Segmentation fault.
__strlen_ia32 () at ../sysdeps/i386/i686/multiarch/../../i586/strlen.S:99
99    ../sysdeps/i386/i686/multiarch/../../i586/strlen.S: No such file or directory.
    in ../sysdeps/i386/i686/multiarch/../../i586/strlen.S
(gdb)

可以使用 man 手册来查看其他 GDB 选项。

最有用的命令包括:

  • bt(回溯)
  • info locals(显示本地变量的值)
  • info registers(显示 CPU 寄存器的值)
  • frame X(切换到堆栈帧 X
  • updown(在堆栈帧(调用链)中导航)

55

使用GDB调试核心转储文件的简单方法:

gdb <executable_path> <coredump_file_path>

一个名为“core.pid”的文件将作为“进程”的核心转储文件进行创建。

在进入GDB提示符后(执行上述命令),输入:

...
(gdb) where

这将为您提供有关堆栈信息的内容,您可以分析崩溃/故障的原因。 其他指令,用于同样的目的是:

另一个用于相同目的的命令是:

...
(gdb) bt full

这与上面相同。按照惯例,它列出了整个堆栈信息(最终导致崩溃位置)。


32

GDB最小可运行示例

simple.c

int myfunc(int i) {
    *(int*)(0) = i;
    return i - 1;
}

int main(int argc, char **argv) {
    (void)argv;
    int i = argc * 2;
    int ret = myfunc(i);
    return ret;
}

编译:

gcc -ggdb3 -std=c99 -Wall -Wextra -pedantic -o simple.out simple.c

为了生成核心文件,我们首先必须在当前终端中运行:

ulimit -c unlimited

这意味着“不限制dump核心文件的大小”。这是因为核心文件包含了崩溃进程的整个内存,所以它们可能非常大。

从Ubuntu 16.04开始测试,您必须删除一个预先存在的核心文件(TODO强制执行吗?我忘记了):

rm -f core

在Ubuntu 22.04上测试过,获取你的core文件需要与apport进行斗争:https://askubuntu.com/questions/1349047/where-do-i-find-core-dump-files-and-how-do-i-view-and-analyze-the-backtrace-st/1442665#1442665,例如:

echo 'core' | sudo tee /proc/sys/kernel/core_pattern

然后我们运行该程序:

./simple.out

并且终端包含:

Segmentation fault (core dumped)

核心文件已生成。在Ubuntu 16.04中,该文件仅命名为:

core

在Ubuntu 22.04中执行echo 'core' | sudo tee /proc/sys/kernel/core_pattern之后,生成的文件名称为:

core.<pid>

PID 是进程号,是一个数字,例如:

core.162152

我认为这是由于Linux内核更新开始添加 .pid 后缀所致。TODO确认。

现在我们可以使用核心文件作为

gdb simple.out core
gdb simple.out core.162152

现在我们进入了一个 GDB 会话,它与程序崩溃时的情况完全相同,但当然我们不能“继续运行”,因为程序即将结束:

#0  0x0000557097e0813c in myfunc (i=2) at simple.c:2
2           *(int*)(0) = i; /* line 7 */
(gdb) bt
#0  0x0000557097e0813c in myfunc (i=2) at simple.c:2
#1  0x0000557097e0816b in main (argc=1, argv=0x7ffcffc4ba18) at simple.c:9
(gdb) up
#1  0x0000557097e0816b in main (argc=1, argv=0x7ffcffc4ba18) at simple.c:9
9           int ret = myfunc(i);
(gdb) p argc
$1 = 1

运行bt命令后,我们可以立即了解代码崩溃时的位置,这有时足以解决bug。

从这个例子中可以看出,您现在可以检查程序内存在崩溃时的状态,以尝试确定失败的原因。进程虚拟内存完全包含在核心文件中。

已在Ubuntu 16.04和22.04 amd64上进行测试。

您也可以直接通过GDB运行程序

如果问题容易重现(即快速而确定地崩溃),并且您可以轻松控制命令行(即不是由另一个您不想/不能修改的程序调用的程序),那么最好的方法就是直接通过GDB运行程序:

gdb -ex run simple.out

当信号被接收时,默认情况下GDB会在信号原因处中断,我们将处于一个看起来与使用核心文件时完全相同的情况。

直接使用Binutils进行分析

让我们尝试在没有GDB的情况下观察核心文件的内容,以更好地理解它。因为我们可以这样做。

让我们创建一个程序,打印出它自己的一些内存地址,以便我们可以进行关联:

main.c

#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int myfunc(int i) {
    *(int*)(NULL) = i; /* line 7 */
    return i - 1;
}

int main(int argc, char **argv) {
    /* Setup some memory. */
    char data_ptr[] = "string in data segment";
    char *mmap_ptr;
    char *text_ptr = "string in text segment";
    (void)argv;
    mmap_ptr = (char *)malloc(sizeof(data_ptr) + 1);
    strcpy(mmap_ptr, data_ptr);
    mmap_ptr[10] = 'm';
    mmap_ptr[11] = 'm';
    mmap_ptr[12] = 'a';
    mmap_ptr[13] = 'p';
    printf("text addr: %p\n", text_ptr);
    printf("data addr: %p\n", data_ptr);
    printf("mmap addr: %p\n", mmap_ptr);

    /* Call a function to prepare a stack trace. */
    return myfunc(argc);
}

程序输出:

text addr: 0x4007d4
data addr: 0x7ffec6739220
mmap addr: 0x1612010
Segmentation fault (core dumped)

首先:

file core

告诉我们core文件实际上是一个ELF文件:

core: ELF 64-bit LSB core file x86-64, version 1 (SYSV), SVR4-style, from './main.out'
这就是为什么我们能够使用常规的binutils工具更直接地检查它。

快速查看ELF标准,可以发现实际上有一个专门用于它的ELF类型:

Elf32_Ehd.e_type == ET_CORE

更多格式信息可以在以下找到:

man 5 core

接着:

readelf -Wa core

提供有关文件结构的一些提示。内存似乎包含在常规程序头中:

Program Headers:
  Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
  NOTE           0x000468 0x0000000000000000 0x0000000000000000 0x000b9c 0x000000     0
  LOAD           0x002000 0x0000000000400000 0x0000000000000000 0x001000 0x001000 R E 0x1000
  LOAD           0x003000 0x0000000000600000 0x0000000000000000 0x001000 0x001000 R   0x1000
  LOAD           0x004000 0x0000000000601000 0x0000000000000000 0x001000 0x001000 RW  0x1000

同时在一个notes区域中还有一些元数据,特别是prstatus包含PC信息

Displaying notes found at file offset 0x00000468 with length 0x00000b9c:
  Owner                 Data size       Description
  CORE                 0x00000150       NT_PRSTATUS (prstatus structure)
  CORE                 0x00000088       NT_PRPSINFO (prpsinfo structure)
  CORE                 0x00000080       NT_SIGINFO (siginfo_t data)
  CORE                 0x00000130       NT_AUXV (auxiliary vector)
  CORE                 0x00000246       NT_FILE (mapped files)
    Page size: 4096
                 Start                 End         Page Offset
    0x0000000000400000  0x0000000000401000  0x0000000000000000
        /home/ciro/test/main.out
    0x0000000000600000  0x0000000000601000  0x0000000000000000
        /home/ciro/test/main.out
    0x0000000000601000  0x0000000000602000  0x0000000000000001
        /home/ciro/test/main.out
    0x00007f8d939ee000  0x00007f8d93bae000  0x0000000000000000
        /lib/x86_64-linux-gnu/libc-2.23.so
    0x00007f8d93bae000  0x00007f8d93dae000  0x00000000000001c0
        /lib/x86_64-linux-gnu/libc-2.23.so
    0x00007f8d93dae000  0x00007f8d93db2000  0x00000000000001c0
        /lib/x86_64-linux-gnu/libc-2.23.so
    0x00007f8d93db2000  0x00007f8d93db4000  0x00000000000001c4
        /lib/x86_64-linux-gnu/libc-2.23.so
    0x00007f8d93db8000  0x00007f8d93dde000  0x0000000000000000
        /lib/x86_64-linux-gnu/ld-2.23.so
    0x00007f8d93fdd000  0x00007f8d93fde000  0x0000000000000025
        /lib/x86_64-linux-gnu/ld-2.23.so
    0x00007f8d93fde000  0x00007f8d93fdf000  0x0000000000000026
        /lib/x86_64-linux-gnu/ld-2.23.so
  CORE                 0x00000200       NT_FPREGSET (floating point registers)
  LINUX                0x00000340       NT_X86_XSTATE (x86 XSAVE extended state)

objdump 可以轻松通过以下命令转储所有内存:

objdump -s core

其中包含:

Contents of section load1:

 4007d0 01000200 73747269 6e672069 6e207465  ....string in te
 4007e0 78742073 65676d65 6e740074 65787420  xt segment.text 

Contents of section load15:

 7ffec6739220 73747269 6e672069 6e206461 74612073  string in data s
 7ffec6739230 65676d65 6e740000 00a8677b 9c6778cd  egment....g{.gx.

Contents of section load4:

 1612010 73747269 6e672069 6e206d6d 61702073  string in mmap s
 1612020 65676d65 6e740000 11040000 00000000  egment..........

这与我们运行中的标准输出值完全匹配。

在 Ubuntu 16.04 amd64、GCC 6.4.0、binutils 2.26.1 中进行了测试。

作为“核心文件”的终极反向调试工具,Mozilla 的 rr 让您能够回到过去以进一步确定根本故障原因。

核心文件允许您在中断时检查堆栈。

但是通常您真正需要做的是回溯时间,进一步确定根本故障原因。

令人惊叹的 Mozilla rr 允许您这样做,代价是更大的跟踪文件和轻微的性能损失。

示例: 如何进行反向调试?

另请参阅


1
请认真阅读以下内容,不要仅仅因为篇幅长就用“TL;DR”来显得酷。特别是当问题很简短时,回答却非常详细时,请认真阅读。 - ReubenBeeler
@ReubenBeeler 太长不看,那我就拿酷炫点数吧。 - Ciro Santilli OurBigBook.com
严肃地说,这被用作“总之”的同义词。因为这三个要点是一个总结,然后我跳到一个例子,所以稍后有些重复。 - Ciro Santilli OurBigBook.com
只要天空是蓝色的,那就不是TL;DR的同义词。如果您这么认为,建议删除“TL;DR”,因为这会让人们期望您的回复缺乏可信度。 - ReubenBeeler
"TL;DR" 意味着非常可信的答案。其中的“没看”部分应理解为“我,读者,没看”,而不是作者。无论如何,这种英语用语相当令人讨厌,应该尽量避免使用。 - Johan Boulé

25

只需跳过参数。GDB 不需要它们:

gdb ./exe core.pid

但是这并不起作用。GDB输出警告:核心文件可能与指定的可执行文件不匹配。 无法从内存中读取有效的对象文件映像。 - Treper
6
"core file may not match specified executable"。 在生成核心文件后,您是否修改了exe文件?或者您可能使用不同的命令行选项重新构建了它?非常重要的是,向GDB提供与生成核心文件相同的二进制文件。如果不这样做,您将得到无用的输出。 - Employed Russian
2
请确保传递给 gdb 的二进制文件没有被剥离。您可以运行“file <binary name>”命令来检查它是否已被剥离。 - Diwakar Sharma

11

有一种稍微不同的方法可以让您完全跳过GDB。如果您只想要一个回溯,Linux-specific实用程序'catchsegv'将捕捉SIGSEGV并显示一个回溯。


11

来自RMS的GDB调试器教程

prompt > myprogram
Segmentation fault (core dumped)
prompt > gdb myprogram
...
(gdb) core core.pid
...

确保您的文件确实是一个 core 图像--使用file命令检查它。


4
无论可执行文件是否带有参数,都可以使用以下语法在生成的核心文件上运行GDB来调试任何二进制文件。
Syntax:
gdb <binary name> <generated core file>
Eg:
gdb l3_entity 6290-corefile

让我举一个以下的例子以便更好地理解。
bash-4.1$ **gdb l3_entity 6290-corefile**

**Core was generated** by `/dir1/dir2/dir3/l3_entity **Program terminated with signal SIGABRT, Aborted.**
#0
#1
#2
#3
#4
#5
#6
#7
#8
#9
#10
(gdb)

从上面的输出中,你可以猜测一些关于core的东西,比如它是否是NULL访问、SIGABORT等等。

这些数字#0到#10是GDB的堆栈帧。这些堆栈帧不属于你的二进制文件。在上述0-10帧中,如果你怀疑有任何问题,请选择该帧。

(gdb) frame 8

现在来看看更多有关它的细节:
(gdb) list +

为了进一步调查问题,您可以在此时打印出疑似变量的值。
(gdb) print thread_name

2

我只需使用 coredumpctl debug(在Fedora 32上),它会提供给我一个GDB控制台,用于调试我的最近的核心转储。


2
或者使用 coredumpctl gdb 命令加载最近的核心转储文件。 - U. Windl

0

只需输入以下命令:

$ gdb <Binary> <codeDump>

或者

$ gdb <binary>

$ gdb) core <coreDump>

不需要提供任何命令行参数。这是由之前的练习生成的代码转储。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接