如何在托管代码环境之外安全编程?

时间:2009-10-07 05:35:51

标签: c++ c pointers memory-leaks buffer-overrun

如果您是使用C或C ++编程的人,没有内存管理的托管语言优势,类型检查或缓冲区溢出保护,使用指针算法,您如何确保您的程序是安全的?您是否使用了大量的单元测试,或者您只是一个谨慎的编码器?你有其他方法吗?

9 个答案:

答案 0 :(得分:24)

以上所有。我用:

  1. 非常谨慎
  2. 尽可能智能指针
  3. 已经过测试的数据结构,很多standard library
  4. 始终进行单元测试
  5. 内存验证工具,如MemValidator和AppVerifier
  6. 每天晚上祈祷它不会在客户现场崩溃。
  7. 实际上,我只是夸大其词。它不是太糟糕,如果你正确地构建代码,实际上并不太难控制资源。

    有趣的说明。我有一个大型应用程序,它使用DCOM并具有托管和非托管模块。非托管模块通常在开发期间更难调试,但由于在其上运行了许多测试,因此在客户站点上执行得非常好。托管模块有时会遇到错误的代码,因为垃圾收集器非常灵活,程序员在检查资源使用时会变得懒惰。

答案 1 :(得分:16)

我使用了大量的断言,并构建了“调试”版本和“发布”版本。我的调试版本运行速度远远低于我的发布版本,并且所有检查都是如此。

我经常在Valgrind下运行,我的代码没有内存泄漏。零。保持程序无泄漏比采取错误程序并修复所有泄漏要容易得多。

此外,我的代码编译没有任何警告,尽管事实上我有编译器设置额外的警告。有时警告是愚蠢的,但有时它们指向一个错误,我修复它而不需要在调试器中找到它。

我正在编写纯C(我不能在这个项目中使用C ++),但我以非常一致的方式做C语言。我有面向对象的类,有构造函数和析构函数;我必须亲自调用它们,但一致性有帮助。如果我忘了打电话给一个析构函数,Valgrind就会打我的头,直到我解决它。

除了构造函数和析构函数之外,我还编写了一个自检函数来查看对象并确定它是否合理;例如,如果文件句柄为空但关联的文件数据未归零,则表示存在某种错误(句柄被破坏,或文件未打开,但对象中的那些字段中有垃圾)。此外,我的大多数对象都有一个“签名”字段,必须设置为特定值(特定于每个不同的对象)。使用对象的函数通常断言对象是理智的。

每当我malloc()一些内存时,我的函数就用0xDC值填充内存。未完全初始化的结构变得显而易见:计数太大,指针无效(0xDCDCDCDC),当我查看调试器中的结构时,很明显它是未初始化的。调用malloc()时,这比零填充内存要好得多。 (当然0xDC填充仅在调试版本中;不需要发布版本来浪费那段时间。)

任何时候我释放内存,我擦除指针。这样,如果我有一个愚蠢的错误,代码试图在其内存被释放后使用指针,我立即得到一个空指针异常,这指向我正确的错误。我的析构函数不接受指向对象的指针,它们接受指针指针,并在破坏对象后破坏指针。此外,析构函数在释放它们之前擦除它们的对象,因此如果某些代码块具有指针的副本并尝试使用对象,则会立即触发完整性检查断言。

Valgrind会告诉我是否有任何代码写下缓冲区的末尾。如果我没有那个,我会在缓冲区结束后放置“canary”值,并进行完整性检查测试它们。这些金丝雀值,如签名值,将仅调试版本,因此发布版本不会有内存膨胀。

我有一系列单元测试,当我对代码进行任何重大更改时,运行单元测试非常令人欣慰,并且有一些信心我没有可怕的破坏。当然,我在调试版本和发布版本上运行单元测试,所以我的所有断言都有机会发现问题。

将所有这些结构放置到位需要付出额外的努力,但它每天都会得到回报。当一个断言触发并指出我正确的错误时,我感到非常高兴,而不是必须在调试器中运行该错误。从长远来看,一直保持清洁的工作要少一些。

最后,我不得不说我其实喜欢匈牙利符号。几年前我在微软工作,和乔尔一样,我学习了匈牙利应用程序而不是破碎的变种。它确实make wrong code look wrong

答案 2 :(得分:13)

同样相关 - 如何确保你的文件和套接字关闭,你的锁被释放,yada yada。内存不是唯一的资源,使用GC,你本身就会失去可靠/及时的破坏。

GC和非GC都不会自动优越。每个都有好处,每个都有它的价格,一个优秀的程序员应该能够应付这两个。

我在对this question的回答中说了很多。

答案 3 :(得分:3)

我已经使用C ++ 10年了。我使用过C,Perl,Lisp,Delphi,Visual Basic 6,C#,Java和其他各种语言,这些都是我无法忘记的。

你的问题的答案很简单:你必须知道你在做什么,而不是C#/ Java。 以上就像Jeff Atwood关于"Java Schools"那样产生了这样的咆哮。

从某种意义上说,你的大部分问题都是荒谬的。你提出的“问题”只是硬件真正起作用的事实。我想挑战你写一个CPU& VHDL / Verilog中的RAM,看看它们是如何工作的,即使真的简化了。您将开始意识到C#/ Java方式是一种关于硬件的抽象论文。

更容易的挑战是从初始上电开始为嵌入式系统编程基本操作系统;它会告诉你你需要知道什么。

(我也写过C#和Java)

答案 4 :(得分:3)

我们用C语言编写嵌入式系统。除了使用任何编程语言或环境常用的一些技术外,我们还采用:

  • 静态分析工具(例如PC-Lint)。
  • 符合MISRA-C(由静态分析工具强制执行)。
  • 根本没有动态内存分配。

答案 5 :(得分:2)

安德鲁的答案很好,但我也会在列表中加入纪律。我发现在经过足够的C ++练习之后,您会对安全性和begging for the velociraptors to come eat you.感觉良好。您倾向于开发一种编码风格,在遵循安全实践时感觉舒适并让您感觉到heebie-jeebies应该是尝试将智能指针强制转换回原始指针并将其传递给其他东西。

我喜欢把它想象成商店里的电动工具。一旦你学会正确使用它,只要你确保始终遵守所有安全规则,它就足够安全了。当你认为你可以放弃你受伤的安全护目镜时。

答案 6 :(得分:1)

我已经完成了C ++和C#,但我没有看到关于托管代码的所有宣传。

哦,对,内存有一个垃圾收集器,这很有用......除非你当然不使用C ++中的普通旧指针,如果你只使用smart_pointers,那么你没有那么多问题。

但后来我想知道......你的垃圾收集器是否可以保护你免受:

  • 保持数据库连接打开?
  • 保持锁定文件?
  • ...

资源管理比内存管理要多得多。好的是C ++是你快速学习资源管理和RAII的含义,以便它成为一种反射:

  • 如果我想要一个指针,我想要一个auto_ptr,一个shared_ptr或一个weak_ptr
  • 如果我想要数据库连接,我想要一个对象'Connection'
  • 如果我打开一个文件,我想要一个对象'文件'
  • ...

至于缓冲区溢出,好吧,它不像我们在任何地方使用char *和size_t。我们确实有一些东西称为'字符串','iostream',当然还有已经提到的vector :: at方法可以让我们摆脱这些限制。

经过测试的库(stl,boost)很好,使用它们可以解决更多功能性问题。

答案 7 :(得分:1)

除了这里给出的很多好的提示,我最重要的工具是DRY - 不要重复自己。我不会在我的代码库中传播容易出错的代码(例如,使用malloc()和free()来处理内存分配)。我的代码中只有一个位置,其中调用了malloc和free。它位于包装器函数MemoryAlloc和MemoryFree中。

所有的参数检查和初始错误处理通常都是围绕对malloc的调用重复的样板代码给出的。此外,它支持任何需要仅修改一个位置的内容,从简单的调试检查开始,例如计算成功调用malloc和free,并在程序终止时验证两个数字是否相等,直到各种扩展安全检查。

有时候,当我在这里读到一个问题时,“我总是要确保strncpy终止字符串,是否有另一种选择?”

strncpy(dst, src, n);
dst[n-1] = '\0';

接下来几天的讨论,我总是想知道将重复功能提取到函数中的艺术是否是在编程讲座中不再教授的高级编程艺术。

char *my_strncpy (dst, src, n)
{
    assert((dst != NULL) && (src != NULL) && (n > 0));
    strncpy(dst, src, n);
    dst[n-1] = '\0';
    return dst;
}

解决了代码重复的主要问题 - 现在让我们考虑一下strncpy是否适合这项工作。性能?过早优化!在它被证明是瓶颈后,一个单一的位置开始。

答案 8 :(得分:0)

C ++具有您提到的所有功能。

有内存管理。 您可以使用智能指针进行非常精确的控制。或者有几个垃圾收集器可用,虽然它们不是标准的一部分(但大多数情况下智能指针绰绰有余)。

C ++是一种强类型语言。就像C#。

我们正在使用缓冲区。您可以选择使用界面检查版本的界面。 但是,如果您知道没有问题,那么您可以自由使用未经检查的界面版本。

比较()(已检查)的方法与operator [](未选中)。

是的,我们使用单元测试。 就像你应该在C#中使用一样。

是的,我们是谨慎的程序员。 就像你应该在C#中一样。唯一的区别是两种语言的陷阱不同。