资源受限分布式嵌入式系统的动态软件更新
1. 引言
如今,嵌入式系统中微控制器的使用数量不断增加。在更复杂的系统里,会采用多个不同微控制器组成的网络,从而形成大小节点混合的异构系统。特别是分布式嵌入式系统,一方面存在资源极度受限的设备,另一方面也有功能更强大的设备。
以传感器网络为例,它由大量小型、低成本且资源受限的节点构成,这些节点配备传感器来收集环境数据并具备通信能力。通常,这些传感器节点由一个更强大的节点——基站来支持和管理。
一旦系统初始设置完成,由于漏洞修复、功能改进、扩展以及参数更改等原因,选择性的软件更新就变得必要。例如,若校准过程未能得出理想的参数集,就需要进行软件修改,可能会替换采样或预处理算法,或者添加使用其他传感器的软件。而且,考虑到系统预计有几年的使用寿命,一个具有前瞻性的系统必须能够替换系统的任何部分,如调度系统或通信协议。
在更新节点软件时,我们希望尽可能减少影响。鉴于传感器节点资源有限,更新过程本身不能消耗过多资源。此外,不同的更新会影响系统的不同部分,应用程序的更新不应影响系统服务。例如,添加传感器驱动程序导致整个节点重启是不可接受的,因为节点重启会导致应用程序和操作系统库的状态信息丢失。像多跳通信协议会存储路由信息来表示网络拓扑,重启后这些信息丢失,需要重新构建,这不仅影响本地节点,还会影响其他节点的通信。
我们的目标是动态更新传感器节点,同时保留应用程序和系统状态。在更新过程中,节点会暂停,但更新完成后无需重启。这样做的优势是显著缩短离线时间,并由于更新后系统状态无需不必要地重建,从而实现快速恢复,提升性能。
我们描述了一种基于应用程序二进制代码对节点进行动态重新编程的更新基础设施。更新系统的大部分组件位于更强大的节点(如基站)上。我们利用编译器生成的信息,如符号表、重定位表和调试信息,来识别安全更新的可行情况。在极少数情况下,执行状态可能会阻碍动态更新,此时会向管理员发出信号,以便采取额外措施,如重新设计应用程序以提高可更新性。
2. 相关工作
动态更新软件有多种方法,下面讨论一些现有的传感器节点更新方法:
| 方法 | 特点 | 缺点 |
| ---- | ---- | ---- |
| TinyOS 的 XNP | 仅支持非常粗粒度的更新,通过替换整个内存映像 | 若仅调整一些参数,影响巨大,且更新后需重启节点 |
| Deluge | 使用 Trickle 作为多跳传播协议分发更新,可实现类似更新 | 更新后需重启节点 |
| FlexCup | 是 TinyCubus 的更新系统,组件以可重定位二进制映像传输,包含符号表和重定位信息,每个节点有链接器组件集成新组件 | 更新后需重启系统以消除悬空指针,且每个节点需要链接器组件 |
| Koshy 和 Pandey 的方法 | 修改开发工具链创建增量链接器,在基站准备更新代码,可控制修改函数的放置位置 | 不考虑系统当前状态,更新代码写入闪存后需重启 |
| SOS | 由模块构建的传感器节点操作系统,模块可在运行时更新、移除和替换,模块以可重定位二进制映像格式传输和安装 | 模块代码使用相对跳转实现位置无关性,限制模块最大大小为 4KB,对模块外函数或数据的引用需通过间接表或不允许 |
| Contiki | 提供动态加载库的框架,库包含重定位信息以支持代码安装,通过存根间接访问库 | |
我们的方法不需要每个节点上的链接器组件,因为这部分处理是远程完成的。并且我们不依赖间接表,而是直接识别和修改引用,这样甚至可以更新操作系统库或内核的代码。
3. 架构
为了在资源受限的节点上实现最小化的更新基础设施,大部分更新准备工作由一个远程节点执行,这个节点称为管理器,通常功能更强大,配备数兆字节的内存。管理器节点负责更新和管理资源受限节点上安装的软件。
当某些情况无法自动决定是否可以对运行系统进行更新时,会通知运行和监督系统的管理员。管理员可以做出决策,或者利用这些信息重新设计应用程序,以便管理器下次能自动处理。
管理器节点的架构包含以下重要元素:
-
仓库管理器
:负责管理和分析代码。代码以二进制目标文件形式提供。已安装并在传感器节点上运行的软件称为节点的初始映像。仓库管理器确定初始映像的编译和链接后的 ELF 对象文件中可用的函数(更准确地说是符号)。要更新或安装在节点上的软件必须以可重定位的 ELF 对象文件形式提供。ELFExtractor 加载这些二进制文件并分析每个输入文件的内容,以识别单个函数和数据对象,实现应用程序的细粒度模块化。之后,仓库管理器从重定位信息中提取已识别对象之间的依赖关系,并构建依赖图,以确定一个函数需要哪些数据以及调用了哪些其他函数。当管理员更改一个函数时,系统会识别依赖图中受影响的函数和数据,以及它们在网络中的安装位置,从而计算出每个节点的差异集。这些差异集是半自动化策略的基础,用于确定是否可以执行更新操作。若有冲突,管理员会介入控制更新操作。
-
映像管理器和内存管理器
:映像管理器创建并管理一个内存映像模型,该模型表示设备中内存的使用情况。首先,该内存模型用节点的初始状态进行初始化,然后用于跟踪当前安装的软件布局。基于此信息,作为交叉链接器一部分的内存管理器确定在节点上放置修改后代码的位置。例如,更新后的函数应尽量放在与原函数相同的位置,以减少需要更新的引用数量。虚拟映像包含当前安装在节点上的应用程序模块列表及其确切位置。映像管理器还会记录链接器为更新应用程序所做的更改,并在新函数链接到虚拟映像后计算更改集。该更改集包含每个更新函数的差异以及所有新函数的代码,删除的函数不在更改集中。系统会确保这些删除的函数不再被使用,内存管理器会重新利用其空间来存储新函数。这些更改可以以编辑命令的形式传输到节点,编辑命令可由常见的差异算法(如 UNIX diff 或 Xdelta)生成。
-
引导加载程序
:每个节点上都有一个小型引导加载程序,它能够接收并处理来自管理器的命令。其主要任务是安装新代码,同时也为管理器提供节点当前状态的重要信息,因为当前状态会影响运行系统的更新是否可行。当传感器节点收到编辑命令时,应用程序会将控制权转移给引导加载程序,引导加载程序会提取请求的数据或修改传感器节点的 SRAM 和闪存内存。代码块的实际传输可以通过多种协议实现,如直接单跳通信或更复杂的多跳协议(如 Trickle 或 MOAP)。
下面是管理器节点的职责和功能的 mermaid 流程图:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A(传感器节点):::process -->|初始状态| B(管理器):::process
B -->|安装| A
B -->|替换| A
B -->|更新| A
B -->|信息不足时通知| C(管理员):::process
C -->|决策或重新设计| B
4. 模块化和依赖分析
通过分析可重定位的 ELF 对象文件来识别函数和数据对象,分析仅基于 ELF 文件中提供的信息,开发人员无需使用特殊构造或注释来支持分析。
ELF 文件按节组织,有些节包含符号表和重定位表,其他节包含可重定位的二进制数据。为确定依赖关系,我们使用 ELF 对象文件提供的符号和重定位信息。对于每个重定位,会给出关联二进制节内的位置,在该位置应插入符号的最终地址。符号的最终地址在链接器确定关联节在目标地址空间中的位置后解析。
为提取函数,我们查看与
.text
节关联的符号,这些符号提供函数的名称、起始偏移量和代码大小,从而知道每个函数的起始位置和占用空间。为确定函数的依赖关系,我们利用重定位表,每个影响函数代码的重定位都代表一个依赖。
我们要求编译器将每个函数放在对象文件的单独节中,这样能确保每个函数可见,且每个依赖都由 ELF 文件中的重定位表示。否则,本地(静态)函数的调用可能在重定位表中不可见。对于程序的数据对象,虽然它们不会与函数放在同一节中,符号通常是可用的,但我们仍要求编译器生成单独的节,以便更轻松地识别每个数据对象。
5. 动态代码更新
在运行系统中替换或更新代码需要了解对旧代码的所有引用,并将其替换为对新代码的引用。如果函数移动到新地址,我们还需要调整跳转的目标地址。此外,当函数被线程使用时进行修改需要特别小心。下面讨论几个问题类别:
5.1 函数引用
首先要确定更新后的函数是否与旧函数起始地址相同。如果新代码大小与原函数相同或更小,它可以放入原空间并从相同地址开始,这是理想情况,因为无需更改任何引用。为了让更大的更新也能放入函数空间,内存管理器在初始放置函数时可以分配额外空间,但在资源受限环境中需谨慎使用,因为未被替换函数使用时,这些额外空间会被浪费。
如果修改后的函数更大且无法放入原空间,则必须安装在不同位置,此时需要识别并更新对该函数的所有引用。函数调用中对函数起始位置的引用可以通过符号和重定位信息检测。调用另一个函数的函数的重定位表中会有一个条目,指定必须插入目标函数地址的位置。
在使用新地址修补位置之前,我们需要确定地址的使用方式,它可能用于调用操作,也可能存储在变量中以便稍后进行间接调用。我们可以轻松地用新地址修补调用操作,但在其他情况下,无法准确确定地址何时以及由哪个函数使用。在最坏的情况下,函数地址存储在全局变量中供其他函数使用。虽然结合代码和数据流分析可能检测到这种情况,但我们认为这种高度依赖架构的操作成本不合理。
我们的方法是确定是否有足够信息正确找到并修补所有引用。因此,我们不能修补通过函数指针的调用,至少对于需要更新的函数禁止使用函数指针。不过,函数指针在已知代码(如中断向量表和操作系统调度器)中是允许的,会使用特定系统层来识别这些代码中的代码和数据引用。实际上,嵌入式应用程序中很少使用函数指针,而且函数指针对经验不足的程序员来说是额外的错误来源,其他嵌入式系统编程范例(如 Misra C 规则)也完全禁止使用函数指针。如果系统检测到更新函数的地址用于非调用操作,会向管理员指示不安全情况,由管理员决定是否继续更新。
5.2 活动函数
在实际更新函数之前,必须确保该函数当前未被使用,因为这种更新的后果是不可预测的。函数在更新过程中被中断,或者线程在暂停前正在执行该函数,都属于函数正在使用的情况。在一些事件驱动系统(如 TinyOS)中,由于没有线程概念,这种情况不会发生,更新过程会在没有其他任务活动时作为任务调度。而在其他提供线程概念的系统(如 Contiki 或 Nut/OS)中,管理器会询问引导加载程序系统退出加载程序后将在哪个地址恢复操作。如果系统支持多线程,引导加载程序会返回所有线程的当前位置。更新管理器会检查更新是否会影响这些地址,若有影响则指示更新问题。
一种简单而有效的解决方法是恢复系统并在稍后重试更新。例如,如果正在更新传感器读取函数,下次尝试更新时该函数可能未被使用。为避免定期检查,可以修改返回地址,使函数返回时调用引导加载程序,然后恢复正常操作并等待函数返回引导加载程序。如果经过超时或多次重试后函数仍在使用,会通知管理员更新失败,管理员可以授权更新后进行重置。
这种方法的成功取决于应用程序的模块化程度。使用类似 TinyOS 这样的框架开发的传感器网络应用程序通常设计得非常模块化。此外,我们会向管理员告知更新失败的原因,管理员可以利用这些信息改进应用程序的模块化,以便下次更新。
5.3 调用栈上的函数
除了线程直接使用的函数外,函数可能也在调用栈上。即要替换的函数调用了另一个函数,而这个被调用的函数当前处于活动状态。当被调用的函数返回时,它会使用栈上的返回地址在调用者中恢复操作。如果我们替换调用者的代码,这个返回地址可能会无效。
一般来说,我们不希望完全禁止更新调用栈上的函数,因为一些小的更改(如漏洞修复)可能会改变地址但不影响代码的功能。因此,如果更新不改变函数的结构,即新代码对相同子函数的调用和本地变量与旧代码相同,那么更新是允许的。例如,只要仍然调用读取函数
hello
并且不插入或删除本地变量,我们就可以修复校准算法
foo
中的漏洞。我们假设旧代码中第 n 次调用特定函数的返回地址 A 可以替换为新代码中的等效返回地址 A’。
为实现这一点,我们需要知道栈的布局和位置,利用这些信息可以遍历栈并检测栈上的每个函数,然后调整返回地址。获取栈信息的方法编码在引导加载程序中,若加载程序请求,管理器会传输旧返回地址到新返回地址的映射。
调试信息可以提供函数本地变量的信息,从而可以离线检查是否添加或删除了新的本地变量。如果函数结构被修改,可以多次重试更新。函数在调用栈中的位置越深,其未被使用的概率越小。如果重试不成功,会通知管理员更新失败,管理员可以授权重置,并利用这些信息开发应用程序的未来版本。
5.4 变量更新
如前面所述,不支持更新局部变量。在某些条件下,允许更新全局变量。如果一个函数引用新的全局变量,只需在节点上分配该变量。相反,如果更新后的函数不再使用某个变量,我们无法可靠地确定该变量是否被其他代码使用,因此在未检测到任何引用时,会询问管理员是否可以释放该变量。如果引入了同名但类型不同的变量,有几种情况。如果旧变量仍被某些代码使用,会生成警告,因为这种情况很可能导致不一致,管理员需要决定是否继续更新。如果新变量替换旧变量,需要转换当前状态,但在没有管理员提供更多信息的情况下无法实现,自动转换面临与 Java 中的对象(反)序列化类似的限制。
6. 结论和当前状态
我们的工作是为异构传感器网络提供强大而高效的软件管理支持的重要基石。它通过将新代码增量链接到现有应用程序并识别未使用的代码,实现了对资源受限节点上安装的软件进行管理,能够在运行系统中更新和替换代码。必要的配置信息存储在更大的节点上,该节点负责准备更新并检查在运行系统中是否可以进行更新。在关键情况下,管理员会介入控制和决定更新方式。
当前管理器软件的原型用 Java 实现,它可以加载和分析 x86、Hitachi H8 和 AVR CPU 的可重定位 ELF 对象文件。目前,该原型可以与单个节点进行通信和更新,未来将扩展以处理相同或相似节点的组。
资源受限分布式嵌入式系统的动态软件更新
7. 总结与优势回顾
我们所提出的动态软件更新方案,旨在解决资源受限分布式嵌入式系统中软件更新的难题,通过保留系统状态、减少重启次数,显著提升了系统的性能和稳定性。下面总结一下该方案的主要优势:
1.
状态保留
:更新过程中暂停节点但无需重启,避免了系统状态的丢失,如路由信息和传感器校准值,减少了重新构建这些信息所需的时间和能量。
2.
资源高效利用
:更新过程本身消耗资源少,适合资源受限的传感器节点。同时,内存管理器合理分配内存,确保修改后的代码能有效放置,减少引用更新的工作量。
3.
细粒度更新
:通过对 ELF 文件的分析,实现了应用程序的细粒度模块化,能够准确识别函数和数据对象及其依赖关系,支持对单个函数或部分代码的更新。
4.
灵活决策机制
:利用编译器生成的信息判断更新的可行性,在关键情况下让管理员介入,确保更新的安全性和可靠性。
8. 实际应用场景与案例分析
为了更好地理解该动态软件更新方案的实际应用效果,下面列举一些可能的应用场景和案例分析。
8.1 传感器网络监测系统
在一个大型的环境监测传感器网络中,分布着大量的传感器节点,用于收集温度、湿度、光照等数据。随着时间的推移,可能需要对传感器节点的软件进行更新,以修复漏洞、优化算法或添加新的功能。
例如,最初的传感器校准算法存在一些小问题,导致数据精度不够。通过我们的动态软件更新方案,可以在不重启节点的情况下,对校准算法所在的函数进行更新。由于更新过程中保留了系统状态,如路由信息和传感器的当前配置,更新完成后节点能迅速恢复正常工作,继续准确地收集和传输数据。
8.2 工业自动化控制系统
在工业自动化生产线上,嵌入式系统用于控制各种设备的运行。这些系统对稳定性和实时性要求极高,软件更新时不能影响生产过程。
假设某个控制模块的软件需要更新以提高设备的运行效率。利用我们的方案,可以在设备运行过程中动态更新软件,暂停节点的时间极短,不会对生产线的正常运行造成明显影响。同时,由于更新过程中考虑了调用栈和活动函数的情况,避免了因更新导致的系统崩溃或错误。
9. 未来发展方向与挑战
虽然我们的动态软件更新方案已经取得了一定的成果,但仍面临一些挑战和未来发展方向。
9.1 支持更多硬件平台
目前的原型主要支持 x86、Hitachi H8 和 AVR CPU,未来需要扩展到更多的硬件平台,以满足不同嵌入式系统的需求。这需要对不同平台的 ELF 文件格式和内存管理机制进行深入研究和适配。
9.2 优化更新算法
随着嵌入式系统的复杂性不断增加,更新过程中可能会遇到更多复杂的情况,如函数指针的使用、多线程并发等。需要进一步优化更新算法,提高更新的成功率和效率,同时减少对管理员的依赖。
9.3 增强安全性
在动态更新过程中,安全性是一个重要的问题。需要确保更新的代码来源可靠,避免恶意软件的注入。可以采用数字签名、加密传输等技术来增强更新过程的安全性。
下面是未来发展方向的 mermaid 流程图:
graph LR
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
A(支持更多硬件平台):::process --> B(优化更新算法):::process
B --> C(增强安全性):::process
10. 操作步骤总结
为了方便读者理解和应用该动态软件更新方案,下面总结一下主要的操作步骤:
-
准备阶段
- 管理员提供要更新或安装的软件,以可重定位的 ELF 对象文件形式。
- 仓库管理器加载并分析这些 ELF 文件,确定函数和数据对象及其依赖关系,构建依赖图。
- 映像管理器初始化内存映像模型,记录节点的初始状态。
-
更新分析阶段
- 当管理员更改函数时,系统计算每个节点的差异集,基于半自动化策略判断更新的可行性。
- 若遇到复杂情况,如函数指针使用、活动函数或调用栈上的函数更新问题,系统会通知管理员。
-
更新执行阶段
- 内存管理器确定修改后代码在节点上的放置位置,尽量保持与原函数相同的地址。
- 映像管理器计算更改集,包含更新函数的差异和新函数的代码。
- 更改集以编辑命令的形式通过合适的协议(如 Trickle 或 MOAP)传输到节点。
- 节点上的引导加载程序接收编辑命令,暂停节点,修改 SRAM 和闪存内存。
-
恢复阶段
- 更新完成后,节点恢复正常运行,无需重启。
- 若更新失败,管理员可以根据系统提供的信息进行决策,如授权重置或改进应用程序的模块化。
11. 总结
资源受限分布式嵌入式系统的动态软件更新是一个具有挑战性但又非常有意义的研究领域。我们的方案通过合理的架构设计、细粒度的模块化分析和灵活的更新机制,实现了在运行系统中安全、高效地更新和替换代码,同时保留了系统状态。未来,我们将继续努力,克服面临的挑战,不断完善和扩展该方案,为嵌入式系统的软件管理提供更强大的支持。
希望本文能为从事嵌入式系统开发和管理的读者提供有价值的参考,帮助大家更好地应对软件更新的问题。如果你有任何疑问或建议,欢迎在评论区留言交流。
超级会员免费看
678

被折叠的 条评论
为什么被折叠?



