李艷麗,楊燕鎏(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],具有很好的應用前景。