馮 晗,夏璐怡,樊玲玲,裴文良
(1.中國科學(xué)院微小衛(wèi)星創(chuàng)新研究院,上海 201210;2.復(fù)旦大學(xué)軟件學(xué)院,上海 201203)
衛(wèi)星嵌入式系統(tǒng)及其他高實(shí)時嵌入式電子學(xué)系統(tǒng)的運(yùn)行環(huán)境,決定了其極高的維護(hù)難度,因此,在地面測試過程中對故障進(jìn)行充分的數(shù)據(jù)記錄、問題診斷并采取相應(yīng)措施,提高系統(tǒng)的可靠性及安全性,具有重要意義[1-4]。
目前,星載電子學(xué)系統(tǒng)平臺中主控CPU以SPARC體系結(jié)構(gòu)較為常見,其芯片有歐空局的TSC695F和AT697F。相應(yīng)操作系統(tǒng)有VxWorks和RTEMS,其中VxWorks操作系統(tǒng)具有一定的應(yīng)用基礎(chǔ)[5-7]。
通常嵌入式平臺的軟件內(nèi)存布局劃分為代碼段、數(shù)據(jù)段和BSS 段,在VxWorks操作系統(tǒng)中,用戶代碼可以獨(dú)立于操作系統(tǒng)內(nèi)核,也可和操作系統(tǒng)鏈接為一個整體。作為不區(qū)分內(nèi)核態(tài)和用戶態(tài)的操作系統(tǒng),系統(tǒng)啟動完成后和用戶進(jìn)程共享進(jìn)程棧,沒有單獨(dú)的內(nèi)核棧。同時,由于VxWorks操作系統(tǒng)沒有區(qū)分內(nèi)核空間以及用戶空間,因此應(yīng)用程序可以訪問系統(tǒng)內(nèi)核的變量。當(dāng)代碼執(zhí)行觸發(fā)異常時,VxWorks 異常處理函數(shù)代碼的運(yùn)行棧為當(dāng)前進(jìn)程棧或者中斷棧。當(dāng)進(jìn)入異常處理函數(shù)時,首先會在棧首部保存當(dāng)前的上下文信息,該信息可以提供觸發(fā)異常問題的診斷信息。堆在VxWorks 中稱為動態(tài)內(nèi)存池,用于動態(tài)內(nèi)存的分配、進(jìn)程TCB 及進(jìn)程棧空間的分配和信號量隊(duì)列等結(jié)構(gòu)的創(chuàng)建。VxWorks 為中斷上下文在內(nèi)存中分配了獨(dú)立的中斷棧,起始地址和大小初始化完成后,不再變動[8-10]。其整體內(nèi)存布局如圖1 所示。

圖1 內(nèi)存布局圖
在TSC695 體系結(jié)構(gòu)中區(qū)分中斷及異常。異常也可稱為同步中斷。VxWorks操作系統(tǒng)對兩者進(jìn)行區(qū)別維護(hù),有不同的入口機(jī)制和現(xiàn)場保護(hù)機(jī)制[11-16]。
其中,中斷亦稱外部中斷,操作系統(tǒng)為其分配獨(dú)立的中斷棧空間,單次中斷以及中斷嵌套,均使用該中斷棧。內(nèi)部中斷亦稱為異常,則嵌套使用當(dāng)前進(jìn)程的棧或者中斷棧,即如果異常發(fā)生時正在運(yùn)行用戶進(jìn)程,則使用當(dāng)前進(jìn)程的進(jìn)程棧。如果中斷處理過程中有異常發(fā)生,則嵌套使用中斷棧。當(dāng)發(fā)生中斷或者異常時,會在棧幀頭部保存現(xiàn)場,因此關(guān)鍵信息如發(fā)生異常前正在運(yùn)行代碼的PC 指針等信息則會記錄在相應(yīng)的棧幀頭部,通過分析該棧幀頭部數(shù)據(jù)即可獲取異常上下文信息,即可用于后續(xù)故障診斷。VxWorks操作系統(tǒng)異常棧ESF 結(jié)構(gòu)主要信息包括當(dāng)前窗口全部寄存器的值,上一窗口全部寄存器的值以及L1、L2的值(即PC 與NPC 值)。同時中斷保存的信息包括當(dāng)前窗口的寄存器值、發(fā)生中斷的PC和NPC。中斷棧幀的結(jié)構(gòu)如表1 所示。

表1 中斷棧幀主要內(nèi)容結(jié)構(gòu)
當(dāng)發(fā)生任務(wù)切換時,原先進(jìn)程的運(yùn)行上下文會進(jìn)行保存,通常有兩種保存方式,進(jìn)程棧保存和進(jìn)程控制塊(TCB)保存。VxWorks的實(shí)現(xiàn)方式主要為TCB保存,即將進(jìn)程當(dāng)前的運(yùn)行環(huán)境所需要的全部寄存器值等信息,放置在進(jìn)程TCB 相應(yīng)的結(jié)構(gòu)體中[17-19]。VxWorks的TCB 主要成員結(jié)構(gòu)如表2 所示。

表2 TCB主要成員結(jié)構(gòu)
在VxWorks 內(nèi)核5.4 版本中,進(jìn)程創(chuàng)建taskspawn返回的指針值,即是進(jìn)程的TCB的指針,通過該指針即可以訪問一個進(jìn)程的TCB 中的相關(guān)信息。
SPARC 體系結(jié)構(gòu)中,異常亦稱同步中斷,主要包括非法指令、指令數(shù)據(jù)校驗(yàn)多位錯、系統(tǒng)硬件錯誤、存儲器地址未對齊等。為判斷是哪個異常,并查找出異常信息,需要及時診斷相應(yīng)異常棧中的上下文信息。
操作系統(tǒng)提供的異常入口函數(shù)執(zhí)行時,首先將相應(yīng)的異常上下文信息保存在異常棧中,同時將棧的地址指針sp 通過參數(shù)傳遞機(jī)制傳遞給操作系統(tǒng)的處理函數(shù),然后在操作系統(tǒng)函數(shù)excExcHandleHandle中將異常棧中的信息保存到該函數(shù)中的局部變量中,然后調(diào)用操作系統(tǒng)的函數(shù)指針變量_func_excBaseHook,調(diào)用用戶指定的處理函數(shù),將異常棧幀中的內(nèi)容傳遞給應(yīng)用層。該函數(shù)同時傳遞的參數(shù)有異常的向量號vec,則是通過對當(dāng)前TBR 寄存器的值進(jìn)行位與操作及移位操作得到。
通過異常棧幀保存的相關(guān)信息對應(yīng)的偽代碼如下:

同時,TSC695 系統(tǒng)寄存器ERRRSR、SYSFSR和FAILAR 保存了相關(guān)的錯誤信息,可以直接訪問并讀取。

VxWorks操作系統(tǒng)的內(nèi)核變量intCnt和taskIdCurrent 亦可以在充分考慮安全的情況下,直接由應(yīng)用代碼進(jìn)行訪問,其中,intCnt 記錄了當(dāng)前中斷嵌套層數(shù),taskIdCurrent 指向了當(dāng)前進(jìn)程的TCB,為WIND_TCB 結(jié)構(gòu)體類型指針。其中在TCB的0x40的偏移位置記錄了進(jìn)程的優(yōu)先級,在無同等優(yōu)先級的應(yīng)用中,可以讀取優(yōu)先級來判斷是哪個進(jìn)程中出現(xiàn)異常。有同優(yōu)先級輪轉(zhuǎn)算法的應(yīng)用中,可以讀取進(jìn)程名字符串。

其中,TRAP_taskname 為進(jìn)程名字符串的起始內(nèi)存地址,可以根據(jù)具體應(yīng)用,讀取相應(yīng)的進(jìn)程名字符串,并判斷當(dāng)前異常發(fā)生時所在的進(jìn)程。
在VxWorks-for-SPARC 中,非屏蔽中斷,即NMI和普通外部中斷具有相同的入口函數(shù)和現(xiàn)場保存方式。如果中斷發(fā)生,則首先切換到系統(tǒng)預(yù)留的中斷棧幀,保存當(dāng)前的中斷上下文信息,保存完畢后即可調(diào)用C 語言中斷服務(wù)函數(shù)。由于存放中斷棧幀的幀底寄存器g6的值沒有通過參數(shù)傳遞機(jī)制傳遞給C 函數(shù),對于相關(guān)信息的保存沒有異常處理場景方便。
所以在用戶處理函數(shù)中,需要采用合理安全的方法,讀取寄存器g6的值,并避免寄存器使用出現(xiàn)沖突,是讀取中斷棧的核心。讀取g6 寄存器后,便可以通過讀取棧幀結(jié)構(gòu)保存其他中斷現(xiàn)場信息。
內(nèi)聯(lián)匯編方法代碼如下:

其中,定義兩個局部變量,g6_value和p_g6_value,由于是局部變量,編譯器在用戶進(jìn)程棧中分配內(nèi)存空間。內(nèi)嵌匯編中的雙百分號,其中一個為寄存器的原有操作符,另一為內(nèi)聯(lián)匯編語法。其最終的執(zhí)行效果為將寄存器g6的值通過指針的方式做過渡保存到局部變量g6_value 中。獲取g6的值后,可以相應(yīng)的保存中斷棧中的核心信息,如被中斷前的PC 值等上下文信息。

獲取相應(yīng)的PC 信息之后,也可以和異常信息保存方法一致,相應(yīng)的獲取NMI_taskid,即發(fā)生NMI 時的所處進(jìn)程號,NMI 前是否發(fā)生中斷嵌套的計(jì)數(shù)intCnt,以及TSC695 異常信息寄存器ERRRSR,SYSFSR、FAILAR和異常信息保存具有完全相同的操作方法。向量號不需要保存因?yàn)槭荖MI,其向量號是固定的,不需要保存。
在基于嵌入式操作系統(tǒng)的應(yīng)用實(shí)現(xiàn)中,通常設(shè)計(jì)最高優(yōu)先級的監(jiān)管進(jìn)程,該進(jìn)程負(fù)責(zé)處理硬件看門狗,同時負(fù)責(zé)排查全部應(yīng)用進(jìn)程的運(yùn)行情況。同時在應(yīng)用進(jìn)程的運(yùn)行代碼中,加入計(jì)數(shù)清零標(biāo)志,當(dāng)以while 循環(huán)為主體的進(jìn)程完全執(zhí)行一遍功能后,將計(jì)數(shù)清零。
當(dāng)最高優(yōu)先級的監(jiān)管進(jìn)程運(yùn)行時,發(fā)現(xiàn)某進(jìn)程長時間未將計(jì)數(shù)標(biāo)志清零,可判斷該進(jìn)程被阻塞(如等待無效信號量或者隊(duì)列)、異常掛起、或者出現(xiàn)進(jìn)程代碼中觸發(fā)死循環(huán)。此時,高優(yōu)先級監(jiān)管進(jìn)程搶占CPU 運(yùn)行,將陷入死循環(huán)進(jìn)程的運(yùn)行上下文保存在該進(jìn)程的TCB 中,通過讀取TCB的相關(guān)狀態(tài),診斷死循環(huán)進(jìn)程的運(yùn)行上下文。
由于此時taskIdCurrent 指向了監(jiān)管進(jìn)程,故在創(chuàng)建所有應(yīng)用進(jìn)程時,需要將全部進(jìn)程的TCB 指針保存到數(shù)組等結(jié)構(gòu)中,并在監(jiān)管進(jìn)程中,通過查表等方法對應(yīng)到該陷入死循環(huán)進(jìn)程的TCB 指針,并將其轉(zhuǎn)換為UINT32 類型,即為TCB 結(jié)構(gòu)體的起始地址,假設(shè)為變量ErrorTaskId。則后續(xù)保存信息為:

該值為異常TCB 中的進(jìn)程狀態(tài),其中WIND_SUSPEND(0x1)為進(jìn)程被TaskSuspend 掛起,WIND_PEND(0x2)為等待信號量,WIND_DELAY(0x4)為任務(wù)延時,WIND_DEAD(0x8)為死進(jìn)程。根據(jù)該值,可以判斷此進(jìn)程的基本狀態(tài),是等待無效的信號量一直無法運(yùn)行,還是被其他進(jìn)程掛起或進(jìn)程自身掛起。如果進(jìn)程狀態(tài)為WIND_READY(0x0),則進(jìn)程代碼本身可能出現(xiàn)死循環(huán),此時在監(jiān)管進(jìn)程中進(jìn)一步讀取PC和NPC:
其中,0X130是由進(jìn)程結(jié)構(gòu)體內(nèi)WIND_TCB_REGS 與REG_SET_PC 相加得到,0X134是由WIND_TCB_REGS 與REG_SET_NPC 相加得到。得到PC和NPC的值后,可以定位導(dǎo)致進(jìn)程死循環(huán)處的具體代碼所在范圍。
由于PC和NPC 只能確定一層函數(shù)名,當(dāng)該函數(shù)為底層公共函數(shù)時,則不能具體定位問題代碼所在范圍,根據(jù)VxWorks的相關(guān)機(jī)理,當(dāng)進(jìn)程切換后,其窗口寄存器的值全部保存到內(nèi)存棧中,故可以通過棧幀回溯的方法,查找上級函數(shù)調(diào)用。棧幀回溯需要讀取SP、FP的值,故最后一級的函數(shù)調(diào)用中的值需要先行獲取。

這兩個數(shù)據(jù)即指明了最后一個棧幀的棧底和棧頂?shù)刂贰?/p>
接下來,如果whileloop_PC 確定的函數(shù)執(zhí)行了SAVE操作,則返回地址和輸入?yún)?shù)通過IN 寄存器獲取。

如果whileloop_PC 確定的函數(shù)未執(zhí)行SAVE,則返回地址和輸入?yún)?shù)通過OUT 寄存器獲取,代碼如下:

至此,當(dāng)前棧幀中可利用分析診斷的信息全部保存完畢,開始棧幀回溯代碼,查找調(diào)用函數(shù)的再次返回地址和再上一層的FP。

Restore0 代表一級回溯,其再次返回地址通過上一個棧幀中的IN7 寄存器獲取,再上一層回溯的FP以及當(dāng)前棧幀的SP,通過IN6 寄存器獲取。
至此,已經(jīng)查找到3 或4 個PC 指針,分別為whileloop_PC和RPC,Restore0_RPC(或RPC_ 當(dāng)最后一個函數(shù)為葉子函數(shù)時,和父函數(shù)公用一個內(nèi)存中的棧幀,但是當(dāng)前可視窗口的O7和I7其實(shí)分別保存了兩個函數(shù)對應(yīng)的返回地址,故這種情況有4層返回),基本上可以確定函數(shù)的3或4層調(diào)用關(guān)系,查找問題代碼。
通常情況下,除非進(jìn)入很底層的驅(qū)動函數(shù),且底層函數(shù)調(diào)用層次數(shù)比較深,則通常都會查出具體問題所在的行數(shù)。但是保險起見,可以迭代查找,直到停止條件。
迭代方法:

停止條件如下:
1)可采用某次迭代PC 指針為零,則即可停止迭代。停止理由是此時的棧幀已經(jīng)為初始棧幀,初始棧幀僅僅是操作系統(tǒng)為進(jìn)程主函數(shù)提供入口參數(shù),其沒有返回地址,設(shè)置地址為零。
2)根據(jù)初始棧幀,當(dāng)某次讀取的FP 值是進(jìn)程分配的棧頂減去112 字節(jié)初始棧幀的內(nèi)存空間大小時,即可以確定停止條件。此時已經(jīng)回溯到第二個棧幀,即真正的進(jìn)程主入口函數(shù)的棧。
進(jìn)程的棧頂可以通過如下代碼確定,其值保存在進(jìn)程TCB的0X78的位置。
stack_vx_base=*(UINT32*)
(ErrorTaskId+0X78);
在3 種常見故障中,PC和NPC的獲取方法各不相同,TRAP 中是通過ESF 中的異常棧幀獲取,NMI中是通過g6 指向的中斷棧幀獲取,死循環(huán)是通過TCB的方法獲取。
不管哪種方法,獲取的PC均可以提供故障前的現(xiàn)場代碼上下文信息。可以初步定位問題的大體范圍。
死循環(huán)問題現(xiàn)場信息上下文由于進(jìn)程進(jìn)行切換,其窗口寄存器的內(nèi)容全部保存到內(nèi)存,故可以棧幀回溯。發(fā)生NMI 時,棧幀進(jìn)行了切換,從原先的進(jìn)程棧切換到中斷棧,且原先進(jìn)程棧的信息可能仍在窗口寄存器中,沒有保存到內(nèi)存。同理,TRAP的棧幀回溯也不可以直接讀取內(nèi)存中的棧信息。
上面NMI 以及發(fā)生TRAP的情形時,如果需要讀取棧幀回溯信息,需要在匯編中通過多次執(zhí)行save 指令,讓窗口寄存器中的內(nèi)容保存到內(nèi)存,該方法有一定的風(fēng)險。
在這些方法中,3 種故障全部可以讀取系統(tǒng)寄存器SYSFSR、ERRRSR、FAILAR的值,其值主要給出了在trap的情況的下的相關(guān)信息。但是值的分析一般在trap 中利用信息比較大,其他情況可以結(jié)合具體分析提供輔助信息。
3 種情況中,均記錄了intCnt、taskId_Current等信息,intCnt 為操作系統(tǒng)的內(nèi)部變量,記錄了內(nèi)核嵌套層數(shù)(中斷退出代碼中的部分代碼可以被再次嵌套),taskId_Current 記錄了活動的進(jìn)程或被中斷打斷前的進(jìn)程,用于分析中斷可能的故障和定位具體進(jìn)程。
文中對常見的系統(tǒng)故障進(jìn)行了概括與分析,并針對相應(yīng)的系統(tǒng)異常、NMI 中斷和進(jìn)程死循環(huán),提出了記錄相應(yīng)現(xiàn)場信息的方法,以及對應(yīng)的分析方法,其可以提供問題出現(xiàn)時的故障現(xiàn)場信息記錄以及診斷信息。
該方法已經(jīng)在量子科學(xué)實(shí)驗(yàn)衛(wèi)星及遙感觀測衛(wèi)星上被成功應(yīng)用,并成功定位了多個代碼問題,為后續(xù)代碼的可靠性貢獻(xiàn)力量。