具体实施方式
下面将结合附图对本发明的程序测试系统及方法的实施方式进行详细的说明。
图1显示了根据本发明的一个实施方式的在被测系统中驻留的调用转向模块10的结构示意图。该模块用于在被测函数调用时将其调用改向,使测试专用函数能替代原有函数来运行。调用转向模块10包括三个子模块,即转向配置子模块101、转向控制子模块102与桩函数调用子模块103,这三者共同地实现了一种在线补丁技术,即:在不停机情况下,修改系统中某些函数的处理过程,以特定的新定义函数替代原有函数。这种函数替换技术在本发明中仅用于软件调测。
需要说明,函数替换技术也就是补丁技术,现有技术中已提供了一些方法使用补丁技术用来修正已发行到市场的软件BUG。在本发明中的补丁技术服务于市场发布前的调测,与现有技术中补丁技术的应用场合不同。本发明的补丁技术与现有技术中通用的补丁实现方法也不一样,常规的补丁技术通常用jump语句绝对跳转,而本发明是对函数调用栈进行处理,通过修改函数返回值来实现。
其中,转向配置子模块101用于设置如下信息:
1)哪些被测函数需要转向;
2)将要转向至哪个目标函数,即测试专用函数;
3)转向控制的状态标记,如激活、去激活等。
转向配置是针对一个个的被测函数进行的。在本发明中,使用“转向控制块”(Transfer Control Block,TCB)来描述一个函数完成一次转向所需的配置项。转向控制表是多个转向控制块TCB的集合。转向配置子模块101管理转向控制表。
转向控制子模块102包含如下功能:
1)分析主调函数(caller)与被调用函数(callee);从当前调用栈遍历最近各层函数调用的返回地址,由返回地址所处范围结合转向控制表中信息,分析出当前主调函数与被调函数。
例如,假定主调函数是caller_function,被调用函数是callee_function。在callee_function函数体执行前先调用转向控制函数pseudo_call,调用次序为“caller_function->callee_function->pseudo_call”。此处,pseudo插在callee_function首部来执行,从逻辑上理解,pseudo_call属于callee_function范围,但不属于callee_function的固有函数定义。在pseudo_call执行中遍历当前调用栈,可获知pseudo_call与callee_function调用后的返回地址。由于pseudo_call函数是按确定规则插装的,所以可根据pseudo_call的返回地址推算出callee_function的入口地址。根据这个入口地址查询“转向控制表”,就可获得callee_function的转向控制块(TCB)。另外,根据callee_function的返回地址查询转向控制表,一样可获得caller_function的TCB。
2)查询当前调用栈获取调用返回地址;
3)控制当前调用转向到测试专用函数;
4)将测试专用函数的调用结果返回给调用者;
5)记录调用覆盖信息。通过分析出主调与被调函数,来记录“主调-被调”函数对是否被覆盖。
桩函数调用子模块103主要实现测试专用函数的调用。即模拟函数调用的方式,先把参数压栈,再调用测试专用函数。另外还需要先判断当前桩是否激活,若激活就调用测试专用函数(即测试桩)。测试桩包括两种形态:预驻留函数与脚本函数,实现调用都是先把参数压栈后调用函数地址。测试专用函数既包括测试中动态定义的脚本函数,也包括在被测系统中预先驻留的函数。在测试中,用户可定义一个脚本函数,再对被测函数打桩,让脚本函数替换被测函数来执行。而且,由于脚本函数支持在线更新,所以脚本打桩也支持测试桩逻辑在线更新。
为了实现函数调用转向,首先在每个被测函数的首部插入一个转向控制函数pseudo_call。如图2所示,当主调函数caller_function调用callee_function时,callee_function中首条语句就是pseudo_call调用,pseudo_call是图1中调用转向模块的主处理函数,其主要功能是实现调用控制转向。
转向控制函数pseudo_call可实现两类返回跳转,其一是替换方式跳转,让测试专用函数完全替代被调用函数,如图2中实线所标示,callee_function在首行pseudo_call调用后,立即返回到主调函数;其二是插入方式跳转,如图2中虚线标示,pseudo_call执行后,被调函数的定义体仍被执行。在这种情况下,相当于测试专用函数插在被调函数之前运行了。这两类返回跳转所实现的脚本桩分别叫替换模式测试桩与插入模式测试桩。
为实现转向控制,需要解决以下问题。
1.转向控制函数的插装
把pseudo_call函数插入被测函数的首部,可采取四种方式:
1)借助编译器提供的插装功能。
某些编译器支持在函数调用前插入特定桩函数,例如Visual C/C++系列版本支持penter桩插入,使用编译开关“/Gh”可做到这一点。
2)借助符号跳转表实现自动插装
某些编程语言的函数调用,是通过符号跳转表实现的,比如在GCC中,调用某函数,实际是调用它在符号跳转表的某地址,该地址再使用一个jmp绝对跳转语句才跳转到真正函数定义的位置。通过修改符号跳转表即可插入pseudo_call函数调用。
3)借助逗号表达式实现插装
例如在C语言中,已知某符号是函数,可以按以下方式插装:
#define printf(pseudo_call(),printf)
使用逗号可连接多个表达式,运算时返回最后一个表达式的值。上例对printf函数做插装,“printf”被替换成“(pseudo_call(),printf)”,由于逗号运算的特点,语句“printf(“example”)”等效于“(pseudo_call(),printf)(“example”)”,“ptr=&printf”等效于“ptr=&(pseudo_call(),printf)”。
这种借助逗号表达式与宏替换的插装,既可以手工实现,也可以由调测工具自动实现。如本处举例的printf宏替换语句可由工具自动生成,只要确定某符号是函数类型,不需知道它的原型就能实现插装。
4)手工插装
也可以通过手工方式在被测函数首部插入pseudo_call函数。若为方便起见,定义一个宏指向pseudo_call调用,关闭这个宏(即:取消该宏定义(undefine),对应C语言的“#undef”指令)即取消插装。例如在C语言中定义如下宏:
#define begin {pseudo_call();
define end }
在编码时,需要一律以begin作为函数定义开始,以end作为函数定义结束。
以上四种方式中,第二种可以是动态、在线方式把pseudo_call函数插入,其它几种方式是静态的,在被测可执行程序生成之前就插入了。
2.转向控制
为实施测试,需要将特定的子函数调用转向到测试专用函数(即测试桩),为此需要执行转向控制。
以C语言调用栈的组织特点为例。可以修改当前调用栈内的返回地址就实现转向控制。例如函数A主调函数B时,先将主调函数B后要返回的地址压栈,然后进入B函数执行。当B函数尚在运行中,如果把刚压栈的返回地址改成另一函数C的地址值,那么,函数B运行结束就不会返回到函数A,而是转向到函数C。
图2中caller_function调用callee_function,callee_function先调用pseudo_call,这两次调用都先将返回地址压栈。如图3所示,在调用callee_function时(步骤302),先将callee_function的返回地址RetAddr_of_callee压栈(步骤301)。而在调用pseudo_call时(步骤304),先将pseudo_call的返回地址RetAddf_of_Pseudo压栈(步骤303)。在pseudo_call执行过程中,如果将调用栈中pseudo_call返回地址改为callee_function的返回地址,就导致callee_function定义体不被执行,相当于插入运行的pseudo_call函数替代callee_function运行了,这就是替换模式桩。
如果不修改栈中的返回地址,pseudo_call插在callee_function定义体之前运行,就是上文所述的插入模式桩。
3.查找转向控制块
转向控制表用于按地址值索引被测函数的转向控制块(TCB),索引可以有两类地址值,一是被测函数的入口地址,二是被测函数调用子函数时的返回地址。TCB中记录的函数名称、函数原型、函数所在源文件及行号、是否存在补丁函数、补丁函数类型(脚本/预驻留/未知)、补丁是否激活等信息,这些信息以及它对应函数的地址范围可从以下渠道获得:
1)从编译器生成的调试数据库中获取,如VC的PDB文件,GCC的GDB信息记录;
2)从编译过程文件中提取,如C语言的OBJ文件、Delphi语言的DCU文件等;
3)从编程语言开发商提供的dump工具获得。
由于打补丁了的被测函数经常在多线程环境下调用,这要求本发明的补丁机制要支持多线程环境,上面描述的TCB查表过程应采用某种互斥机制来保护操作安全。实现时主要保证TCB查表是多线程下安全的,即TCB内容可动态变化。如果要增删补丁,或修改补丁的状态,就得修改相应TCB内容,称为TCB状态维护。转向操作过程中要获取并使用相应的TCB,其过程要与TCB状态维护操作互斥。该互斥按常规的方法实现即可。
下面将参考图4描述在根据本发明的一个实施方式中对一个被测函数启用脚本补丁、实现调用转向,以及调用结束后关闭补丁的操作过程。
图4左侧4个步骤是概要处理过程,右侧是概要步骤的第3步“发起测试”的分解过程。
在步骤401,针对被测函数设置脚本形式的补丁函数,即填写相应TCB表,表中记录某函数对应某补丁。接着激活该脚本补丁(步骤402)。在脚本补丁被激活后,就可以发起测试,(即运行被测函数,步骤403)。
在步骤403中,首先,进入被测的主调函数caller(步骤4031),主调函数caller调用被调用函数callee(步骤4032)。之后,进入转向控制函数pseudo_call(步骤4033),分析出主调函数与被调用函数(步骤4034)。
接下来,判断是否已定义了脚本补丁且该脚本补丁被激活(步骤4035)。如果是,则设置调用返回的转向地址(步骤4036),然后调用脚本函数,并传递脚本调用的返回值(步骤4037)。否则,从pseudo_call直接返回(步骤4038),完成callee函数调用,并传递该函数返回值(步骤4039)。
在步骤4036或4039传递了调用返回值后,返回到caller函数继续执行(步骤4040),完成被测函数caller调用。
最后,删除或去激活该脚本补丁(步骤404)。
图4操作流程是针对补丁函数是脚本函数的情况,如果补丁函数是被测系统中的预先驻留的函数时,整体流程类似,仅有如下差别:
1)设置补丁函数时指向预驻留函数,而不是脚本函数;
2)判断补丁是否激活之后运行补丁函数时,是调用预驻留函数,而不是脚本函数。
4.自扩展调试器
在转向控制函数pseudo_call执行中,可识别主调用caller函数与被调callee函数。利用此特性,本发明可在pseudo_call函数构造支持单步跟踪的调试机制,本发明要求这个自扩展的调试器基本等效于常规开发语言的单步跟踪机制,即:提供诸如设置断点、单步跟踪、查看变量、修改变量等主要调试功能。
自扩展调试机制在用户应用代码层次实现。该机制不依赖于常规调试器所采用的中断方式实现。所以,本调试器与编程语言自带调试器是可兼容并存的,两者能同时使用。而且,两者可以随时、在线进行切换,切换不必重起被测系统。不必重启被测系统是因为两者机理上不冲突,上述自扩展调试器实际等效于用户自己写的程序(其操作界面在使用时象调试器),它的工作原理与编译器自带的调试器不冲突。这一点在测试工具实施时比较有利,因为编译器自带的调试器功能更为全面(比如能看汇编代码、能读写寄存器等),本发明的自行扩展的调试器就不需面面俱到地实现编译器自带的每一项功能,采用在线切换可降低自扩展调试器因功能不足而带来的负面影响。
具体而言,在执行每个被测函数首部的pseudo_call函数时,先分析出主调函数与被调用函数,从被调函数的TCB中获得当前函数所在的源文件与行号,此时若当前处于断点状态,pseudo_call将等待用户输入下一步跟踪指令,比如函数内单步(Step)、跟入下一级函数(StepIn)、跟出当前函数(StepOut)等、继续执行直到下一断点(Run),pseudo_call根据不同指令对行号栈做不同操作。
在本发明中,设计一个“行号栈”来保存单步跟踪中历史行号的位置,以便函数单步跟出时,能准确识别上级函数,并能跳回到历史跟入的位置。例如,在函数1调用函数2、函数2中又调用函数3时,针对行号信息的压栈与出栈过程如图5所示。其中,出栈过程表示函数3调用结束并返回至函数2、函数2调用结束并返回至函数1、直至函数1结束操作返回上一级函数时的行号栈的变化过程。本领域技术人员可以理解,所述栈是典型的先入后出的数据结构。
如果跟踪指令是“继续执行到下一断点”,也由执行路径上各函数的pseudo_call函数识别出当前函数有无断点,若有断点再分析断点条件是否满足,若断点条件满足就自动进入交互式单步状态。
本发明的自扩展调试器还可支持在断点调试中用测试脚本存取被测系统中的变量,包括全局变量与局部变量,以及用脚本发起被测函数调用。脚本化的变量存取与函数调用可采用一种语言映射技术实现。下面将具体说明这种映射。
图6显示了脚本解释器驱动的实现在线测试系统的框架结构。如图所示,测试主机1包含测试外壳(Test Shell)11,用于发起调测命令,以及提供开发、测试的平台。测试主机端可以采用个人桌面系统(如Windows、Linux等)。测试外壳11可以是其中的一个可执行程序。
目标机2(即被测系统)通常是被测单板,或者是一个仿真程序。目标机2中驻留有测试代理(Test Agent)模块21。该测试代理模块21可以是一个脚本语言系统。在本发明中,对脚本语言的种类并没有限定,只要能满足本发明规定的映射规则即可。在目标机2的测试代理模块21内包含有被测试程序的符号表和类型表。关于符号表和类型表将在后文中说明。
测试主机1的测试外壳11与测试代理模块21具有通信连接。此连接的通信方式可以是共享内存通信(对于目标机与测试主机共用同一台计算机的情况、TCP/IP通信,或是其它如串口通信等形式。
如图7所示,测试代理模块21包括脚本系统211和通讯单元212。通讯单元212负责处理测试代理模块21与测试主机1的测试外壳11的上述通信连接。
脚本系统211包括内嵌调测支持模块2111和映射支持模块2112。内嵌调测支持模块2111是常规的调测应用编程接口(API)的集合,提供诸如复位目标机、启动或停止某任务、设置断点、删除断点等功能。映射支持模块2112用于实现被测系统中的C语言变量与函数向脚本系统映射的功能。在完成映射后,脚本系统211中生成与C语言的变量和函数同名的映射变量与映射函数。
为了实现本发明的目的,要求测试主机1和目标机2都应该有对脚本文件的处理能力。为此,测试主机1的测试外壳11中包含了一个命令行编辑输入单元以及命令解释模块(未示出),命令行编辑输入单元用于支持用户输入脚本文件格式的调测命令,命令解释模块把输入的调测命令转化为能够由测试代理模块21中的脚本解释器解释执行的测试命令,并传送给目标机2的测试代理模块21。测试代理模块21中相应地包含一个脚本解释器(未图示),用于实现测试控制。脚本解释器接收命令解释模块传送来的测试命令,对该命令进行解释并实施相应动作。实际传递的格式是中间码格式,类似Java虚拟机解释字节码命令一样,这个中间码格式也称伪编译码。
当目标机2执行测试并反馈结果、或在测试过程中发出了打印信息,测试主机1的测试外壳11将会接收该结果或信息并进行处理。
参见图8,首先,目标机2要进行初始化处理。当测试主机1启动命令解释模块的程序时,目标机2启动被测程序(步骤801)。然后,目标机2初始化其中包含的脚本系统211,包括设置测试代理模块21的相关配置(步骤802)。在此过程中,测试代理模块21的表生成单元(未示出)从目标机2被测试的软件在其最后一次编译后生成的GDB或PDB调试数据库中提取与被映射的变量/函数相关的信息,生成符号表和类型表(后文将要说明),并记录在测试代理模块21中(步骤803)。然后,测试代理模块21的脚本系统211把符号表及类型表映射到一个全局的变量容器(步骤804)。按照预定的脚本把类型表、符号表的符号都映射至变量容器中(步骤805)。该操作将被测程序中的变量与函数在脚本系统中描述为映射对象(即脚本的TData类对象)。由于转换全部符号会导致CPU与内存资源浪费,因此根据本发明的一个实施方式,不对符号作一次性全部转换,而仅根据测试需要来映射。例如,运行测试脚本时,脚本包含哪些映射的变量与函数,就动态转换那些变量或函数,或者由用户自行确定规则来按需转换,比如所有外部定义的符号都不作映射。
此后,进行被测系统的正常测试(步骤806)。在测试主机1,用户使用类似C语言的脚本文件对传送来的编码进行操作。测试外壳11中的命令解释模块解释用户的输入,并发出相应的测试指令。该命令被传送到测试目标机2,并在其脚本系统211中运行,从而得到按编码逻辑所得到的测试运行结果。这个测试运行结果可被反馈到测试主机1,并显示给用户。在上述方法中,由于可以在线编写测试脚本,直观查看或修改被测变量、调用被测函数,所见即所得,可以马上看到测试结果,也可马上改进测试,从而提高了测试的效率。
因为C语言编码对其数据的结构/类型要求很严格,作为它的映射后的脚本,必须能够支持基本的C编程的功能和要求。
为了实现上述对C语言编码的映射,根据本发明,在目标机2的测试代理模块21中驻留了被测试程序的符号表和数据类型表。这两个表将直接支持本发明规定的映射规则的实现。本发明的映射方法依赖于对目标机系统的各种符号及其类型信息的收集与分析。调试数据库在编译过程中由编译器产生,例如Visual C/C++编译过程中会产生PDB文件,GCC在编译时也将GDB调试信息编译到目标程序中。如上所述,这些信息可在编译过程中由编译器产生的调试数据库中提取,根据所提取的信息相应生成符号表和类型表。
不同的C编译器生成的调试数据库格式并不一样。本发明通过对类型表和符号表的生成达到了对不同格式的统一。类型表记录被测系统定义过的各种类型。这些类型必须包括:主类型信息、子类型信息、以及占用字节数。表1是类型定义格式的示例:
表1:
同时要确保类型表下各栏目具有唯一性,即,由相同的主类型与子类型以及占相同字节数而构成的类型项只能存在一个。
符号表记录变量或函数的地址值及其类型ID。其中符号地址有两种形式,既可以记录绝对地址,也可以记录相对地址。例如,存取函数内局部变量或传入参数时,则使用相对于当前栈顶位置的偏移值。再如,某变量在另一变量空间下存在,其地址也按该另一变量的地址加上一个偏移来表达。
符号来源有两种方式,一是来源于另一映射对象,二是来源于某绝对地址。后者是描述变量或函数的常规方式,前者常在将同一内存空间看作多种类型的变量进行操作时使用,例如强制类型转换,是将某字串数组的某一偏移看成整数类型变量。
在生成了符号表和类型表之后,为了脚本的操作方便,系统要定义与表中数据相应的转化脚本类对象,即TType类对象对应于类型表,TData类对象对应于符号表。在TType类对象定义的实例化数据要包含如下信息:该类型唯一的标识,类型主类别信息,类型子类别信息,类型大小。各个符号映射为TData类定义的实例化数据,要包含以下信息:该符号对应的类型ID,符号的来源,相对于来源的偏址,是否是自动释放内存。
表格内容的映射按图6进行。首先,脚本系统211从符号表里找出需要的符号(包括变量和函数)信息,如符号名称,符号的类型ID及符号地址(步骤901)。然后脚本系统211通过该类型ID查找相应的类型是否有生成的TType类对象,若没有则创建该TType类对象,即通过定义TData或TType类对象,调用类定义的建构函数来创建这个类对象的实例(步骤902)。最后,根据该符号的来源、所属的TType对象、地址偏移及是否自动释放内存等信息创建被映射符号的TData类对象(步骤903)。
在创建的映射实例包括映射变量与映射函数时,首先要有相应的TType对象指明类型信息。当类型表中的各项都转化成TType类实例后,原有类型信息表不再有用,其占用的资源可释放,而用新生成的各个TType类实例组成一个新表,即TType类型表。映射变量与映射函数都是TData类实例。如图7所示,每个TData类实例使用该符号对应的类型ID(Data type)指示它使用TType类型表中的哪个类型。
需要注意,TType类的对象可能是嵌套引用的。某些复合类型如struct/union/指针等包含了子类型,而且同一子类型可能被多个复合类型引用。所以,在创建每个TType对象时,该类型涉及的其它类型也同时被创建。
由于测试主机和目标机的系统资源是有限的,为了更有效地进行测试,在相应时期对使用资源的处理是十分重要的。根据本发明,为了有效利用系统资源,变量、函数、类对象等在脚本语言中作为一个实体存在,实现了生存周期自动管理。这些实体所占用的资源会自动申请、自动释放。映射数据作为脚本类对象,也支持它所涉及资源的自动申请与释放。
例如,脚本系统21要创建一个映射变量buff,它是一个长度为24字节的字串数组,这时脚本系统21会向目标机2自动申请它占用的内存,包括buff变量作为脚本TData类实例要占用的内存空间,以及字串数组的24字节空间。为描述方便,在本发明中,将前一类空间称为脚本实例空间,将后一类空间称为C实例空间。当新创建的映射变量的生存周期结束时,这两种空间都将被自动释放。
但是,由于编程的复杂性,脚本实例空间与C实例空间并不总是同时申请或同时释放的。例如对于被测系统的全局变量,其占用空间是静态分配的,全局变量的映射实例在生存周期结束时,应只释放脚本实例空间,而不应释放C实例空间。再如,使用脚本文件中创建一个映射变量a后,这个变量的脚本实例空间与C实例空间应同时申请或释放。但是,如果把这个映射变量a看成另一类型的映射变量b,例如C语言中的强制类型转换时,将同一地址空间下某变量,看成另一类型的变量,则创建变量b时应新申请脚本实例空间,但不应重复申请C实例空间。另外,当变量a或变量b被删除时,不管先删除哪一个,都应保证尚在使用的另一个变量所操作的C实例空间还有效。也即:C实例空间可被多个映射变量共享,只有该C实例空间所全部涉及的映射变量都释放了,这个C实例空间才自动被释放。此外,映射变量使用的C实例空间还需配合测试,必要情况下可修改其中的标志。例如为映射变量设置Autofree属性,该属性为TRUE表示该映射变量的C实例空间是随它的脚本实例空间释放而释放的,否则属性取值为FALSE,表示C实例空间不随脚本实例空间释放而释放。
为实现上述目的,根据本发明的一个实施方式,对映射实例设置了3个属性。图8显示了映射实例的一个示例。如图8所示,该属性包括:是否自动释放内存(Autofree)、符号来源(Owner)、相对于来源的偏移地址(Offset)。其中“是否自动释放内存”的属性用于指示该映射变量的C实例空间是否跟随脚本实例空间一起释放。“符号来源”用于指示该映射实例的归属对象,其取值既可以是某绝对地址值,也可以是另一个映射实例。相对于来源的偏移地址用于指示本映射实例所用C实例空间的起始地址相对于其符号来源指示的空间地址的偏移量是多少。由于脚本语言具备生存周期自动管理特性,所以如果使符号来源属性指示另一映射实例,就实现了同一C实例空间被多个映射实例共用时也能够实现资源的自动管理。
在这些初始阶段的步骤完成后,系统还要生成一个全局的变量容器。该全局变量容器用于包含所有的变量和函数,并将它们置于统一的平台下。变量容器能捕获针对该实例的属性取值与赋值的操作,并根据特定情况进行操作转义。容器本质上也是脚本的类对象,只不过存取它的属性时,系统会做特定的截获处理。
例如,参考下式定义的变量容器:
vc=newVarContainer();
vc.Var3=MapString(“Another string”);
vc.Var3=“Change value”;
vc.Var4=MapString(“It is example!”);
vc.Var4=vc.Var3;
在这个例子中,第3行和最后一行分别是针对Var3的赋值和取值,最后一行是按照C语言风格描述的。为实现这一点,变量容器下的映射变量的取值和赋值将进行转义,如下两条语句:
vc.Var3=“Change value”;
vc.Var4=vc.Var3;
等同于:
vc.Var3.setValue(“Change value”);
vc.Var4.setValue(vc.Var3.getValue());
可以看到,vc.Var3=“Change value”是合乎C风格的赋值,而vc.Var3.setValue(“Change value”)是调用,不是赋值,远离C风格了。
为支持转义,脚本语言本身的类方法操作要支持定向功能,定向后使用赋值语句最终执行这个调用语句(vc.Var3.setValue(“Change value”))。
变量容器除支持以上取值与赋值转义外,还要支持按脚本方式的变量赋值,即:用指定的值代替变量原有值,而不管原有值是不是映射变量。例如:
vc.Var4=MapString(“It is example!”);
setVcValue(vc,“Var4”,“Another string”);
在这个例子中,第一条语句在变量容器下创建字串映射变量Var4,第二条语句将这个Var4替换成脚本的字串值。该句执行后,Var4不再是映射变量了,而是像常规脚本变量那样赋值,其类型是动态变化的。为此,本发明要求提供用于动态赋值的API函数(类似于本例的setVcValue)。
变量容器的使用对于函数的调用也是重要的。例如,要实现cdecl与stdcall调用,在TData类中定义添加callCdecl与callStdcall两个方法,在脚本中按如下方式调用C函数:
vc.result1=vc.func1.callCdecl(IntType,3,“example”);
vc.result2=vc.func2.callStdcall(CharType,‘A’,‘B’);
这两个方法都要求用第一个参数指明该调用的返回值类型,其它参数依次是完成本次C调用的各参数值。由这两个方法发起调用没有基于一种函数原型,使用有点麻烦,因为调用时要指定返回值类型,而且各传入参数的类型是否匹配、参数个数是否正确都无法检查。为改进这一点,在本发明中对TData添加了对_call_调用的支持,其使用方法如下:
vc.result1=vc.func1._call_(3,“example”);
vc.result2=vc.func2._call_(‘A’,‘B’);
为了让描述方法更简单,本发明定义脚本类的支持内嵌call转向功能,从而把上面的脚本改写成如下方式:
vc.result1=vc.func1(3,“example”);
vc.result2=vc.func2(‘A’,‘B’);
改写后执行效果是等同的,即:当类对象自身作为函数去调用时,系统自动将它转向到该对象的_call_方法。一些现有的脚本语言,如Python与CSE可支持这种转向。本发明借助该机制实现新的应用,即,让映射函数(是脚本实例)调用与C语言在表达上保持一致,如vc.func._call_(3,“example”)未保持一致,而vc.func(3,“example”)是一致的。至此,使用脚本调用C函数的风格就与C语言风格一致了。
除了上述常规的cdecl与stdcall固定参数类型的调用,本发明还需要支持cdecl风格的变长参数调用,例如变长参数函数MyPrint的原型为“void MyPrint(char*format,...)”,根据本发明,可以按如下方式调用它:
vc.result=vc.MyPrint.callCdecl(“name:%s”,“george”);
在此,变长参数调用要在C函数调用发起前,按参数传入值组装成压栈数据。然后模拟C语言调用,获取模拟调用的返回值。
如果映射函数有返回值,则调用映射函数应返回一个映射变量。即原C函数的返回类型不为void时,取它的类型及返回的数值,生成映射变量。
在本发明中,调试操作中修改被测变量、与调用被测函数,可以通过上述调测系统中的语言映射技术实现。
另外,借助上述语言映射技术还可实现模拟被测代码的功能,即,脚本化地存取被测变量与调用被测函数。该功能在自扩展调试器中用于设置条件断点,用一个脚本表达式描述断点条件,只有条件满足程序才会在断点位置停住。
由于本调试机制依赖于在函数首部插入pseudo_call,必然导致单步跟踪只在函数调用的时候才起作用,无函数调用的语句,比如C语言的if、while、switch等控制语句的位置无法在单步跟踪时停住。针对这一情况,根据本发明,把这些非函数调用的语句转化成函数调用,通常使用逗号表达式的插装方式做转化,如下:
#define if(expr) if(pseudo_call(),expr)
#define while(expr) while(pseudo_call(),expr)
这样,由于被测函数的所有控制语句(比如C语言中的if、else、while、for、switch等)与函数调用都支持单步跟踪,并且全局变量与各函数的局部变量都可读写、被测函数可调用,因而自扩展调试器的功能就比较完整,而且,也实现了调试过程的脚本化控制。
5.将调试过程转换成脚本描述
调试过程比较随意,如果把每一个单步跟踪过程都转化为测试脚本,必然导致自动生成的用例可维护性很差,为解决这个问题,本发明遵循第4代白盒测试方法的相关要求,实施灰盒方式测试。
灰盒方式是把被测对象(即被测函数)作为一个整体,测试针对它的外在接口,即被测函数的传入参数、返回值,以及该被测函数涉及的全局变量。该灰盒方式是一种粗粒度调试,基于函数接口而非函数实现过程,保证测试描述既不过于琐碎,也相对稳定,不必每次代码修改就要调整用例。
前文叙述的调试过程实现了脚本化描述,现在为了将这种描述转化成规范用例,还需要取其子集,只将粗粒度的、基于接口的操作转化成测试脚本。在本发明中,将能转化成自动用例的调试操作称为检视操作。检视属于调试的子集。
检视操作(灰盒测试)包括如下步骤:
1.通过查看或修改全局变量,或调用被测系统中的函数来构造测试运行环境;
2.调用被测函数发起测试;
3.在被测函数的首部设置断点,可指定断点条件,单步跟踪到该位置时,可用测试脚本查看与修改传入的参数与全局变量。其它被测函数也可以用测试脚本发起调用;
4.单步跟踪时可以在函数尾部停住。此时,函数返回值与全局变量可以用脚本查看与修改,其它被测函数也可以用测试脚本发起调用。
将检视记录转化为测试脚本(用例)时,应满足以下要求:
1.在一次调试的任意检视操作步骤中,可使用断言(如assert语句)判断运行情况是否与期望值相符。在本发明中,其操作过程能够被记录下来的调试被称为“检视”。检视操作类似于调试操作,可以象调试一样进行单步跟踪,单步跟踪过程中可运行断言语句,例如assert(AValue==BValue),这种断言直接服务于测试。
2.调试中在用户操作界面可查看当前范围的各个变量取值,支持选中某些变量,自动按当前取值生成测试断言。用户可以观察当前各个全局变量的取值,如果希望把某几个变量的取值自动生成结果进行判断,选中这几个变量后再在界面按某个快捷键,即能够自动生成结果判断语句,比如:assert(AValue==5)。
3.当运行断点处于函数的首部与尾部时,用户在操作界面可选择是否将调试操作记录为测试脚本,即:用户可自行决定某些调试操作(如查看变量、修改变量、调用函数)生成到自动用例中,而另一些不必纳入到自动用例中。例如当前检视在函数首部停住,如果这个函数传入参数iValue取值为5,则可以在变量查看列表中选中这个变量,之后在检视操作结果后可自动生成相应的assert语句,例如:assert(iValue==5)。
4.一次调试后生成的测试用例脚本,还可以按手工方式进行调整。调整内容可包括:用户可以手工修改这种自动生成的脚本,如上例“assert(iValue==5)”,用户可把它改为“assert(iValue==6)”。
6.测试程度评估
在转向控制函数pseudo_call中能分析出被测系统中每次调用的主调函数与被调用函数,依据这个特性,可以通过分析测试用例对函数中调用的覆盖程度来评估对每个函数的测试程度。
例如,被测函数共有10项子函数调用,如果测试只覆盖了一、两项调用,说明设计的测试用例是不够的。如果10项中有9项或全部调用都被覆盖到了,则说明当前测试很充分了。
在pseudo_call函数中增加调用信息记录,就可实现一种调用覆盖率统计,即统计测试中已覆盖到的调用个数占被测试函数总调用个数的比例。调用覆盖率实际等效于通常白盒测试工具所支持的语句覆盖。
通过将问号表达式与宏替换组合,可以进一步实现分支覆盖统计。例如在C语言中定义如下宏:
#define if(expr) if((expr)?if_1():if_0())
#define while(expr) while((expr)?while_1():while_0())
在if判断中,如果判断条件为TRUE,则执行插装函数if_1,反之执行if_0。在while循环判断中,如果判断条件为TRUE,则执行插装函数while_1,反之执行while_0。这两种情况实现了C语言的if与while的分支记录,其它分支语句处理过程类似。在其它编程语言中,借助and与or的短路判断也能实现类似的分支插装。
基于调用的覆盖率评估还可以支持客户化的评估定制。在某些情况下,特定的函数调用(例如出错处理函数、非测试关注函数等)不影响测试程度评估,可以将这些特定函数登记到忽略函数表中。在pseudo_call执行中分析出被调函数后,查询忽略函数表判断该被调函数是否该被忽略。若忽略就不纳入覆盖率统计。这种可定制的评估机制,可保障软件研发过程能持续、平稳地以可控的质量向前推进。
本发明适用于软件开发领域,主要针对C语言(但不限于C语言)开发环境的软件调试与测试。