使C不受缓冲区溢出和其他错误影响的选项



是否有任何东西使C可以证明不受缓冲区溢出的影响(EDIT:和其他由于C通常未经检查编译而产生的错误,即可能是某种边界检查)?而且兼容性是否足以用于大型生产代码(EDIT:用于所有内容)?

我尝试了带挡泥板的gcc,它让它运行起来没有错误。

#include <stdio.h>
int main()
{
    int a[2];
    a[-1] = 5;
    printf("%un", a[-1]);
    return 0;
}

所以,就像我尝试过的其他方法一样,挡泥板似乎是不完整的,只会降低利用的可能性。此外,它似乎旨在调试而非生产使用。我想知道可证明的不可开发性。这是可以做到的。你有没有想过为什么它没有在任何地方被普遍使用?一个小的性能打击(甚至慢了10倍,但可能慢了2倍)似乎是一个小小的代价,可以弥补这类漏洞所造成的数十亿美元甚至数万亿美元的损失。

编辑:澄清:

所谓"缓冲区溢出",我指的不是拥有允许溢出的代码的程序员,而是允许目标变量/数组/(m)分配块之外的内存由其写入(或读取)的编译器(例如:int a,b;*(&b-1)应该被编译器捕获,而不仅仅是a)。

所谓"证明",我的意思是通俗地说,就像在"旧的简单Pascal不允许缓冲区溢出,几乎100%的确定性,我们可以说它是被证明的",尽管它可能使用不安全的系统函数,但如果它们也是在边界检查Pascal中编写的,那么它们也不会有溢出。我用"证明"这个词来区别于各种不完美的硬化工具。

我所说的"可利用性"指的是"缓冲区溢出可利用率",这是一个在其他语言中以牺牲速度和内存为代价解决的简单问题。

"你是认真的吗?如果它存在,我们早就这么做了。"——这就是我好奇的地方。技术就在这里——胖指针(C标准允许编译器生成任何大小的指针),并对每个指针进行完整的边界检查。但是,当我想要的是一个完整的C编译器来做这件事,以及用它构建整个Linux发行版时,我再也找不到关于它的概念证明、讨论和论文了。没有人会很快用更安全的语言重写所有东西(Linux、Apache等)(遗憾的是,他们一直在用C编写新东西),但我们可以让C/C++更安全,并重新编译所有东西。至少对于那些需要安全性高于一切的用途。

这个问题有各种各样的解决方案,而C语言在很大程度上是独立的。他们所做的大多是以一定的运行时成本跟踪"危险"的指针访问(静态分析无法证明是安全的)。

参见

  • Cyclone中基于区域的内存管理定义了一种类似C的语言。(研究)
  • 一种高效且向后兼容的转换,以确保声称在全C上运行的C程序的内存安全(研究)
  • 没有运行时检查或垃圾收集的内存安全适用于C的子集(研究)
  • SoftBound:C的高度兼容和完全的空间内存安全性,声称开销为67%(研究,但LLVM的alpha版本可用)
  • CheckPointer-一个C内存访问验证器,用于诊断堆、堆栈和线程中的内存访问错误,适用于完整的C。可从我的公司获得,请参阅个人简介。[诊断你的玩具程序不会有问题]

当然,你可能会争辩说,所有这些都是编译C"checked"的解决方案,而你在问题中似乎对此表示反对。在最坏的情况下,我认为这只是构建过程中的又一步。

真正的问题是,这些解决方案在时间和空间上都有可测量的开销。在构建嵌入式系统时,额外的成本显示为在分配的时间内获得更昂贵的处理器以完成工作所花费的实际美元,和/或用于跟踪麻烦指针的额外内存。大多数制造商在选择坏程序的低概率(或者更黑的是,"在我卖掉所有这些之前,没有人会注意到!")和绝对真实的额外成本时,往往会优化成本,现在你可以在没有运行时检查的情况下编译原始C程序了。"便宜"会损害"质量"或"进度"。我们在飞机座椅的舒适性和软件安全性方面都看到了这一点。

当然,只要你愿意放弃一些东西:

  • 指针数学
  • 未经检查的第三方图书馆(有点破坏了这一点,不是吗?)
  • 以null结尾的C字符串

您只需要滚动自己的分配器和解除分配器,将每个分配的大小写入标头,然后使用指针/数组查找宏来检查该分配大小:

void *myalloc(size_t size) {
    if (size == 0) return NULL;
    void *data = malloc(size + sizeof(size_t));
    if (data == NULL) return NULL;
    *((size_t *)data) = size;
    return data + sizeof(size_t);
}
void mydealloc(void *data) {
    free(data - sizeof(size_t));
}
#define item(data, index) ({ 
    __typeof__(data) item_data = data; 
    __typeof__(index) item_index = index; 
    assert(item_index >= 0 && (item_index + 1) * sizeof(*item_data) <= *((size_t *)((void *)item_data - sizeof(size_t)))); 
    *(item_data + item_index); 
})

任何字符串函数都需要使用写入标头的size_t值,而不是查找空字节。无论如何,您都应该这样做,因为null终止符在Unicode世界中毫无意义。也可以在同一个item()宏中检查堆栈分配,但我认为这必须取决于平台和实现。

你之所以不经常看到这种情况,是因为如果你愿意接受显著的性能打击,使用一种更高级的语言会更有意义,这种语言内置了这些保证,以及大量其他漂亮的语言功能。

我们无法修复C,因此处理此类已知危害是专业程序员的责任。C的优点是它一直存在,所以所有的弱点、陷阱和定义不清的行为都是众所周知的,并有文档记录。

你能做些什么来防止这样的错误:

  • 将编译器设置为尽可能挑剔。如果GCC针对这个特定的错误,它似乎没有帮助。。。据说有一些选项被称为-Warray边界和-fbounds检查,但这些选项似乎不起作用,至少在GCC 4.9.1/Mingw64中不起作用。无论如何,您可以通过始终使用std=cxx -pedantic-errors -Wall -Wextra进行编译来捕获许多其他常见的错误
  • 使用静态分析工具。这些程序至少应该能够找到所有与边界检查相关的编译时错误
  • 外部工具的一个更便宜的替代方案是使用防御性编程。在这种情况下,您可能已经用断言捕获了错误。例如:

    #include <stdio.h>
    #define ARR_SIZE 2
    #define INDEX -1
    int main()
    {
        _Static_assert(INDEX < ARR_SIZE, "Array index too large.");
        _Static_assert(INDEX >= 0,       "Array index too small.");
        int a[ARR_SIZE];
        a[INDEX] = 5;
        printf("%un", a[-1]);
        return 0;
    }
    

    您甚至可以在项目的调试构建中使用运行时assert()来捕捉类似的运行时错误。这也节省了程序开发过程中的大量时间,因为您可以更快地发现错误。

  • 提高代码质量的一般措施:代码审查、编码标准等

最新更新