李艳丽,杨燕鎏(1.重庆科创职业学院人工智能学院,重庆 40160;.中移物联网有限公司,重庆 401336)
由于传感网通常部署在环境恶劣的地方,很多传感网节点一经部署便很难回收,如果要对节点的功能进行更新维护,则需要使用基于远程升级的重编程技术[1],而传感网节点一般具有低内存、低功耗、低带宽的特点,且升级维护过程中一般要求节点不能够离线,如果使用替换整个代码映像这种简单的方式实现重编程,必然对节点的内存、功耗带来极大的开销,同时可能导致整个传感器网络出现拥塞,严重影响其他节点的工作[2]。
Dunkels在研究TinyOS无线传感网操作系统的远程更新的时候就提出整体重编程的方法[3]。该方法是传统的IAP,在代码分发阶段需要传输整个TinyOS镜像,并将新镜像文件完整覆盖替代旧存储区的旧镜像,并由BootLoader重新引导系统启动而完成升级,但这种方式对网络的带宽、节点的内存和flash均带来了很大的压力,且需要重写整个内部Flash,消耗的时间和能量比较大,不利于节点的低功耗。针对Dunkels的不足,差异代码包的更新成为了新的研究方向,如参考文献提出了一种基于代码克隆检测技术的WSNs重编程方法[4],该方法通过代码克隆检测机制来生成差异代码补丁,然后将差异补丁以无线方式传输给传感器节点,实现WSNs重编程,类似生成差异代码的算法比较多,但这类算法均比较复杂,最终传感器节点都需要精确寻找原映像中和新映像的不同之处后对代码进行合并,这个过程对节点的运算能力要求较高,耗时较长,因此耗能也比较多[5]。
因此为了解决上述问题,本文研究并实现了一种动态加载器,通过动态加载功能可以支持用户对系统进行升级维护,不需要链接整个程序镜像就能够实现二次开发,有效地降低了升级过程中带来的资源消耗,降低了维护成本。
重编程技术涉及3个方面,首先是数据分发协议,解决了更新文件如何传输到节点的问题[6-7];接着是文件系统,解决了更新文件如何存储的问题[8-9];最后就是节点收到更新文件后需要合理的策略将其更新,其中更新策略有以下3种方式:
①系统映像替换
这种方式最简单,目前在嵌入式系统中使用最广泛,原理就是直接把flash中的代码全部替换掉,但缺点也是显然的,对节点的内存资源、带宽要求很高,数据传输的过程中将会耗费大量的功耗[10],且更新后需要重启节点,无法实现在线更新。
②差异代码覆盖
这种方式只替换掉不同的代码,数据传输量最小,因此耗能少,但缺点需要实现精确的算法找出原映像中和新映像的不同之处[11],更新算法也极其复杂,基本上没发现实际的应用例子。
③模块动态加载
模块的动态加载,使用了动态链接的原理,在编译期不进行链接,直到运行的时候才使用可重定位执行文件ELF(Executable and Linking Format)中的重定位信息来链接文件中无法确定的符号,典型的应用如Linux系统的模块加载,但在嵌入式系统中应用不多,这种模式很好地平衡了先前两种模式的优缺点,且由于常用的嵌入式开发工具如GCC/IAR/Keil生成的中间文件(后缀名为.o)就是可重定位的elf文件,因此基于ELF文件的动态加载成为了本文的研究对象。
图1中显示了ELF可重定位文件的构成,ELF文件头的开始16 byte描述了整个文件的大小和字节序(大端模式还是小端模式)。文件头还包含了ELF头的大小,文件类型(可重定位,可执行和共享),机器类型,节头表的位置和大小,其中节头表中的每项对应于文件中的一个节,用于描述节的位置和大小[12]。
图1 ELF可重定位文件结构图
动态链接必须对这些可重定位目标文件完成两个主要任务[13-14]:
①符号解析
将每个符号引用和一个符号定义联系起来。符号又分为导出符号(本地符号),导入符号(外部符号),静态符号(本地符号),局部符号(本地符号)4种,其中导出符号,在本模块定义,能够被其他模块引用的符号;导入符号,在其他模块定义,被本模块引用的符号;静态符号,在本模块定义,只能被本模块引用的符号;局部符号,不出现在符号表,由栈管理,链接器不理会这类符号。
②重定位
链接器把每个符号定义与一个物理地址联系起来,然后修改所有对这些符号的引用,使得它们指向这个存储位置,从而重定位这些节。
编译器生成可重定位目标文件后,内部符号都已被正确地进行了符号解析,外部符号可能会引用了非本模块的符号定义,编译器无法找到符号定义,因此无法解析,但编译器会把外部符号放入符号表“symtab”,同时把如何解析该符号的方法放入重定位表中rel.text和rel.data/rel.rodata中,动态加载器根据rel文件就可以修改相应session里面的符号引用。
动态加载器的实现和CPU体系结构以及底层驱动有关,为了屏蔽这些底层细节,实现通用性和可移植性,设计时需要将这部分与底层有关的功能独立出来,图2为动态加载器的软件体系结构图。
如图2所示,动态加载器的软件架构有以下部分组成:
Loader core 加载器核心模块,该模块首先读取文件系统的sym.txt文件,将内容存储进symlib库中,然后加载指定的可重定位elf文件,分析elf文件中rel重定位节,决定哪些地方需要重定位,然后交给重定位模块进行处理。
图2 动态加载器的软件体系结构
运行空间分配 运行空间模块为核心模块提供相应接口,核心模块调用该接口分别为elf文件的data/bss段分配ram空间,为text/rodata段在flash中分配运行空间。
代码搬移 代码搬移模块为核心模块提供相应的接口,核心模块调用该接口将重定位后的elf文件中的data/bss段和text/rodata分别搬移到预先分配好的ram空间和flash运行空间,代码搬移示意如图3 所示。
图3 代码搬移示意图
文件系统API 考虑到兼容不同的文件系统,设计统一的文件系统接口并将其独立出来形成一组API,接口包括文件的打开、关闭、读/写、位置定位 5个接口。
symlib symlib模块为加载器提供系统中已存在的符号的物理地址,如果被加载模块调用了系统中存在的符号比如函数或者变量,则加载器通过该模块来查询这些符号的物理地址,重而完成重定位,symlib模块中的内容则来自map文件,编译器在生成系统镜像的时候同时产生一个map文件,该文件存在系统镜像所有符号的地址信息,在这里通过手工的方式将需要用到的符号地址信息放进一个txt文件中,并将该文件随elf文件一起传输至节点的文件系统中供核心模块调用分析后转换为如下的格式存储进symlib中,其中name数组存储符号的名称,value指针存储该符号所在的物理地址。
Struct symbols
{
const char name[20];
void*value;
};
Dirver 为核心模块提供ram和flash驱动。
重定位 重定位模块的实现和具体的CPU架构有关,主要有两种重定位类型,分别为绝对地址重定位,如全局变量引用,对应汇编指令mov;相对地址重地位,如elf文件引用的外部函数,对应汇编指令B/BL/BLX。以ARM和Cortex-M系列CPU为例,相对重定位和绝对重定位的修正方法[2]如表1所示。
表1 相对重定位和绝对重定位的修正方法
图4 动态加载器工作流程
表1中,A=保存在被修正位置的值;P=被修正的位置(相对于段开始的偏移量),通俗的说就是该函数在目标代码中被调用的地址;S=重定位后符号引用所在的运行地址。
动态加载器的工作流程如图4所示。
具体如下:
①通过文件系统打开sym.txt文件,分析该文件的内容,将内容转换为symlib指定的格式后存放进symlib中,等待重定位时查询使用,该文件的内容由用户通过查询map文件符号地址并手动添加,文件的内容格式如下:
Start
符号:物理地址
符号:物理地址
……
……
End
②通过文件系统加载指定的.o文件,按照elf文件的语法来分析该文件中的所有段,并建立本地符号表用于绝对地址重定位,同时确定哪些段需要重定位,并存储这些段的可重定位信息。
③根据重定位段的信息,为这些段分配合适的空间,如果是text/rodata段则分配代码运行空间,如果是data/bss则分配内存空间,为重定位后进行代码搬移做准备。
④根据可重定位信息,分别对需要进行重定位的段的内容进行修改,主要就是对引用到的本地或者外部符号的运行地址进行校正,如果引用到外部的符号则需要通过查询symlib里面的内容来确定该符号的运行地址。
⑤重定位完成后,加载器将重定位的段搬移到相应的空间,如果是text/rodata则搬移到代码空间,如果是data/bss段则搬移到内存空间。
⑥代码搬移结束后就可以运行加载的模块,加载器通过本地符号表查询模块的入口函数,执行该函数,模块开始运行。
本文使用应用广泛的Stm32f103系列片上系统来搭建硬件测试平台,系统时钟频率为72 MHz,其中该系列芯片基于Cortex-M3架构,重定位类型只有绝对重定位和相对重定位两种,算法相对简单;软件上使用freertos操作系统+fatfs文件系统作为软件框架,在此框架内实现了动态加载器;使用Keil应用程序作为集成开发环境。
测试目的:验证动态加载器的功能和性能
测试方法:加载一个外部模块,该模块调用了系统的printf函数并打印“hello world”,如果成功则说明了动态加载器能够对模块中调用printf的语句进行重定位,实现了对printf函数的动态链接;在加载器开始分析外部模块和找到外部模块入口函数的地方分别打印当前的系统时间,两个时间差就是动态加载过程所耗费的时间。
测试过程如下:
步骤1 编译freertos操作系统+fatfs文件系统+动态加载器的系统镜像,并下载进硬件平台中。
步骤2 如图5所示,在生成的map文件中找到printf函数的地址,手动生成sym.txt文件,如图6所示。
图6 sym.txt文件内容
图5 map文件中printf函数的运行地址
步骤3 将sym.txt文件和待加载模块的update.o文件使用ymodem协议通过串口的方式下载进硬件平台中,如图7和图8所示。
图7 udpdate.o的文件属性
图8 sym.txt和update.o文件的下载过程
图9 动态加载update.o文件的过程
步骤4 进入测试平台的控制台界面,通过命令行的方式加载指定的模块,并观察控制台的打印结果。
由图9可以看出,执行update.o文件后打印了hello world字符串,说明了动态加载的功能已经实现;其中开始加载的时间为14 960,结束时间为15 013,单位为5 ms,所以加载过程耗时t=(15 013-14 960)×5=265 ms=0.265 s。
传感网节点的重编程技术保证了传感网络的可扩展性和可维护性,特别适合大规模组网以及难以回收的传感网节点的升级维护,但由于传感网节点能耗小、带宽少和硬件资源受限的特点,传统的整体固件置换的更新方式已经不合适,因此本文通过借鉴Linux模块加载的方式和原理,设计并实现了一种适合传感网节点的动态加载器,通过动态加载的方式可以有效减少更新文件的大小,降低了更新过程对传感节点功耗、内存和网络带来的损耗。
本文最后通过搭建硬件测试平台对动态加载器的功能和性能进行了测试,测试结果显示了动态加载器的执行过程和效率。后面的研究中将裁减elf文件的冗余信息,进一步降低更新文件的大小,如果结合高效的无线数据分发协议[9],就可以实现节点的远程更新[3],具有很好的应用前景。