張 正 賈小林
(西南科技大學計算機科學與技術學院 四川 綿陽 621000)
物聯網的進步與當今通信技術的不斷發展有不可分割的關系,而現在出現的窄帶物聯網(NB-IoT)以及5G技術都是為了使整個系統中的通信成本與用戶的需求成比例[1]。NB-IoT是為了用更小資源實現更高的設備連接數量,而5G的優勢則是獲取更低的延時以及更高的傳輸速率,怎么樣在更少的資源上實現更多的功能是一個可以不斷進行優化的問題。在NB-IoT中因為數據傳輸的帶寬比較低,長時間的數據傳輸必然會增加能量的消耗,故應當盡量減少數據的傳輸量。但是上線設備通常是廣泛分布且基數較大[2],如果NB-IoT設備出現系統漏洞而需要進行軟件升級將導致巨大的不必要的通信流量與能量的消耗。因此,減少傳輸數據傳輸量和系統能耗就成了一個值得研究的問題。
目前NB-IoT中的終端設備多為嵌入式設備,其處理器通常為單地址空間的處理器,系統的功能為定制開發的程序。由此引發了新的問題,主要包括系統拓展性差、功能實現受到資源限制、軟件需要升級更新等。針對這些問題,本文提出在單地址空間的嵌入式操作系統上實現動態鏈接庫技術。利用動態鏈接庫技術進行程序開發和升級維護,可以為NB-IoT的設備帶來更多的優勢:(1) 程序模塊化開發,維護方便,程序復用性提高;(2) 程序遠程升級只需要更新指定軟件模塊,能夠提高更新速度與成功率;(3) 系統的可拓展性大大提高,模塊能夠進行動態卸載與動態加載以實現程序功能的切換。
在一些高端的嵌入式系統上,其硬件帶有存儲保護單元(MMU),能夠運行Linux等已經帶有成熟動態鏈接庫技術的操作系統,遠程升級只需要更新指定的程序包即可,但是其實時性仍然不高,且成本高、功耗大、性能經常過剩[3]。而在一些低端的嵌入式系統上雖然有動態加載的解決方案,但是其只支持單應用加載,模塊化的加載方式仍然不被支持[4]。本文提出的單地址空間的嵌入式操作系統上動態鏈接庫的實現能夠在極少的資源上實現程序的模塊化加載,程序更新只需要更新指定模塊,應用到NB-IoT設備遠程升級中能夠減少傳輸的流量以節省設備消耗的能量,同時提高系統的可擴展性。
傳統的嵌入式程序開發,程序的每一次修改都需要經過編譯、調試和燒寫的步驟,而且以上步驟可能還需要重復多次才能完全達到要求,這整個流程繁瑣且周期長,無法快速地迭代。并且程序無法像Linux等操作系統以動態庫的方式進行鏈接,程序的任意一部分改動都需要程序進行重新編譯、調試、燒寫,這就使得應用的程序的復用性和可拓展性大大降低[5]。在帶有MMU的嵌入式芯片上,能夠完成虛擬內存到物理內存的轉換,還能夠提供內存讀寫保護功能,這就使得經過編譯的二進制文件可以通過虛擬內存映射的方式實現程序的動態加載,而在軟件上不需要進行其他特殊的操作[6]。
通常程序在編譯完成后,都需要完成鏈接的步驟,然后生成二進制文件。程序的鏈接方式分為三種:靜態鏈接,裝入時動態鏈接,運行時動態鏈接。傳統的嵌入式程序開發只支持靜態鏈接方式,即在程序運行前就將各個目標模塊等鏈接成一個完整的程序,但是其生成的程序體積大,修改程序麻煩[7]。其優點是占用資源少,程序無重定向代碼,運行效率高。
本文中的嵌入式程序的動態加載技術采用了裝入時動態鏈接,即程序首先放置在外部的文件系統中,需要運行時再加載到內存中來運行,每一個動態加載的程序棧空間獨立,其運行時的靜態空間也獨立。而本文中的動態鏈接庫既可以使用裝入時動態鏈接也可以采用運行時動態鏈接,即程序在裝入時,判斷自己需要用到的動態庫文件進行裝入,程序在運行時也可以指定外部文件系統的動態庫文件進行裝入,并運行其中的代碼塊。
在單地址空間的芯片上,所有的代碼段都共用一個存儲空間,所有的函數與變量的地址在運行時都必須確定下來,必須被加載到指定位置,否則運行將會產生致命錯誤[8]。本文中的動態加載方式與Linux上傳統的實現原理不盡相同,傳統的Linux加載方式采用Unix的標準ELF格式,該加載文件復雜且含有大量的多余信息,對于嵌入式操作系統的使用資源占用較大。本文為其專門設計了一個軟件包,去掉不需要的信息,只保留運行時必要的信息,大大地縮減了程序運行時占用的資源。嵌入式動態庫的加載還需要解決如下幾個問題:
(1) 編譯器必須要能夠生成可動態加載的可執行文件。
(2) 程序包之間能夠進行相互調用和遞歸調用。
(3) 不同應用程序之間能夠有可靠的通信方式。
(4) 動態加載平臺要能夠實現中斷管理以及加載平臺與動態加載的程序間的相互調用。
(5) 系統依賴文件系統進行動態加載,系統能夠將程序包解壓到文件系統中。
程序包既可以作為程序運行也可以作為動態庫使用,程序包中包含四部分:代碼段,可讀寫段,符號文件,資源文件,其中:代碼段、可讀寫段、符號文件是必須的,資源文件不是必須的。代碼段是直接加載到ROM或者RAM中執行的部分,可讀寫段在運行時需要將內容拷貝到動態申請的RAM中去,符號文件在動態加載時進行重定向時使用。該程序包去掉了調試信息等,使得程序的體積能夠最小化,程序可以直接加載到ROM中,程序包結構如圖1所示。

圖1 程序包結構
本文中動態加載平臺采用armcc編譯器作為編譯平臺,動態加載平臺的測試硬件平臺為Cortex-M3,程序要實現動態加載需要保證:(1) 函數間的調用必須采用相對地址調用。(2) 程序中全局變量地址以及靜態變量的起始地址需要使用專用的寄存器進行保存。(3) 程序需要顯式給出該程序中符號的名字以及在內存中的相對偏移地址[9]。下面通過對編譯器的配置能夠實現編譯出與位置無關的代碼,通過對鏈接器的配置能夠得出程序的符號表,編譯命令如表1所示。

表1 編譯器命令
利用表1中的命令對armcc編譯器配置可將程序文件編譯為可重定向的代碼,其內部所有跳轉都將采用相對跳轉方式,其數據段的訪問將采用R9寄存器的地址作為基地址,其目標訪問地址為:dest=R9+offset。參數中采用global_reg=5命令限制編譯器使用R8寄存器,R8寄存器將作為嵌入式實時操作系統(RTOS)保存目標程序數據的專用寄存器[10]。
鏈接器命令如表2所示。鏈接器將導出可執行的ELF文件與可執行文件的符號表,符號表中包含了可執行文件所有的符號信息(函數名,函數地址偏移,變量名,變量地址偏移等),符號表經過處理后將作為動態庫之間的重定向的必備信息。

表2 鏈接器命令

續表2
Fromelf命令如表3所示。Fromelf工具將鏈接器生成的ELF文件轉換為可執行的.bin文件以及數據段文件,這兩個文件最終將要加載到內存中。

表3 Fromelf命令
動態庫重定向具體流程如圖2所示,動態庫中函數的重定向主要分為5步。

圖2 動態重定向的具體流程
(1) 解壓程序包并讀取代碼段與數據段到內存:將程序包中的符號文件、代碼段與數據段解壓到外部存儲器,然后將代碼段載入到Flash或者RAM中,將數據段載入到RAM中。
(2) 讀取并解析符號表的數據條目:從外部存儲器中讀取一條符號表數據并解析獲取符號的偏移、類型、符號名,以及目標動態庫名。
(3) 將與指定KEY值匹配的函數指針變量標記為需要重定向:KEY為需要被重定向函數指針的標識,該值應當盡量保證不被用戶用作其他變量的初始化值。
(4) 生成或查找標記的函數指針變量的跳轉函數:跳轉函數鏈接重定向函數指針與目標函數。
(5) 重定向需要重定向的函數到跳轉函數:將跳轉函數的入口地址賦值給重定向的函數指針。
符號表為動態庫實現的一個核心部分,它包含了各個程序自有的符號表信息,默認的符號表由編譯器導出,默認只包含偏移、類型、符號名三項信息,不能夠滿足重定向的使用要求,經過修改的內容如表4所示,包含四部分:偏移,類型,符號名,目標庫。當類型為T表示為一個Thumb類型調用的函數,其偏移地址為在代碼段區域內的偏移;當類型為D時標識為一個變量,其偏移地址為在數據段區域內的偏移。當類型為F時表示該程序需要用到的目標動態庫。需要進行重定向的函數目標庫一欄將會有效。為提升運行時的效率,其中的符號表信息將在編譯階段完成修改。

表4 符號表
為了實現動態庫之間的相互調用,動態加載平臺還支持函數的重定向。與傳統重定向不同,本文中的動態加載平臺的重定向不直接作用于函數的地址,而是作用于函數指針,其基本思路如圖3所示。

圖3 重定向示意圖
程序調用函數指針,函數指針在加載階段將會指向全局跳轉表,而全局跳轉表將指向目標程序的目標函數。函數指針實際也是一個變量,將在符號表中以類型D的形式表示,函數指針調用的一個典型示例如下,默認為需要重定向的函數指針賦一個KEY值0xfedcba98,該KEY在編譯器進行符號表信息預處理時用來識別該函數指針是否需要進行重定向。
typedef int (*_add)(int a,int b);
_add add=0xfedcba98;
void main(void){
add(1,2);
}
跳轉函數由匯編函數實現,且在加載時動態生成,其主要作用是保護當前程序的R9基地址值,并切換到目標程序的R9基地址值,其匯編指令如表5所示,每一個重定向函數的跳轉函數只占用20個字節,5條指令。

表5 跳轉函數匯編的實現
實現中斷管理是嵌入式動態加載平臺模塊化實現的一個重要功能,將系統的中斷功能交給動態加載平臺的一個專門的模塊來實現,以不斷拓展系統的可拓展性。動態加載平臺的中斷管理流程如圖4所示,其中中斷中繼表包含一個函數指針向量,動態加載平臺獲取該中繼表并將其與真實的中斷映射,而程序與動態庫則注冊自有程序的中斷函數到中斷中繼表,于是中斷執行的步驟就變成:系統中斷→中斷中繼表→注冊的中斷函數。

圖4 中斷管理流程示意圖
在單地址空間上實現的多任務為偽多任務,其并不支持虛擬內存映射,且通常單地址空間的處理器多為單核處理器,其多任務的切換也只是時間片的劃分。動態加載平臺也能夠支持簡單的多任務,也具有多任務程序的特征:(1) 棧空間獨立;(2) 數據段獨立;(3) 任務區切換時能夠保存當前運行狀態。其中數據段獨立,保證了不同程序加載到程序運行時不會受到其他程序的影響[11]。在動態加載平臺上的多任務切換存在兩個問題:
(1) 通過R9寄存器進行內存訪問基地址的多任務切換與傳統RTOS存在差異,其主要原因是每一個單獨運行的應用程序都是靠R9寄存器來進行基地址重定向,任務切換時會導致程序的變量域還沒有切換,而代碼域已經切換到了嵌入式操作系統的代碼域,這將會導致訪問變量錯誤。
(2) 在任務調度中的代碼主要由匯編語言完成,而在匯編中不能夠直接進行變量或函數的調用,主要原因是利用匯編直接進行的函數跳轉其目的地址必須確定,而重定向后的程序地址并不確定,真實地址是在程序運行后才能夠知曉。
上述問題的解決可通過R8寄存器保護實現,即將RTOS模塊的變量域起始地址與需要保護的變量與函數存入一個數組,將數組的首地址在執行初始化時放入R8寄存器。在訪問嵌入式操作系統代碼域時,通過匯編代碼將R8寄存器的值賦值給R9,通過代碼控制即可切換到嵌入式操作系統模塊的變量域,而需要訪問保護變量時能夠通過R8寄存器找到需要的變量值。具體部分實現的匯編程序如下:
ldr r1,[r8];
//保存棧頂值
ldr r1,[r1]
str R0,[r1]
push r9,lr;
//保護R9的值
ldr.w r0,[r8,#4]
ldr.w r1,[r8,#8]
mov r9,r1;
//賦值成為本模塊的R9值
blx r0;
//調用任務切換函數
pop r9,lr
系統測試主要分為性能分析與功能測試,功能測試主要完成對系統的功能完整性進行測試,性能分析主要利用軟件算法對軟件在未進行動態加載與加載后執行的性能做一個比較。
利用快速排序算法以及斐波那契數列(遞歸法)生成來分別測試程序在內存訪問與函數調用與原生軟件之間的性能差異。分別測試了在選擇排序算法與斐波那契數列生成在加載與未加載執行時在不同存儲器內的執行效率。通過表6可知,在選擇排序算法測試中,加載到內部RAM中執行效率相比于未加載前在Flash中的執行效率只降低了3.46%,而加載到外部RAM中執行效率相比于未加載前在Flash中執行效率降低了1 131.7%。在斐波那契數列生成測試中,加載到內部RAM中執行效率相比于未加載前在Flash中的執行效率提升了5.58%,而加載到外部RAM中執行效率相比于未加載前在Flash中執行效率降低了625.87%。從測試數據可以看出,動態庫加載到內部RAM中測試時相比未加載前運行效率差異較小,但是在加載到外部RAM中測試時運行效率卻損失較大,這是因為外部RAM的訪問受到了外部總線帶寬的限制。

表6 性能測試表
測試的硬件平臺為Cortex-M3的STM32F103ZET6處理器,測試程序的功能框架如圖5所示,該測試程序中的功能模塊包含了文件系統、TPC/IP協議棧、RTOS、中斷管理模塊、動態加載平臺、遠程應用升級模塊、板級支持包、驅動程序,傳統的升級方式需要將所有的代碼文件進行全部燒寫,測試程序中利用動態庫進行模塊化的加載,在進行程序升級時,只需要對需要的程序進行升級即可。

圖5 測試程序功能框架
測試場景:溫度采集驅動升級,將程序中溫度報警模塊的閾值提高到28 ℃。
將本文系統的更新方式與傳統的更新方式在更新體積以及更新時間上做對比,測試結果數據如表7所示,傳統的更新方式是指每次都對所有的應用程序進行更新。

表7 更新測試對比表
可以看出本文系統在更新同樣的功能代碼時,需要更新的代碼大小相比傳統方案縮小了218 KB,更新時間縮短了59.2 s,更小的更新代碼體積意味著更小的更新時間與更高的更新成功率。這種更新方式對于復雜的嵌入式系統極為有用,在復雜的嵌入式系統上系統功能復雜,功能劃分清晰,采用模塊化的程序升級方式不僅僅能夠減少更新的代碼體積,還能夠約束開發者更好地規劃系統功能模塊的實現。
本文針對傳統的NB-IoT設備,利用自制Bootloader進行程序升級中存在的弊端進行了改進。利用動態鏈接庫實現模塊化的程序加載方式,將傳統的一個程序拆分為動態鏈接庫的方式加載運行,可以大幅度減少程序更新時的數據傳輸量和系統能耗,同時還能提高系統的可拓展性與程序的復用性。本文中的單地址空間處理器上的動態鏈接庫技術不僅能夠應用到NB-IoT程序升級,還能夠應用到其他通信方式的遠程程序升級中。該技術能夠提高單地址空間處理器的可拓展性,程序的模塊化加載還能夠提高處理器資源的利用效率。相比于傳統的程序開發模式,該技術能夠拓展單地址空間處理器的應用范圍,在嵌入式系統和智能終端領域具備良好的應用和推廣價值。