Answers:
它可以是软件,也可以是硬件,也可以是两者,或者都不是。
有两种溢出:增长堆栈时(进入函数时)溢出,以及访问堆栈上的数组时溢出。可以通过对函数入口进行边界检查来检测扩展堆栈时的溢出,以验证是否有足够的空间(如果没有错误,则发出错误信号或增大堆栈)。在访问堆栈中的数组时,溢出仅是在不验证数组边界的低级语言中的一个问题。解决方案是验证数组边界。
这些软件方法的优势在于它们可以完全可靠地工作:您可以确保检测到任何堆栈溢出。它们的缺点是它们增加了代码大小和执行时间。只要没有溢出发生,硬件就可以通过提供一种免费检测大多数溢出的方法来提供帮助。在具有MMU¹的体系结构上,运行时环境可以安排将堆栈映射到页面边界,而下一页面保持未映射状态。
+---------------+---------------+---------------+---------------+
| stack | unmapped | other stuff |
| ----> direction of growth | | |
+---------------+---------------+---------------+---------------+
^ ^ ^ ^ ^ page boundaries
这样,如果软件尝试访问超出页面边界的数据(无论是因为堆栈指针已移出边界还是因为数组访问超出边界并超出边界),它将通过访问未映射的区域而导致错误。这仅在溢出量足够小的情况下才会发生:如果溢出量太大,则程序可能最终访问地址空间中间隙另一侧的其他内容。
硬件方法的缺点是它不完全可靠,因为可能无法检测到大量的溢出,并且它无法检测到可寻址空间内残留的阵列溢出。
为了检测数组溢出,另一种软件技术是canary:在堆栈的顶部或帧之间放置一个特殊值,并检查函数返回时canary值没有改变。这也是一种不完善的技术,因为溢出可能完全避开了金丝雀,或者由于在检查时已经恢复了金丝雀值而可能无法检测到。尽管如此,使难以利用某些安全漏洞还是很有用的。
避免堆栈溢出的最安全,最便宜的方法是,通过静态分析来计算程序在开始执行之前所需的堆栈量。但是,这并不总是可行的:程序所需的堆栈量通常是不确定的,并且取决于程序处理的数据。
¹ 如果只有一个线程的堆栈位于现有物理映射的边缘,则仅在MPU或没有内存保护的情况下也可以应用相同的原理。