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

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

上述所有的。 我用:

  1. 很多谨慎
  2. 智能指针尽可能多
  3. 已经过测试的数据结构,很多标准库
  4. 一直进行unit testing
  5. 内存validation工具,如MemValidator和AppVerifier
  6. 每天晚上祈祷它不会在客户现场崩溃。

实际上,我只是夸大其词。 它不是太糟糕,如果你正确地构建你的代码,它实际上不太难控制资源。

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

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

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

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

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

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

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

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

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

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

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

最后,我不得不说我实际上喜欢匈牙利符号。 几年前我在微软工作,和Joel一样,我学习了匈牙利应用程序,而不是破碎的变体。 它确实使错误的代码看起来错了 。

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

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

我在回答这个问题时也说了很多。

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

你的问题的答案很简单: 你必须知道你在做什么 ,而不是C#/ Java。 产生杰夫阿特伍德关于“Java学校”的咆哮的原因不止于此。

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

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

(我也写过C#和Java)

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

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

安德鲁的答案很好,但我也会在列表中加入纪律。 我发现经过足够的C ++练习之后,你会对安全性有什么好感觉,还有什么乞求快速消费者来吃你。 你倾向于开发一种在遵循安全实践时感觉舒适的编码风格,并且如果你试图将智能指针强制转换回原始指针并将其传递给其他东西,那么你会感觉到这种风格。

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

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

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

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

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

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

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

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

经测试的库(stl,boost)很好,使用它们并继续解决更多function问题。

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

所有参数检查和初始error handling通常作为围绕调用malloc的重复样板代码给出。 此外,它只需要修改一个位置,从简单的调试检查开始,例如计算成功调用malloc和free,并在程序终止时validation两个数字是否相等,直到各种扩展安全检查。

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

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

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

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

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

C ++具有您提到的所有function。

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

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

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

将()(已检查)的方法与运算符[](未选中)进行比较。

是的,我们使用unit testing。 就像你应该在C#中使用一样。

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