【问题标题】:How come Go doesn't have stackoverflows为什么 Go 没有 stackoverflows
【发布时间】:2011-05-12 17:38:44
【问题描述】:

我在此演示文稿中读到 http://golang.org/doc/ExpressivenessOfGo.pdf 第 42 页:

安全

- 没有堆栈溢出

这怎么可能?和/或 Go 如何避免这种情况?

【问题讨论】:

    标签: memory-management stack language-design stack-overflow go


    【解决方案1】:

    这是一个称为“分段堆栈”的功能:每个 goroutine 都有自己的堆栈,allocated on the heap

    在最简单的情况下,编程语言实现对每个进程/地址空间使用一个堆栈,通常使用称为 pushpop(或类似名称)的特殊处理器指令进行管理,并实现为堆栈帧的动态数组从固定地址开始(通常是虚拟内存的顶部)。

    这是(或曾经)很快,但不是特别安全。当大量代码在同一地址空间(线程)中同时执行时,它会导致麻烦。现在每个人都需要自己的堆栈。但是,所有的堆栈(可能除了一个)都必须是固定大小的,以免它们相互重叠或与堆重叠。

    但是,任何使用堆栈的编程语言也可以通过以不同的方式管理堆栈来实现:通过使用列表数据结构或类似的结构来保存堆栈帧,但实际上是在堆上分配的。在堆满之前不会出现堆栈溢出。

    【讨论】:

    • 每个线程都有自己的堆栈,因此“当大量代码在同一个地址空间(线程)中同时执行时,它会引起麻烦。”不可能是真的。
    • 这有很多是不正确的,Go 仍然使用 push/pop 指令(嗯,实际上他们没有,但 GCC [mov off(%esp)] 也没有),它只是设置堆栈并将基址寄存器堆栈到堆分配的堆栈中。
    • @poolie:堆栈溢出异常是 C 或 C++ 中的一个难题。它迫使您手动创建递归函数的迭代版本,或者像 CLang 在过去两周所做的那样,将执行移至单独的线程...如果您可以预见该问题。为了规避这一点,许多人会简单地设置一个大堆栈(我工作的地方是每个线程 8MB),但这仍然涉及调整和猜测。不必担心堆栈大小会增加安全性(而不是安全性)。
    • 基于这里的 cmets,这个答案需要更多。分段堆栈不仅仅是堆分配的。 Go 运行时确保在函数开始时堆栈足够大(参见 runtime·morestack),如果不是,它会为堆栈分配更多空间(如果没有足够的内存,它会恐慌)。
    • 我理解它的好处,我只是不相信这就是他们在这种情况下所说的“堆栈溢出”的意思。顺便说一句,在 64 位机器上,这种好处在一定程度上被稀释了,因为那里有如此丰富的地址空间来容纳宽间距的堆栈:给它们每个 4GB 并感到高兴。 (显然不是所有的机器都是 64 位的。)
    【解决方案2】:

    它使用分段堆栈。这基本上意味着它使用链表而不是固定大小的数组,因为它是堆栈。当它用完空间时,它会使堆栈变大一点。

    编辑:

    这里有更多信息:http://golang.org/doc/go_faq.html#goroutines

    之所以如此出色,不是因为它永远不会溢出(这是一个很好的副作用),而是因为您可以创建内存占用非常小的线程,这意味着您可以拥有很多线程。

    【讨论】:

      【解决方案3】:

      我认为他们不能“完全”避免堆栈溢出。它们提供了一种方法来防止最典型的与编程相关的错误产生堆栈溢出。

      当内存用完时,没有办法防止堆栈溢出。

      【讨论】:

      • 但是任何编写溢出基于堆的堆栈的程序的人都做错了。
      • 这就是 90% 的人,这就是为什么 go 的设计者试图阻止这种情况
      • 90% 的人会溢出基于 heap 的堆栈(类似于 go 的堆栈)?
      • 根据定义,您不能真正通过基于堆的分段堆栈。堆栈溢出是 stack_growth->*collision*
      • 对于 C 语言,堆栈默认大小为 1 到 8 MB,这通常比任何计算机内存要小得多。事实上,当递归是最简单的解决方案时,它可能会迫使您避免递归。
      【解决方案4】:

      即使是 C 也可以通过一些基本上影响编译器的约束来完成。

      这是一项令人印象深刻的工程壮举,但不是语言设计。

      【讨论】:

      • 我没有。我基本上发明了这项技术。通过一点组装和一点想象力,你也可以。没那么难。
      • 将 esp 设置为堆中的某处?
      【解决方案5】:

      我认为他们在这里指的是对数组的访问总是根据数组的实际长度进行检查,从而禁用了 C 程序意外崩溃或恶意崩溃的最常见方式之一。

      例如:

      package main
      
      func main() {
          var a [10]int
      
          for i:= 0; i < 100; i++ {
              a[i] = i
          }
      }
      

      panic 尝试更新数组中不存在的第 11 个元素时会出现运行时错误。 C 会在堆上乱涂乱画,并且可能还会崩溃,但以一种不受控制的方式。每个数组都知道它的长度。在某些情况下,如果编译器可以证明它们没有必要,编译器将有优化检查的空间。 (或者一个足够聪明的编译器可能会静态检测到这个函数中的问题。)

      许多其他答案都在谈论堆栈的内存布局,但这实际上并不相关:您也可以进行堆溢出攻击。

      基本上,Go 的指针应该始终是类型安全的,包括数组和其他类型,除非您专门使用 unsafe 包。

      【讨论】:

      • 我认为您混淆了缓冲区溢出和堆栈溢出。不过你是对的。
      • 您描述的是边界检查,它与OP询问的堆栈溢出无关。 Pascal 也这样做,但它(在典型实现中)容易受到堆栈溢出的影响。
      • 我无法确定那里的评论是否在谈论堆栈缓冲区溢出或堆栈溢出。如果我在描述 Go 的关键特性(对于了解 C 的读者),我肯定会在提到几乎无限大小的堆栈之前提到检查数组。另一方面,这是在关于并发的部分中提到的,所以也许它们确实意味着您可以添加线程而无需给它们小堆栈。
      猜你喜欢
      • 2011-04-24
      • 2014-08-12
      • 1970-01-01
      • 2015-05-20
      • 1970-01-01
      • 1970-01-01
      • 2021-06-17
      • 2016-12-03
      • 2020-06-19
      相关资源
      最近更新 更多