0. 简介

相较于其他报错,stack smashing detect这个报错是最令人头疼的段错误种类。“Stack smashing detect” 是指在程序运行过程中检测到栈溢出的情况。栈溢出是一种常见的安全漏洞,发生在程序尝试往栈空间写入超过其边界范围的数据时。

1. 常见分类

通常,导致 “Stack smashing detect” 错误的原因可能包括:

  1. 缓冲区溢出:当向一个缓冲区写入超过其分配大小的数据时,会覆盖到相邻的内存地址,导致栈被破坏。
  2. 函数调用错误:函数调用时参数传递错误或者返回值处理不当,可能引起栈结构被破坏。
  3. 格式化字符串漏洞:使用不当的格式化字符串函数(如printf)可能造成栈溢出。
  4. 栈溢出:如果递归调用层数过多,可能导致栈空间耗尽而触发 stack smashing detect。
  5. 内存泄漏:未正确释放之前分配的内存。
  6. 函数指针错误:调用或引用一个无效的函数指针。

下面我们来看一下每个可能原因的解决方法


2. 缓冲区溢出(使用GDB)

在C++中,数组的索引从0开始,因此需要确保不要超出数组的边界进行访问。如果程序中存在数组越界问题,可以按照以下步骤进行排查和解决:

  1. 检查数组的大小和边界。
  2. 确保数组的访问索引不会超出边界。
  3. 使用调试器(如gdb)来跟踪代码的执行,定位到越界访问的代码行。
  4. 修复代码中的越界访问错误,并确保访问索引正确计算。

3. 函数调用错误(使用GDB)

这种还是比较好排查的,也是实用GDB获取对应的函数,需要看一下传入的函数调用的参数个数、类型和顺序与函数定义一致。然后检查函数返回值是否被正确处理,避免因为忽略错误码而导致的问题。


4. 格式化字符串漏洞(使用GDB)

  1. 避免直接使用外部输入作为格式化字符串:不要将不受信任的字符串直接用作printf或其他格式化输出函数的格式串。
  2. 使用格式化字符串函数的安全版本:尽量使用安全的字符串函数,如strncpy代替strcpy,snprintf代替sprintf等,这些函数允许指定最大可写入字符数。

5. 栈溢出(AddressSanitzer&Valgrind&gperftools)

这种是最难排查的问题,栈溢出是指当程序中使用的栈空间超过其分配的限制时发生的情况。这通常是由于递归调用或过多的局部变量导致的。

为了解决栈溢出问题,可以考虑以下解决方法:

  1. 使用循环替代递归调用。
  2. 优化算法,减少需要使用的栈空间。
  3. 使用堆内存替代栈内存。
  4. 增加可用的栈空间(通过调整编译器选项),但需要注意堆栈溢出的风险。

5.1 分析递归深度

  1. 限制递归深度:为递归函数设置一个最大深度限制,当达到这个限制时停止递归。这可以帮助确定是否是递归深度导致的栈溢出。
  2. 递归到循环:如果可能,尝试将递归逻辑重写为循环逻辑。循环不会增加栈空间消耗,因此可以有效减少栈溢出的风险。

5.2 检查局部变量大小(这种一般程序在没有任何明显逻辑错误的情况下崩溃,特别是在程序退出或返回到上一级函数调用时)

  1. 减少局部变量:尤其是大型的数组或结构体,它们会占用大量的栈空间。考虑将它们改为动态分配的堆内存。

  2. 使用动态内存分配:对于需要大量内存的变量,使用new(C++)在堆上分配内存,而非在栈上。记得使用delete(C++)来释放分配的内存,以避免内存泄漏。

    5.2.1 如何修改

    作者这一次遇到的就是大量的struct被反复创建调用导致的栈资源消耗掉导致的,这里作者使用ulimit -s 查看linux默认栈空间的大小。然后通过命令 ulimit -s 设置大小值临时改变栈空间大小发现即解决了。然后进一步打log发现是struct传入

    我们这里使用指向指针的指针(双重指针)。使用LocalizationEstimate** locIns意味着locIns是一个指向另一个指针的指针,这个指针指向一个LocalizationEstimate实例。你需要在堆上动态分配LocalizationEstimate实例,并正确管理这些指针,包括分配和释放内存。当然这取决于你使用的操作

    单指针(T* ptr)
    用途:当你想在函数中操作指向对象的指针,或者分配/释放指向对象的内存时,会使用单指针。
    传参问题:如果你将单指针作为参数传递给函数,并在该函数内对指针进行重新分配(例如,使用new或delete),这个改变不会反映到调用者那里。这是因为指针本身是按值传递的,函数内的操作仅影响局部副本。
    双重指针(T** ptr)
    用途:当你需要在函数中改变指针本身的指向,或者你想在函数内部分配或释放内存,并将新的内存地址反映到调用者那里时,你会使用双重指针。
    传参动机:双重指针允许你在函数内部改变指针指向的地址,并通过函数参数将这个改变传递回调用者。这在动态内存管理和数据结构(如链表、树等)的修改中非常有用。

5.3 使用工具进行分析

  1. 编译器警告:启用编译器的所有警告(例如,使用-Wall -Wextra标志),可能会有关于栈空间使用的警告。
  2. 静态分析工具:使用静态分析工具检查代码,这些工具可以帮助发现潜在的问题,包括可能导致栈溢出的代码模式。
  3. 动态分析工具:使用如Valgrind、AddressSanitizer等动态分析工具运行程序,它们可以帮助检测栈溢出、内存泄漏和其他内存问题。

5.4 优化递归算法

  1. 尾递归优化:如果编译器支持尾递归优化,尝试将递归函数重写为尾递归形式。尾递归优化可以减少栈空间的使用。
  2. 分而治之:对于某些问题,考虑使用分而治之等策略,将问题分解为可以并行解决的小问题,这样可以减少单一递归调用链的深度。

5.5调整编译器栈大小设置

增加栈大小:在某些情况下,可以通过调整编译器设置来增加程序的栈大小。例如,在GCC中,可以使用链接器选项-Wl,–stack,来设定栈的大小。


6. 内存泄漏(AddressSanitzer&Valgrind&gperftools)

如果程序中存在内存泄漏问题,即未正确释放之前分配的内存,可能会导致栈溢出,并可能触发“stack smashing detected”错误。

为了解决内存泄漏问题,可以考虑以下解决方法:

  1. 使用动态内存分配的对象(如new/delete or malloc/free)时,确保正确释放内存。
  2. 使用智能指针(如std::shared_ptr或std::unique_ptr)来管理内存,以确保资源的正确释放。
  3. 使用编译器提供的内存分析工具来检测和解决内存泄漏问题。

7. 函数指针错误(使用GDB)

如果程序中存在函数指针错误,即调用或引用一个无效的函数指针,可能会触发“stack smashing detected”错误。

为了解决函数指针错误问题,可以考虑以下解决方法:

  1. 确保函数指针的初始化和使用是正确的。
  2. 使用nullptr来初始化和检查函数指针,以避免使用无效的指针。
  3. 使用调试器来跟踪并定位函数指针错误的位置。
  4. 确保函数指针的类型匹配。

8. 堆栈的划分

在C++中,变量可以在栈(stack)上分配,也可以在堆(heap)上分配,这取决于你如何声明和使用它们。下面是一些基本的规则和示例,帮助区分哪些声明是在栈上,哪些声明是在堆上。

8.1 在栈上的声明

  • 局部变量:在函数内部声明的变量(包括函数的参数)默认在栈上创建。这些变量的生命周期限定在其声明的块(如函数体)中。
    void function() {
        int localVariable = 10; // 栈上
        std::string localString = "Hello"; // 栈上
    }
    
  • 局部静态变量:虽然局部静态变量的生命周期贯穿整个程序执行期,但它们的存储位置通常不是堆,而是程序的静态存储区(不是栈)。
    void function() {
        static int localStaticVariable = 10; // 非堆,静态存储区
    }
    

8.2 在堆上的声明

  • 动态分配的对象:使用new操作符动态创建的对象在堆上分配。这些对象的生命周期不受其创建位置(如函数体)的限制,需要显式地使用delete操作符来释放。
    int* heapVariable = new int(10); // 堆上
    std::string* heapString = new std::string("Hello"); // 堆上
    
    delete heapVariable; // 释放堆内存
    delete heapString; // 释放堆内存
    
  • 动态分配的数组:使用new[]操作符动态创建的数组也在堆上。
    int* heapArray = new int[10]; // 堆上
    
    delete[] heapArray; // 释放堆内存
    

8.3 特别说明

  • 全局变量和静态变量:全局变量和静态变量(包括静态成员变量)不是在堆上分配的,它们存储在程序的静态存储区域,这个区域在程序启动时分配,在程序结束时释放。
  • std::vector 和类似的STL容器(比如 std::mapstd::string 等)在C++中表现出了有趣的双重特性:
    1. 容器的元数据在栈上:当你声明一个 std::vector 作为局部变量时,这个容器对象(包括指向其数据的指针、大小、容量等元数据)是存储在栈上的。这意味着,容器对象的生命周期与它被声明的作用域绑定。
      void function() {
          std::vector<int> myVector; // myVector对象本身在栈上
      } // myVector在此处离开作用域,被自动销毁
      
    2. 容器的数据在堆上:然而,std::vector 所管理的实际数据(即你放入容器的元素)是存储在堆上的。当你向 vector 中添加元素时,vector 负责在堆上分配足够的空间来存储这些元素,并在需要时(如扩容时)自动管理这些内存。当 vector 被销毁时(例如,当它离开作用域时),它也负责释放存储其元素的堆内存。

8.4 小结

  • 在栈上分配的变量包括函数内的局部变量,它们的生命周期由其所在的作用域决定。
  • 在堆上分配的对象和数组是通过new(或new[])操作符创建的,它们的生命周期由程序员通过delete(或delete[])操作符显式管理。
  • 全局变量和静态变量存储在静态存储区,既不在堆上也不在栈上。

9. 左值和右值

一句话,右值可以赋值给左值,不可以直接赋值给左值引用,但可以赋值给常量左值引用。而左值不能赋值给右值,只能是右值赋值给右值。一般来说&以及数字是右值。

具体可以参考下面的文章:https://gutsgwh1997.github.io/2020/02/13/C-%E4%B8%AD%E7%9A%84%E5%B7%A6%E5%80%BC%E5%92%8C%E5%8F%B3%E5%80%BC/


在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

…详情请参照古月居

Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐