段错误出现的常见场景

什么是段错误(Segmentation fault)

段错误主要在unix-like的操作系统中出现(在windows中,它叫做 access violation),

当我们的进程访问了他不该访问的内存地址时(后面会介绍),操作系统就会发送一个SIGSEGV信号给到进程。该信号可以被捕获,但是通常情况下,我们会使用操作系统默认的信号处理器,这个默认的SIGSEGV信号处理器的策略就是终止进程。如下图所示:
img

额,这就是段错误。那什么情况下会导致段错误呢?

SIGSEGV产生的条件

如前所述,进程访问了它不该访问的内存地址就会出现段错误。
img
如上图所示,不允许进程访问的有:

  1. 内核空间
  2. 待分配区域
  3. 保留区

除此之外,进程修改只读内存,也会导致段错误发生。接下来我们来看看常见的段错误场景吧:

  1. 解引用空指针
    int *p = 0;
    std::cout << *p << std::endl; // Segmentation fault
    

    访问了保留区

  2. 访问非未初始化的指针
    int *p;
    std::cout << *p << std::endl;
    
    这段代码不一定会导致段错误,最终还是要看p具体指向何处。
  3. 内存越界
     char *s = new char[10];
     for (int i = 0; i < 12; ++i) {
       s[i] = 'c';
     }
    
    额,事实上,这段代码并没有出现段错误。为啥,我想是内存申请都是以页(通常是4096)为单位,而且new/malloc本身也需要一些内存来记录内存信息。因此我们换一个例子:
     char *ptr = (char*)sbrk(10);
     strcpy(ptr, "123456789");
     ptr[9] = '\0';
     std::cout << ptr << std::endl;
     *(ptr + 4095) = 'x'; // ok
     std::cout << "4095 ... ok" << std::endl;
     *(ptr + 4096)= 'x'; // segmentation fault
    
    我们通过sbrk系统调用来直接分配内存,在测试环境,段错误是必现的。内存越界会导致段错误的原因还是访问了未分配的内存。
  4. 修改只读内存。
    const char *str = "hello, world!!!";
    const_cast<char*>(str)[0] = 'H'; // segmentation fault
    
  5. 堆栈溢出
    int main()
    {
      main();
      return 0;
    }
    
  6. ...

大家发现没,段错误的出现几乎都和访问/修改内存有关。

我们再来看一个例子:

class A {
public:
  int func_a() {}
  virtual int func_b() {}
};

int main() {
  A *a = nullptr;
  a->func_a();
  std::cout << "你猜我能不能打印出来~~" << std::endl;
  a->func_b();
  return 0;
}

嗯,上面的例子打印是会正常出来的。为啥??
func_a作为A的成员函数,其调用等价于:

A *a = nullptr;
A::func_a(a);

这里,并不涉及到内存访问/修改,因此并不会出现段错误。
相反,func_b的调用,由于涉及到查虚函数表的操作,会出现段错误。

最后

段错误的出现条件还是比较简单的,但是实际开发中出现的段错误往往都令人头疼(之前就遇到过由悬空指针导致的段错误,还是偶现的)。为了减少段错误的发生,我们需要仔细检查代码中指针的使用、数组边界控制等,并尽可能采用现代C++提供的安全特性(如智能指针、基于范围的for循环等)。

posted @ 2024-07-22 10:32  liutimo  阅读(23)  评论(0编辑  收藏  举报