C语言出现分段错误怎么办? 求大神救救
题主大概是C语言初学者,这段代码的主要问题是指针的使用问题,其实没必要那么复杂,这么改就可以了:
下面介绍几种常用调试方法。 引言:什么是段错误 每个在Linux环境下工作的程序员,都遇到过段错误(segmentation fault)。所谓段错误,本质上是程序访问了非法内存地址而引起的一种错误类型。 导致程序访问非法地址的原因有很多,如野指针、内存被踩、栈溢出、访问没有权限的内存等。 之前更新调试专题文章时,有朋友问到段错误的调试方法,我承诺会更新文章专门介绍,本文就是来填这个坑的。 本文将介绍9种非常实用的段错误调试方法。 1. 日志 日志是一种非常实用的调试手段,我们可以从系统日志中获得很多非常有用的信息,从而反推问题出现的前后系统中究竟发生了什么异常状况。 printf可能是最简单的日志记录方法,大家都懂的,不再赘述。 2. GDB GDB的强大无需多言,对于段错误,利用GDB很容易就能定位到触发问题的那一行代码。如下图示例代码: 编译时加上-g选项: 在GDB中运行程序:
段错误触发时,GDB会直接告诉我们问题出现在哪一行代码,并且可以利用backtrace命令查看完整调用栈信息。此外,还可以利用其他常规调试命令来查看参数、变量、内存等数据。 这种方式虽然非常有效,但很多时候,问题并不是100%必现的,我们不可能一直把程序运行在GDB中,这对程序的执行性能等会有很大的影响。 这时,我们可以让程序在异常终止时生成core dump文件,然后用调试工具对它进行离线调试。 3. Core Dump + GDB Core dump是Linux提供的一种非常实用的程序调试手段,在程序异常终止时,Linux会把程序的上下文信息记录在一个core文件中,然后可以利用GDB等调试工具对core文件进行离线调试。 很多系统中,根据默认配置,程序异常退出时不会产生core dump文件。可以通过下面这条命令查看: 如果值是0,则默认不会产生core dump文件。可以用下面命令设置生成core dump文件的大小: 上面命令把core dump文件大小设置为10MB。如果存储空间不受限的话,可以直接取消大小限制:
然后重新运行示例程序,段错误触发后,默认会在当前目录下生产一个core文件:
然后用GDB加载调试core文件。调试时,除了core dump文件外,GDB还需要从可执行文件中加载调试信息。 结果如下图:
与直接在GDB运行程序类似,core dump文件加载起来之后,GDB会直接显示触发问题的那一行代码,也可以使用backtrace、print等常规命令从core dump文件中信息。 在大多数系统中,这种core dump + GDB的手段非常有效,而且应该优先考虑使用。 但是有时候,由于某种原因,系统可能无法生存core dump文件。比如出于安全考虑,core dump功能可能是被彻底禁止的,或者在一些存储空间受限的嵌入式系统中,也无法生成core dump文件。 此时,我们就不得不考虑其它的调试手段了。 4. signal capture + backtrace 4.1 段错误在Linux系统上的处理过程 在Linux系统中,程序访问非法地址时,会被CPU捕获后触发硬件异常处理机制,并通知Linux kernel程序运行出现异常,kernel会对各种异常进行区分,然后向应用程序发送不同的signal,由应用程序自己进行故障恢复处理。 对于访问非法地址引起的段错误,Linux kernel会向应用程序发送11号signal,也就是SIGSEGV信号,该信号的默认处理是终止程序运行。 我们可以注册一个信号处理函数,当接受到Linux kernel发送过来的SIGSEGV信号后,在信号处理函数中把当前程序的上下文信息记录下来,方面后续问题定位。 4.2 两个有用的函数 backtrace程序的调用栈地址信息,并存储在buffer指定的一个数组中,数组大小为size。 backtrace_symbols_fd根据backtrace得到的调用栈地址数据,地址对应的符号信息,并把结果写到fd指定的文件中。 4.3 示例 对上面的示例做下修改,增加一个信号处理函数,如下: 在信号处理函数signal_handler中,先把寄存器信息打印出来,然后用backtrace和backtrace_symbols_fd调用栈信息,并写入stdout。 然后,在main函数中注册SIGSEGV的信号处理函数,如下: 编译一下: 看下运行结果:
为了方便演示,示例中的信号处理函数只记录了寄存器和调用栈信息,实际项目中根据需求,可以同时记录其它重要信息,如stack dump、全局变量、数据段dump等。 有两点需要注意:示例信号处理函数中打印寄存器的部分是针对x64 CPU的,其它CPU请参考sys/ucontext.h文件中对mcontext_t的定义。编译时需要加上-rdynamic选项,否则backtrace_symbols_fd无法正确符号信息。 5. signal capture + GDB 有些问题很难重现,直接在GDB里运行调试的话,可能要浪费很多时间去不停的尝试重现它。 那有没有一种方式,可以让问题重现时自动启动GDB呢?当然有! 与上面的一种方法类似,我们仍然利用signal capture的方式。只不过,在信号处理函数中,我们不再使用backtrace调用栈信息,而是直接启动GDB。 对信号处理函数作一些修改,如下: 原理很简单,就是段错误发生时,在SIGSEGV信号处理函数中执行命令: 启动GDB,并attach到当前进程,然后执行backtrace命令打印调用栈信息。-q选项只是让GDB启动时不要打印版本信息,避免视觉干扰。 编译一下,需要加上-g选项: 运行,结果如下图:
注意:这种方法只能在测试环境中使用,且要确保GDB可以正常使用。生产环境中不要使用! 6. libSegFault.so 除了上面提到的几种方式外,其实glibc也已经很贴心地提供了一种问题定位的方案:libSegFault.so libSegFault.so是glibc提供的一个动态链接库,用于捕捉程序运行异常并记录调用栈等调试信息。 它的实现原理和上面提到的第4种方法是一样的,即通过signal capture的方式,程序发生异常时,在信号处理函数中记录调试信息。 使用时,先确定系统中是否存在这个动态链接库。在我的系统中,有这么几个:
根据自己的实际情况,选择一个使用。比如我的测试环境是x64的,我选择使用: 然后利用环境变量LD_PRELOAD,在测试程序运行前,把libSegFault.so链接进来。 仍以本文第一个测试程序为例: 编译: 运行: 测试程序触发段错误后,libSegFault.so中的信号处理函数会把寄存器、调用栈、内存映射全部dump出来。结果如下图(信息太多,分成了两张图片):
libSegFault.so默认只捕捉SIGSEGV,可以通过设置环境变量SEGFAULT_SIGNALS指定要捕捉的信号,如: 环境变量SEGFAULT_USE_ALTSTACK可以指定是否让信号处理函数使用独立的栈,这在程序发送栈溢出时会很有用。 libSegFault.so默认把调试信息输出到stderr,可以通过设置环境变量SEGFAULT_OUTPUT_NAME,指定调试信息记录到一个文件中。比如: 此外,为了方便用户使用,很多系统中还提供了一个名为catchsegv的脚本: 其效果与通过LD_PRELOAD加载libSegFault.so是相同的:
7. Valgrind Valgrind是一个很强大的工具集,它可以检测内存泄露、栈溢出、非法内存访问等多种内存相关的错误,还可以对程序进行性能剖析、生成函数调用关系图、统计Cache命中率、监测多线程竞争等,是程序调试的利器。 Valgrind功能非常强大,但文章篇幅有限,不对其展开讨论,后续会更新文章专门讲解它的各种功能,感兴趣的朋友可以右上角一下。 下面演示用Valgrind检测示例程序的内存访问错误。 编译时加上-g选项: 然后用Valgrind启动示例程序: 显示数据较多,仅截取感兴趣的部分信息,如下图所示:
Valgrind成功检测出地址0x既不是栈地址,也不是malloc分配的动态内存。并且它也会把调用栈信息dump出来。 Valgrind虽然在检测内存相关的错误时非常强大,但是它有一个致命的缺点,就是慢。据统计,通过Valgrind运行程序时,速度会降低10倍。这在调试大型项目时,尤其是对实时性非常敏感的程序,是无法接受的。 不过,我们还有一个更好的选择 — AddressSanitizer。 8. AddressSanitizer AddressSanitizer最初是Google开发的一个检测多种内存相关问题的工具,AddressSanitizer现在已经集成到GCC和LLVM中。它最大的特点是:功能强大。它可以检测内存泄露、访问越界、栈溢出、多次释放等各种内存问题。快。使用AddressSanitizer检测内存问题时,原始程序运行速度只会降低2倍左右,相比Vagrind来说,运行效率有了很大的提升。 本文只简单演示用AddressSanitizer检测示例程序中的内存访问错误,后续会专门更新文章详细讲解它的各种功能,感兴趣的朋友可以一下。 AddressSanitizer的使用方法也非常简单,只需要在编译时加上相应的编译选项,然后正常运行程序即可。 这里,我只使用最简单的一个编译选项-fsanitize=address开启AddressSanitizer功能。 然后正常运行即可,如下图:
9. dmesg + objdump 有时,可能由于各种原因,以上几种方法都不适用,比如程序中无法添加调试信息、程序无法重新编译、没有GDB和Valgrind等调试工具等。 这种情况下,调试起来,会相对比较困难一些,但也并不是完全不可能。 大多数情况下,程序发生segmentation fault而异常退出时,会在系统日志中记录一些信息,可以用dmesg查看:
可以从中得到触发异常的指令地址和被访问的内存地址,然后利用系统中现有的一些工具进行调试,如利用objdump对可执行文件进行反汇编,然后从汇编代码入手进行分析,限于篇幅,不再展开讨论。 Linux下有很多非常有用的工具,如binutils工具集(objdump、nm、readelf等)、strace等,熟悉并善用这些工具,会事半功倍。 —————- 欢迎知乎/:原点技术,分享真正有用的东西! 技术探讨,欢迎添加作者:CreCoding
2024最新激活全家桶教程,稳定运行到2099年,请移步至置顶文章:https://sigusoft.com/99576.html
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。 文章由激活谷谷主-小谷整理,转载请注明出处:https://sigusoft.com/33161.html