应用EDAC容错技术的星载软件堆栈溢出实时检测方法

2018-09-15 08:38张睿周波郝维宁李露铭乔梁
航天器工程 2018年4期
关键词:堆栈隔离区雷区

张睿 周波 郝维宁 李露铭 乔梁

(北京空间飞行器总体设计部,北京 100094)

星载软件是运行在卫星上的嵌入式软件,典型的星载软件有数管中心计算机软件和控制应用软件。数管中心计算机软件主要负责整个卫星的数据业务[1],对卫星提供遥测、遥控业务。控制应用软件主要对卫星进行姿态轨道控制操作。上述软件按照关键等级划分都属于A类软件,其功能对卫星至关重要。堆栈是软件运行过程中最常用的存储空间资源之一,堆栈中不仅存放了调用函数传递的参数及返回地址,还存放了函数的局部变量。但是,在软件开发的过程中,对于堆栈的分配及实际使用情况并不能获得直观的数据进行比对验证。堆栈分配过多,会造成星载计算机资源严重浪费;分配不足,有可能产生堆栈溢出,造成软件瘫痪。

对于堆栈深度的检测,通常分静态测试方法[2]和动态测试方法。静态测试方法一般由专用工具提供支持,例如由AbsInt公司开发Stack Analyzer堆栈分析工具。该工具没有得到广泛使用,一是由于价格较为昂贵,二是分析过程对函数指针、递归逻辑分析等支持不太到位。动态测试方法即在系统运行过程中对软件使用的堆栈情况进行测试,如上海创景公司的RTInsight工具在和目标系统接口匹配后可用于堆栈的动态测试。另外,也有人通过在系统中设计了专门的测试线程对堆栈进行周期性检查,通过比对特征字的方式获取当前堆栈的使用情况,起到了一定的效果[3]。动态测试方法对测试用例的设计要求较高,如果不能分析出堆栈最深的函数调用路径,即便程序运行相当长的时间,仍然无法获得堆栈最大使用深度,不能确保后续使用过程中不发生堆栈溢出的问题。对于堆栈溢出的防护技术应用比较广泛的有StackGuard和StackSheild[4],其原理是对每个函数都加入部分用于调用时创建“哨兵”和返回时进行比对的代码,由于每个函数调用和返回都要运行这些代码,因此软件运行的效率受到比较大的影响,难以满足星载软件的实时性要求。

综上所述,星载软件开发过程中堆栈使用存在如下问题。①设计师无法掌握堆栈实际使用和余量的情况,凭经验开辟堆栈空间,普遍存在余量不足或浪费的现象。②堆栈深度检测困难,静态测试比较依赖工具,动态测试比较依赖用例设计,效果都难以令人满意。③软件在堆栈溢出前不能提前进行预防,无法提供相应的报警或保护措施。④软件在堆栈溢出后,软件崩溃时的程序运行位置一般都不在产生溢出的代码附近,故障的滞后特性使得问题很难排查[5-6]。⑤软件崩溃后的行为具有不可预测性,存在引起二次故障的风险。另外,也很难设计针对性的防护措施。本文针对上述问题,提出了一种应用EDAC容错技术的堆栈溢出实时检测方法,以BM3803处理器为例,介绍其EDAC容错机制,通过EDAC容错技术,使星载软件具备动态、实时检测堆栈深度和堆栈溢出的能力。

1 BM3803处理器

1.1 堆栈介绍

星载软件在运行时,其程序和数据在内存中被分为代码段(Text Section)、数据段等几个部分,其中,数据段又包括有初值的数据段(Data Section)和无初值的数据段(Bss Section)。内存中剩余的空间被分配给堆空间和栈空间,一般由操作系统对它们进行统一分配和管理。图1为一个星载软件内存分配的示意。

图1 星载软件内存分配Fig.1 Memory map of on-board software

BM3803是采用SPARC架构的处理器[7],由于其具备错误监测与纠正(EDAC)保护机制,对存储空间具备“纠一检二”的能力,因此在航天领域广泛使用。该处理器具有8组窗口寄存器,每组窗口寄存器由8个输入寄存器、8个本地寄存器和8个输出寄存器组成,输入、输出寄存器主要用于向函数传递参数、接收函数的返回值。第6个输出寄存器保存栈指针(sp),指向当前堆栈的栈顶,不再用于保存程序传递参数。因此,输出寄存器最多可以传递6个参数,更多参数通过堆栈传递。第6个输入寄存器保存堆指针(fp),指向当前堆栈的栈底,不再用于保存程序传递参数。程序堆栈由堆指针和栈指针组成,堆栈由高地址向低地址方向生长。每个函数所占用的堆栈称为一个栈帧。如函数A调用函数B,函数B又调用函数C,其堆栈的示意如图2所示。

在操作系统软件的支持下,星载软件一般都由几个线程承载其功能,每个线程的堆栈都从图1中的栈空间中分配获得。线程的堆栈发生溢出,有可能更改了其他线程的堆栈,造成其他线程启动运行时发生错误。

图2 BM3803处理器堆栈示意Fig.2 Stack of BM3803 processor

1.2 BM3803处理器EDAC保护及造错机制

BM3803处理器对外部存储器访问具有EDAC纠错检错功能,其外部存储器宽度一般为40 bit,其中32 bit存放程序指令或数据,8 bit存放指令或数据对应的EDAC校验(BM3803仅产生7 bit校验),其校验生成算法参见文献[7]。当处理器EDAC功能处于使能状态,读取存储器中的数据时,能够纠正32 bit数据和7 bit校验中的1 bit错误,并能够检测2 bit错误。本文利用上述保护机制中的检错部分,处理器对某地址进行字节写入或半字写入操作时,首先需要将包含该字节或半字的32 bit数据和对应的7 bit校验读回,将待写入的字节或半字替换32 bit数据中的相应内容,并重新生成7 bit校验,最终写回该地址。在读取原始数据的过程中,如果该地址存储的40 bit数据存在2 bit错误,则处理器触发0x2B陷阱。

正常情况下,处理器对外部存储器进行写操作时,处理器对待写入的数据进行EDAC计算,得到校验,然后将数据连同校验一起写入存储器,此时,数据和校验是一一对应的。本文方法中,需要利用处理器造错机制,使写入的数据和校验非一一对应,即写入的40 bit数据存在2 bit错误。

造错机制需要设置BM3803处理器的存储器容错配置寄存器1、2、3,具体操作如下。

(1)设置容错配置寄存器1的写旁路(EWB)字段,第22位,第20位,第18位分别为0,1,1,使能写旁路(即对存储器进行造错)。

(2)设置容错配置寄存器2(32 bit数据位对应的翻转位)、容错寄存器3(7 bit校验位对应的翻转位)对需要造错的数据位或校验位进行设置。

(3)操作外部存储器地址,写入32 bit数据,此时待写入的32 bit数据同容错寄存器2中的32 bit翻转位进行异或操作,校验位同容错寄存器3中的低7 bit翻转位进行异或操作。处理器把经过异或的数据位和校验位写入外部存储器。

造错机制的示意如图3所示。

图3 BM3803处理器造错机制示意Fig.3 Error making mechanism of BM3803 processor

2 堆栈溢出实时检测方法

2.1 检测原理

本文针对上述问题,提出一种应用EDAC容错技术的星载软件堆栈溢出实时检测方法,以采用BM3803处理器的星载软件为例,其检测原理为:运用BM3803处理器对随机存储器(RAM)空间的EDAC保护和造错机制,通过造错机制在堆栈空间中设置雷区和隔离区(雷区和隔离区仅仅为逻辑区分,实际功能和设置均相同,具体参见第2.2节),并将雷区和隔离区按照4字节地址对齐进行初始化,使每个32 bit数据均存在双比特错误。在软件运行过程中,随着栈区的生长,当堆栈超过初始允许范围并尝试对雷区进行字节或半字写操作时,BM3803处理器会立即产生EDAC错误陷阱(陷阱号0x2B)。软件通过挂接相应的陷阱处理程序,获得陷阱出错地址,对相应雷区进行清扫,满足堆栈生长,使软件可以继续正常运行,并根据清扫的雷区计算当前堆栈的使用深度。如果堆栈一直生长,直至触碰隔离区,尝试对隔离区进行字节或半字写操作,软件进入陷阱处理程序后可以通过陷阱出错地址分析得出该线程即将发生堆栈溢出,并针对堆栈溢出设计有针对性的恢复措施,如主动复位、切换至备份计算机工作等。本文方法中,核心的2个过程是堆栈初始化和陷阱处理(见图4)。

图4 堆栈初始化和陷阱处理流程Fig.4 Flow of stack initialization and trap processing

需要说明的是,本文方法触发陷阱的条件是对雷区和隔离区进行字节或半字写操作,对应于程序函数中需要存在局部变量,且数据类型为8 bit或16 bit,程序进入雷区后对雷区中存在双比特错误的地址进行读操作,会引发0x9号陷阱,由于C语言编程规范[8]要求软件编码时对所有使用变量进行初始化操作,软件不应存在对局部变量未初始化就进行读取使用的操作,因此不会产生0x9号陷阱。同样的,规范中要求对局部变量进行初始化,对应于程序函数中定义的所有局部变量(包括8 bit或16 bit变量),都需要进行赋值写操作。对于卫星数管软件,从功能上分析,大多围绕遥测、遥控开展,这些功能包括大量协议格式操作和数据转换操作,对应于软件实现过程存在大量的8 bit和16 bit操作,另外,其底层硬件接口,包括串口和1553B总线等,也均为8 bit或16 bit端口,因此,程序在实现过程中,大多数函数均需要定义8 bit或16 bit局部变量进行数据交互操作。综上所述,本文方法非常适用于此类软件。如果软件中绝大多数变量定义是32 bit或64 bit的,则需要在函数中显示增加哨兵,即定义一个8 bit或16 bit局部变量并进行初始化,用于提高程序在雷区或隔离区触发陷阱的概率。

图5给出了一个使用本文方法的线程堆栈生长实例。

注:N为初始雷区的个数;M为堆栈生长经过雷区的个数。图5 一个堆栈生长实例Fig.5 An instance for growth of stack

2.2 堆栈初始化

在线程创建时,一般需要指定其分配堆栈空间的大小,系统为线程分配堆栈后,软件记录堆栈的起始地址和分配深度。本文方法中,对于线程分配的堆栈要求其大小为1 Kbyte的整数倍,便于后续划分和处理,也可以按照其他单位长度进行分配,本文以1 Kbyte为示例进行说明。

将分配的堆栈空间进行划分,具体分为以下3个部分,见图5左半部分。

(1)使用区:初始大小为2 Kbyte,起始地址。该区域为软件正常运行时允许使用的堆栈区域。使用区域的顶部为当前栈顶,软件需要单独记录堆栈当前栈顶,由于使用区初始设置为2 Kbyte,因此初始记录的当前栈顶为起始地址减0x800;

(2)隔离区:位于堆栈分配空间的最后1 Kbyte,隔离区的起始地址等于堆栈区的起始地址减分配深度,再加0x400;

(3)雷区:位于使用区和隔离区之间,每个雷区占用1 Kbyte的空间。

正常情况下,软件在对堆栈空间进行初始化时,将其全部初始化为0。本文方法中,同样将雷区和隔离区中的数据均初始化为0,32 bit数据0对应的7 bit EDAC校验为0xC,利用处理器的造错机制,将实际写入存储器的校验修改为0x0,使其存在双比特错误。软件通过操作BM3803的存储器容错配置寄存器1,2,3,可以对指定地址32 bit数据位和7 bit校验位进行造错。通过软件设置存储器容错配置寄存器1的EWB位使能写旁路,并设置存储器容错配置寄存器2为0x0(即数据位不翻转)、存储器容错配置寄存器3为0xC(即校验的第2位和第3位进行翻转),然后对雷区和隔离区进行清0操作,实际写入的32 bit数据为0x0,实际写入的校验为0x0,即7 bit校验按照设置,其中第2位和第3位发生了翻转。

软件对堆栈进行初始化时,将堆栈中使用区初始空间进行清0。使用上述方法,将雷区和隔离区的空间全部清0,使雷区和隔离区中的所有32 bit数据0存在双比特错误。

2.3 陷阱处理

在初始化堆栈完成后,线程进入正常运行状态,随着软件调用函数等操作,堆栈逐渐生长,其使用范围向低地址扩展,当使用范围超过使用区(初始2 Kbyte)后,进入雷区。由于雷区内双比特错误的存在,程序运行对堆栈使用时的操作类型受到了极大的限制,不可随意进行读写操作,如果程序对雷区任意地址进行字节或半字写操作,该操作会立即引发处理器异常,产生0x2B陷阱。

软件进入陷阱处理程序后,通过读取失效地址寄存器获取发生陷阱的错误地址。通过地址范围比对确认错误地址所在的范围。①将该地址和各个线程的堆栈地址范围进行比较,确定错误地址是否在线程堆栈区域中。②如果错误地址属于某线程堆栈范围,则将错误地址与软件记录的当前栈顶进行比较。若错误地址小于当前栈顶,且大于隔离区首地址,则确定错误发生在雷区;错误地址小于隔离区首地址,则确定错误发生在隔离区。否则,错误发生在堆栈正常工作区域,属于其他问题引起的错误,不在本文的讨论范围内。

对于发生在雷区的错误进行以下操作:①关闭EDAC校验功能;②将当前栈顶减0x400,即释放一个雷区,使当前栈顶向上生长;③按照4 byte地址对齐,依次读取该雷区中的所有数据,每读取一个32 bit数据,将其写回原地址(由于回写过程没有启用造错机制,因此实际写入的32 bit数据为存储器中原来实际存储的数据,实际写入的7 bit校验为32 bit数据对应的校验,即雷区内的双比特错误全部被清除)。④若当前栈顶小于错误地址,则表示当前堆栈使用区已经覆盖程序被访问空间,则使能EDAC校验功能,退出陷阱处理。否则,跳转至②继续执行。

对于发生在隔离区的错误,软件可以根据需要自行制定故障恢复策略,如线程复位、等待狗咬复位或切换至备份设备等。

2.4 堆栈使用深度

对于堆栈的当前使用深度,在软件实时运行过程中,可以通过访问各个线程的当前栈顶,计算得出各个线程堆栈的使用深度(使用区的起始地址减去使用区的当前栈顶,精确到1 Kbyte),堆栈占用率为使用深度除以分配深度,再乘以100%。

3 实例验证及结果分析

本文方法经软件代码实现后,对其运行性能和精度在目标硬件平台上进行测试验证。目标系统处理器采用BM3803平台,主频设置为30 MHz,分别对初始化模块和陷阱处理模块进行性能测试,测试结果如表1所示。从性能测试结果可以看出,与堆栈清0操作的初始化模块相比,本文方法中的初始化模块时延几乎没有增加。另外,由于软件启动后所有堆栈仅初始化1次,因此该延时几乎可以忽略不计。多数情况下,陷阱处理模块每次都只需要清扫1个雷区便可以退出,每个雷区至多进行一次清扫,因此558 μs的处理时间对系统性能基本没有影响。

表1 模块性能测试结果

以某综合电子系统星载软件为被测对象,利用本文方法对其创建的8个线程的堆栈使用情况进行测试,并与采用动态测试工具的测试结果进行比对,测试结果如表2所示。

表2 堆栈使用深度测试结果

本文选取上述测试用例中堆栈使用深度最大的线程进行堆栈溢出测试,测试前将该线程堆栈分配从32 Kbyte缩小为9 Kbyte。测试程序陷阱处理对堆栈溢出的线程进行挂起操作,并通过串口打印产生陷阱的地址、程序指针和线程ID。运行测试程序后,打印串口输出如图6所示。查看程序指针(操作雷区)地址0x4001BD54对应的函数为Insert_Pk_To_Vchannel,操作雷区的汇编指令为对一个字节地址清0。触发陷阱的雷区地址为0x403E9422,位于编号为1的线程分配的堆栈区最后1 Kbyte内,即该地址处于隔离区,因此触发陷阱,线程被挂起。

图6 测试结果输出Fig.6 Output of test result

上述测试结果表明:采用本文方法可以获得堆栈的使用深度,其精确度为1 Kbyte,比动态测试工具测量数据的精确度差,但就堆栈余量观察而言,该精度已经可以满足使用要求。另外,本文方法和大多数动态测试方法相同,如果无法提供使软件达到最大堆栈使用深度的测试用例,便无法测量堆栈最大使用深度。不同的是,本文方法可以在线实时获取堆栈使用深度,并且溢出前可以提供溢出故障现场的关键信息(包括溢出地址、线程ID和溢出时的程序指针等),这些故障现场信息对后续的故障排查工作至关重要。

4 结束语

星载软件在其运行过程中难以感知堆栈的生长过程,不能确定其使用余量是否充足,也难以对堆栈溢出故障进行定位。本文提出了一种应用EDAC容错技术的堆栈溢出实时检测方法,并以采用BM3803处理器的星载软件为例,通过在堆栈中设置多个雷区,使软件具备感知堆栈动态生长并实时计算堆栈占用率的能力,在堆栈栈顶设置隔离区,使软件具备提前获知堆栈溢出的能力。本文方法的优点在于:软件可以实时获取当前堆栈使用深度(精度精确到1 Kbyte);原理简单,容易实现,实现后软件可以在线、实时进行检测。与文献[3]中的方法相比,本文方法无需通过在轨上注测试程序,无需长期在轨周期性运行,因此对星上软件运行与在轨维护影响更少。而且,采用本文方法时,软件在堆栈溢出时会立即由陷阱处理程序接管,从而消除堆栈崩溃后软件行为的不确定性,极大简化系统故障处理应对措施的设计,提升软件的可靠性。

猜你喜欢
堆栈隔离区雷区
绕开老年理财五大“雷区”
4个补钙雷区,您踩过几个?
传染病隔离区护理人员心理状态质性研究
中国英雄
外保内贷,又一个雷区?
因式分解八大“雷区”
动物园饲养动物损害责任的类型化与规则设计
Windows栈缓冲区溢出攻击原理及其防范
缓冲区溢出安全编程教与学
一种航天器软件进程堆栈使用深度的动态检测方法