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

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

表1 中斷棧幀主要內容結構
當發生任務切換時,原先進程的運行上下文會進行保存,通常有兩種保存方式,進程棧保存和進程控制塊(TCB)保存。VxWorks的實現方式主要為TCB保存,即將進程當前的運行環境所需要的全部寄存器值等信息,放置在進程TCB 相應的結構體中[17-19]。VxWorks的TCB 主要成員結構如表2 所示。

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

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

VxWorks操作系統的內核變量intCnt和taskIdCurrent 亦可以在充分考慮安全的情況下,直接由應用代碼進行訪問,其中,intCnt 記錄了當前中斷嵌套層數,taskIdCurrent 指向了當前進程的TCB,為WIND_TCB 結構體類型指針。其中在TCB的0x40的偏移位置記錄了進程的優先級,在無同等優先級的應用中,可以讀取優先級來判斷是哪個進程中出現異常。有同優先級輪轉算法的應用中,可以讀取進程名字符串。

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

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

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

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

這兩個數據即指明了最后一個棧幀的棧底和棧頂地址。
接下來,如果whileloop_PC 確定的函數執行了SAVE操作,則返回地址和輸入參數通過IN 寄存器獲取。

如果whileloop_PC 確定的函數未執行SAVE,則返回地址和輸入參數通過OUT 寄存器獲取,代碼如下:

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

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

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