0. 简介
相较于其他报错,stack smashing detect这个报错是最令人头疼的段错误种类。“Stack smashing detect” 是指在程序运行过程中检测到栈溢出的情况。栈溢出是一种常见的安全漏洞,发生在程序尝试往栈空间写入超过其边界范围的数据时。
1. 常见分类
通常,导致 “Stack smashing detect” 错误的原因可能包括:
- 缓冲区溢出:当向一个缓冲区写入超过其分配大小的数据时,会覆盖到相邻的内存地址,导致栈被破坏。
- 函数调用错误:函数调用时参数传递错误或者返回值处理不当,可能引起栈结构被破坏。
- 格式化字符串漏洞:使用不当的格式化字符串函数(如printf)可能造成栈溢出。
- 栈溢出:如果递归调用层数过多,可能导致栈空间耗尽而触发 stack smashing detect。
- 内存泄漏:未正确释放之前分配的内存。
- 函数指针错误:调用或引用一个无效的函数指针。
下面我们来看一下每个可能原因的解决方法
2. 缓冲区溢出(使用GDB)
在C++中,数组的索引从0开始,因此需要确保不要超出数组的边界进行访问。如果程序中存在数组越界问题,可以按照以下步骤进行排查和解决:
- 检查数组的大小和边界。
- 确保数组的访问索引不会超出边界。
- 使用调试器(如gdb)来跟踪代码的执行,定位到越界访问的代码行。
- 修复代码中的越界访问错误,并确保访问索引正确计算。
3. 函数调用错误(使用GDB)
这种还是比较好排查的,也是实用GDB获取对应的函数,需要看一下传入的函数调用的参数个数、类型和顺序与函数定义一致。然后检查函数返回值是否被正确处理,避免因为忽略错误码而导致的问题。
4. 格式化字符串漏洞(使用GDB)
- 避免直接使用外部输入作为格式化字符串:不要将不受信任的字符串直接用作printf或其他格式化输出函数的格式串。
- 使用格式化字符串函数的安全版本:尽量使用安全的字符串函数,如strncpy代替strcpy,snprintf代替sprintf等,这些函数允许指定最大可写入字符数。
5. 栈溢出(AddressSanitzer&Valgrind&gperftools)
这种是最难排查的问题,栈溢出是指当程序中使用的栈空间超过其分配的限制时发生的情况。这通常是由于递归调用或过多的局部变量导致的。
为了解决栈溢出问题,可以考虑以下解决方法:
- 使用循环替代递归调用。
- 优化算法,减少需要使用的栈空间。
- 使用堆内存替代栈内存。
- 增加可用的栈空间(通过调整编译器选项),但需要注意堆栈溢出的风险。
5.1 分析递归深度
- 限制递归深度:为递归函数设置一个最大深度限制,当达到这个限制时停止递归。这可以帮助确定是否是递归深度导致的栈溢出。
- 递归到循环:如果可能,尝试将递归逻辑重写为循环逻辑。循环不会增加栈空间消耗,因此可以有效减少栈溢出的风险。
5.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 使用工具进行分析
- 编译器警告:启用编译器的所有警告(例如,使用-Wall -Wextra标志),可能会有关于栈空间使用的警告。
- 静态分析工具:使用静态分析工具检查代码,这些工具可以帮助发现潜在的问题,包括可能导致栈溢出的代码模式。
- 动态分析工具:使用如Valgrind、AddressSanitizer等动态分析工具运行程序,它们可以帮助检测栈溢出、内存泄漏和其他内存问题。
5.4 优化递归算法
- 尾递归优化:如果编译器支持尾递归优化,尝试将递归函数重写为尾递归形式。尾递归优化可以减少栈空间的使用。
- 分而治之:对于某些问题,考虑使用分而治之等策略,将问题分解为可以并行解决的小问题,这样可以减少单一递归调用链的深度。
5.5调整编译器栈大小设置
增加栈大小:在某些情况下,可以通过调整编译器设置来增加程序的栈大小。例如,在GCC中,可以使用链接器选项-Wl,–stack,来设定栈的大小。
6. 内存泄漏(AddressSanitzer&Valgrind&gperftools)
如果程序中存在内存泄漏问题,即未正确释放之前分配的内存,可能会导致栈溢出,并可能触发“stack smashing detected”错误。
为了解决内存泄漏问题,可以考虑以下解决方法:
- 使用动态内存分配的对象(如new/delete or malloc/free)时,确保正确释放内存。
- 使用智能指针(如std::shared_ptr或std::unique_ptr)来管理内存,以确保资源的正确释放。
- 使用编译器提供的内存分析工具来检测和解决内存泄漏问题。
7. 函数指针错误(使用GDB)
如果程序中存在函数指针错误,即调用或引用一个无效的函数指针,可能会触发“stack smashing detected”错误。
为了解决函数指针错误问题,可以考虑以下解决方法:
- 确保函数指针的初始化和使用是正确的。
- 使用nullptr来初始化和检查函数指针,以避免使用无效的指针。
- 使用调试器来跟踪并定位函数指针错误的位置。
- 确保函数指针的类型匹配。
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::map
、std::string
等)在C++中表现出了有趣的双重特性:- 容器的元数据在栈上:当你声明一个
std::vector
作为局部变量时,这个容器对象(包括指向其数据的指针、大小、容量等元数据)是存储在栈上的。这意味着,容器对象的生命周期与它被声明的作用域绑定。void function() { std::vector<int> myVector; // myVector对象本身在栈上 } // myVector在此处离开作用域,被自动销毁
- 容器的数据在堆上:然而,
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/