如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?

Posted

技术标签:

【中文标题】如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?【英文标题】:How to detect possible / potential stack overflow problems in a c / c++ program? 【发布时间】:2010-09-17 00:25:18 【问题描述】:

是否有一种标准方法可以查看您的应用有多少堆栈空间以及在运行期间堆栈使用的最高水位线是多少?

同样在实际溢出的可怕情况下会发生什么?

它会崩溃、触发异常或信号吗?是否有标准,或者所有系统和编译器都不同?

我正在专门寻找 Windows、Linux 和 Macintosh。

【问题讨论】:

相关:***.com/questions/389219/… “我正在寻找专门用于 Windows、Linux 和 Macintosh 的产品”- 非常具体 :) 【参考方案1】:

在 Linux 上,如果您的代码试图越过堆栈写入,则会出现分段错误。

栈的大小是进程间继承的一个属性。如果您可以在 shell 中使用ulimit -sshkshzsh)或limit stacksizetcshzsh)等命令读取或修改它。

从程序中,可以使用读取堆栈的大小

#include <sys/resource.h>
#include <stdio.h>

int main() 
    struct rlimit l;
    getrlimit(RLIMIT_STACK, &l);
    printf("stack_size = %ld\n", l.rlim_cur);
    return 0;

我不知道获取可用堆栈大小的标准方法。

堆栈以argc 开头,然后是argv 的内容和环境的副本,然后是您的变量。然而,由于内核可以随机化堆栈开始的位置,并且在argc 之上可能存在一些虚拟值,因此假设您在&amp;argc 之下有可用的l.rlim_cur 字节是错误的。

检索堆栈的确切位置的一种方法是查看文件/proc/1234/maps(其中1234 是您程序的进程ID)。一旦知道了这些界限,您就可以通过查看最新的局部变量的地址来计算使用了多少堆栈。

【讨论】:

我不相信有一个标准的方法来获取可用堆栈的大小。标准是否定义了堆栈的存在? 我刚刚查看了 C 标准,实际上它甚至没有使用 stack 这个词。这很有趣。它区分静态自动分配存储;但是我找不到一个地方会暗示调用函数可能由于内存限制而失败。 @GregD 有一个间接的方法 1. 获取最大堆栈大小 2. 获取当前堆栈大小 3. 做 A-B【参考方案2】:

可以在 Visual Studio 中使用 editbin 来更改堆栈大小。该信息可以在msdn.microsoft.com/en-us/library/35yc2tc3.aspx找到。

【讨论】:

【参考方案3】:

在 Linux 上,Gnu libsigsegv library 包含函数 ***_install_handler,它可以检测(并且在某些情况下帮助您从中恢复)堆栈溢出。

【讨论】:

【参考方案4】:

在 Windows 上,堆栈(针对特定线程)按需增长,直到达到创建之前为此线程指定的堆栈大小。

按需增长是使用保护页面实现的,因为最初只有一个可用的堆栈片段,然后是一个保护页面,当被命中时,将触发异常 - 此异常是特殊的,由系统适合您 - 处理会增加可用堆栈空间(还检查是否已达到限制!)并重试读取操作。

一旦达到限制,就不会再增长,这会导致堆栈溢出异常。 当前堆栈基数和限制存储在线程环境块中,名为_NT_TIB(线程信息块)的结构中。 如果您手边有调试器,您会看到以下内容:

0:000> dt ntdll!_teb @$teb nttib.
   +0x000 NtTib  : 
      +0x000 ExceptionList : 0x0012e030 _EXCEPTION_REGISTRATION_RECORD
      +0x004 StackBase : 0x00130000 
      +0x008 StackLimit : 0x0011e000 
      +0x00c SubSystemTib : (null) 
      +0x010 FiberData : 0x00001e00 
      +0x010 Version : 0x1e00
      +0x014 ArbitraryUserPointer : (null) 
      +0x018 Self   : 0x7ffdf000 _NT_TIB

StackLimit 属性将按需更新。 如果你检查这个内存块的属性,你会看到类似的东西:

0:000> !address 0x0011e000 
    00030000 : 0011e000 - 00012000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000004 PAGE_READWRITE
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageStack
                    Pid.Tid  abc.560

然后检查它旁边的页面会显示 guard 属性:

0:000> !address 0x0011e000-1000
    00030000 : 0011d000 - 00001000
                    Type     00020000 MEM_PRIVATE
                    Protect  00000104 PAGE_READWRITE | PAGE_GUARD
                    State    00001000 MEM_COMMIT
                    Usage    RegionUsageStack
                    Pid.Tid  abc.560

希望对你有帮助。

【讨论】:

【参考方案5】:

如果您在 linux 上,我建议您使用备用信号堆栈。

    在这种情况下,所有信号都将通过备用堆栈进行处理。 如果发生堆栈溢出,系统会生成一个 SEGV 信号,这可以通过备用堆栈进行处理。 如果您不使用它......那么您可能无法处理信号,并且您的程序可能会在没有任何处理/错误报告的情况下崩溃。

【讨论】:

【参考方案6】:

一些编译器支持 stackavail() 函数,它返回堆栈的剩余可用空间量。您可以在调用程序中需要大量堆栈空间的函数之前使用此函数,以确定调用它们是否安全

【讨论】:

【参考方案7】:

堆栈溢出可能是最难处理的异常类型——因为您的异常处理程序必须处理最少量的堆栈(通常为此目的只保留一个页面)。

有关处理此类异常的困难的有趣讨论,请参阅以下博客文章:来自 Chris Brumme 的 1 和 2,它们从 .NET 角度关注该问题,特别是托管 CLR。​​

【讨论】:

【参考方案8】:

gcc 在“不安全”函数调用中在返回地址和普通变量之间放置了一个额外的内存块,例如(在本例中,函数是 void test() char a[10]; b[20]:

call stack:
-----------
return address
dummy
char b[10]
char a[20]

如果函数在指针“a”中写入 36 个字节,溢出将“破坏”返回地址(可能存在安全漏洞)。但它也会改变 'dummy' 的值,即指针和返回地址之间的值,因此程序将崩溃并发出警告(您可以使用 -fno-stack-protector 禁用它)

【讨论】:

【参考方案9】:

Windows上会产生堆栈溢出exception

下面的 windows 代码说明了这一点:

#include <stdio.h>
#include <windows.h>

void ***()

  CONTEXT context;

  // we are interested control registers
  context.ContextFlags = CONTEXT_CONTROL;

  // get the details
  GetThreadContext(GetCurrentThread(), &context);

  // print the stack pointer
  printf("Esp: %X\n", context.Esp);

  // this will eventually overflow the stack
  ***();


DWORD ExceptionFilter(EXCEPTION_POINTERS *pointers, DWORD dwException)

  return EXCEPTION_EXECUTE_HANDLER;


void main()

  CONTEXT context;

  // we are interested control registers
  context.ContextFlags = CONTEXT_CONTROL;

  // get the details
  GetThreadContext(GetCurrentThread(), &context);

  // print the stack pointer
  printf("Esp: %X\n", context.Esp);

  __try
  
    // cause a stack overflow
    ***();
  
  __except(ExceptionFilter(GetExceptionInformation(), GetExceptionCode()))
  
    printf("\n****** ExceptionFilter fired ******\n");
  

运行此 exe 时会生成以下输出:

Esp: 12FC4C
Esp: 12F96C
Esp: 12F68C
.....
Esp: 33D8C
Esp: 33AAC
Esp: 337CC

****** ExceptionFilter fired ******

【讨论】:

如果我错了,请纠正我,因为我不确定。您的代码说明了简单的情况。但是如果堆栈暂时溢出到堆中然后重新处理这并不总是会触发溢出异常,因为这种检测机制是在另一个线程中运行的。 我绝对不是专家,但我认为当移动堆栈指针的请求导致该指针引用无效内存时会生成堆栈异常。堆栈上的变量可能会损坏堆栈,但我认为这不会导致堆栈溢出异常。 在示例中,我发布了对 *** 函数的每次调用都会推进堆栈指针(如打印输出所示),最终该指针命中无效内存。

以上是关于如何在 c / c++ 程序中检测可能/潜在的堆栈溢出问题?的主要内容,如果未能解决你的问题,请参考以下文章

线程安全堆栈 C++ 中的潜在死锁

系统在此应用程序中检测到基于堆栈的缓冲区溢出。溢出...

如何在 Windows x64 C++ 应用程序中捕获堆栈溢出

C++ 检测内存分配

预先在运行时检测堆栈溢出

如何使用 C# 从 C++ 应用程序获取调用堆栈?