这个星期的工作中,我花了整整一周的时间尝试调试段错误。我以前从未做过此事,涉及到的一些基本事项(获取核心转储!找到存在段错误的行号!)使我花了很长时间才弄清楚。因此,这里有一篇博客文章,解释了如何做这些事情!
在此博客文章的结尾,您应该知道如何从“我的程序没有段错误,而且我不知道发生了什么”到“至少知道段错误时它的堆栈/行号是什么! ”。
“分段错误”是指程序尝试访问不允许访问的内存或试图访问。这可能是由于:
尝试取消引用空指针(不允许访问内存地址0
)
试图取消引用不在内存中的其他指针
损坏的C ++ vtable指针指向错误的位置,这导致程序尝试执行一些不可执行的内存
我不理解的其他一些事情,例如我认为未对齐的内存访问也可能导致段错误
这个“ C ++ vtable指针”就是我的segfaulting程序所发生的事情。我可能会在以后的博客文章中对此进行解释,因为在本周初我不了解任何C ++,而此vtable查找是一种我不知道的段隔离程序的新方法。
但!这篇博客文章与C ++错误无关。让我们讨论一些基础知识,例如,如何获得核心转储?
我发现弄清楚为什么我的程序存在段错误的最简单方法是使用valgrind:
valgrind -v your-program
这给了我一堆发生的痕迹。
但是我还想进行更深入的调查,发现不仅仅是valgrind告诉我的内容!因此,我想获取一个核心转储并进行探索。
一个核心转储是你的程序的内存的副本中,当你试图调试什么地方错了你的问题的程序是有益的。
当程序出现段故障时,Linux内核有时会向磁盘写入核心转储。当我最初尝试获取核心转储时,很长时间以来我一直感到沮丧,因为– Linux并未编写核心转储!我的核心转储在哪里???
我最终要做的是:
ulimit -c unlimited
在启动程序之前运行
跑 sudo sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t
ulimit -c
设置核心转储的最大大小。通常将其设置为0,这意味着内核根本不会编写核心转储。以千字节为单位。ulimits是每个进程的 -您可以通过运行查看进程的限制cat /proc/PID/limit
例如,这些是系统上随机Firefox进程的限制:
$ cat /proc/6309/limits Limit Soft Limit Hard Limit Units Max cpu timeunlimitedunlimitedseconds Max file size unlimitedunlimitedbytes Max data size unlimitedunlimitedbytes Max stack size8388608unlimitedbytes Max core file size0unlimitedbytes Max resident setunlimitedunlimitedbytes Max processes 3057130571processes Max open files1024 1048576files Max locked memory 6553665536bytes Max address space unlimitedunlimitedbytes Max file locksunlimitedunlimitedlocks Max pending signals 3057130571signals Max msgqueue size 819200 819200 bytes Max nice priority 00Max realtime priority 00Max realtime timeoutunlimitedunlimitedus
内核在确定要写入的核心文件大小时使用软限制(在这种情况下,“最大核心文件大小= 0”)。您可以使用ulimit
内置的外壳程序(ulimit -c unlimited
!)将软限制提高到硬限制。
kernel.core_pattern
是内核参数或“ sysctl设置”,用于控制Linux内核将核心转储写入磁盘的位置。
内核参数是一种在系统上设置全局设置的方法。您可以通过运行来获取每个内核参数的列表sysctl -a
,或者使用专门sysctl kernel.core_pattern
查看 kernel.core_pattern
设置。
所以sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t
将核心转储写入/tmp/core-<a bunch of stuff identifying the process>
如果你想知道更多关于这些%e
,%p
参数看,看到的人的核心。
重要的是要知道这kernel.core_pattern
是一个全局设置–在更改它时要多加小心,因为其他系统可能会以某种方式设置它,这很重要。
在Ubuntu系统上默认情况下,这kernel.core_pattern
是设置为
$ sysctl kernel.core_patternkernel.core_pattern = |/usr/share/apport/apport %p %s %c %d %P
这引起了我很多困惑(这是什么东西,它对我的核心转储有什么作用?),所以这是我从中学到的东西:
Ubuntu使用称为“ apport”的系统报告apt包中的崩溃
设置kernel.core_pattern=|/usr/share/apport/apport %p %s %c %d %P
意味着核心转储将通过管道传输到apport
apport在/var/log/apport.log中有日志
默认情况下,apport将忽略不属于Ubuntu软件包的二进制文件的崩溃
我最终只是覆盖了Apport的业务,并开始使用它kernel.core_pattern
,sysctl -w kernel.core_pattern=/tmp/core-%e.%p.%h.%t
因为我在开发机器上,我不在乎Apport是否在工作,并且我不想试图说服Apport将我的核心信息转给我。
好的,现在我们知道ulimits了,kernel.core_pattern
并且您实际上在中的磁盘上有一个核心转储文件/tmp
。惊人!怎么办???我们仍然不知道为什么程序会出现段错误!
下一步是使用打开文件gdb
并获取回溯。
您可以使用gdb打开核心文件,如下所示:
$ gdb -c my_core_file
接下来,我们想知道程序崩溃时堆栈是什么。bt
在gdb提示符下运行将给您回溯。在我的情况下,gdb尚未为二进制文件加载符号,因此就像??????
。幸运的是,加载符号修复了它。
这是加载调试符号的方法。
symbol-file /path/to/my/binarysharedlibrary
这将从二进制文件和二进制文件使用的任何共享库中加载符号。一旦我做到了,当我运行时,gdb给了我一个漂亮的带有行号的堆栈跟踪信息bt
!
如果您希望这样做,则二进制文件应使用调试符号进行编译。试图找出程序崩溃的原因时,在堆栈跟踪中添加行号非常有帮助:)
这是获取gdb中每个线程的堆栈的方法!
thread apply all bt full
如果您具有核心转储和调试符号以及gdb,那么您的处境将非常令人惊奇!您可以上下移动调用堆栈,打印出变量,并在内存中四处查看以查看发生了什么。这是最好的。
如果您仍在从事gdb向导的工作,也可以仅使用打印出堆栈跟踪, bt
这样就可以了:)
解决段错误的另一种方法是使用AddressSanitizer(“ ASAN”)($CC -fsanitize=address
)编译程序并运行它。我不会在这篇文章中讨论这个问题,因为它已经很长了,无论如何,由于某种原因,在ASAN开启的情况下,segfault消失了,可能是因为ASAN构建使用了不同的内存分配器(系统malloc而不是tcmalloc) 。
如果将来可以,我可能会写更多关于ASAN的文章:)
这篇博客文章听起来很像,我在做这件事时感到很困惑,但实际上并没有太多步骤可以从segfaulting程序中获取堆栈跟踪:
尝试valgrind
如果这不起作用,或者您想进行核心转储调查:
确保二进制文件已使用调试符号进行编译
设置ulimit
和kernel.core_pattern
正确
运行程序
使用打开您的核心转储gdb
,加载符号并运行bt
尝试找出发生了什么!
我能够使用gdb来发现有一个C ++ vtable条目指向一些损坏的内存,这很有帮助,让我觉得我对C ++的了解更好。也许我们会再讨论如何使用gdb解决问题!