嵌入式系统基于动态命令的软件局部更新技术
2023-02-17王宜怀许粲昊
刘 强 王宜怀 许粲昊
(苏州大学计算机科学与技术学院 江苏 苏州 215000)
0 引 言
嵌入式系统的开发与应用已经不再满足于局限在微控制器(Microcontroller Unit,MCU)中的固定功能和固定运行流程,而寄希望于通过串口等通信方式临时调整MCU的功能与流程。一种普遍的解决方案是在MCU的中断处理程序中,根据串口接收的不同命令字,转向多个不同功能的程序分支。但这些程序分支本质上仍然是预先驻留在微控制器中无法改动的功能,而并非新功能。随着需求不断增多,为了利用有限的硬件资源实现更多的内容,软件开放性设计的需求也越来越明显[1]。并且每当有新的功能需求时,除了要对软件进行扩充,还必须要对软件进行整体更新,特别是在远程更新时,由于通信的不稳定,往往会要求更多的备份存储空间和更高的传输带宽,Steger等[2]的工作中强调了更新时的指定区域和指定网络就是为了在高数据量的情况下保证稳定性。此外传统的“停机—更新软件版本—重启”的应用软件更新方法势必会影响系统的可用性[3]。现有的嵌入式设备的远程更新模式仍然是以“BootLoader+应用程序”为主,并由BootLoader对应用程序进行复位和整体更新[4]。应用程序在运行时的更新仍然是软件工程中一个具有挑战性的问题[5]。
王婷等[1]通过采用“APP1(带BootLoader)+APP2”的方式,提出了一种软件重构技术,实现了在不影响APP1正常运行的情况下更新APP2。但这本质上并不是软件的局部更新,APP1与APP2都有各自的中断向量表,其本质还是整体更新,两者的共性并没有得到充分利用,且与Boyer等[6]的工作一样,在更新后系统仍然需要重启,并没有很好地解决问题。谢国珍等[7]和Hayden等[8]都提出了一种面向C语言的动态更新技术,但谢国珍等把更多的精力放在了整体程序版本的一致性上,并且比单个函数更小的更新单元也是不必要的;而Hayden等同样为了面向具有大量状态的系统做了相当多的措施,不适用于嵌入式系统。Chen等[9]提出了一种较为实用的动态软件更新框架,但框架的诸多技术因素更多面向大型计算机考虑,针对堆栈、函数活动级别等因素的鲁棒性处理在嵌入式场景下可能反而会使得整体的框架占据更多的空间。总结发现,目前对于软件工程动态更新的研究更多是致力于软件整体的重构和模组化,如Mahdi等[10]和Varghese等[11]的研究,而忽略了嵌入式资源受限的场景下对单一功能添加的需求。
因此,针对嵌入式系统的特殊性,同时受到Tao等[12]提出的动态配置智能算法DC-IA的启发,本文提出一种基于动态命令的软件局部更新技术。动态命令(Dynamic Command,DC)的提出和使用,可以实现在不影响MCU正常运行的前提下,将临时的一段代码动态地注入MCU并运行,这一段临时的具有一定功能的代码就称为动态命令,多个动态命令同时使用同一块存储空间。这样使得MCU从自身固有代码的功能中解放出来,可以运行更加丰富的动态命令,并且由于动态命令的数量基本不受存储空间限制,且代码注入技术占用的内存小[13],空间资源的问题也得到了解决。这样,当嵌入式系统中需要增加一个新的功能,或是现有的软件需要做部分的调整,都可以通过动态命令实现软件的局部更新,数据传输时的存储空间和带宽问题也随之解决。并且,本文提出的软件局部更新技术可以更新系统中固有的软件,极大程度上保证了系统长期的自我维护能力。
动态命令的实现主要存在三个要解决的问题。(1) 动态命令的正确性问题,动态命令的执行不能够使MCU出现致命错误而影响MCU固有功能,即动态命令的正确性如何保证;(2) 动态命令获取的问题,动态命令如何产生并被正确获取;(3) 动态命令执行的问题,获取到的动态命令如何、何时被MCU执行。
1 动态命令技术的基本思想
MCU所执行的功能取决于其自身存储空间内所存放的机器码,这些机器码是高级语言经由编译器生成,再经过写入工具写入至MCU中的。动态命令技术的实质就是MCU可以通过串口等通信方式接收非自身最初所存储的机器码并执行。为了实现这一目的,需要对MCU的系统工程Porigin做一定程度的改造。
首先,为了存储和运行动态命令,在MCU内需要预设一个空函数Fdc(可以包含若干个子函数,以下统称为Fdc),并开辟一块新的FLASH空间Sdc用于存储这个函数。Fdc是后续动态命令的载体,Sdc为动态命令的存储空间,此外还需预留调用Fdc和维护Sdc的相关代码,这样,得到新的MCU系统工程Pnew,将其编译后的机器码Hnew写入MCU并开始运行。其次,为了获取动态命令,需要利用MCU系统工程Pnew的副本Pcopy,在Pcopy的Fdc中编写期望MCU临时执行的功能,编译工程Pcopy后得到工程的机器码Hcopy,从中筛选出Fdc对应的机器码Hdc,Hdc即为要获取的动态命令。最后,为了向被写入了Hnew的MCU发送动态命令(即Hdc),以串行通信这一通信方式为例,借助编写好的PC机软件,向MCU的串口发送Hdc,并在希望执行这一动态命令时,发送相关的调用Fdc的命令字(已在Pnew中预留)。
这样,即便MCU最初并未存储Hdc这段机器码,也可以在这样的机制下获取Hdc并运行,如图1所示。
图1 动态命令体系
2 技术细节
接下来从正确性、获取和执行三个方面阐述动态命令的技术细节。
2.1 动态命令的正确性
为了减少动态命令Hdc的传输和存储成本,也为了动态命令函数代码Fdc的编写更加方便,Fdc允许使用工程Pcopy内所有原有构件的对外函数、全局变量和宏定义,但由此也带来了动态命令的正确性问题。动态命令的正确性问题主要有两个方面。
1) 调用的正确性。调用的正确性是指动态命令函数代码Fdc调用工程Pcopy和Fdc自身定义的内容时,Fdc经由编译器转化为Hdc注入工程Pnew后仍然能够正确的调用Pnew中对应于Pcopy的内容和Fdc自身定义的内容。
函数和变量的调用主要有绝对地址调用和相对地址调用两种方式。绝对地址调用的方式对调用者的地址没有要求,因此不存在正确性问题。Fdc对自身定义的内容采用相对调用,而相对地址不因Fdc的存储地址而改变,因而Fdc对自身定义的内容的相对调用不存在正确性问题。为了满足Fdc中涉及相对地址的调用在Pnew和Pcopy中保持一致,Pcopy必须为Pnew的副本。这样,在Fdc未动态注入Pnew前,因为在Pnew和Pcopy中预留给Fdc存储空间的起始地址和空间大小相同,Pnew和Pcopy中除Fdc以外的内容将完全一致,且Hnew和Hcopy中除Hdc以外的内容也将完全一致。这样在Fdc动态注入Pnew后,Pnew和Pcopy中的所有内容将完全一致,相对调用将不会存在问题。
可以得出一个结论:Pnew和Pcopy中除Fdc以外的内容保持一致,且Hnew和Hcopy中除Hdc以外的内容也保持一致,这是保证调用正确性的重要前提。以下简称差异的唯一性。
但此外,还存在一些特殊情况需要给出使用限制。
(1) 静态内联函数的使用。内联函数是用来建议编译器对一些特殊函数进行内联扩展的。内联扩展是指编译器会将指定的函数体插入并取代每一处调用该函数的地方,从而节省了每次调用函数带来的额外时间开支(保护现场等)。因此,内联函数在本质上是一种用空间换取时间的程序优化方案。
在嵌入式系统的编程中,由于空间资源的稀缺,芯片的内核头文件中一般不使用内联函数,而都是使用静态内联函数,关键字一般为“_ _STATIC_INLINE”。这是因为,如果不加以静态约束,则表示该函数有可能会被其他编译单元所调用,所以一定会产生函数本身的代码;以静态约束后,一般可以使得最终生成的机器码文件变小,这是因为多数的编译器会将未使用过的静态内联函数直接从机器码文件中删除。
此外,在嵌入式系统使用的多数编译器中,静态内联函数不是面向单个调用语句一一展开的,而是对于一个源文件的所有调用语句,会将内联函数的代码放置在一块空间内,该源文件所有静态内联函数的调用,都使用指向这块空间的同一绝对地址Asi。这个Asi一般在工程主程序代码的前部,因此,在某个源文件第一次调用内联函数时,这个内联函数的调用语句会使得对应的Asi后的所有程序代码(相比较于未调用内联函数时的程序代码)向后偏移一段位置。这个偏移的长度取决于被调用的内联函数的大小。
接下来讨论静态内联函数在动态命令中的使用带来的问题。在工程Pcopy的函数Fdc中加入调用一个静态内联函数的语句,Pcopy生成的机器码Hcopy与Pnew生成的机器码Hnew显然会存在差异。但由于静态内联函数的使用,会使得Hnew和Hcopy中除Hdc以外的内容也存在差异,不再满足差异的唯一性,且这些额外差异是函数Fdc正确运行所需要的环境。这样,当Hdc动态注入工程Pcopy(即Hcopy)时,函数Fdc将无法正确运行,产生难以预计的错误。
为了兼顾动态命令的正确性与丰富性,针对静态内联函数的使用给出了一个方案:将动态命令所有可能会使用到的静态内联函数,在创建Pnew时就新建一个构件,将所有静态内联函数加上一层函数封装,动态命令在使用静态内联函数时,调用对应的函数封装即可。由于这一层函数封装的存在,被封装的静态内联函数和该封装函数在动态命令调用该静态内联函数之前,就已经存在于最终的机器码Hnew中了。因此,不论Fdc调用静态内联函数与否,Pcopy最终生成的机器码Hcopy与Hnew的差异都存在且仅存在于Hdc段,满足了差异的唯一性。
(2) 变量和常量的使用。函数Fdc中定义的局部变量和常量,与工程内的所有其他局部变量和常量一样,使用同一个主堆栈指针,因此在Fdc编写结束后,只要工程Pnew可以正确编译出Hnew,就无须考虑普通局部变量和常量空间溢出的问题,在Hdc注入Hcopy后,也是可以正常运行的。然而对于静态变量和特殊的常量,需要给出使用限制。
静态变量是以关键字static修饰的、定义时赋初值的量,其初值被存储在程序空间的.data段(位于FLASH),一般由工程内启动文件中的系统复位函数在MCU系统时钟初始化后,从FLASH中被拷贝至RAM。因此虽然静态变量实际使用的地址在RAM中,但本质上的初值来源于FLASH,如果在函数Fdc中定义静态变量,差异的唯一性将被破坏,因为额外的FLASH空间也被修改,因而在函数Fdc中不可以使用静态变量。
此外,一些特殊的常量也不允许直接使用。主要有以下两种情况:① 对长度超过1的数组,初始化时赋初值;② 调用函数时,以字符串常量传参。这两种情况下的初值和字符串常量,一般都是存储在程序空间中的.rodata段(Read only data),同样位于FLASH,程序对于这些变量直接使用其FLASH的地址。所以和静态变量一样,这两种情况中的常量无法使用于函数Fdc,因为差异的唯一性也将被破坏。
对于这些常量的使用,有一种替代的解决方案。对长度超过1的数组,避免初始化时赋初值,改为在初始化后逐个元素赋初值。对于传参时使用的字符串常量,也改为以数组名传参,数组的赋值方式同上。
(3) 子程调用。函数Fdc可以包含若干个子函数,为保证差异的一致性,每一个子函数都要存储在与函数Fdc相同的存储空间。且由于动态命令的运行是从空间Sdc的首地址开始的,所以主函数Fdc必须为第一个定义的函数,其余子函数只能在其前部进行声明,在其后进行定义。
至此,调用的正确性得到了保证。
2) 固有程序运行的正确性。当Fdc涉及对Pnew固有内容的更改时,Pcopy不因对应内容的更改而影响其固有的运行流程,即固有程序的正确运行不因动态命令的执行而受影响。
动态命令可以通过直接操纵FLASH来修改MCU中的固有代码,以此来实现对部分程序的更新等功能。这个场景下涉及的机器码不再局限在动态命令自身,而是整个工程的所有代码,需要综合考虑存储空间和调用的正确性问题。由于这种工程层次上的修改涉及各个方面,很难做到以一套规定来规避所有可能的问题,因此固有程序运行的正确性主要依赖于测试。即在Fdc动态注入正式使用的MCU前,在测试板上写入Pnew工程,在Fdc动态注入测试板的Pnew后,观察现象,多次测试无误后,再投入正式版中使用。
2.2 动态命令获取的问题
动态命令的获取主要分为两个方面。(1) 如何从Pnew的机器码文件Hnew中获取动态命令Fdc对应的机器码Hdc;(2) 运行工程Pcopy的MCU如何获取机器码Hdc。
(1)Pnew中动态命令的获取。要从Pnew的机器码文件Hnew中获取Hdc,本质上是要从机器码文件中提取指定程序段的机器码。机器码文件的每一行都有着相应的地址信息,这样对于已知地址的程序,提取机器码就十分简单了。这样问题就转化为了如何确定指定程序的实际地址。一些编译器在通过设置之后可以产生工程的lst文件,lst文件给出了工程内几乎所有的程序代码及其对应的地址和汇编代码等内容。这样就得到了一种普适的方案:在lst文件中查找指定的程序对应的起始地址,根据得到的起始地址在机器码文件中找到对应的机器码片段。
又由上文可知,为了保证差异的一致性,在程序中为动态命令开辟了固定地址和大小的一段空间,这个操作可由链接文件实现,在定义函数Fdc及其子函数时,只需利用关键字_ _attribute_ _((section()))和开辟的空间名,即可将指定的函数放置在这块开辟的空间中。由此,固定了动态命令程序代码存储的地址,即为这块空间的起始地址。这样只需要在机器码文件中查找这一固定地址空间的机器码,就可以实现从机器码文件Hnew中直接获取Hdc了。
(2)Pcopy中动态命令的获取。要想使得MCU获取一段新的机器码,必然要通过一定的通信方式,本文采用串行通信将MCU与PC机的程序相连。这样在MCU的串行中断处理函数中,增加接收和写入动态命令机器码的程序代码,PC机的上位机程序就可以将准备好的动态命令机器码发送至MCU,并由MCU接收和写入在Pcopy中Fdc对应的FLASH空间Sdc。至此Pcopy便成功获取了动态命令。
2.3 动态命令执行的问题
动态命令执行的问题也包含两个方面,(1) 动态命令何时被执行;(2) 动态命令如何被执行。
对于动态命令何时被执行,本文采用的方式是由上位机发送执行动态命令的指令。这是为了兼顾一些需要在指定时间运行动态命令的情况,这样即便有立即运行动态命令的需求,也只需要在发送完动态命令后,紧跟执行动态命令的指令即可。
有了前文的铺垫,对于运行着Pcopy的MCU,在接收并写入了上位机发来的动态命令后,即便Pcopy中原本的函数Fdc是一个空函数,但在Hdc被MCU写入Sdc后,函数Fdc对应的FLASH空间已经被动态命令取代而不再是空函数。此时为了执行动态命令,只需在MCU的串行中断处理程序中,加入接收执行动态命令指令的程序代码并加入对函数Fdc的调用即可。
3 基于动态命令的软件局部更新技术
基于动态命令的软件局部更新技术实质上是三种不同动态命令的应用,分别是用于添加功能的动态命令、修改固有软件的动态命令、综合的动态命令。
3.1 用于添加功能的动态命令
在现有的嵌入式系统有新的功能需求时,本文的软件局部更新技术所使用的动态命令就用于功能添加,即动态命令对应的函数即为新功能需求的函数。这一类应用较为初级,遵循前文中给出的规则即可实现。需要注意的是,这种情况下的新功能需求不能够改动现有程序。
3.2 修改固有软件的动态命令
在固有程序的软件有某(几)个函数需要修改时,动态命令便使用于软件修改。这种情况的主要思想是在动态命令中借助FLASH在线编程技术来实现对目标函数的擦除和写入,而目标函数的机器码由动态命令中开辟的数组存储,因而要求修改后的函数总大小必须小于动态命令的存储空间。主要方法如下。
首先在工程Pcopy中对目标函数进行修改并编译,比较修改前后的函数大小。若函数大小不变,则直接提取机器码交由动态命令进行相关处理;若修改后的目标函数变小,则在Pcopy修改后的目标函数末尾补足NOP语句,直至与原函数一致,提取后交由动态命令处理;若修改后的目标函数变大,在动态命令函数后新建一个函数(共同使用动态命令的空间),将修改后的函数体复制至新函数,新函数要与目标函数的传参保持一致,目标函数清空后改为调用动态命令中的新函数,并在目标函数的末尾补足NOP语句,直至与原函数大小一致,再次编译后提取目标函数的机器码交由动态命令处理,此时的动态命令包含了动态命令函数和一个新函数,目标函数的真正实现是由这个新函数来完成的。
特别地,由于动态命令具有修改固有软件的能力,这也表明了本文提出的局部更新技术可以更新其自身的启动代码。能否更新固有的启动代码,标志着一个动态更新系统是否有着长期的自我维护能力[5]。
3.3 综合的动态命令
特别地,在嵌入式系统有新的功能需求且这个需求需要修改固有软件时,只需结合前两种情况的处理方式即可。这里以新的功能需求需要借助新的中断服务例程为例。
首先在动态命令空间中,位于动态命令函数后要新建一个函数用于编写需要的中断服务例程。其次,将动态命令分为两部分(这个划分可以通过不同的传参来实现),第一部分是对系统的中断向量表的改写和中断的初始化,第二部分是具体的新功能。其中对系统中断向量表的改写即为将对应的中断指针指向动态命令中的中断服务例程。这样,在动态命令发送完毕后,只需以对应的传参调用动态命令来修改中断向量表并初始化,然后切换参数调用新功能即可正常地触发新中断了。
4 实验对比分析
为了说明软件局部更新技术的优势,以下进行两项对比。
4.1 软件功能扩展的能力对比
将软件局部更新技术与传统的增加程序分支并整体更新的技术进行对比。将十段不同功能的程序代码通过两种技术分别加入相同的工程并执行,并由此对比两者的优劣。
本文以TI公司MSP432P401R(简称MSP432)作为硬件平台,硬件编程语言选用C语言,编译器选用GNU V7.2.1;PC机软件选用C#语言;PC机软件与MCU的通信方式选用串行通信。MSP432基于ARM Cortex-M4F内核,最大运行速率48 MHz,Flash大小为256 KB,RAM空间为64 KB[14]。
首先对传统方法进行实验。通过编译生成的map文件可知,在这十段代码分支加入串行中断处理函数前后,串行中断处理函数EUSCIA0_IRQHandler的占用空间增加了3 264 B。
接下来通过软件局部更新技术将这十段代码依次注入工程。再将这十段程序依次填充函数Fdc(工程中命名为dynamic_command),并获取了它们对应的机器码Hdc1-Hdc10,其中最长的机器码占据空间为242 B。因此,可以在链接文件中将动态命令空间Sdc的大小调整为400 B,以此减少不必要的开销。
至此,动态命令的程序代码所产生的存储空间的开销为400 B,但这不是软件局部更新技术带来的全部空间开销,还要将接收、写入和执行动态命令的代码(以下简称动态命令固有程序)产生的存储空间的开销也计算入。这部分程序开销可计算出为1 020 B。因此采用软件局部更新技术将这十段不同功能的程序代码注入工程所带来的总开销为1 420 B。可以发现这一开销远小于传统的方式,如表1所示。
表1 不同方案下的Flash空间开销
因此,对于同等数量和大小的扩展功能,采用软件局部更新带来的存储空间的开支更小。并且,对用同样大小的存储空间,采用软件局部更新技术可扩展的功能理论上来说是不受限制的,仅需满足各个功能的大小都可以被这同一块存储空间接收即可。此外,不同于整体更新技术,软件局部更新技术的每一次功能扩展仅带来单个功能存储空间大小的数据传输,且整个系统无须重启复位即可立即调用这一扩展功能。
4.2 综合的技术特点对比
接下来将软件局部更新技术与整体更新、增量更新、王婷等的软件重构、郭宗芝等[15]使用的VxWorks操作系统和Chen等的动态软件更新框架做对比。
与整体更新相比,局部更新无须重启,且对数据传输和存储空间的需求都较低,更新时间更短,且拥有自我维护的能力。而与增量更新相比,除了增量更新要求重新设计整体框架来优化每一次程序对比以外,程序的一致性对比也会带来额外的数据传输和更新时间。王婷等的软件重构技术与整体更新类似,更新单位较大因此对存储空间和数据传输的要求也较高,更新时间也较长,且更新后需要重启。Chen等的动态软件更新,框架和VxWorks操作系统的缺陷则在于为了实现动态更新,其自身的框架较为复杂,不适用于资源受限的嵌入式系统。
5 结 语
针对嵌入式系统开发对功能灵活性日益增加的需求与嵌入式系统存储空间受限的矛盾,本文提出一种基于动态命令的软件局部更新技术,可以在存储空间受限的情况下极大程度地满足嵌入式系统开发的灵活性。针对动态命令的正确性问题,对静态内联函数、静态变量、常量、子函数和固有程序给出了详细的说明和限制,并且阐述了动态命令获取和执行的方式。通过实验对比分析可知,在程序灵活性需求越高的场合,软件局部更新技术的表现将越为出色。