编程无需安全网

对于不熟悉嵌入式环境的工程师来说,嵌入式系统编程提出了特殊的挑战。


George V. Neville-Neil,Neville-Neil Consulting


对于不熟悉嵌入式环境的工程师来说,嵌入式系统编程提出了特殊的挑战。在某些方面,它更像是内核内部的工作,而不是在桌面上编写应用程序。以下是需要注意的事项。

如果您的程序在意外访问 NULL 指针时没有退出会怎么样?如果系统中的所有其他应用程序都看到了它们的所有全局变量会怎么样?您是否检查过您的程序使用了多少内存?与更传统的软件平台不同,嵌入式系统几乎不为程序员提供针对这些和许多其他类型问题的保护。这并非随意为之,只是为了增加工作的难度。传统的软件平台,那些支持进程模型的平台,在总系统复杂性、程序响应时间、内存需求和执行速度方面付出了巨大的代价。

安全性和响应性之间的权衡形成了一个连续统[见图 1]。一个极端是在裸机硬件上编程,没有任何支持操作系统或库。在这里,程序员可以完全控制系统的各个方面,但也必须为程序错误提供所有自己的运行时检查。

图 1

另一个极端是在 Unix(包括所有变体,如 BSD、OS/X、Linux、HP-UX、AIX 等)或 Windows XP 等系统上进行应用程序编程,在这些系统中,操作系统协调应用程序对 CPU、内存和辅助存储等资源的访问。在这样的系统上运行的应用程序被赋予了一种错觉,即它们是系统资源的唯一用户。之所以维持这种错觉,是因为操作系统为应用程序提供了一个称为进程的容器。任何提供这种类型容器的系统都是进程模型操作系统 (PMOS)。PMOS 依赖于内存保护来仲裁对物理内存的访问,以及一个虚拟内存 (VM) 系统,该系统为每个程序提供不间断地址空间的错觉。内存保护机制通常通过 VM 系统提供,但这两个概念应保持distinct。当然,有可能在没有虚拟内存的情况下提供内存保护,反之亦然,但只有在 PMOS 中,两者才结合在一起。

嵌入式系统占据了连续统的中间位置。大多数嵌入式系统不是在裸机上编写的。通常有一个嵌入式操作系统 (EOS),它提供诸如调度程序和一组库之类的服务,以帮助程序员构建他们的应用程序。

有两个趋势使这个讨论变得复杂。现在有几个“嵌入式” Linux 或 BSD 的例子。这些是其较大同类的精简版本,已针对嵌入式用途进行了调整。从另一个极端来看,传统的嵌入式系统已经添加了某种形式的内存保护(Wind River 的 VxWorks AE)。为了本文的讨论目的,我们关注的是传统的嵌入式系统,即那些来自频谱的低安全性端的系统,并且其血统可以追溯到实时操作系统。尽管在使用传统嵌入式操作系统时,Linux 或 BSD 进入嵌入式环境时也会出现一些困难,但这些操作系统为应用程序员保留了进程模型。

使用这些中间道路系统的程序员仍然应该关注传统的嵌入式编程是如何完成的,原因有两个:首先,了解嵌入式程序员如何思考解决问题将有助于深入了解您将在系统中遇到的嵌入式软件的设计。其次,即使您在进程模型上下文中工作,一旦设计转为嵌入式,您将不得不处理用户/内核边界之下的系统某些部分的可能性也会大大增加。

PMOS 安全性描述

尽管 PMOS 已为人所知和使用多年,但解释它们保护程序员的方式是有意义的。在 PMOS 中,应用程序不能直接触摸或修改物理内存。应用程序的所有内存访问都是针对虚拟地址,PMOS 的 VM 系统将其转换为物理地址。在虚拟到物理的转换过程中,会进行几项检查以捕获程序错误。例如,引用的地址必须是程序已知的有效地址,并且不得为 0 (NULL)。

进程容器有几个部分:程序指令(文本)、初始化数据、未初始化变量的零数据 (bss)、公共内存池(堆)和堆栈,每个部分各一个。这些部分中的每一个都设置了不同的保护,这也有助于捕获编程错误。例如,尝试直接修改其程序文本的程序将因错误而终止,因为该区域被标记为只读。全局变量保存在堆中,并且仅对应用程序本身是全局的;它们不会污染其他应用程序或操作系统内核的空间。

VM 系统在与辅助存储设备(如磁盘)结合使用时,会为程序员提供另一种错觉:即应用程序可以使用比机器中实际存在的内存更多的内存。每个应用程序都认为其内存从略大于 0 的地址扩展到 CPU 可以寻址的最后一个字节。

EOS 描述

嵌入式操作系统不支持进程模型。它们提供一个单一的地址空间,其中内存地址直接映射到 RAM 中的物理位置,并且操作系统几乎不采取任何措施来保护程序免受彼此或操作系统本身的干扰。在支持硬件内存管理的 CPU 上,一些 EOS 会保护程序的文本页免受修改,但这与 PMOS 提供的保护相去甚远。

EOS 来自速度与安全性连续统的低安全性、高响应性端。它们最初被设计为实时系统,可以在确定的时间内对外部刺激做出反应。为了实现确定的响应时间,常见操作不能需要可变的时间量。许多 VM 操作,例如虚拟到物理的转换或从磁盘分页内存,都是不确定的。尽管许多 EOS 现在在硬实时区域之外使用,但它们保留了最初的设计,其中确定性是最重要的目标。

支持进程模型的系统是分时系统的直接后裔。分时的目标是为庞大的用户社区提供服务,并尽可能公平地共享计算资源,同时还要确保用户不会互相妨碍。这些系统的设计者愿意付出与增加复杂性相关的成本,以确保高水平的安全性。

分时系统是通用平台,人类用户社区可以在其中运行任意应用程序。在这种类型的环境中,两个应用程序一起完成一项工作的情况很少见。尽管一个程序的输出可能成为另一个程序的输入,但这只是一种有限的合作形式。

与分时系统相比,嵌入式系统通常包含多个应用程序,这些应用程序都应该协同工作以完成一项工作(例如,网络路由器运行路由协议、管理应用程序并执行数据包转发)。这种合作期望加上对确定性响应的需求意味着任务间通信机制必须具有非常低的开销。让所有任务在单一地址空间中执行可以保持较低的通信开销。必要时,任务只需通过相互传递指针来共享大型数据区域。这是嵌入式系统编程中最常用的任务间通信形式。

在嵌入式系统上编程与多线程编程有很多相似之处。它共享了多线程编程的所有缺陷,并增加了一些缺陷。当线程在 PMOS 上运行的程序内部失败时,失败的是单个程序,而不是整个系统。

危险

为嵌入式系统编写代码所涉及的危险可以分为几个一般领域:指针错误、命名空间污染、不适当的共享、内存膨胀/内存需求、代码效率低下和任务调度。

指针错误。大多数嵌入式系统程序是用 C 语言编写的,只有极少数是用其他语言编写的,例如 C++ 和 Java。C 语言中常见的编程错误包括访问 NULL 指针引用和访问垃圾指针。

在许多嵌入式系统中,地址 0 对于读取和写入都是有效的。例如,设备将其内存保存在 RAM 的前几页中是很常见的,并且它们可能会将一些数据存储在地址 0。一些处理器将其中断跳转表保存在内存的第一页中,并将系统复位中断向量放在地址 0。这意味着覆盖 NULL 指针可能会导致看起来与系统中任何程序无关的错误。系统可能只是崩溃、执行硬复位,或者设备可能开始喷出垃圾数据。

使用垃圾指针(不是 0,但对于程序的任何数据也无效)可能会导致几种不同类型的错误。从错误的地址读取数据将为计算提供错误的答案。写入垃圾指针不会产生错误,但可能会覆盖系统中属于另一个任务的数据。在 EOS 不将包含程序指令的内存页标记为只读的情况下,写入垃圾指针可能会导致不相关的程序崩溃,因为其指令已损坏。检查崩溃程序的错误不会发现任何问题,因为它不是问题的原因;而是其他程序造成的。

命名空间污染。在单一地址空间中,所有代码和数据(包括 EOS 本身)共享相同的内存空间。这意味着所有全局数据和非静态函数对于系统中的所有其他代码都是可见的。变量或函数名称选择不当将导致命名空间冲突。对于函数来说,这不是什么大问题,因为链接器会注意到两个同名函数之间的冲突。在具有运行时动态链接的系统中,此错误更为严重,因为它会阻止代码加载到系统中。当在现场升级系统时,这是一个灾难性的错误,因为此时修复问题可能是不可能的。

更严重的问题是重复的全局变量。链接器或操作系统不会检测到这些变量。在单一地址空间中,对全局变量的额外引用只是额外的引用。重要的是仔细选择名称以避免冲突。特别大的麻烦来自为全局变量选择单字母名称。经验丰富的嵌入式系统程序员如果必须使用全局变量,会将它们放在由单个描述性全局变量指向的结构中。访问数据时的额外间接寻址是以不污染命名空间并可能在以后引起意外副作用为代价的小代价。

不适当的共享。任务之间快速通信的能力伴随着有效保护共享数据的需求。在嵌入式系统中共享信息的最常见方法是在一组协作任务之间传递内存块的地址。为了使之有效,共享内存的任务必须就它们访问共享内存的协议达成一致。

大多数 EOS 都提供基本的互斥锁(如信号量)作为系统的一部分。关键是正确使用所提供的工具。如果一个任务导致死锁,则所有其他任务,甚至可能是整个系统都无法取得进展,并且它们会失败。此时从错误中恢复可能需要系统复位,或者看门狗可能会定期检查某些子系统的健康状况并在必要时重新启动它们。显然,最好首先避免这种情况。

内存膨胀/内存需求。在具有虚拟内存的系统中,可以继续分配内存块,远远超出系统的物理内存。尽管这会导致系统性能下降,但很少会导致程序崩溃。在嵌入式系统上,一旦物理内存耗尽,所有进一步的内存分配尝试都将失败。人们很容易认为硬件的经济性已经缓解了这个问题,计算机的物理内存高达 4GB。在嵌入式世界中,更常见的数字是 16-32MB 的 RAM,尽管仍然很慷慨,但如果程序员不小心分配,很容易被用完。在没有辅助存储支持的虚拟内存的情况下,当物理内存耗尽时,EOS 无能为力来帮助应用程序。它所能做的只是告诉程序它想要的内存不可用。

在嵌入式系统上,内存膨胀是一个严重的问题。每次分配都必须仅在必要时存在,并且应尽快释放。防御性编程,以分配大量内存以防以后需要它们的形式,不是一个好的策略。此环境中的程序必须仅使用它们需要的内存,以便所有协作任务都可以有足够的空间来完成它们的工作。通常,内存池与特定的子系统相关联,以便可以控制问题,但这只能缓解问题,而不能消除问题。对于那些习惯于使用无限内存错觉进行编程的人来说,这种纪律起初可能很难。

代码效率。行业专家经常引用摩尔定律作为代码效率并非至关重要的证据。今天的处理器无法完成的事情将在明天轻松实现。嵌入式系统肯定像软件世界的其他部分一样受益于这种慷慨,但这并非全部。与较大的桌面同类产品不同,嵌入式设备通常受到物理尺寸、散热和功耗要求的限制。通常不可能通过提高处理器速度来解决问题。想象一下试图将奔腾处理器塞进手机中。这对嵌入式系统程序员来说意味着高效的代码仍然是首要任务。

任务调度。在 EOS 内部调度任务也对嵌入式程序员构成危险。在传统的操作系统平台上,程序员永远不必考虑其程序的调度。即使在使用标准线程包编写多线程程序时,调度方面的考虑也很少。这是因为传统的操作系统和线程包都使用轮询作为其默认调度策略。这种调度策略确保每个程序或线程都能轮流运行,只要它没有被某些外部事件(I/O、等待互斥锁等)阻塞。

EOS 默认使用抢占式优先级调度程序。系统中的每个任务都有一个分配给它的优先级,并且最高优先级的任务一直运行,直到它阻塞或完成。在这种类型的环境中,添加到系统的所有程序员都必须很好地理解每个任务的工作、其重要性、运行频率以及运行时间。

最著名的缺陷之一是优先级反转。当两个任务正在协作完成某项工作,但优先级较高的任务正在等待来自优先级较低的任务的信息时,就会发生这种情况。由于抢占式优先级调度的性质,如果优先级较高的任务在等待优先级较低的任务的数据时不放弃 CPU,则两者都无法取得任何进展,并且它们将保持死锁状态,直到人为干预为止。一些 EOS 提供了特殊的互斥锁,可以检测这种情况,并在优先级较低的任务完成其工作并且优先级较高的任务可以再次运行之前,暂时允许优先级较低的任务继承优先级较高的任务的优先级级别。尽管这可能是一个救命稻草,但它也是低效的,因为 EOS 每次发生这种情况都必须做额外的工作。最好第一次就正确调度任务。

代码集成

为嵌入式系统编写代码已经够难了,但是集成第三方代码呢?您第一次接触嵌入式系统更有可能涉及集成代码而不是编写全新的代码。代码集成有三种形式:集成旨在与您正在使用的 EOS 一起使用的第三方组件、从 PMOS 系统移植应用程序以及移植为 PMOS 内核编写的代码。

集成 EOS 的代码。当您在 PMOS 上执行程序时,它在一个容器中运行,并且不会干扰系统中运行的其他任何内容。安装程序可能会意外覆盖某些文件或损坏注册表项,但这些错误相对容易防范。向嵌入式系统添加新应用程序与将新的代码块链接到预先存在的程序中相同。所有新代码的符号都必须是唯一的,并且任何外部引用都必须在新代码变为活动状态之前解析。如果代码与 EOS 或其他应用程序共享任何信息,则它必须使用相同的保护协议——例如,它获取和释放锁的顺序——与系统中任何预先存在的代码相同。

直接将新代码链接到系统(无论是动态链接还是静态链接)的必要性要求对整个系统进行全面的重新测试和验证,以验证新代码是否没有引起不必要的副作用。

直接从 PMOS 移植代码。这种形式的代码集成通常被视为在 EOS 上启动并运行某些内容的快速方法,但这通常是一个错误的观念。为 PMOS 创建的软件是在假设它将在具有进程模型的系统上运行的情况下编写的。这些程序通常包含数十个(如果不是数百个)命名不佳的全局变量和函数,并且具有它们不希望暴露给其他人的接口。

必须做出的第一个决定是移植 PMOS 代码还是从头开始编写相同的东西会更费力。此决定最困难的部分不是你自己获得技术理解,而是说服经理移植不是一个有效的解决方案。代码存在并且可供使用这一事实足以说服大多数经理应该使用它。

在承诺将 PMOS 代码移植到 EOS 之前,您需要研究代码的几个参数。这些都与上一节关于为嵌入式系统编写代码中提出的问题有关,并且涉及以下内容:查找和隔离全局数据、防止命名空间污染、找出内存膨胀以及确保程序可以与其他预先存在的任务在系统中正确调度。

从 PMOS 内核移植代码。一个常见的例子是移植 BSD TCP/IP 堆栈以在嵌入式系统中使用。此代码可能看起来是独立的且易于移植,但事实并非如此。

大多数 PMOS 内核都是单片的,这意味着内核中的数据结构不必受到系统中其他部分的保护。当内核的不同部分需要共享数据时,它们使用简单的代码锁定技术来确保关键信息得到正确更新。内核代码还期望仅通过系统调用接口调用。在 EOS 中,移植的代码可以在任何任意点调用(即,任何未声明函数为 static 的地方)。集成代码更像是库,而不是操作系统内核的一部分。

单片内核在内部不支持线程。这意味着许多服务不是作为线程运行,而是由设备中断或定时器触发调度。在中断上下文中执行大量代码(如在 PMOS 内核中完成的那样)将破坏原始嵌入式系统中存在的任何确定性。如果所有处理都不在设备驱动程序的中断处理程序例程中完成,则必须创建一个任务来处理此工作。

结论

嵌入式系统编程对在传统 PMOS 上成长起来的软件工程师提出了许多挑战。其中一些挑战是嵌入式系统的历史和要求的产物,而另一些挑战则是在 PMOS 上编写应用程序的习得实践的产物。这里的目的是让您了解为嵌入式系统编写代码是什么样的,以便您可以通过此信息在嵌入式世界以及非嵌入式世界中工作。

最初发表于 Queue 第 1 卷,第 2 期

喜欢还是讨厌?请告诉我们

[email protected]

GEORGE V. NEVILLE-NEIL, [email protected], 为乐趣和利润从事网络和操作系统代码工作,并教授与编程相关的各种主题的课程。他的兴趣领域是代码探险、操作系统和网络。他获得了马萨诸塞州波士顿东北大学计算机科学学士学位。他是 、USENIX 协会和 IEEE 的成员。他是一位狂热的自行车手和旅行家,目前居住在纽约市。

© 2012 1542-7730/03/0200 $5.00

acmqueue

最初发表于 Queue 第 1 卷,第 2 期
数字图书馆 中评论本文





更多相关文章

George W. Fitzmaurice, Azam Khan, William Buxton, Gordon Kurtenbach, Ravin Balakrishnan - 通过多样化的设备社会实现情境数据访问
自从 ATM 和杂货店 UPC 收银台等“信息设备”问世以来,已经过去了十年以上。对于办公环境,Mark Weiser 于 1991 年开始阐明 UbiComp 的概念,并确定了该趋势的一些显着特征。嵌入式计算也变得越来越普遍。例如,微处理器发现自己嵌入到看似传统的钢笔中,这些钢笔可以记住它们写了什么。汽车中的防抱死制动系统由模糊逻辑控制。


Rolf Ernst - 整合一切
随着嵌入式系统的日益复杂,越来越多的系统部件被重用或供应,通常来自外部来源。这些部件的范围从单个硬件组件或软件进程到硬件-软件 (HW-SW) 子系统。它们必须与新开发的部件协作并共享资源,以满足所有设计约束。简单来说,这就是集成任务,理想情况下,它应该是一个即插即用的过程。然而,这在实践中并没有发生,不仅是因为不兼容的接口和通信标准,还因为专业化。


Homayoun Shahri - 硬件和软件之间界限的模糊
在技术导致芯片上可提供数百万个门电路的推动下,一种新的设计范例正在兴起。这种新范例允许在一个芯片上集成和实现整个系统。


Ivan Goddard - 嵌入式系统中的劳动分工
嵌入式应用程序越来越多地需要比单个处理器能够提供的更多的处理能力,即使是大量流水线化的处理器,它也使用了高性能架构,例如超长指令字 (VLIW) 或超标量。在嵌入式世界中,简单地提高时钟频率通常是令人望而却步的,因为更高的时钟频率需要成比例的更多功率,而功率在嵌入式系统中通常是稀缺的商品。多处理是一种自然途径,可以在固定的功率预算内获得更多的处理器周期,其中应用程序在两个或多个处理器上同时运行。





© 保留所有权利。

© . All rights reserved.