这周在工作中,我花了整整一周的时间来调试段错误。我以前从未这样做过,我花了很长时间才弄清楚所涉及的一些基本内容(获取核心转储,找到导致段错误的行号)。因此这篇博文解释了如何做这些事情!阅读此博客后,您应该知道如何从“哦,我的程序出现段错误,但我不知道发生了什么”到“我知道发生段错误时的堆栈和行号!”。什么是段错误?“分段错误”是您的程序试图访问不允许访问的内存地址的情况。这可能是由于:试图解引用空指针(不允许访问内存地址0);试图解引用其他一些不在你内存中的指针(LCTT译注:指不在合法的内存地址范围内);一个已经被破坏并且C++vtable指针C++vtable指针指向了错误的地方,导致程序试图去执行它没有执行权限的内存中的指令;还有一些我不明白的地方,比如我觉得访问未对齐的内存地址也可能会导致segmentError(LCTT译注:在需要自然边界对齐的架构中,比如MIPS,ARM,更容易产生segmentationfaultsdueto未对齐的访问)。这个“C++vtable指针”是我的程序出现段错误的地方。我可能会在以后的博客中对此进行解释,因为我最初对C++一无所知,而且我也不知道这种导致段错误的vtable查找。但!这篇博文与C++问题无关。让我们谈谈基本的东西,比如,我们如何获得核心转储?第1步:运行valgrind我发现找出我的程序出现段错误的最简单方法是使用valgrind:我运行valgrind-vyour-program,这在失败时为我提供了一个堆栈调用序列。简洁的!但我认为并希望做更深入的调查,找出一些valgrind没有告诉我的东西!所以我想得到一个核心转储并探索它。如何获取核心转储核心转储是程序内存的副本,当您尝试调试有问题的程序出错的地方时,它非常有用。当您的程序出现段错误时,Linux内核有时会将核心转储写入磁盘。当我第一次尝试获取核心转储时,我很沮丧很长一段时间,因为——Linux不生成核心转储!我的核心转储在哪里?这就是我最终做的事情:在开始我的程序之前运行ulimit-cunlimitedc设置核心转储的最大大小。它通常设置为0,这意味着内核根本不会写入核心转储。它以千字节为单位。ulimits是基于每个进程设置的——您可以通过运行cat/proc/PID/limit查看进程的各种资源限制。例如,这些是我系统上随机Firefox进程的资源限制:$cat/proc/6309/limitsLimitSoftLimitHardLimitUnitsMaxcputimeunlimitedunlimitedsecondsMaxfilesizeunlimitedunlimitedbytesMaxdatasizeunlimitedunlimitedbytesMaxstack大小8388608无限字节最大核心文件大小0无限字节最大驻留集无限无限字节最大进程3057130571进程最大打开文件10241048576个文件最大锁定内存6553665536字节最大地址空间无限无限字节最大文件锁无限unlimitedlocksMaxpendingsignals3057130571signalsMaxmsgqueuesize819200819200bytesMaxnicepriority00Maxrealtimepriority00Maxrealtimetimeoutunlimitedunlimitedus内核在决定写入多大的核心转储文件时使用软限制(在这种情况下,maxcorefilesize=0)可以使用shell内置命令ulimit(ulimit-cunlimited)将软限制增加到硬限制硬限制。kernel.core_pattern:保存核心转储的位置kernel.core_pattern是一个内核参数,或“sysctl设置”,它控制Linux内核将核心转储文件写入磁盘的位置。内核参数是为系统设置全局设置的一种方式。您可以通过运行sysctl-a获取每个内核参数的列表,或使用sysctlkernel.core_pattern专门查看kernel.core_pattern设置。所以sysctl-wkernel.core_pattern=/tmp/core-%e.%p.%h.%t将coredump保存到目录/tmp下,并添加一系列可以识别(故障)带有core的进程后缀由参数构成的是文件名。如果想知道%e、%p这些参数是什么意思,请参考mancore。重要的是要注意kernel.core_pattern是一个全局设置-修改它时要小心,因为可能有其他系统功能依赖于以某种方式设置它(才能正常工作)。kernel.core_pattern和Ubuntu默认情况下,在ubuntu系统中,kernel.core_pattern被设置为以下值:我的困惑(这是什么意思,它对我的??核心转储做了什么?)。这是我了解到的: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-wkernel.core_pattern=/tmp/core-%e.%p.%h.%t,因为我在开发机器上,所以我不关心apport是否有效,我不想尝试让apport将我的核心转储留在磁盘上。现在您有了核心转储,下一步该做什么?好的,现在我们了解了ulimit和kernel.core_pattern,我们实际上在磁盘上的/tmp目录中有一个核心转储文件。非常好!下一步是什么?我们仍然不知道为什么程序会出现段错误!下一步是用gdb打开核心转储文件并获取堆栈调用序列。从gdb中获取堆栈调用序列你可以像这样用gdb打开一个核心转储文件:$gdb-cmy_core_file接下来,我们想知道当程序崩溃时堆栈是什么样子的。在gdb提示符下运行bt将为您提供调用序列的回溯。在我的例子中,gdb没有加载二进制文件的符号信息,所以函数名称类似于“??????”。幸运的是,(我们通过)加载符号来修复它。以下是加载调试符号的方法。symbol-file/path/to/my/binarysharedlibrary这会从二进制文件和它引用的任何共享库中加载符号。一旦我这样做了,当我做bt时,gdb给了我一个很好的堆栈跟踪和行号!如果您希望二进制文件工作,则应使用调试符号信息对其进行编译。当试图找出程序崩溃的原因时,堆栈跟踪中的行号非常有用。:)查看各个线程的堆栈通过以下方式获取gdb中各个线程的调用堆栈!threadapplyallbtfullgdb+coredump=surprise如果你有一个带有调试符号的核心转储和gdb,太棒了!您可以上下查看调用堆栈、打印变量并查看内存以了解发生了什么。这是独一无二的。如果您仍在使用gdb向导,只需使用bt打印出堆栈跟踪就可以了。:)ASAN另一种找出段错误的方法是使用AddressSanitizer选项(“ASAN”,即$CC-fsanitize=address)编译程序,然后运行它。我不打算在这篇文章中讨论这个问题,因为这篇文章已经很长了,在我的例子中,段错误在打开ASAN后消失了,可能是因为ASAN使用了不同的内存分配器(系统内存分配器,而不是tcmalloc)。将来如果我能让ASAN工作,我可能会写更多关于它的文章。(LCTT译注:这里指的是使用ASAN也可以重现segmentationfault)从一个coredump得到一个stacktrace真是太好了!这个博客听起来很多,当我写的时候我很困惑,但说真的,从一个出现段错误的程序中获取一系列堆栈调用并不需要那么多步骤:如果不起作用,请尝试使用valgrind,或者您想要获取核心转储进行调查:确保二进制文件是使用调试符号信息编译的;正确设置ulimit和kernel.core_pattern;运行程序;使用gdb调试核心转储后,加载符号并运行bt;试着找出发生了什么!我可以使用gdb来确定有一个C++vtable条目指向一些损坏的内存,这有点帮助,让我觉得我更了解C++。也许有一天我们会更多地讨论如何使用gdb来查找问题!
