确定引起分段错误的代码行?

241

如何确定代码中导致 段错误 的错误位置?

我的编译器(gcc)是否能显示程序中的故障位置?


11
GCC/GDB无法确定具体的错误位置。虽然可以找出段错误发生的“位置”,但实际上错误可能完全来自其他地方。 - Aryabhatta
9个回答

335

GCC不能做到这一点,但GDB(一个调试器)可以。请使用-g开关编译您的程序,像这样:

gcc program.c -g

然后使用gdb:

$ gdb ./a.out
(gdb) run
<segfault happens here>
(gdb) backtrace
<offending code is shown here>

这里有一个不错的教程可以帮助你开始使用GDB。

段错误发生的位置通常只是代码中可能导致它的错误的线索,给出的位置不一定是问题所在。


40
请注意,段错误发生的位置通常只是代码中导致错误的“错误”的线索。这是一个重要的提示,但问题所在不一定就在那里。 - mpez0
13
您可以使用“bt full”来获取更多详细信息。 - ant2009
4
我会尽力为您翻译此内容:我发现这个链接很有用:https://www.gnu.org/software/gcc/bugs/segfault.html - Loves Probability
5
使用“bt”作为“backtrace”的简写。 - rustyx
1
@MehdiCharife 当解引用错误指针时会出现段错误,而不是当指针信息错误时。也就是说,您的有效指针可能会变得无效,因为目标被释放、移动或以其他方式不再有效,或者因为指针本身未初始化或已更改并且现在无效。系统无法检测到这些事件,只能检测到稍后发生的错误解引用。 - mpez0
显示剩余3条评论

84

此外,您可以尝试使用valgrind:如果您安装了valgrind并运行

valgrind --leak-check=full <program>

然后它将运行您的程序,并显示任何段错误、无效内存读取或写入以及内存泄漏的堆栈跟踪。这非常有用。


7
Valgrind在查找内存错误方面比较快速、易于使用。在非优化构建和带有调试符号的情况下,它可以准确指示段错误发生的位置和原因。 - Tim Post
1
遗憾的是,当我使用-g -O0和valgrind编译时,我的段错误消失了。 - JohnMudd
5
--leak-check=full 无法帮助调试段错误。它只有在调试内存泄漏时才有用。 - ks1322
@JohnMudd 我的程序有时会出现段错误,但只有大约 1% 的输入文件测试才会出现。如果重复使用失败的输入,它就不会失败。我的问题是由多线程引起的。到目前为止,我还没有找出导致这个问题的代码行。我现在正在使用重试来暂时遮盖这个问题。如果使用“-g”选项,错误将消失! - Kemin Zhou

32

有许多工具可用于帮助调试分段错误,我想将我的最爱工具添加到列表中:Address Sanitizers(通常缩写为ASAN)

现代¹编译器配备了方便的-fsanitize=address标志,增加了一些编译时间和运行时间开销,以进行更多的错误检查。

根据文档,这些检查默认包括捕获分段错误。这里的优点是您可以获得类似于gdb输出的堆栈跟踪,但无需在调试器内运行程序。例如:

int main() {
  volatile int *ptr = (int*)0;
  *ptr = 0;
}
$ gcc -g -fsanitize=address main.c
$ ./a.out
AddressSanitizer:DEADLYSIGNAL
=================================================================
==4848==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x5654348db1a0 bp 0x7ffc05e39240 sp 0x7ffc05e39230 T0)
==4848==The signal is caused by a WRITE memory access.
==4848==Hint: address points to the zero page.
    #0 0x5654348db19f in main /tmp/tmp.s3gwjqb8zT/main.c:3
    #1 0x7f0e5a052b6a in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x26b6a)
    #2 0x5654348db099 in _start (/tmp/tmp.s3gwjqb8zT/a.out+0x1099)

AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /tmp/tmp.s3gwjqb8zT/main.c:3 in main
==4848==ABORTING

输出比gdb的输出稍微复杂一些,但有以下优点:

  • 无需重新生成问题即可接收堆栈跟踪。在开发期间启用该标志即可。

  • ASAN可以捕获许多不仅仅是分段错误的问题,即使该内存区域对进程是可访问的,也会捕获许多越界访问情况。


¹ 这是Clang 3.1+GCC 4.8+提供的功能。


3
这对我非常有帮助。我有一个非常微妙的 bug,它随机发生,大约频率为1%。我使用(16个主要步骤;每个步骤由不同的C或C++二进制文件完成)处理大量输入文件。后面的一个步骤只有在多线程的情况下才会触发分段错误,而且是随机发生的。这很难调试。这个选项触发了调试信息输出,至少给了我一个起点进行代码审查,以找到 bug 的位置。 - Kemin Zhou

26
你也可以使用核心转储文件,然后使用gdb进行检查。为了获得有用的信息,你还需要使用-g标志进行编译。
每当你收到以下消息时:
 Segmentation fault (core dumped)

一个核心文件会被写入到你当前的目录中。你可以使用命令检查它:

 gdb your_program core_file

当程序崩溃时,文件包含内存状态。核心转储在软件部署期间可能很有用。

确保系统不将核心转储文件大小设置为零。您可以使用以下命令将其设置为无限:

ulimit -c unlimited

需要注意的是!核心转储可能会变得非常大。


我最近转换到了Arch-Linux。我的当前目录中没有核心转储文件。我该如何生成它? - Abhinav
你不需要生成它;Linux会自动处理。在不同的Linux系统中,核心转储文件存储在不同的位置 - 可以通过Google搜索来了解。对于Arch Linux,请参考https://wiki.archlinux.org/index.php/Core_dump。 - Mawg says reinstate Monica
1
我不得不使用 gdb --core=core - Julia
1
您可以使用 ulimit -c 命令来检查当前状态,要查看更多信息,请使用 ulimit -a 命令。 - Alexis Wilke

9
以上所有答案都是正确且推荐的;如果无法使用前述方法,则此答案仅作为最后的备选方案。
如果所有其他方法都失败了,您总是可以在代码中添加各种临时调试打印语句(例如fprintf(stderr, "CHECKPOINT REACHED @ %s:%i\n", __FILE__, __LINE__);),并重新编译程序。然后运行该程序,并观察在崩溃发生之前最后一个打印的调试信息 - 您知道程序已经执行到那里,因此崩溃肯定发生在该点之后。添加或删除调试打印语句,重新编译并再次运行测试,直到缩小到仅有一行代码。此时,您可以修复该错误并删除所有临时调试打印语句。
这相当繁琐,但它有一个优点,就是几乎可以在任何地方工作 - 唯一可能无法使用的情况是如果由于某种原因您无法访问stdout或stderr,或者如果您尝试修复的错误是一个竞争条件,其行为会随着程序时间的变化而改变(因为调试打印会减慢程序并改变其计时)。

3

Lucas关于核心转储的回答很好。在我的.cshrc文件中,我有以下内容:

alias core 'ls -lt core; echo where | gdb -core=core -silent; echo "\n"'

通过输入“core”来显示回溯信息。同时,为了确保我正在查看正确的文件,需要查看日期戳 :(。

补充:如果存在堆栈损坏漏洞,则应用于核心转储的回溯信息通常是无用的。在这种情况下,在gdb中运行程序可以得到更好的结果,如接受的答案所述(假设故障很容易重现)。还要注意多个进程同时转储核心的情况;一些操作系统将PID添加到核心文件名中。


6
不要忘记首先运行 ulimit -c unlimited 来启用核心转储。 - James Morris
@James:没错。Lucas已经提到过这个了。对于那些仍然被困在csh中的人,可以使用'limit'命令。我自己从来没有成功读取过CYGWIN的堆栈转储(但是我已经有2到3年没有尝试过了)。 - Joseph Quinsey

2
如果你和我一样在寻找一个类似的问题,但不是针对gcc而是gfortran的话,那么现在的编译器更加强大了。在使用调试器之前,你可以尝试这些编译选项。对我来说,这确定了出错的代码行以及访问哪个变量超出了边界,从而导致分段错误。
-O0 -g -Wall -fcheck=all -fbacktrace

gcc 10报告:-fcheck=all和-fbacktrace适用于Fortran而不是C。 - Valerio
是的,我应该已经说明了 - 我会编辑答案。 - ClimateUnboxed

1

这是一种粗略的方法,用于找到发生分段错误后的确切行。

  1. 定义行日志记录函数
#include \<iostream> 

void log(int line) {  
std::cout << line << std::endl;  
}
  1. 查找并替换所有log函数后面的分号为"; log(_LINE_);"

  2. 确保在for (;;)循环中替换为函数的分号已被删除


1

如果您有一个可重现的异常,比如分段错误,您可以使用调试器等工具来重现错误。

我曾经为了找到甚至是不可重现的错误的源代码位置而苦恼。这是基于微软编译器工具链的想法。

  1. 在将二进制文件(DLL、EXE)交给客户之前,请保存每个MAP文件。
  2. 如果发生异常,请在MAP文件中查找地址,并确定函数的起始地址刚好在异常地址下方。这样,您就知道了异常发生的函数。
  3. 从函数起始地址中减去异常地址。结果是函数中的偏移量。
  4. 重新编译包含该函数的源文件,并启用汇编清单。提取函数的汇编清单。
  5. 汇编清单包括函数中每个指令的偏移量。查找与函数中偏移量匹配的源代码行。
  6. 评估特定源代码行的汇编器代码。偏移量恰好指向引发抛出异常的汇编指令。评估此单个源代码行的代码。通过对编译器输出的一些经验,您可以知道是什么原因导致了异常。
  7. 请注意,异常的原因可能完全不同的位置。例如,代码解引用了一个空指针,但指针为空的实际原因可能在其他地方。

步骤6和7很有益处,因为您只需要代码行。但我建议您要意识到这一点。

希望您能在您的平台上获得与GCC编译器类似的环境。如果您没有可用的MAP文件,请使用工具链工具获取函数的地址。我相信ELF文件格式支持此功能。


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