【问题标题】:Why is bounds checking not implemented in some of the languages?为什么某些语言没有实现边界检查?
【发布时间】:2011-11-16 15:21:57
【问题描述】:

根据维基百科 (http://en.wikipedia.org/wiki/Buffer_overflow)

通常与缓冲区溢出相关的编程语言包括 C 和 C++,它们没有提供内置保护以防止访问或覆盖内存任何部分中的数据,并且不会自动检查写入数组的数据(内置缓冲区类型) 在该数组的边界内。边界检查可以防止缓冲区溢出。

那么,为什么 C 和 C++ 等某些语言没有实现“边界检查”?

【问题讨论】:

  • 开销并不总是必需的。
  • 这是一些程序员认为不需要的开销。那些从不犯错的人。
  • @Hans,开发应用程序和运行单元测试时需要;但是当它实际运行时,它并没有帮助,因为大概代码已经将边界检查作为访问它的逻辑的一部分实现了。
  • 这将适合那些认为他们从未犯过错误的单元测试人员的类别。他们应该会面并思考为什么程序仍然存在错误。可能会得出结论是用户的错。
  • 这是一些应用程序真正不需要的开销。指责程序员的无知/傲慢是幼稚的。

标签: programming-languages implementation buffer-overflow


【解决方案1】:

基本上,这是因为这意味着每次更改索引时,都必须执行 if 语句。

让我们考虑一个简单的 C for 循环:

int ary[X] = {...};  // Purposefully leaving size and initializer unknown

for(int ix=0; ix< 23; ix++){
    printf("ary[%d]=%d\n", ix, ary[ix]);
}

如果我们有边界检查,ary[ix] 的生成代码必须类似于

LOOP:
    INC IX          ; add `1 to ix
    CMP IX, 23      ; while test
    CMP IX, X       ; compare IX and X
    JGE ERROR       ; if IX >= X jump to ERROR
    LD  R1, IX      ; put the value of IX into register 1
    LD  R2, ARY+IX  ; put the array value in R2
    LA  R3, Str42   ; STR42 is the format string
    JSR PRINTF      ; now we call the printf routine
    J   LOOP        ; go back to the top of the loop

;;; somewhere else in the code
ERROR:
    HCF             ; halt and catch fire

如果我们没有那个边界检查,那么我们可以改写:

    LD R1, IX
LOOP:
    CMP IX, 23
    JGE END
    LD R2, ARY+R1
    JSR PRINTF
    INC R1
    J   LOOP

这在循环中节省了 3-4 条指令,这(尤其是在过去)意义重大。

事实上,在 PDP-11 机器中,它甚至更好,因为有一种叫做“自动增量寻址”的东西。在 PDP 上,所有的寄存器等都变成了类似

CZ  -(IX), END    ; compare IX to zero, then decrement; jump to END if zero

(任何碰巧比我记得 PDP 更好的人,请不要为精确的语法等问题困扰我;你和我一样是个老屁,你知道这些东西是如何溜走的。)

【讨论】:

  • 当然。见鬼,它是在 Algol-60 中完成的。这也是一个开销。当 Kernighan、Ritchie、Thompson 等人构建 C 和 UNIX 时,他们选择不这样做,以保存所有这些指令以用于他们自己的邪恶目的。当 Jim Gosling 和 Oak 人构建 proto-Java 时,他们认为这是一个错误——ergo bounds-checking。
  • 您可能夸大了它。例如,对于您的特定示例,任何优化编译器都可以将比较提升到循环之外。
  • Charlie - 随着 CPU 速度的提高,实现边界检查并使其成为默认选项是否有意义。当我更新 Windows 或 Ubuntu 操作系统时,我发现很多补丁都与缓冲区溢出有关。
  • @ergosys 优化编译器?对于 C? 1970 年?没有这种动物。
  • @Praveen,确实如此,但那是另一种语言;有一些不正当的 C 技巧可以有效地利用这种灵活性,如果编译器突然进行边界检查,它们就会中断。另外,请记住 C 指针/数组对偶性:如何编译使用 extern int ** 作为在另一个文件中进行 malloc 编辑的二维数组的程序? PL/I 是 C 的直接祖先之一,具有边界检查功能。谷歌“PL/I dope vector”了解详情。
【解决方案2】:

一切都与性能有关。但是,C 和 C++ 没有边界检查的断言并不完全正确。每个库都有“调试”和“优化”版本是很常见的,在各种库的调试版本中启用边界检查也很常见。

这具有在开发应用程序时快速、轻松地发现越界错误的优点,同时消除了运行 realz 程序时的性能损失。

我还应该补充一点,性能损失是不可忽略的,除 C++ 之外的许多语言将提供各种在缓冲区上运行的高级函数,这些函数直接在 C 和 C++ 中实现,以避免边界检查。例如,在 Java 中,如果您比较使用纯 Java 与使用 System.arrayCopy 将一个数组复制到另一个数组的速度(它只进行一次边界检查,然后直接复制数组而不检查每个单独的元素),您会看到这两个操作的性能有相当大的差异。

【讨论】:

    【解决方案3】:

    在编译和运行时都更容易实现和更快。它还简化了语言定义(如果跳过,可以省略很多内容)。

    目前,当你这样做时:

    int *p = (int*)malloc(sizeof(int));
    *p = 50;
    

    C(和 C++)只是说,“Okey dokey!我会在内存中的那个位置放一些东西”。

    如果需要边界检查,C 将不得不说:“好的,首先让我们看看我是否可以在那里放置一些东西?它是否已分配?是的?很好。我现在就插入。”通过跳过测试以查看是否可以在那里编写一些东西,您可以节省一个非常昂贵的步骤。另一方面,(她戴着手套),我们现在生活在一个“优化是为那些买不起内存的人”的时代,所以关于速度的争论越来越弱。

    【讨论】:

      【解决方案4】:

      主要原因是向 C 或 C++ 添加边界检查的性能开销。虽然这种开销可以通过最先进的技术大幅减少(降低到 20-100% 的开销,具体取决于应用程序),但它仍然足以让许多人犹豫不决。我不确定这种反应是否合理——我有时怀疑人们过于关注绩效,仅仅是因为绩效是可量化和可衡量的——但无论如何,这是生活中的事实。这一事实降低了主要编译器将边界检查的最新工作集成到其编译器中的动力。

      第二个原因是担心边界检查可能会破坏您的应用。特别是如果您使用违反标准的指针算术和强制转换做一些时髦的事情,边界检查可能会阻止您的应用程序当前正在执行的操作。大型应用程序有时会做出令人吃惊的粗鲁和丑陋的事情。如果编译器破坏了应用程序,那么将问题归咎于粗糙的代码是没有意义的。人们不会继续使用会破坏其应用程序的编译器。

      另一个主要原因是边界检查与ASLR + DEP 竞争。 ASLR + DEP 被认为可以解决,哦,80% 左右的问题。这减少了对全面边界检查的感知需求。

      【讨论】:

        【解决方案5】:

        因为它会削弱那些 通用 语言的 HPC 要求。在许多应用程序中,缓冲区溢出实际上并不重要,仅仅是因为它们不会发生。这些功能在库中要好得多(实际上您已经可以在其中找到 C/C++ 的示例)。 对于特定领域的语言,将这些功能融入语言定义并以由此产生的性能损失换取更高的安全性可能是有意义的。

        【讨论】:

          猜你喜欢
          • 2014-03-09
          • 2021-11-13
          • 2011-03-30
          • 1970-01-01
          • 1970-01-01
          • 2022-10-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多