assembly - 编程语言是否有可能自信地防止堆栈溢出时的未定义行为?

标签 assembly programming-languages stack-overflow undefined-behavior callstack

鉴于以下假设:

  • 我们无法在每次pushsub 操作之前检查栈指针(%rsp)
  • 我们无法在编译时计算最大堆栈大小(例如,我们的编程语言支持递归)

是否可能有一种编程语言可以在 100% 的堆栈溢出情况下防止未定义的行为?

例如,许多语言使用 MMU 来捕获堆栈溢出。但在我看来,如果语言使用动态堆,MMU 无法保护所有内存,因此从理论上讲,如果程序进入一个非常大的函数,该函数将堆栈大小增加超出 MMU 保护区域,它随后可以写入未 protected 内存并导致未定义的行为而不触发 MMU。

我的推理有缺陷吗?编程语言是否有一种万无一失的方法来防止堆栈溢出时的未定义行为?

最佳答案

如果您有 MMU,您可以获得明确定义的安全行为:堆栈溢出导致无效页面错误(POSIX 上的段错误)。这需要操作系统的一些帮助,或手动映射低于堆栈增长限制的只读页面。只要确保在增加堆栈时触及堆栈空间的每一页即可。 (或者如果您保留更多的保护空间,则每 64kiB 一个探针)。如果你想在 POSIX 操作系统中,你可以捕获 SIGSEGV。其他操作系统可能有不同的机制。


GCC -fstack-check 结合操作系统在堆栈映射下方具有未映射页面的“保护区”,可以相当便宜地完成此操作。 (或者更具体地说,低于堆栈的最大增长限制,因此堆栈仍然可以增长,但不会超过该保护区。)

1MiB 保护区(当前 Linux 默认值)通常就足够了,您甚至不需要堆栈探测来防止 stack clash堆栈与堆栈下方的动态分配重叠的错误。但是,使用未经检查的用户输入作为 alloca 或 C99 VLA 大小的有缺陷/易受攻击的程序可能会一直跳过保护区。

并且 Windows 始终需要“堆栈探测”(在每个 4kiB 页面中触摸内存以实现大型或可变大小的堆栈增长,就像 gcc -fstack-protector 所做的) . Windows 甚至需要它来触发堆栈增长;如果您触摸最后使用的堆栈页面下方的多个页面,它不会增加您的堆栈。

Linux process stack overrun by local variables (stack guarding)有更多详细信息。

堆栈探测本质上是一种万无一失的方法,通过在程序执行任何危险操作之前触摸未映射的页面(不会触发堆栈增长)来确保您的程序发生段错误。这可以在任何操作系统和任何带有 MMU 的 ISA。

总的运行时成本只是函数入口(以及包含 VLA 的每个分配或范围)上的一个循环,它以 4kiB 的步幅接触内存,直到它覆盖堆栈增长的距离。如果在编译时知道该大小,则可以将其完全展开/剥离为一条或几条指令。

或者在大多数函数中只有少数局部变量,不包括任何巨大或可变大小的数组,根本没有开销。进行另一个函数调用涉及写入堆栈内存以保存返回地址,或者作为 x86 call 的一部分,或者在 RISC ISA 的函数入口处,在链接寄存器中传递返回地址。因此,即使分配中小型数组并且不接触它们的整个函数链也无法使堆栈指针偷偷通过保护页。将返回地址保存到堆栈或从堆栈恢复返回地址实际上是一种探测。

关于assembly - 编程语言是否有可能自信地防止堆栈溢出时的未定义行为?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/60934968/

相关文章:

assembly - 防止 AX 寄存器被 DIV 指令破坏

programming-languages - 什么是关系参数?

java - 如何避免递归函数的 StackOverflowError

c++ - c++中多个+=运算符引起的段错误?

assembly - 学习汇编语言有哪些优点和缺点?

assembly - 为什么需要内存对齐?

programming-languages - 您可以在您的语言中将哪些表情符号放入类(class)名称中?

php - 类型安全的语言是否需要静态类型?

c# - 最大继承级别

c - 助记符中的无效字符 '\'