对于Signal 11 SIGSEGV错误的简单总结

date
Oct 12, 2024
slug
2024-10-12-the-signal-11-sigsegv-issue-summary
status
Published
tags
Linux
type
Post
AI summary
Signal 11(SIGSEGV)是Unix/Linux系统中因内存访问错误而发出的信号,通常由C/C++代码中的空指针引用、缓冲区溢出、内存访问权限问题或栈溢出引起。调试SIGSEGV问题的策略包括开启编译警告、使用静态分析工具、利用Valgrind进行动态内存分析、借助coredump分析崩溃信息,以及检查内核日志以定位问题。通过这些方法,可以有效提高代码质量和调试效率。
summary
本文对Linux应用编程中经常出现的Signal 11也就是SIGSEGV错误的原因及其在kernel中的处理流程进行了总结,以及整理了在实践中遇到这类错误通用的调试手段。

SIGSEGV问题的简介以及原因总结

Signal 11,也就是SIGSEGV(Signal Segmentation Violation),是Unix/Linux系统在应用程序运行中,遇到内存访问错误的情况下,向应用程序所发出的错误信号。一般是应用程序代码在运行中尝试访问和读写非法内存地址时,由系统内核向应用程序发出这个信号。这类错误一般发生在可以通过指针等访问底层内存地址的C/C++语言代码中,发生后可能会导致数据污染、系统崩溃或者其他不可预计的行为,应该是嵌入式C/C++应用开发过程中最普遍,但是也最严重的问题。
导致SIGSEGV的原因一般是:
  • 引用空指针或者野指针。如果指针在引用前,既没有进行正确的初始化和设置,也没有做必要的检查,直接引用这些指针就会导致程序代码对内存的访问处于失控的状态,报出SIGSEGV的错误。
  • Buffer溢出。无论是在堆空间、栈空间还是全局变量空间的Buffer或者队列,当访问这些buffer超出其地址范围时,会导致内存buffer溢出的问题。
  • 内存访问权限问题。各个应用程序运行过程中所分配的内存区域,以及其对应的虚拟内存页,都因其类型的不同有着不同的访问权限,最典型的就是代码段内存空间是只读的,如果向只读类型的内容空间尝试写入时,也会发生SIGSEGV的错误。
  • 栈溢出。如果在子函数的内部栈空间上申请大量的内存,子函数在执行的过程中对于其栈空间内存的使用超过其限制后,发生栈溢出的问题,也会导致SIGSEGV的问题。

从Linux Kernel的角度看待SIGSEGV信号

对于Linux系统中运行的应用程序而言,每个进程都对应于一个独立的虚拟地址空间,其所占用的内存按照类型来区分,包含有多个段,例如代码段、数据段、堆空间、栈空间等。各个段又按照4KB为单位,分为很多个虚拟内存页,每个虚拟内存页都有自己的地址和权限设置。这些虚拟地址页再通过MMU以及内核中维护的各个进程的页表来与物理内存页一一对应。每个应用程序进程在kernel中都有自己的页表,保存着自己的虚拟内存空间与实际占用的物理内存页之间的对应关系。
那么对于Linux Kernel而言,什么时候会向一个进程发出SIGSEGV信号呢?
当应用程序对某个虚拟内存地址发出读写访问的请求时,这个请求会转到内核空间,此时:
  • Kernel会查询这个进程对应的页表,寻找与请求虚拟地址对应的页表及其权限,如果请求的虚拟地址页包含在页表中,并且访问权限匹配正确,则kernel会按照应用程序的请求访问指定的内存地址。如果权限不一致,kernel就会向应用程序发出一个SIGSEGV信号,表示这个内存访问的权限出错。
  • 如果Kernel在页表中找不到与应用程序指定虚拟地址的虚拟地址页,则会发生page fault的错误。针对page fault会存在三种情况,分别由page fault handler进行处理:
    • Major Page Fault,也称为硬缺页错误(hard page fault),要访问的虚拟内存地址在物理内存中没有对应的页帧,这个时候需要从硬盘等设备中载入(例如读写硬盘上的文件到内存,或者把硬盘上保存的cache交换到内存中),然后再由MMU建立物理内存页与虚拟内存页之间的映射关系。因为要从硬盘上读入文件,这个处理过程当然就比较慢,一般是异步操作。
    • Minor Page Fault,也称为软缺页错误(soft page fault),是指要访问的内存页不在当前进程对应的虚拟地址空间中,但是在物理内存中,这个时候就只需要MMU在当前进程的虚拟地址空间与物理地址空间建立关联关系即可,无需从硬盘读取内容,这个过程的处理一般是多个进程之间共享一部分物理内存空间(例如共享动态库)。因此这个过程的响应速度会比较快,一般是同步操作。
    • 无论是Major Page Fault还是Minor Page Fault都属于正常操作,kernel不会像应用程序上报异常的SIGSEGV信号。
    • page fault还有一种就完全是无效的缺页错误了。最典型的就是用户进程访问的内存地址越界所导致的非法内存访问、对空地址的引用,此时Kernel就会向应用程序上报一个SIGSEGV的信号,默认行为是终止应用程序运行。

调试SIGSEGV问题的思路

1. 编译开启所有的warning配置

首先一点,就是应该在编译应用程序的时候,养成开启所有的warning message,并解决所有导致warning message的应用程序代码问题。因为编译器在编译的过程中,如果产生了warning message,极大概率这个警告消息背后隐藏着一定的bug,即使这个bug短期内没有发现明显的测试问题,在某些应用逻辑的运行下,就有概率会导致出现一些几率性的问题,非常难以定位和解决。所以养成习惯去自动开启所有的warning message,并且提前解决掉这些问题,可以防患于未然,提前把可能导致问题的bug解决掉。
例如当使用gcc进行编译的时候,可以通过-Wall以及-Wextra等编译选项开启所有的warning message,这些错误在实践中一定都相对比较好解决:
notion image

2. 静态扫描分析

在开发流程中导入静态分析工具的思路与以上在编译过程中开启warning配置的思路一致,都是尝试在软件代码运行之前,想办法通过扫描和编译静态代码的过程中提前侦测可能的问题,并提前解决之。只不过,代码的静态扫描和分析工具完全是对静态源代码本身进行扫描,是在编译流程之前完成的工作;而编译过程中开启warning配置,则是利用编译工具在编译代码的过程中同步进行的代码扫描和分析,两者大同小异。
目前无论是开源还是商业的代码静态扫描分析工具,均已经非常成熟。例如针对嵌入式C、C++应用代码的扫描,开源的cppcheck,商业软件的Coverity、Fortify等工具在实践中都得到了非常普遍的应用,确实是可以很高效的提升团队开发的代码质量,提前解决掉导致测试问题的代码缺陷。
因此,在开发实践中,从开发质量和效率的提升角度考虑,一定要积极的把静态分析工具的扫描及其扫码问题的解决流程导入到代码提交的开发流程中。

3. 使用Valgrind进行动态内存分析

不同于cppcheck和Coverity这类静态代码扫描和分析工具,Valgrind是一种动态分析工具,也就意味着,Valgrind是在应用程序的运行中对其内存的使用情况进行监控和分析的:Valgrind会跟待分析的应用程序一起运行,在后台监控待分析应用程序的内存使用情况,更准确的给出内存出错的位置和原因。
当然,Valgrind的作用不仅仅是对内存的使用情况进行动态跟踪和分析,还可以对程序进行各种性能分析、代码覆盖测试、堆栈分析及CPU的Cache命中率、丢失率分析等,是一个非常强大的开源工具。
利用Valgrind的memcheck可以实现对应用程序运行中的内存覆盖、内存泄漏、内存越界检测等方面的检测。
Valgrind在嵌入式系统调试中的使用步骤大致如下:
  • 首先需要编译Valgrind。
  • 使用Valgrind加上memcheck选项启动应用程序的运行:valgrind —tool=memcheck a.out
但是,对于valgrind在嵌入式系统中的应用而言,存在比较大的实用性的问题,尤其是考虑到大部分的嵌入式系统在某些环节的处理上都有实时或者一定时序的严格要求。这是因为Valgrind的运行要实时的监控应用程序的内存使用情况,会导致应用程序的运行速度比不开启valgrind慢20-30倍,这一点在绝大多数嵌入式应用程序的调试中是不可接受的。
所以,valgrind更大的用于之地还是在PC、服务器这类资源比较充裕的环境中,如果要使用valgrind对嵌入式应用程序的内存进行分析,比较现实的做法是对软件的架构做更合理的设计和实现,可以让部分软件模块运行在PC中,然后在PC中利用valgrind来针对这些软件模块做独立的单元测试或者模块级别的测试。

4. 借助于coredump

当Linux的coredump功能开启的时候,应用程序运行中出现各种异常或者严重bug导致程序退出的时候,Linux系统会把该程序运行时的内存、寄存器状态、堆栈指针、内存管理信息、各种函数的堆栈调用信息保存到一个core文件中。对这个core文件进行分析和跟踪就可以大致定位出来导致内存问题的具体原因。
首先需要开启core dump功能:ulimit -c unlimited。
然后运行自己的应用程序,当应用程序的内存错误导致程序退出的时候,就会在当前目录下自动产生一个core文件。然后再用gdb来分析这个core文件,在gdb的交互式环境中,通过bt命令查看程序退出之前的函数调用栈的信息,就可以大致的定位出来导致这个内存错误的具体位置和原因。

5. 检查Kernel Log定位问题

当发生段错误的时候,内核同步会在其kernel log buffer里面写一条日志,通过对这个日志进行分析也可以得到一些有用的信息,基于这些信息可以初步定位出来发生段错误的原因和大致的参考位置。kernel的运行log一般保存在/var/log/syslog里面。
  • 当然要kernel的运行log要自动记录再/var/log/syslog这个文件里面,需要在后台运行一个rsyslog的服务,才能看到并利用这个文件对问题进行分析处理。
下面是一个发生段错误以后在kernel log文件中看到的信息:
notion image
  • at <address> :表示应用层代码在发生段错误时所尝试访问的虚拟内存地址。
  • ip <pointer> :错误发生时,应用层程序代码在内存中执行的位置。
  • sp <pointer> :错误发生时的应用程序堆栈指针。
  • error <code> :错误发生时应用程序正在执行的内存操作类型。主要的类型包括以下:
    • 4:表示程序正在尝试读尚未分配的内存地址。
    • 5:表示程序正在尝试读write-only的内存地址。
    • 6:表示程序正在尝试写尚未分配的内存地址。
    • 7:表示程序正在尝试写没有写权限的内存地址。

参考文档


© Pavel Han 2020 - 2024