在调用和从方法调用返回的情况下,什么是 C++ 中导致堆栈下溢的简单示例?
我熟悉调用约定,即thiscall
,stdcall
以及cdecl
他们清理堆栈的方式。编译器生成的代码不会自动处理堆栈下溢吗?
哪些情况会使我遇到堆栈下溢问题?
在调用和从方法调用返回的情况下,什么是 C++ 中导致堆栈下溢的简单示例?
我熟悉调用约定,即thiscall
,stdcall
以及cdecl
他们清理堆栈的方式。编译器生成的代码不会自动处理堆栈下溢吗?
哪些情况会使我遇到堆栈下溢问题?
我可以看到这种情况实际发生的唯一方法是,如果您声明一个函数以使用stdcall
(或任何其他指定被调用方清理堆栈的调用约定),然后通过指定为 a 的函数指针调用该函数cdecl
(或任何调用者清理堆栈的其他调用约定)。如果你这样做,被调用的函数将在返回之前弹出堆栈,然后调用者也会弹出堆栈,导致下溢和可怕的事情。
在成员函数的特定情况下,调用约定通常被称为,thiscall
调用者或被调用者是否清理堆栈取决于编译器。
有关调用约定的详细信息,请参见此处。
我不确定您是在谈论数据结构堆栈及其中的下溢问题还是其他问题。就stack(data structure) underflow
问题而言,这里是一个解释。
stack
是一种后进先出 (LIFO) 的抽象数据类型和数据结构。堆栈可以将任何抽象数据类型作为元素,但其特征在于只有三个基本操作:push、pop和stack top。
push操作将新项目添加到堆栈顶部,如果堆栈为空,则初始化堆栈。如果堆栈已满并且没有足够的空间来接受给定的项目,则认为堆栈处于溢出状态。pop 操作从栈顶移除一个项目。
弹出要么显示以前隐藏的项目,要么导致空堆栈,但如果堆栈为空,则进入下溢状态(这意味着堆栈中没有要删除的项目)。
栈顶操作从最顶层位置获取数据并返回给用户而不删除它。如果堆栈为空,则堆栈顶部操作中也会出现相同的下溢状态。
考虑一个堆栈实现示例:
template <class Item> class Stack
{
public:
bool isEmpty() const;
size_t size() const;
Item pop();
void push(const Item& it);
private:
};
现在考虑在此堆栈上执行的以下操作。
C++ command resulting stack
------------------------------------------------
Stack<int> S;
_____ (empty stack of ints)
S.push(7);
| 7 | <-- top
-----
S.push(2);
| 2 | <-- top
| 7 |
-----
S.push(73);
|73 | <-- top
| 2 |
| 7 |
-----
S.pop();
| 2 | <-- top
| 7 | -----
S.pop();
-----
S.pop();
| 7 | <-- top
-----
S.pop();
----- (empty)
S.pop();
ERROR "stack underflow"
通常这将由编译器处理。实际上,我能想到的唯一可能导致堆栈下溢的方法是调用使用一种调用约定实现的方法,就好像它正在使用另一种调用约定一样。
如果您曾经处于可能发生调用堆栈下溢的位置,那么您的程序很可能会在它发生之前死于暴力。至少,如果我理解函数调用的工作方式是准确的。
基本上,发生这种情况的唯一方法是,如果你调用一个函数,其中被调用者清理堆栈,并且它弹出太多值......如果调用者认为一个函数接受两个参数,但被调用者实际上需要三个,这可能会发生。变体将是一个函数,其中被调用者清理堆栈,然后调用者再次清理堆栈,如果您的调用约定错误,可能会发生这种情况。在这两种情况下,当你去链接时你可能会遇到问题,并且错误的名称是错误的,但也许你真的很不走运。
无论哪种方式,重要的是在函数调用之后,堆栈比它应该的短一个或多个字节。从理论上讲,程序将继续运行,弹出正确数量的数据,但仍比预期的少一个或多个字节。最终,没有更多的数据可以弹出,并且您有一个堆栈下溢。
但是,当引用堆栈时,地址是相对于顶部的。因此,如果它距离栈顶三个字节,编译器将在 [栈顶 + 3] 处查找特定对象。如果堆栈最终比预期的短,它仍然会这样做,并在错误的位置查找该对象。假设那个物体还在那里……它可能已经不小心弹出了。当您到达您所在的任何函数的末尾时,出于同样的原因,它可能无法找到正确的返回地址,但即使找到了,让您的所有对象突然损坏也是一个非常可怕的情况.
警告:这一切都是基于这样一个假设,即现代系统的行为与我十年前使用的旧微控制器相同。也许他们现在比那更聪明。