段错误(segmentation fault ):9种实用调试方法,你用过几种?
ahcoder 2025-05-22 08:58 5 浏览
引言
每个在Linux环境下工作的程序员,都遇到过段错误(segmentation fault)。所谓段错误,本质上是程序访问了非法内存地址而引起的一种错误类型。
导致程序访问非法地址的原因有很多,如野指针、内存被踩、栈溢出、访问没有权限的内存等。
之前更新调试专题文章时,有朋友问到段错误的调试方法,我承诺会更新文章专门介绍,本文就是来填这个坑的。
本文将介绍9种非常实用的段错误调试方法。
1. 日志
日志是一种非常实用的调试手段,我们可以从系统日志中获得很多非常有用的信息,从而反推问题出现的前后系统中究竟发生了什么异常状况。
printf可能是最简单的日志记录方法,大家都懂的,不再赘述。
2. GDB
GDB的强大无需多言,对于段错误,利用GDB很容易就能定位到触发问题的那一行代码。如下图示例代码:
编译时加上-g选项:
gcc -g segfault.c -o segfault
在GDB中运行程序:
段错误触发时,GDB会直接告诉我们问题出现在哪一行代码,并且可以利用backtrace命令查看完整调用栈信息。此外,还可以利用其他常规调试命令来查看参数、变量、内存等数据。
这种方式虽然非常有效,但很多时候,问题并不是100%必现的,我们不可能一直把程序运行在GDB中,这对程序的执行性能等会有很大的影响。
这时,我们可以让程序在异常终止时生成core dump文件,然后用调试工具对它进行离线调试。
3. Core Dump + GDB
Core dump是Linux提供的一种非常实用的程序调试手段,在程序异常终止时,Linux会把程序的上下文信息记录在一个core文件中,然后可以利用GDB等调试工具对core文件进行离线调试。
很多系统中,根据默认配置,程序异常退出时不会产生core dump文件。可以通过下面这条命令查看:
ulimit -c
如果值是0,则默认不会产生core dump文件。可以用下面命令设置生成core dump文件的大小:
ulimit -c 10240
上面命令把core dump文件大小设置为10MB。如果存储空间不受限的话,可以直接取消大小限制:
ulimit -c unlimited
然后重新运行示例程序,段错误触发后,默认会在当前目录下生产一个core文件:
然后用GDB加载调试core文件。调试时,除了core dump文件外,GDB还需要从可执行文件中加载调试信息。
gdb segfault core
结果如下图:
与直接在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 两个有用的函数
int backtrace(void **buffer, int size);
void backtrace_symbols_fd(void *const *buffer, int size, int fd);
backtrace获取程序的调用栈地址信息,并存储在buffer指定的一个数组中,数组大小为size。
backtrace_symbols_fd根据backtrace得到的调用栈地址数据,获取地址对应的符号信息,并把结果写到fd指定的文件中。
4.3 示例
对上面的示例做下修改,增加一个信号处理函数,如下图所示:
在信号处理函数signal_handler中,先把寄存器信息打印出来,然后用backtrace和backtrace_symbols_fd获取调用栈信息,并写入stdout。
然后,在main函数中注册SIGSEGV的信号处理函数,如下图:
编译一下:
gcc -rdynamic segfault.c -o segfault
看下运行结果:
为了方便演示,示例中的信号处理函数只记录了寄存器和调用栈信息,实际项目中根据需求,可以同时记录其它重要信息,如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 --pid=xxx -ex bt -q
启动GDB,并attach到当前进程,然后执行backtrace命令打印调用栈信息。-q选项只是让GDB启动时不要打印版本信息,避免视觉干扰。
编译一下,需要加上-g选项:
gcc -g siggdb.c -o siggdb
运行,结果如下图:
注意:这种方法只能在测试环境中使用,且要确保GDB可以正常使用。生产环境中不要使用!
6. libSegFault.so
除了上面提到的几种方式外,其实glibc也已经很贴心地提供了一种问题定位的方案:libSegFault.so
libSegFault.so是glibc提供的一个动态链接库,用于捕捉程序运行异常并记录调用栈等调试信息。
它的实现原理和上面提到的第4种方法是一样的,即通过signal capture的方式,程序发生异常时,在信号处理函数中记录调试信息。
使用时,先确定系统中是否存在这个动态链接库。在我的系统中,有这么几个:
根据自己的实际情况,选择一个使用。比如我的测试环境是x64的,我选择使用:
/usr/lib/x86_64-linux-gnu/libSegFault.so
然后利用环境变量LD_PRELOAD,在测试程序运行前,把libSegFault.so链接进来。
LD_PRELOAD=/usr/lib/debug/lib/x86_64-linux-gnu/libSegFault.so ./myapp
仍以本文第一个测试程序为例:
编译:
gcc -rdynamic segfault.c -o segfault
运行:
LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libSegFault.so ./segfault
测试程序触发段错误后,libSegFault.so中的信号处理函数会把寄存器、调用栈、内存映射全部dump出来。结果如下图(信息太多,分成了两张图片):
libSegFault.so默认只捕捉SIGSEGV,可以通过设置环境变量SEGFAULT_SIGNALS指定要捕捉的信号,如:
export SEGFAULT_SIGNALS="all" # "all" signals
export SEGFAULT_SIGNALS="segv bus abrt " #SIGSEGV, SIGBUS and SIGABRT
环境变量SEGFAULT_USE_ALTSTACK可以指定是否让信号处理函数使用独立的栈,这在程序发送栈溢出时会很有用。
export SEGFAULT_USE_ALTSTACK=1
libSegFault.so默认把调试信息输出到stderr,可以通过设置环境变量SEGFAULT_OUTPUT_NAME,指定调试信息记录到一个文件中。比如:
export SEGFAULT_OUTPUT_NAME="./debug.log"
此外,为了方便用户使用,很多系统中还提供了一个名为catchsegv的脚本:
catchsegv ./segfault
其效果与通过LD_PRELOAD加载libSegFault.so是相同的:
7. Valgrind
Valgrind是一个很强大的工具集,它可以检测内存泄露、栈溢出、非法内存访问等多种内存相关的错误,还可以对程序进行性能剖析、生成函数调用关系图、统计Cache命中率、监测多线程竞争等,是程序调试的利器。
Valgrind功能非常强大,但文章篇幅有限,不对其展开讨论,后续会更新文章专门讲解它的各种功能,感兴趣的朋友可以右上角关注一下。
下面演示用Valgrind检测示例程序的内存访问错误:
编译时加上-g选项:
gcc -g segfault.c -o segfault
然后用Valgrind启动示例程序:
valgrind --tool=memcheck --leak-check=yes -v --leak-check=full --show-reachable=yes ./segfault
显示数据较多,仅截取感兴趣的部分信息,如下图所示:
Valgrind成功检测出地址0x12345678既不是栈地址,也不是malloc分配的动态内存。并且它也会把调用栈信息dump出来。
Valgrind虽然在检测内存相关的错误时非常强大,但是它有一个致命的缺点,就是慢。据统计,通过Valgrind运行程序时,速度会降低10倍。这在调试大型项目时,尤其是对实时性非常敏感的程序,是无法接受的。
不过,我们还有一个更好的选择 — AddressSanitizer。
8. AddressSanitizer
AddressSanitizer最初是Google开发的一个检测多种内存相关问题的工具,AddressSanitizer现在已经集成到GCC和LLVM中。它最大的特点是:
- 功能强大。它可以检测内存泄露、访问越界、栈溢出、多次释放等各种内存问题。
- 快。使用AddressSanitizer检测内存问题时,原始程序运行速度只会降低2倍左右,相比Vagrind来说,运行效率有了很大的提升。
本文只简单演示用AddressSanitizer检测示例程序中的内存访问错误,后续会专门更新文章详细讲解它的各种功能,感兴趣的朋友可以关注一下。
AddressSanitizer的使用方法也非常简单,只需要在编译时加上相应的编译选项,然后正常运行程序即可。
这里,我只使用最简单的一个编译选项-fsanitize=address开启AddressSanitizer功能。
gcc -g -fsanitize=address segfault.c -o segfault
然后正常运行即可,截图如下图:
9. dmesg + objdump
有时,可能由于各种原因,以上几种方法都不适用,比如程序中无法添加调试信息、程序无法重新编译、没有GDB和Valgrind等调试工具等。
这种情况下,调试起来,会相对比较困难一些,但也并不是完全不可能。
大多数情况下,程序发生segmentation fault而异常退出时,会在系统日志中记录一些信息,可以用dmesg查看:
可以从中得到触发异常的指令地址和被访问的内存地址,然后利用系统中现有的一些工具进行调试,如利用objdump对可执行文件进行反汇编,然后从汇编代码入手进行分析,限于篇幅,不再展开讨论。
Linux下有很多非常有用的工具,如binutils工具集(objdump、nm、readelf等)、strace等,熟悉并善用这些工具,会事半功倍。
结语
本文简单介绍了段错误的常用的9种调试方式,其中很多方法都是值得深入探讨的。
比如signal capture、Valgrind、AddressSanitizer、GDB等,都有很多更为高阶的使用技巧,但限于篇幅,无法展开讲解,后续会更新相关文章进一步深入讲解。
除了文中介绍的9中方法外,还有其它一些相似或衍生的方法,文中并未提及,欢迎童鞋们留言补充,相互学习!
本文是程序调试系列专题的第六篇。本系列专题旨在介绍一些高阶调试技巧、调试器的工作原理以及常见问题的定位方法和思路等内容。
其它已更新内容:
GDB动态打印:让你随时随地printf,不需修改代码,不需重新编译
调试引入的不确定性:必现的BUG神秘消失,断点改变代码执行逻辑
Linux调试技巧:GDB自定义命令,按需定制适合自己的调试工具
C语言:当GDB遇到复杂数据结构,两分钟带你掌握四个高效调试技巧
C语言:GDB调试时遇到宏定义怎么办?一个小技巧帮你一秒钟搞定
若对文中内容有疑问,欢迎留言讨论,对本系列专题有任何建议也欢迎提出!
原创不易,别忘了转发点赞,把知识分享给志同道合的朋友,谢谢!
对编译器、OS内核、性能调优、虚拟化等技术感兴趣的童鞋,欢迎右上角关注!
版权声明:未经允许,禁止转载。文中部分图片来源网络,如有侵权,请通知删除!
- 上一篇:Linux 基础命令和调试工具
- 下一篇:Linux内核调试方法
相关推荐
- Linux抓包工具tcpdump安装和使用,监视网络接口小工具大用途
-
Tcpdump工具是一个抓包工具也是一个协议分析软件。强大的功能和灵活的截取策略,使它成为Linux统下网络分析和问题排查的首选工具。tcpdump可以将网络中传送的数据包的头截获下来做分析。它支持...
- linux安装lnmp一键安装包
-
一般企业正式服环境用的lnmp.org一键安装包,下面做下简单介绍:官网:https://lnmp.org1.安装(官网上有详细的安装步骤)screen-Slnmp是为了在安装的过程中,断线的后台...
- Linux 安装Oracle11.2.0.4 (静默安装法)
-
一、环境准备1下载安装包已上传至对象存储,一共两个包#oracle11.2.0.4_1of7.zipwgethttps://oss-cn-north-1.unicloudsrv.com/sc-...
- Ubuntu入门使用之 24.04 如何安装命令工具(或软件包)
-
如果你是初学者,在Ubuntu24.04上运行命令时遇到错误,这意味着运行该特定命令所需的软件包在你的系统中不可用。无论你是刚开始探索Linux世界,还是从旧版本升级而来,你可能会想知道如何...
- Linux 安装代理 实现Windows Proxifier 功能
-
场景:linux上的应用---------->代理服务器(socket5)--------------------目标服务实现方案通过ProxyChains+Socat这2个工具来实现,具体...
- Python保姆级安装教程(CPU+GPU)
-
以下是为您整理的2024年Python保姆级安装教程(CPU+GPU详细版),涵盖Windows、macOS和Linux系统,并详细说明GPU环境的配置(如CUDA、cuDNN等...
- linux安装oracle
-
需要安装oracledataguard,所以先要安装单台oracle11g,下面是单台oracle11g的详细安装过程。1,安装环境硬件环境:2台linux虚拟机,Centos6.4,4G,4核...
- Linux安装Nginx详细教程
-
Nginx是一款高性能的开源Web服务器软件,它被广泛应用于构建高性能的网站和应用程序。本文将向您介绍如何在Linux操作系统上安装和配置Nginx服务器。一、下载nginx1.1、手动下载进入ngi...
- 选择LINUX安装平台
-
您已经选择了Linux发行版,并准备开始安装过程,但您需要确定您的硬件选项。以下是从哪里开始。译自Linux:ChooseanInstallationPlatform,作者Damon...
- 用Linux“还原”Win11,AnduinOS创始人公布1.4/1.5版本更新计划
-
IT之家5月24日消息,据外媒Neowin今日报道,AnduinOS的唯一开发者AnduinXue近日公布了“类Windows风格”Linux系统未来的版本规划。他表示,A...
- Linux lsof命令使用小结
-
推荐理由lsof(listopenfiles)是一个列出当前系统打开文件的工具。在Linux环境下,任何事物都是以文件的形式存在,通过文件不仅可以访问常规数据,还可以访问网络连接和硬件。所以,如传...
- Linux进程管理—信号、定时器使用详解
-
信号:1.信号的作用:背景:进程之间通信比较麻烦。但进程之间又必须通信,比如父子进程之间。作用:通知其他进程响应。进程之间的一种通信机制。信号:接受信号的进程马上停止,调用信号处理函数...
- Nexus 3 本地搭建与使用实战指南(适用于 Linux 与 Win11)
-
一、背景与介绍在DevOps流程中,本地镜像仓库能显著提升镜像下载速度、增强安全性并保障离线可用性。本文将手把手教你在Linux和Win11上分别部署并使用Nexus3搭建Dock...
- 字节跳动介绍使用AI优化Linux内核成果,可减少30%内存用量
-
IT之家11月23日消息,据外媒zdnet报道,字节跳动日前在LinuxPlumbersConference上介绍了通过使用AI优化Linux内核的成果,号称可以取得“显著...
- 一文带你了解 Linux 文件权限,从基础到高级
-
在Linux中,每个文件和目录都关联了一组权限,定义了不同用户对其的访问能力。权限分为三类:读取(read,r)、写入(write,w)和执行(execute,x),分别用字母r、w、x...
- 一周热门
- 最近发表
- 标签列表
-
- linux 远程 (37)
- u盘 linux (32)
- linux 登录 (34)
- linux 路径 (33)
- linux 文件命令 (35)
- linux 是什么 (35)
- linux 界面 (34)
- 查看文件 linux (35)
- linux 语言 (33)
- linux代码 (32)
- linux 查看命令 (33)
- 关闭linux (34)
- root linux (33)
- 删除文件 linux (35)
- linux 主机 (34)
- linux与 (33)
- linux 函数 (35)
- linux .ssh (35)
- cpu linux (35)
- 查看linux 系统 (32)
- linux 防火墙 (33)
- linux 手机 (32)
- linux 镜像 (34)
- linux ip地址 (34)
- linux 用户查看 (33)