基于TSC695和VxWorks操作系统平台故障诊断方法设计及实现
2022-04-13夏璐怡樊玲玲裴文良
冯 晗,夏璐怡,樊玲玲,裴文良
(1.中国科学院微小卫星创新研究院,上海 201210;2.复旦大学软件学院,上海 201203)
卫星嵌入式系统及其他高实时嵌入式电子学系统的运行环境,决定了其极高的维护难度,因此,在地面测试过程中对故障进行充分的数据记录、问题诊断并采取相应措施,提高系统的可靠性及安全性,具有重要意义[1-4]。
1 系统原理及栈帧结构
目前,星载电子学系统平台中主控CPU以SPARC体系结构较为常见,其芯片有欧空局的TSC695F和AT697F。相应操作系统有VxWorks和RTEMS,其中VxWorks操作系统具有一定的应用基础[5-7]。
1.1 VxWorks操作系统运行内存布局
通常嵌入式平台的软件内存布局划分为代码段、数据段和BSS 段,在VxWorks操作系统中,用户代码可以独立于操作系统内核,也可和操作系统链接为一个整体。作为不区分内核态和用户态的操作系统,系统启动完成后和用户进程共享进程栈,没有单独的内核栈。同时,由于VxWorks操作系统没有区分内核空间以及用户空间,因此应用程序可以访问系统内核的变量。当代码执行触发异常时,VxWorks 异常处理函数代码的运行栈为当前进程栈或者中断栈。当进入异常处理函数时,首先会在栈首部保存当前的上下文信息,该信息可以提供触发异常问题的诊断信息。堆在VxWorks 中称为动态内存池,用于动态内存的分配、进程TCB 及进程栈空间的分配和信号量队列等结构的创建。VxWorks 为中断上下文在内存中分配了独立的中断栈,起始地址和大小初始化完成后,不再变动[8-10]。其整体内存布局如图1 所示。
图1 内存布局图
1.2 异常栈帧及中断栈帧
在TSC695 体系结构中区分中断及异常。异常也可称为同步中断。VxWorks操作系统对两者进行区别维护,有不同的入口机制和现场保护机制[11-16]。
其中,中断亦称外部中断,操作系统为其分配独立的中断栈空间,单次中断以及中断嵌套,均使用该中断栈。内部中断亦称为异常,则嵌套使用当前进程的栈或者中断栈,即如果异常发生时正在运行用户进程,则使用当前进程的进程栈。如果中断处理过程中有异常发生,则嵌套使用中断栈。当发生中断或者异常时,会在栈帧头部保存现场,因此关键信息如发生异常前正在运行代码的PC 指针等信息则会记录在相应的栈帧头部,通过分析该栈帧头部数据即可获取异常上下文信息,即可用于后续故障诊断。VxWorks操作系统异常栈ESF 结构主要信息包括当前窗口全部寄存器的值,上一窗口全部寄存器的值以及L1、L2的值(即PC 与NPC 值)。同时中断保存的信息包括当前窗口的寄存器值、发生中断的PC和NPC。中断栈帧的结构如表1 所示。
表1 中断栈帧主要内容结构
1.3 VxWorks for SPARC任务TCB结构
当发生任务切换时,原先进程的运行上下文会进行保存,通常有两种保存方式,进程栈保存和进程控制块(TCB)保存。VxWorks的实现方式主要为TCB保存,即将进程当前的运行环境所需要的全部寄存器值等信息,放置在进程TCB 相应的结构体中[17-19]。VxWorks的TCB 主要成员结构如表2 所示。
表2 TCB主要成员结构
在VxWorks 内核5.4 版本中,进程创建taskspawn返回的指针值,即是进程的TCB的指针,通过该指针即可以访问一个进程的TCB 中的相关信息。
2 故障处理机信息保存
2.1 异常的处理及信息保存
SPARC 体系结构中,异常亦称同步中断,主要包括非法指令、指令数据校验多位错、系统硬件错误、存储器地址未对齐等。为判断是哪个异常,并查找出异常信息,需要及时诊断相应异常栈中的上下文信息。
操作系统提供的异常入口函数执行时,首先将相应的异常上下文信息保存在异常栈中,同时将栈的地址指针sp 通过参数传递机制传递给操作系统的处理函数,然后在操作系统函数excExcHandleHandle中将异常栈中的信息保存到该函数中的局部变量中,然后调用操作系统的函数指针变量_func_excBaseHook,调用用户指定的处理函数,将异常栈帧中的内容传递给应用层。该函数同时传递的参数有异常的向量号vec,则是通过对当前TBR 寄存器的值进行位与操作及移位操作得到。
通过异常栈帧保存的相关信息对应的伪代码如下:
同时,TSC695 系统寄存器ERRRSR、SYSFSR和FAILAR 保存了相关的错误信息,可以直接访问并读取。
VxWorks操作系统的内核变量intCnt和taskIdCurrent 亦可以在充分考虑安全的情况下,直接由应用代码进行访问,其中,intCnt 记录了当前中断嵌套层数,taskIdCurrent 指向了当前进程的TCB,为WIND_TCB 结构体类型指针。其中在TCB的0x40的偏移位置记录了进程的优先级,在无同等优先级的应用中,可以读取优先级来判断是哪个进程中出现异常。有同优先级轮转算法的应用中,可以读取进程名字符串。
其中,TRAP_taskname 为进程名字符串的起始内存地址,可以根据具体应用,读取相应的进程名字符串,并判断当前异常发生时所在的进程。
2.2 非屏蔽中断的处理及信息保存
在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,其向量号是固定的,不需要保存。
2.3 进程死循环的处理及信息保存
在基于嵌入式操作系统的应用实现中,通常设计最高优先级的监管进程,该进程负责处理硬件看门狗,同时负责排查全部应用进程的运行情况。同时在应用进程的运行代码中,加入计数清零标志,当以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 信息分析与诊断
3.1 PC和NPC的分析
在3 种常见故障中,PC和NPC的获取方法各不相同,TRAP 中是通过ESF 中的异常栈帧获取,NMI中是通过g6 指向的中断栈帧获取,死循环是通过TCB的方法获取。
不管哪种方法,获取的PC均可以提供故障前的现场代码上下文信息。可以初步定位问题的大体范围。
死循环问题现场信息上下文由于进程进行切换,其窗口寄存器的内容全部保存到内存,故可以栈帧回溯。发生NMI 时,栈帧进行了切换,从原先的进程栈切换到中断栈,且原先进程栈的信息可能仍在窗口寄存器中,没有保存到内存。同理,TRAP的栈帧回溯也不可以直接读取内存中的栈信息。
上面NMI 以及发生TRAP的情形时,如果需要读取栈帧回溯信息,需要在汇编中通过多次执行save 指令,让窗口寄存器中的内容保存到内存,该方法有一定的风险。
3.2 系统寄存器的分析
在这些方法中,3 种故障全部可以读取系统寄存器SYSFSR、ERRRSR、FAILAR的值,其值主要给出了在trap的情况的下的相关信息。但是值的分析一般在trap 中利用信息比较大,其他情况可以结合具体分析提供辅助信息。
3.3 内核变量的分析
3 种情况中,均记录了intCnt、taskId_Current等信息,intCnt 为操作系统的内部变量,记录了内核嵌套层数(中断退出代码中的部分代码可以被再次嵌套),taskId_Current 记录了活动的进程或被中断打断前的进程,用于分析中断可能的故障和定位具体进程。
4 结束语
文中对常见的系统故障进行了概括与分析,并针对相应的系统异常、NMI 中断和进程死循环,提出了记录相应现场信息的方法,以及对应的分析方法,其可以提供问题出现时的故障现场信息记录以及诊断信息。
该方法已经在量子科学实验卫星及遥感观测卫星上被成功应用,并成功定位了多个代码问题,为后续代码的可靠性贡献力量。