張 正 賈小林
(西南科技大學計算機科學與技術(shù)學院 四川 綿陽 621000)
物聯(lián)網(wǎng)的進步與當今通信技術(shù)的不斷發(fā)展有不可分割的關(guān)系,而現(xiàn)在出現(xiàn)的窄帶物聯(lián)網(wǎng)(NB-IoT)以及5G技術(shù)都是為了使整個系統(tǒng)中的通信成本與用戶的需求成比例[1]。NB-IoT是為了用更小資源實現(xiàn)更高的設(shè)備連接數(shù)量,而5G的優(yōu)勢則是獲取更低的延時以及更高的傳輸速率,怎么樣在更少的資源上實現(xiàn)更多的功能是一個可以不斷進行優(yōu)化的問題。在NB-IoT中因為數(shù)據(jù)傳輸?shù)膸挶容^低,長時間的數(shù)據(jù)傳輸必然會增加能量的消耗,故應(yīng)當盡量減少數(shù)據(jù)的傳輸量。但是上線設(shè)備通常是廣泛分布且基數(shù)較大[2],如果NB-IoT設(shè)備出現(xiàn)系統(tǒng)漏洞而需要進行軟件升級將導致巨大的不必要的通信流量與能量的消耗。因此,減少傳輸數(shù)據(jù)傳輸量和系統(tǒng)能耗就成了一個值得研究的問題。
目前NB-IoT中的終端設(shè)備多為嵌入式設(shè)備,其處理器通常為單地址空間的處理器,系統(tǒng)的功能為定制開發(fā)的程序。由此引發(fā)了新的問題,主要包括系統(tǒng)拓展性差、功能實現(xiàn)受到資源限制、軟件需要升級更新等。針對這些問題,本文提出在單地址空間的嵌入式操作系統(tǒng)上實現(xiàn)動態(tài)鏈接庫技術(shù)。利用動態(tài)鏈接庫技術(shù)進行程序開發(fā)和升級維護,可以為NB-IoT的設(shè)備帶來更多的優(yōu)勢:(1) 程序模塊化開發(fā),維護方便,程序復用性提高;(2) 程序遠程升級只需要更新指定軟件模塊,能夠提高更新速度與成功率;(3) 系統(tǒng)的可拓展性大大提高,模塊能夠進行動態(tài)卸載與動態(tài)加載以實現(xiàn)程序功能的切換。
在一些高端的嵌入式系統(tǒng)上,其硬件帶有存儲保護單元(MMU),能夠運行Linux等已經(jīng)帶有成熟動態(tài)鏈接庫技術(shù)的操作系統(tǒng),遠程升級只需要更新指定的程序包即可,但是其實時性仍然不高,且成本高、功耗大、性能經(jīng)常過剩[3]。而在一些低端的嵌入式系統(tǒng)上雖然有動態(tài)加載的解決方案,但是其只支持單應(yīng)用加載,模塊化的加載方式仍然不被支持[4]。本文提出的單地址空間的嵌入式操作系統(tǒng)上動態(tài)鏈接庫的實現(xiàn)能夠在極少的資源上實現(xiàn)程序的模塊化加載,程序更新只需要更新指定模塊,應(yīng)用到NB-IoT設(shè)備遠程升級中能夠減少傳輸?shù)牧髁恳怨?jié)省設(shè)備消耗的能量,同時提高系統(tǒng)的可擴展性。
傳統(tǒng)的嵌入式程序開發(fā),程序的每一次修改都需要經(jīng)過編譯、調(diào)試和燒寫的步驟,而且以上步驟可能還需要重復多次才能完全達到要求,這整個流程繁瑣且周期長,無法快速地迭代。并且程序無法像Linux等操作系統(tǒng)以動態(tài)庫的方式進行鏈接,程序的任意一部分改動都需要程序進行重新編譯、調(diào)試、燒寫,這就使得應(yīng)用的程序的復用性和可拓展性大大降低[5]。在帶有MMU的嵌入式芯片上,能夠完成虛擬內(nèi)存到物理內(nèi)存的轉(zhuǎn)換,還能夠提供內(nèi)存讀寫保護功能,這就使得經(jīng)過編譯的二進制文件可以通過虛擬內(nèi)存映射的方式實現(xiàn)程序的動態(tài)加載,而在軟件上不需要進行其他特殊的操作[6]。
通常程序在編譯完成后,都需要完成鏈接的步驟,然后生成二進制文件。程序的鏈接方式分為三種:靜態(tài)鏈接,裝入時動態(tài)鏈接,運行時動態(tài)鏈接。傳統(tǒng)的嵌入式程序開發(fā)只支持靜態(tài)鏈接方式,即在程序運行前就將各個目標模塊等鏈接成一個完整的程序,但是其生成的程序體積大,修改程序麻煩[7]。其優(yōu)點是占用資源少,程序無重定向代碼,運行效率高。
本文中的嵌入式程序的動態(tài)加載技術(shù)采用了裝入時動態(tài)鏈接,即程序首先放置在外部的文件系統(tǒng)中,需要運行時再加載到內(nèi)存中來運行,每一個動態(tài)加載的程序??臻g獨立,其運行時的靜態(tài)空間也獨立。而本文中的動態(tài)鏈接庫既可以使用裝入時動態(tài)鏈接也可以采用運行時動態(tài)鏈接,即程序在裝入時,判斷自己需要用到的動態(tài)庫文件進行裝入,程序在運行時也可以指定外部文件系統(tǒng)的動態(tài)庫文件進行裝入,并運行其中的代碼塊。
在單地址空間的芯片上,所有的代碼段都共用一個存儲空間,所有的函數(shù)與變量的地址在運行時都必須確定下來,必須被加載到指定位置,否則運行將會產(chǎn)生致命錯誤[8]。本文中的動態(tài)加載方式與Linux上傳統(tǒng)的實現(xiàn)原理不盡相同,傳統(tǒng)的Linux加載方式采用Unix的標準ELF格式,該加載文件復雜且含有大量的多余信息,對于嵌入式操作系統(tǒng)的使用資源占用較大。本文為其專門設(shè)計了一個軟件包,去掉不需要的信息,只保留運行時必要的信息,大大地縮減了程序運行時占用的資源。嵌入式動態(tài)庫的加載還需要解決如下幾個問題:
(1) 編譯器必須要能夠生成可動態(tài)加載的可執(zhí)行文件。
(2) 程序包之間能夠進行相互調(diào)用和遞歸調(diào)用。
(3) 不同應(yīng)用程序之間能夠有可靠的通信方式。
(4) 動態(tài)加載平臺要能夠?qū)崿F(xiàn)中斷管理以及加載平臺與動態(tài)加載的程序間的相互調(diào)用。
(5) 系統(tǒng)依賴文件系統(tǒng)進行動態(tài)加載,系統(tǒng)能夠?qū)⒊绦虬鈮旱轿募到y(tǒng)中。
程序包既可以作為程序運行也可以作為動態(tài)庫使用,程序包中包含四部分:代碼段,可讀寫段,符號文件,資源文件,其中:代碼段、可讀寫段、符號文件是必須的,資源文件不是必須的。代碼段是直接加載到ROM或者RAM中執(zhí)行的部分,可讀寫段在運行時需要將內(nèi)容拷貝到動態(tài)申請的RAM中去,符號文件在動態(tài)加載時進行重定向時使用。該程序包去掉了調(diào)試信息等,使得程序的體積能夠最小化,程序可以直接加載到ROM中,程序包結(jié)構(gòu)如圖1所示。
圖1 程序包結(jié)構(gòu)
本文中動態(tài)加載平臺采用armcc編譯器作為編譯平臺,動態(tài)加載平臺的測試硬件平臺為Cortex-M3,程序要實現(xiàn)動態(tài)加載需要保證:(1) 函數(shù)間的調(diào)用必須采用相對地址調(diào)用。(2) 程序中全局變量地址以及靜態(tài)變量的起始地址需要使用專用的寄存器進行保存。(3) 程序需要顯式給出該程序中符號的名字以及在內(nèi)存中的相對偏移地址[9]。下面通過對編譯器的配置能夠?qū)崿F(xiàn)編譯出與位置無關(guān)的代碼,通過對鏈接器的配置能夠得出程序的符號表,編譯命令如表1所示。
表1 編譯器命令
利用表1中的命令對armcc編譯器配置可將程序文件編譯為可重定向的代碼,其內(nèi)部所有跳轉(zhuǎn)都將采用相對跳轉(zhuǎn)方式,其數(shù)據(jù)段的訪問將采用R9寄存器的地址作為基地址,其目標訪問地址為:dest=R9+offset。參數(shù)中采用global_reg=5命令限制編譯器使用R8寄存器,R8寄存器將作為嵌入式實時操作系統(tǒng)(RTOS)保存目標程序數(shù)據(jù)的專用寄存器[10]。
鏈接器命令如表2所示。鏈接器將導出可執(zhí)行的ELF文件與可執(zhí)行文件的符號表,符號表中包含了可執(zhí)行文件所有的符號信息(函數(shù)名,函數(shù)地址偏移,變量名,變量地址偏移等),符號表經(jīng)過處理后將作為動態(tài)庫之間的重定向的必備信息。
表2 鏈接器命令
續(xù)表2
Fromelf命令如表3所示。Fromelf工具將鏈接器生成的ELF文件轉(zhuǎn)換為可執(zhí)行的.bin文件以及數(shù)據(jù)段文件,這兩個文件最終將要加載到內(nèi)存中。
表3 Fromelf命令
動態(tài)庫重定向具體流程如圖2所示,動態(tài)庫中函數(shù)的重定向主要分為5步。
圖2 動態(tài)重定向的具體流程
(1) 解壓程序包并讀取代碼段與數(shù)據(jù)段到內(nèi)存:將程序包中的符號文件、代碼段與數(shù)據(jù)段解壓到外部存儲器,然后將代碼段載入到Flash或者RAM中,將數(shù)據(jù)段載入到RAM中。
(2) 讀取并解析符號表的數(shù)據(jù)條目:從外部存儲器中讀取一條符號表數(shù)據(jù)并解析獲取符號的偏移、類型、符號名,以及目標動態(tài)庫名。
(3) 將與指定KEY值匹配的函數(shù)指針變量標記為需要重定向:KEY為需要被重定向函數(shù)指針的標識,該值應(yīng)當盡量保證不被用戶用作其他變量的初始化值。
(4) 生成或查找標記的函數(shù)指針變量的跳轉(zhuǎn)函數(shù):跳轉(zhuǎn)函數(shù)鏈接重定向函數(shù)指針與目標函數(shù)。
(5) 重定向需要重定向的函數(shù)到跳轉(zhuǎn)函數(shù):將跳轉(zhuǎn)函數(shù)的入口地址賦值給重定向的函數(shù)指針。
符號表為動態(tài)庫實現(xiàn)的一個核心部分,它包含了各個程序自有的符號表信息,默認的符號表由編譯器導出,默認只包含偏移、類型、符號名三項信息,不能夠滿足重定向的使用要求,經(jīng)過修改的內(nèi)容如表4所示,包含四部分:偏移,類型,符號名,目標庫。當類型為T表示為一個Thumb類型調(diào)用的函數(shù),其偏移地址為在代碼段區(qū)域內(nèi)的偏移;當類型為D時標識為一個變量,其偏移地址為在數(shù)據(jù)段區(qū)域內(nèi)的偏移。當類型為F時表示該程序需要用到的目標動態(tài)庫。需要進行重定向的函數(shù)目標庫一欄將會有效。為提升運行時的效率,其中的符號表信息將在編譯階段完成修改。
表4 符號表
為了實現(xiàn)動態(tài)庫之間的相互調(diào)用,動態(tài)加載平臺還支持函數(shù)的重定向。與傳統(tǒng)重定向不同,本文中的動態(tài)加載平臺的重定向不直接作用于函數(shù)的地址,而是作用于函數(shù)指針,其基本思路如圖3所示。
圖3 重定向示意圖
程序調(diào)用函數(shù)指針,函數(shù)指針在加載階段將會指向全局跳轉(zhuǎn)表,而全局跳轉(zhuǎn)表將指向目標程序的目標函數(shù)。函數(shù)指針實際也是一個變量,將在符號表中以類型D的形式表示,函數(shù)指針調(diào)用的一個典型示例如下,默認為需要重定向的函數(shù)指針賦一個KEY值0xfedcba98,該KEY在編譯器進行符號表信息預處理時用來識別該函數(shù)指針是否需要進行重定向。
typedef int (*_add)(int a,int b);
_add add=0xfedcba98;
void main(void){
add(1,2);
}
跳轉(zhuǎn)函數(shù)由匯編函數(shù)實現(xiàn),且在加載時動態(tài)生成,其主要作用是保護當前程序的R9基地址值,并切換到目標程序的R9基地址值,其匯編指令如表5所示,每一個重定向函數(shù)的跳轉(zhuǎn)函數(shù)只占用20個字節(jié),5條指令。
表5 跳轉(zhuǎn)函數(shù)匯編的實現(xiàn)
實現(xiàn)中斷管理是嵌入式動態(tài)加載平臺模塊化實現(xiàn)的一個重要功能,將系統(tǒng)的中斷功能交給動態(tài)加載平臺的一個專門的模塊來實現(xiàn),以不斷拓展系統(tǒng)的可拓展性。動態(tài)加載平臺的中斷管理流程如圖4所示,其中中斷中繼表包含一個函數(shù)指針向量,動態(tài)加載平臺獲取該中繼表并將其與真實的中斷映射,而程序與動態(tài)庫則注冊自有程序的中斷函數(shù)到中斷中繼表,于是中斷執(zhí)行的步驟就變成:系統(tǒng)中斷→中斷中繼表→注冊的中斷函數(shù)。
圖4 中斷管理流程示意圖
在單地址空間上實現(xiàn)的多任務(wù)為偽多任務(wù),其并不支持虛擬內(nèi)存映射,且通常單地址空間的處理器多為單核處理器,其多任務(wù)的切換也只是時間片的劃分。動態(tài)加載平臺也能夠支持簡單的多任務(wù),也具有多任務(wù)程序的特征:(1) ??臻g獨立;(2) 數(shù)據(jù)段獨立;(3) 任務(wù)區(qū)切換時能夠保存當前運行狀態(tài)。其中數(shù)據(jù)段獨立,保證了不同程序加載到程序運行時不會受到其他程序的影響[11]。在動態(tài)加載平臺上的多任務(wù)切換存在兩個問題:
(1) 通過R9寄存器進行內(nèi)存訪問基地址的多任務(wù)切換與傳統(tǒng)RTOS存在差異,其主要原因是每一個單獨運行的應(yīng)用程序都是靠R9寄存器來進行基地址重定向,任務(wù)切換時會導致程序的變量域還沒有切換,而代碼域已經(jīng)切換到了嵌入式操作系統(tǒng)的代碼域,這將會導致訪問變量錯誤。
(2) 在任務(wù)調(diào)度中的代碼主要由匯編語言完成,而在匯編中不能夠直接進行變量或函數(shù)的調(diào)用,主要原因是利用匯編直接進行的函數(shù)跳轉(zhuǎn)其目的地址必須確定,而重定向后的程序地址并不確定,真實地址是在程序運行后才能夠知曉。
上述問題的解決可通過R8寄存器保護實現(xiàn),即將RTOS模塊的變量域起始地址與需要保護的變量與函數(shù)存入一個數(shù)組,將數(shù)組的首地址在執(zhí)行初始化時放入R8寄存器。在訪問嵌入式操作系統(tǒng)代碼域時,通過匯編代碼將R8寄存器的值賦值給R9,通過代碼控制即可切換到嵌入式操作系統(tǒng)模塊的變量域,而需要訪問保護變量時能夠通過R8寄存器找到需要的變量值。具體部分實現(xiàn)的匯編程序如下:
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;
//調(diào)用任務(wù)切換函數(shù)
pop r9,lr
系統(tǒng)測試主要分為性能分析與功能測試,功能測試主要完成對系統(tǒng)的功能完整性進行測試,性能分析主要利用軟件算法對軟件在未進行動態(tài)加載與加載后執(zhí)行的性能做一個比較。
利用快速排序算法以及斐波那契數(shù)列(遞歸法)生成來分別測試程序在內(nèi)存訪問與函數(shù)調(diào)用與原生軟件之間的性能差異。分別測試了在選擇排序算法與斐波那契數(shù)列生成在加載與未加載執(zhí)行時在不同存儲器內(nèi)的執(zhí)行效率。通過表6可知,在選擇排序算法測試中,加載到內(nèi)部RAM中執(zhí)行效率相比于未加載前在Flash中的執(zhí)行效率只降低了3.46%,而加載到外部RAM中執(zhí)行效率相比于未加載前在Flash中執(zhí)行效率降低了1 131.7%。在斐波那契數(shù)列生成測試中,加載到內(nèi)部RAM中執(zhí)行效率相比于未加載前在Flash中的執(zhí)行效率提升了5.58%,而加載到外部RAM中執(zhí)行效率相比于未加載前在Flash中執(zhí)行效率降低了625.87%。從測試數(shù)據(jù)可以看出,動態(tài)庫加載到內(nèi)部RAM中測試時相比未加載前運行效率差異較小,但是在加載到外部RAM中測試時運行效率卻損失較大,這是因為外部RAM的訪問受到了外部總線帶寬的限制。
表6 性能測試表
測試的硬件平臺為Cortex-M3的STM32F103ZET6處理器,測試程序的功能框架如圖5所示,該測試程序中的功能模塊包含了文件系統(tǒng)、TPC/IP協(xié)議棧、RTOS、中斷管理模塊、動態(tài)加載平臺、遠程應(yīng)用升級模塊、板級支持包、驅(qū)動程序,傳統(tǒng)的升級方式需要將所有的代碼文件進行全部燒寫,測試程序中利用動態(tài)庫進行模塊化的加載,在進行程序升級時,只需要對需要的程序進行升級即可。
圖5 測試程序功能框架
測試場景:溫度采集驅(qū)動升級,將程序中溫度報警模塊的閾值提高到28 ℃。
將本文系統(tǒng)的更新方式與傳統(tǒng)的更新方式在更新體積以及更新時間上做對比,測試結(jié)果數(shù)據(jù)如表7所示,傳統(tǒng)的更新方式是指每次都對所有的應(yīng)用程序進行更新。
表7 更新測試對比表
可以看出本文系統(tǒng)在更新同樣的功能代碼時,需要更新的代碼大小相比傳統(tǒng)方案縮小了218 KB,更新時間縮短了59.2 s,更小的更新代碼體積意味著更小的更新時間與更高的更新成功率。這種更新方式對于復雜的嵌入式系統(tǒng)極為有用,在復雜的嵌入式系統(tǒng)上系統(tǒng)功能復雜,功能劃分清晰,采用模塊化的程序升級方式不僅僅能夠減少更新的代碼體積,還能夠約束開發(fā)者更好地規(guī)劃系統(tǒng)功能模塊的實現(xiàn)。
本文針對傳統(tǒng)的NB-IoT設(shè)備,利用自制Bootloader進行程序升級中存在的弊端進行了改進。利用動態(tài)鏈接庫實現(xiàn)模塊化的程序加載方式,將傳統(tǒng)的一個程序拆分為動態(tài)鏈接庫的方式加載運行,可以大幅度減少程序更新時的數(shù)據(jù)傳輸量和系統(tǒng)能耗,同時還能提高系統(tǒng)的可拓展性與程序的復用性。本文中的單地址空間處理器上的動態(tài)鏈接庫技術(shù)不僅能夠應(yīng)用到NB-IoT程序升級,還能夠應(yīng)用到其他通信方式的遠程程序升級中。該技術(shù)能夠提高單地址空間處理器的可拓展性,程序的模塊化加載還能夠提高處理器資源的利用效率。相比于傳統(tǒng)的程序開發(fā)模式,該技術(shù)能夠拓展單地址空間處理器的應(yīng)用范圍,在嵌入式系統(tǒng)和智能終端領(lǐng)域具備良好的應(yīng)用和推廣價值。