是否可以在 Linux 上预测 C 中的堆栈溢出?
Posted
技术标签:
【中文标题】是否可以在 Linux 上预测 C 中的堆栈溢出?【英文标题】:Is it possible to predict a stack overflow in C on Linux? 【发布时间】:2010-09-30 10:39:34 【问题描述】:在 x86 Linux 系统上,某些情况会导致堆栈溢出:
struct my_big_object[HUGE_NUMBER]
在堆栈上。穿过它最终会导致SIGSEGV
。
alloca()
例程(类似于malloc()
,但使用堆栈,自动释放自身,如果它太大,也会与SIGSEGV
一起爆炸)。 更新:alloca() 并没有像我最初所说的那样被正式弃用;只是不鼓励。
有没有办法以编程方式检测本地堆栈对于给定对象是否足够大?我知道堆栈大小可以通过ulimit
调整,所以我希望有一种方法(但它可能是不可移植的)。理想情况下,我希望能够做这样的事情:
int min_stack_space_available = /* ??? */;
if (object_size < min_stack_space_available)
char *foo = alloca(object_size);
do_stuff(foo);
else
char *foo = malloc(object_size);
do_stuff(foo);
free(foo);
【问题讨论】:
这是否有机会发生在一个线程中?我试图重现段错误,但在尝试非常大的尺寸时,alloca() 只得到 NULL。 是的,alloca() 最初是在一个从多个线程调用的函数中。 【参考方案1】:您可以通过查找进程的堆栈空间大小然后减去已使用的量来确定进程可用的堆栈空间。
ulimit -s
显示 linux 系统上的堆栈大小。如需程序化方法,请查看getrlimit()。然后,要确定当前堆栈深度,从一到底部减去一个指向堆栈顶部的指针。例如(代码未经测试):
unsigned char *bottom_of_stack_ptr;
void call_function(int argc, char *argv)
unsigned char top_of_stack;
unsigned int depth = (&top_of_stack > bottom_of_stack_ptr) ?
&top_of_stack-bottom_of_stack_ptr :
bottom_of_stack_ptr-&top_of_stack;
if( depth+100 < PROGRAMMATICALLY_DETERMINED_STACK_SIZE )
...
int main(int argc, char *argv)
unsigned char bottom_of_stack;
bottom_of_stack_ptr = &bottom_of_stack;
my_function();
return 0;
【讨论】:
是这样吗? Bottom_of_stack 可能不是真正的栈底,对吧?全局变量不是放在堆栈上吗,还有编译器决定它想要的其他垃圾? ulimit -s 和 getrlimit(RLIMIT_STACK) 只会告诉你初始线程的大小。除非您知道自己在初始线程中运行,否则它不会告诉您任何信息。 全球通常有自己的空间。启动代码可以增加堆栈深度,所以上面的代码为了安全起见,在深度上添加了一个很好的因素。是的,RLIMIT_STACK 仅适用于初始堆栈,但是 pthread 允许获取和设置堆栈大小。【参考方案2】:不推荐使用的 alloca() 例程(如 malloc(),但使用堆栈, 自动释放自己,如果它太大也会用 SIGSEGV 炸毁)。
为什么不推荐使用 alloca?
无论如何,在您的情况下,alloca 与 malloc 相比要快多少? (值得吗?)
如果没有足够的空间,你不会从 alloca 得到 null 吗? (和malloc一样?)
当你的代码崩溃时,它在哪里崩溃?是在 alloca 中还是在 doStuff() 中?
/约翰
【讨论】:
(1) GNU 手册页说不要使用它。 (2) alloca 在恒定时间内运行,而 malloc 是非确定性的,可能涉及系统调用和锁定线程。 (2) 如果 alloca 导致堆栈溢出,则行为未定义(它在使用时出现段错误,而不是在 alloca 上)。 然而,malloc 提供的 null-return 通常只是一种虚假的安全性:Linux 上的 malloc for 确实返回非 null,并且会在使用内存时崩溃。你首先必须在内核中切换一些位来改变它(见 man malloc)【参考方案3】:不确定这是否适用于 Linux,但在 Windows 上,即使成功分配了大堆栈分配,也可能会遇到访问冲突!
这是因为默认情况下,Windows 的 VMM 实际上仅将堆栈 RAM 的顶部几个(不确定到底有多少)4096 字节页面标记为可分页(即由页面文件支持),因为它认为堆栈访问通常会从顶部向下行进;随着访问越来越接近当前的“边界”,越来越低的页面被标记为可分页。但这意味着远低于堆栈顶部的早期内存读/写将触发访问冲突,因为该内存尚未实际分配!
【讨论】:
Linux 也这样做。您可以 malloc() 很多大块,并且在您真正开始使用所有这些内存之前不会耗尽空间。 OOM 杀手?我认为相关但不同。默认情况下,Linux 允许 heap 分配在交换用尽时成功返回;我相信 Windows VMM 在这种情况下会提前失败。我发现这是 Windows 的 stack 行为有问题... :) 你的意思是OOM杀手可以关掉吧?我不知道如何关闭 Windows 的堆栈行为...也许您可以在链接时提供一个开关?【参考方案4】:alloca() 将在失败时返回 NULL,我相信 alloca(0) 的行为是未定义的和平台变体。如果你在 do_something() 之前检查了这一点,那么你永远不会被 SEGV 击中。
我有几个问题:
-
为什么,哦,为什么,你需要在堆栈上那么大的东西?大多数系统的默认大小是 8M,还是太小了?
如果调用 alloca() 的函数阻塞,通过 mlock() / mlockall() 保护相同数量的堆是否会随着时间的推移保证接近相同的访问性能(即“不要交换我,兄弟!”)?如果您使用更激进的 'rt' 调度程序,建议还是调用它们。
这个问题很有趣,但引起了人们的注意。它提高了我方钉圆孔 o-meter 上的指针。
【讨论】:
(1) 在我正在查看的机器上,堆栈大小配置为远小于 8M。 (2) 页面交换绝对是一个问题,虽然现在你提到它,也许我最好还是预先分配和 mlock()ing。 如果堆栈溢出,alloca 会导致未定义的行为。根据其手册页,它不会返回 0 alloca() 本身是平台相关的。 :)【参考方案5】:你没有说太多为什么要在堆栈上分配,但如果堆栈内存模型很吸引人,你也可以在堆上实现堆栈分配。在程序开始时分配一大块内存,并保留一个指向它的指针堆栈,这将对应于常规堆栈上的帧。你只需要记住在函数返回时弹出你的私有堆栈指针。
【讨论】:
我想避免堆分配(这可能很昂贵)。我相信,为每个线程预分配一个静态缓冲区也可以。【参考方案6】:几个编译器,例如 Open Watcom C/C++,支持 stackavail() 函数,可以让你做到这一点
【讨论】:
【参考方案7】:您可以使用GNU libsigsegv
来处理页面错误,包括发生堆栈溢出的情况(来自其网站):
在某些应用程序中,堆栈溢出处理程序会执行一些清理或通知用户,然后立即终止应用程序。在其他应用程序中,堆栈溢出处理程序 longjmps 回到应用程序的中心点。这个库支持这两种用途。第二种情况,handler必须保证恢复正常的信号掩码(因为handler执行的时候很多信号被阻塞了),还必须调用sigsegv_leave_handler()来转移控制;那么只有它可以longjmp离开。
【讨论】:
我在阅读 libsigsegv 页面时感到困惑,它没有提到在堆栈溢出发生后确保程序可以有意义地继续执行的看似深奥的不可能。如果溢出发生在 malloc() 之类的东西中,正在摆弄堆怎么办?如果溢出发生在编译器注入的内部支持函数中,您甚至看不到函数调用怎么办?除了试图继续跑步之外,我还会对一些做了一点点然后退出的事情持怀疑态度——在这组经过审查的“一点点”事情中,你被承诺可以做什么? :-/ @Hostile 这么多年后我不记得手册页了,但我不明白为什么你不能继续你正在做的事情,如果页面之前没有映射然后使故障可用。分叉后写入内存时总是会发生小段错误(写时复制),而且效果很好。 但是 IIRC,你现在可以在 Linux 上调用userfaultfd
来“创建一个文件描述符来处理用户空间中的页面错误”,这似乎比挂钩到信号处理程序更干净。跨度>
我使用了带 MMF 的写时复制,但这似乎有所不同。扩展 C 堆栈并继续运行在机械上是不可能的。因此,如果您有void *malloc(size_t size) /* fiddle heap */ helper() /* finish fiddling heap */ return p;
,并且在helper()
期间发生溢出...您所能做的就是在信号处理程序期间提供用于堆栈的少量内存——它必须终止或longjmp。什么都不会运行 finalize,因此堆可能会损坏。编译器有时会实现带有辅助函数的“原语”,因此即使它是“所有你自己的代码”,它似乎也很冒险。对吗?
@hostile 我明白你现在在说什么了。我同意。如果堆栈命中堆,而您不知道哪个函数可能处于活动状态以及它在做什么,那将是致命的。【参考方案8】:
alloca 函数不已弃用。但是,它不在 POSIX 中,它也依赖于机器和编译器。 alloca 的 Linux 手册页指出“对于某些应用程序,与使用 malloc 相比,使用它可以提高效率,并且在某些情况下,它还可以简化使用 longjmp() 或 siglongjmp() 的应用程序中的内存释放。否则,不鼓励使用它。”
手册页还说“如果堆栈帧无法扩展,则没有错误指示。但是,在分配失败后,程序很可能会收到 SIGSEGV。”
malloc的性能其实在*** Podcast #36上也提到过。
(我知道这不是您问题的正确答案,但我认为无论如何它可能有用。)
【讨论】:
谢谢,我去看看那个播客。【参考方案9】:即使这不是对您问题的直接答案,我希望您知道valgrind 的存在 - 一个在 Linux 上检测运行时此类问题的绝佳工具。
关于堆栈问题,您可以尝试从检测到这些溢出的固定池中动态分配对象。通过一个简单的宏向导,您可以使其在调试时运行,在发布时运行真实代码,从而知道(至少对于您正在执行的场景)您不会占用太多。 Here's more info and a link 示例实现。
【讨论】:
我知道 valgrind,但它对这个问题没有帮助。【参考方案10】:我想不出一个好的方法。也许可以通过使用 getrlimit() (之前建议)和一些指针算术?但首先问问自己你是否真的想要这个。
无效 *closeToBase; 主要的 () 诠释 closeToBase; stackTop = &closeToBase; int stackHasRoomFor(int bytes) 诠释当前顶部; 返回 getrlimit(...) - (¤tTop - closeToBase) > bytes + SomeExtra;就我个人而言,我不会这样做。在堆上分配大的东西,堆栈不适合它。
【讨论】:
【参考方案11】:堆栈区域的结束由操作系统动态确定。虽然您可以通过以高度依赖操作系统的方式查看虚拟内存区域 (VMA) 来找到堆栈的“静态”边界(请参阅libsigsegv/src/ 中的 stackvma* 文件),但您还必须考虑
getrlimit 值, 每个线程的堆栈大小(请参阅pthread_getstacksize)【讨论】:
【参考方案12】:抱歉,如果这是显而易见的,但您可以轻松地编写一个函数来测试特定的堆栈分配大小,只需尝试(该大小的)alloca 并捕获堆栈溢出异常。如果您愿意,可以将其放入一个函数中,并为函数堆栈开销使用一些预先确定的数学。例如:
bool CanFitOnStack( size_t num_bytes )
int stack_offset_for_function = 4; // <- Determine this
try
alloca( num_bytes - stack_offset_for_function );
catch ( ... )
return false;
return true;
【讨论】:
而且即使是 C++,也没有标准的、独立于平台的机制来触发堆栈溢出异常。 这实际上是可行的——不是按照您描述的方式,而是通过非常仔细使用 SIGSEGV 处理程序。 好点;我错过了它是 C。我只是想到使用异常处理程序本身可能是从 A 点到 B 点的最简单方法,可以这么说。 :)以上是关于是否可以在 Linux 上预测 C 中的堆栈溢出?的主要内容,如果未能解决你的问题,请参考以下文章