跳转到主要内容
Chinese, Simplified

category

为什么我们不用c++重写我们的物联网应用程序

Image for post

这是关于我们dwell如何在Rust中重写他们的物联网网关软件的系列文章的第2部分。这个系列从这里开始,下一章在这里。

我本来打算写为什么我们在dwell不选择c++来重写物联网,但后来我意识到还有一个更广泛的问题需要讨论。我想把这个决定放在历史上我们所有的错误代码的背景下,作为一个物种。在80年代和90年代,我们使用了普罗米修斯的天赋——C语言来为世界各地的每个嵌入式系统编写软件。我们所能完成的事情是没有限制的。但很多我们当时认为很聪明的做法,现在回想起来显然是糟糕的习惯。这些熟悉的错误在几十年后继续造成痛苦和破坏。我将提出我在C和c++的产品代码中看到的一些无可争辩的可怕的东西。我将讨论我所犯过和看到过的错误,在下一章中,我们将讨论简单的语言选择如何彻底地消除大量错误,同时仍然给我们足够的灵活性来编写低级软件。

我们本可以用c++重写我们的物联网平台应用程序。它检查了所有的箱子。这对我来说很熟悉,甚至很容易。但这也会让我或其他人很容易犯错误。使用C就像用蜡烛来照明。它的基本特性是众所周知的,它从人类文明开始就存在了,如果你滥用它,它会点燃你周围的房子。(在这个比喻中,c++是“所有可以被点燃产生光的事物的集合”。)

在过去的30年里,我读了很多糟糕的代码。我也写了不少。没有人是完美的程序员,我们应该选择能够反映这种谦逊的工具。让我们看看您是否熟悉这些陷阱。

神秘指针参数

void myfunc(char* c);
char c;
myfunc(&c);

myfunc的参数是输入吗?一个输出?都有?它实际上是一个以空结尾的数组吗?所有的代码路径都初始化传递的值吗?myfunc segfault如果我传递它为空?函数会保留我传入的指针并在以后使用它吗?查看函数定义并确定它将做什么是不可能的,因为它允许做很多事情。对于小程序来说,这有点烦人。对于拥有超过12个函数的程序,很快就不可能推断出程序的正确性,我们不得不依赖API文档。我们都知道,文档总是正确和最新的。

我敢肯定,你们当中的学究们都在对监视器大喊:如果指针参数只是作为输入,那么签名应该采用const指针。虽然这是正确的,但我从未使用过正确且一致声明参数为const的遗留代码库。(注意:我不确定这是因为古代的编译器不支持const关键字,还是因为在过去的几十年里,一些集体的关节炎让输入这些额外的字符变得很痛苦。无论如何,这在旧代码中很常见。)

Const正确性仍然没有解决房间里的另一个大象……

空指针

const char* foo = 0; /* I remembered to initialize my variable! */
printf(foo);

我将假设您已经读过关于十亿美元错误的文章,并且您已经在您的编程经验中看到过“Segmentation Fault”或“NullPointerException”。如果还没有,请点击上面的链接。我还会在这里。

回来了吗?太棒了。

我要在这里说:托尼·霍尔爵士不仅才华横溢,而且非常谦虚。很少有计算机科学家或软件工程师会直截了当地承认一个设计决策是错误的。不幸的是,这个起源于20世纪60年代的错误非常普遍,我相信许多人看到上面的代码时会想,“编译器会警告您,有什么大不了的?”

当你在8个不同的文件中通过10个不同的函数从A到B到C传递指针参数时,问题就来了,对于编译器(或审阅者)来说,你刚刚在函数上下文之间传递了空指针或未初始化的指针并不是很明显。

您没有在运行时检查每个函数条目是否有非零指针,我没有,标准库肯定也没有。我们不要自欺欺人了。

好吧,让我们使用c++引用

#include <string>bool isEqualToLast(const std::string& s) {
  static const char * last = "";
  bool foo = s.compare(last) == 0;
  last = s.c_str();
  return foo;
}

这段代码在GCC 8上使用-Wextra编译时没有警告,我们正确地使用了const,并且知道参数指向真实数据。只要每个参数都是静态分配的,或者是堆分配的,而不是提前释放,它就可以正常工作。无Bug,直到您使用堆栈变量调用它一次后调用为止。

隐式投射的缺陷(Implicit Cast Bugs)

char data[ENORMOUS_BUF_SZ];
for (int i = 0; i < sizeof(data); ++i) {
/* do stuff */
}

C语言中最具创新性的概念之一是类型系统。每个表达式都有一个类型,例如,如果在需要整数的地方尝试使用字符数组,就会得到一个编译错误。这通常可以防止您犯严重的错误,比如混淆函数参数的顺序。然而,编译器有时会通过静默地将8位类型转换为32位类型、有符号类型转换为无符号类型、稍微篡改类型直到它们对齐来“帮助”它们。需要遵循某些隐含的强制转换规则,即使这些规则可能与您的直觉不符。因为这是预期的行为,所以编译器在这样做时不需要警告您。

我们使用size_t已经有几十年了。不幸的是,现代代码中仍然充斥着应该使用size_t却使用int或unsigned int的循环,而且错误指定的函数实参类型比比皆是。它工作得很好,直到它不再工作。不要让我开始讲time_t的错误用法。

显式类型转换错误

const int data[512] = {0};
volatile uint32_t* WDT_REG = 0xFFFFFFE0;
/* ... */
byte_sending_function((char *)data, sizeof(data));
handle_watchdog((uint32_t *)WDT_REG);

byte_sending_function的转换应该是(const char *), handle_watchdog的签名需要接受一个volatile指针。

是的,我知道c++中的static_cast和reinterpret_cast。但是C风格的强制类型转换仍然存在于书本和课堂中,而且它们仍在将其写入新的c++代码中。在我所见过的所有编译器中,抛弃const或volatile都是完全合法的。

谁需要错误处理呢?

#include <stdio.h>
#include <stdlib.h>int main() {
   FILE * fp = fopen("file.txt", "w+");
   fprintf(fp, "This cannot possibly go wrong.\n");
   fclose(fp);
   
   return 0;
}

这个可爱的例子来自谷歌#1的“fopen example”(稍微重新格式化一下),它并没有费心去检查我们是否真的可以打开文件,编译器也不要求甚至不提醒我们这么做。对我来说工作,不是一个bug,赶快编译,因为我有更多的潜在段错误,我需要添加到这个代码库。砍砍。

(在查看这篇文章时,我意识到fprintf和fclose也可以返回负值来表示失败。我忘记了,因为即使是正确验证fopen句柄的代码,也经常不检查单个fprintf返回值。忽略这个检查不会出现segfault,但是代码也不会知道它是否不能正确地写入文件。)

Buffer overfl$%^&#\b0x9328A7F0Segmentation fault

本段执行了非法操作,必须终止。

如果您认为您看到的是错误的信息,请联系技术支持。

联盟类型(Union types)

union {
  int id;
  void * widget_ptr;
} widget;#ifdef LINUX
widget.id = 42;
#else
widget.widget_ptr = malloc(64);
#endif/* many lines later... */
/* I'm quite certain I stored a pointer in here */
free(widget.widget_ptr);

伙计们,我要把这个按钮和一个小牌子放在这里,上面写着“请勿触摸”。“只要没人滥用它,我们就没事。”

线程安全

我甚至不打算深入可重入函数、副作用、原子性、互斥和信号量、内存屏障等细节。C和c++是为单线程命令式编程而设计的——你给计算机一个按顺序执行的计算列表。如果你想聪明一点,在多线程应用程序中使用它,那么你就有责任熟练使用指针、别名和共享状态,避免在代码中的任何地方犯任何错误。如果它成功了,你将拥有世界上最快的商业应用。如果没有,会有人在你离开之前发现问题吗?据说,天才和疯狂是一枚硬币的两面;让我们来探讨一下这个界限。下面是您的最佳实践指南。瓦尔哈拉殿堂等待。

好吧,我懂了。但是我喜欢C语言,我们不能直接修复C/ c++吗?

大量的工作已经投入到使警告更智能、改进文字、记录最佳实践等方面。c++ 11/14/17中有一些非常有用的新特性:unique_ptr、基于范围的for循环和RAII,它们都可以帮助防止bug(如果你使用它们的话)。有像MISRA这样的标准组织和像CERT这样的安全组织来帮助你发现和修复关键的安全和安全问题,如果你的团队中的每个人都严格遵守这些建议的话。但K&R C的尖锐碎片仍在地板上乱扔,没有什么能阻止你或你旁边的人无视警戒线,绊倒在上面。

尽管在工具和过程上投入了大量的努力,C标准仍然有大量未定义的行为。总之,驱动世界上大多数软件的这两种编程语言都被微妙而又根本地破坏了,因为受过训练的、聪明的专业人员在生产过程中总是犯代价高昂的错误。我们共同努力掩盖这个问题,这证明了仅两位贝尔实验室工程师的惊人技能,他们的语言足够灵活,可以让我们尝试所有这些修复!但我们确实需要解决根本的结构性问题。而且,不幸的是,我们不能在不破坏向后兼容性的情况下修复C语言。

C和c++代码管理你的汽车的油门控制,安全气囊,和防抱死制动系统。它是关键和非关键客机的航空电子软件的基础。它悄无声息地在一堆你不加思索地使用的东西上运行嵌入式操作系统,你所期望的东西是如此简单,它们应该是默认的安全和稳定的。信用卡终端。电力电网系统。电梯。军事硬件。无线路由器。自动取款机。投票机。作为一个以编写嵌入式软件为生的不可靠的人,这让我感到害怕。

总的来说,我们之所以使用这些工具,是因为它们很熟悉,但经验表明,我们无法安全地使用它们。我喜欢C语言,我们非常感谢它的遗产。但我们也有责任用能够帮助我们克服自身缺点的语言来写作。问题不在于我们在道路上安装照明弹来照亮我们的房子,而煤气灯更合适,尽管周围确实有足够的煤气灯。问题是我们根本不应该使用火。我们现在有LED手电筒。是时候停止点蜡烛了。

 

原文:https://medium.com/dwelo-r-d/abusing-fire-for-light-a6e6774289fd

本文:http://jiagoushi.pro/node/1439

讨论:请加入知识星球【全栈和低代码开发】或者微信【it_training】或者QQ群【10777】

本文地址
Tags
 
Article
知识星球
 
微信公众号
 
视频号