具体实施方式
在下文中,将参考附图通过实施例对本发明的调试方法以及调试工具进行详细的描述。以下实施例以JAVA环境为例进行说明,所属技术领域的技术人员应当明白,本发明并不限于JAVA环境。
在现有的调试方法中,当发现实际应用平台上以正常模式运行的一个应用程序出现问题时,测试人员通常需要停止应用程序的运行。然后,在调试器中以调试模式重新启动应用程序,调试应用程序,发现并纠正错误。最后,在调试完成后,在实际应用平台上以正常模式重新启动应用程序。图1示意性示出了调试器100和应用程序APP101的关系。APP 101在调试器100模拟的环境中运行,就象在调试器形成的容器内一样。
调试器100提供的模拟环境不同于实际应用平台上的环境。例如,在现有的JAVA调试器100模拟的环境中,不允许在运行时进行实时编译(JIT)。调试器100会产生系统表以返回源代码。调试器100在自己的指令集仿真器上运行程序以便控制程序的运行以及修改程序计数器。而且,调试器100能够得到操作系统的支持来执行诸如陷入(Trap)之类的系统指令。因此,在调试器100中容易读取系统表并且可以中断(或挂起)被调试的APP 101的执行。在中断APP101的执行后,用户可以容易地进行各种调试操作,例如显示和检查代码,计算和编辑程序中的变量,查看寄存器,以及查看应用程序所占用的内存空间。然而,当应用程序在实际应用平台上以正常模式运行时,中断(或挂起)应用程序的执行变成了一个难题。这源于以下事实:当应用程序以正常模式运行时,实时编译和垃圾回收机制会动态改变代码;程序计数器是不可写的;得不到操作系统的帮助来执行Trap等系统操作。因此难以中断以正常模式运行的应用程序的执行,从而难以对正常模式下运行的应用程序进行调试操作。换言之,现有技术不支持在调试模式和正常模式之间热切换。
本发明提供了一种调试方法,可以在不重新启动应用程序的情况下,对运行中的应用程序进行调试。运行中的应用程序一般是已经投入到实际应用中的应用程序,例如购物网站的交易处理程序。运行中的应用程序还可以是运行中的被调试程序,但该被调试程序不在调试环境中运行而是在运行环境中运行,而通过本发明的调试工具调试。
下面参考图2详细描述本发明的调试方法的一个优选实施例的流程,其中在JVM上运行的一个应用程序被调试。
在步骤S202,接收调试命令,所述调试命令至少指定运行中的应用程序中被调试的代码。调试命令的类型可以是设断点、单步执行、条件执行、监视对象、监视堆/栈等等。
例如,用户可以在调试客户端中点击要调试的应用程序的一行源代码以表示在该行设置断点,希望当程序执行到该行时停止,使程序员可以检查程序的执行状态是否正确。此外,调试程序还可以提供按钮或菜单,例如<next step>、<step into>按钮或菜单项,从而命令调试程序进行相应的操作。以下结合用户希望在指定行设置断点的操作进行说明。
调试客户端发送的调试命令至少指定运行中的应用程序被调试的代码。设断点调试命令中指定被调试的应用程序中要设断点的代码的位置,例如指定设断点的类或函数的行号。监视对象调试命令则指定应用程序中要监视的对象名称等等。监视堆/栈调试命令则指定堆/栈、编码的形式和深度等等。此外,调试命令还可以包含实现调试功能的代码、所要调试的类的源代码或字节码等等。
在调试客户端接收了调试命令后,将调试命令传递至调试装置(下文将参考图6,对调试装置进行详细说明)。在调试客户端和调试装置在同一机器上的情况下,调试客户端可以将调试命令直接传输给调试装置。在调试客户端和调试装置在不同的机器上的情况下,调试客户端将调试命令打包,通过网络传递所述调试命令至调试装置。调试客户端可以使用各种已知的传输机制来进行传输。这是本领域技术人员熟知的,在这里不作更详细的描述。
在步骤S204中,调试装置将收到的调试命令解释为执行调试命令的代码。执行测试命令的代码可以预先存储在存储器(例如一个文件)中,这样,调试装置可以根据调试命令中的类型,从存储存储器中查找执行该调试命令的代码。执行测试命令的代码还可以与调试命令一起发送至调试装置。这样,调试装置可以直接将调试命令解释为执行调试命令的代码。
作为例子,调试装置可以将设断点的调试命令解释成执行设断点的代码如下:
示例代码1:
Function breakpoint(){
While(true){//进入死循环,挂起程序
package=readCommandPackageFromUser();//等待从用户接收命
令
cmd=package.command
if(cmd==CONTINUE){//在用户命令是继续时,跳出死循环
return;
} else if(cmd==GET_PARAM_VALUE){//在用户命令是查
看对象时,根据对象的名称获取对象的值
name=package.params[0].name
value=getObjectValueFromJvm(name);
response=new ResponsePackage();
response.value=value;
sendResponseRackageToUser();
}else if(cmd==GET_STACK_TRACE){//在用户命令是查看
栈的信息时,获取当前运行的栈的信息
(*(gdata->jvmti))->GetStackTrace(gdata->jvmti,thread,0,DEPTH,
&frames,&frameCount);
response=new ResponsePackage();
response.trace=&frames;
response.traceDepth=&frameCount;
sendResponseRackageToUser();
}
}
}
上述代码被插入应用程序中要调试的类中用户指定的需要设置断点的行之前。当应用程序运行到该段代码时挂起。这样,通过该段代码即可实现调试时设置断点的功能。
与设断点命令相似,调试装置也将单步调试命令解释为以上代码,只不过将上述代码插入每个步骤之前,以实现单步执行调试。
调试装置将条件执行命令解释为基本上与设断点代码相同,只是将上述示例代码中的while(true)改成while(条件==用户输入)来实现。
在步骤S206中,调试装置将执行调试命令的代码加入运行中的应用程序中被调试的代码。仍然以设断点为例。调试装置首先找到调试命令中指定的类,例如是类A。如果客户端传送了类A的源代码或其地址,则调试装置在类A的源代码中的指定行,例如第10行前,插入上文例示的执行设断点的代码,得到修改的类A的源代码,然后调用编译器将其编译成字节码。另外,如果客户端只提供了类A的字节码,则可以通过反编译得到类A的源代码。如果客户端只提供了类A的名称,则调试装置可以根据类名称从JVM中的代码存储区找到类A的字节码,然后通过反编译得到源代码。JAVA的反编译和实时编译是技术人员所熟知的,在此不作更详细的描述。
可选地,还可以存储在插入执行调试命令的代码之前的原始的类,并建立和维护被修改的类的列表,以便在将来恢复运行原始的应用程序。
接着,调试装置调用JVM的类装载器重新载入修改后的的类以替代原始的类。例如,这可以通过RedefineClasses接口来实现。在RedefineClasses接口的执行过程中,首先在堆(heap)中查找指定的类。找到后(即,指定的类正在执行中),悬挂正在执行的程序,然后通知类装载器从指定的位置重新载入该指定的类的修改后的版本,类装载器可以触发实时编译将重新装载的类的字节码文件即时生效,替换运行中的实时编译过的类的代码。接着通过PopFrame使JVM重新执行改过的方法(注:一个类中可以包括一个或多个方法)。如果在堆中没有找到指定的类(即,指定的类不在执行中),则通知类装载器从指定的位置重新载入指定的类的修改后的版本。RedefineClasses和PopFrame接口的参数是修改的类的名称或修改的方法的名称以及用来替换原始定义的类的字节码的文件地址。在所述的例子中,RedefineClasses和PopFrame接口的参数是类的名称A以及修改后的A的字节码的文件地址。上述的RedefineClasses接口和PopFrame是JVM提供的现有的API。上述的类装载器是现有技术的JVM的一部分,都是技术人员所熟知的,在此不作更详细的介绍。
在步骤S208中,执行带有执行调试命令的代码的应用程序。当应用程序运行到执行设断点的代码时,中断(挂起)应用程序,以等待接收用户进一步的调试命令。也即,程序从正常运行模式进入调试模式。
此时,由于被调试的应用程序处于等待用户的命令的状态,用户可以输入其他调试命令。例如,通过堆数据探测来查看对象的值。通过运行时堆/栈探测来查看堆/栈的状态,下面将结合图3、4此进行说明。用户还可以根据对应用程序的调试结果而修改应用程序或返回运行程序的原始状态,下面将结合图5对此进行说明。这样本发明即可实现不重启运行中的应用程序,而能够在期望的时候进入调试模式。
图3示出了堆数据探测过程和图4示出了运行时堆/栈信息探测过程。该功能通过上述示例代码1中相关代码实现。
如示例代码1所示,当被调试的应用程序执行breakpoint函数后进入调试模式而等待用户进一步到调试命令。如示例代码1所示,支持用户监测对象的值的代码嵌入在breakpoint函数内。当调试装置从接收到监测对象的调试命令后,根据调试命令中指定的对象名称,通过调用getObjectValueFromJvm(),访问虚拟机内部的内容,获得对象的值,并且将对象的值返回给用户。
同样,支持用户检查运行时的栈的代码也嵌入在示例代码1的breakpoint函数内。其中,根据调试命令中指定的编码形式和深度,通过GetStackTrace()获得栈的信息,并且将栈的信息返回给用户。getObjectValueFromJvm()和GetStackTrace()是现有的JVM/TI(JavaVirtual Machine/Tool Interface,Java虚拟机/工具接口)定义的接口。其中,JVM/TI是Java虚拟机提供的用于访问虚拟机内部内容的应用程序接口(API,Application Programming Interface)。TI现在属于Java虚拟机规范的一部分,在任何标准虚拟机中均有实现。利用这些接口,可以实现支持各种调试功能的代理,即通过基于TI编码实现的、经编译后形成的类库(library)。
技术人员容易理解,可以在本发明的执行调试命令的代码中,加入支持其他各种调试功能的代码。例如,支持对运行时堆信息的探测等等。应该注意,上述示出的代码是示例性的,而不是对本发明的限制。在其它实施例中,执行调试命令的代码可以包括更多或更少的功能。
返回图3,详细说明执行堆数据探测的过程。例如,示例代码1中的对象监视通过堆数据探测过程来实现。
在步骤S302中,数据探测器跟踪从堆上进行分配空间产生对象的过程(比如对应于应用程序的“new object”语句的执行),对产生的每个对象标记一个标识(ID)。数据探测器的一个实施例可以是利用JVM/TI定义的接口实现的代理(下面简称为JVM/TI代理)。
在步骤S304,调试装置接收用户输入的观察对象的调试命令后,激活数据探测器。需要注意,当用户发现运行程序的异常想触发即时调试功能时,可以输入观察对象的调试命令,帮助在进入调试模式后对数据探测器进行初始化,为用户提供数据探测和调试数据的功能。
在步骤S306中,数据探测器利用对象的ID,在堆中找到要观察的对象,读取其值和状态。对象的状态是对象的状态属性,例如对象存活的时间长度,来帮助判断是否应该对该对象进行回收检测以及回收该对象占用的堆空间。
在步骤S308,将观察到的对象值和状态信息返回给用户。
图4说明了调试模式中探测运行时堆/栈信息的过程。例如,示例代码1中的运行时栈的监视通过运行时栈信息的探测过程来实现。应该理解,运行时堆信息的探测过程与运行时栈的探测过程类似。
在步骤S402,调试装置接收探测运行时堆/栈信息的调试命令。当应用程序进入调试模式后,用户可能希望获取当前堆/栈的信息以判断应用程序当前状态。这样,用户通过调试客户端向调试装置发送探测运行时堆/栈信息的调试命令。
在步骤S404,调试装置从调试命令中获取用户指定的堆/栈的编码形式和深度。在用户不指定的情况下,可以使用默认的编码形式和深度。
在步骤S406,数据探测器根据所指定的编码形式和深度获得当前运行堆/栈的信息。如上文已经说明的,数据探测器的一个实施例可以是JVM/TI代理。
在步骤S408,调试装置将当前运行的堆/栈的信息返回给用户。
优选地,本发明的调试方法还包括回退步骤。在回退步骤中,删除插入的执行调试命令的代码。
在调试完一段程序后,程序员可以删除指定的一个或多个断点。例如,程序员点击一行源代码上的断点标记表示需要取消在该行设置的断点。于是调试客户端将取消断点的命令、相关的类和源程序的行号打包传输给调试装置。调试装置根据回退命令删除在该行前执行设断点命令的代码。然后,调试装置可以调用类加载器重新加载修改后的类。这样,当再次运行到应用程序的对应行时,应用程序不会在该行挂起,从而删除了该行上的断点。
此外,程序员还可以删除所有断点,以将应用程序恢复到原始状态。例如,程序员可以点击结束调试的命令,或者直接退出调试窗口,表示希望应用程序返回原始状态。于是,调试客户端将结束调试的命令传输给调试装置。调试装置根据在步骤S206中存储的被修改的类列表来确定曾经被修改的类。接着,从所确定的类中删除加入的执行调试命令的代码。然后,调用JVM的类加载器来重新装载不再包括执行调试命令的代码的类。从而消除本发明的调试工具对应用程序的代码带来的改变。
可选地,调试装置还可以在每次修改应用程序的时候,存储不含有执行调试命令的代码的类的版本。于是,在回退步骤中,可以重新加载这些不含有执行调试命令的代码的类版本,以便消除调试工具对应用程序的改变。然后结束调试模式。
下面结合图5具体说明修改应用程序的过程。
在步骤S502,调试装置接收修改应用程序的调试命令。当程序员在调试过程中发现了程序中的错误需要修改应用程序时,调试客户端将修改应用程序的命令以及与修改相关的信息打包成信息包传输给调试装置。其中,与修改相关的信息可以是被修改的完整的类或其所在的地址。可选地,与修改相关的信息可以是被修改的类的名称和修改的代码及其行号。
在步骤504中,调试装置确定被修改的类。如果调试客户端传输了完整的类或其所在的地址,则调试装置可以容易地得到被修改的类。如果调试客户端传输了被修改的类的名称和修改的代码及其行号,则与步骤S206中描述的情形类似。调试装置可以根据类名称从JVM中的代码存储区找到所述类的字节码,通过反编译技术得到源代码。接着,根据接收的修改的代码及其行号,修改所述类的源代码。然后,通过编译器将修改后的类的源代码编译成字节码。如上面已经提到的,JAVA的反编译技术和JIT是技术人员所熟知的,在此不作更详细的描述。
可选地,在步骤S504中,还可以存储修改前的类和被修改的类的列表,以及/或者存储不合用于执行调试命令的代码的类及其类列表。
在步骤S506中,通过调用JVM的类装载器重新载入修改后的类,从而修改了运行中的应用程序。该修改可以是对应用程序的代码的增加、删除和替换。
在步骤S506,如上面已经提到的,可以通过RedefineClasses接口来实现类的重新载入。
此外,用户也可以在设断点的同时修改代码。应当理解,设断点的功能是通过修改应用程序的代码实现的,因此,其他修改应用程序的代码的情形与设断点的过程类似。在此,不再详细说明。
下面结合图6对本发明的调试系统进行详细说明。
图6示意性示出了可以应用本发明的调试技术的调试工具的一个实施例的结构框图。本发明的调试工具包括:调试客户端610和调试装置620。
调试客户端610和调试装置620可以位于同一机器上。或者,如图6所示,调试客户端610和调试装置620可以位于不同的机器上,通过网络连接。
调试客户端610作为一个用户接口,用于接收用户的调试命令、发送调试命令至调试装置、显示调试结果等等。调试客户端610可以是IDE集成开发环境,也可以是一个独立的调试器。调试客户端610可以是简单的命令行形式,或者是可视的调试窗口。可视的调试客户端还可以提供按钮或菜单,例如<next step>,<step into>按钮或菜单项。
调试客户端610包括接收单元611和传输单元612。接收单元611用于从用户接收调试命令。用户可以通过点击按钮、菜单或者源程序所在的行来输入,或者是通过命令行的形式输入。传输单元612用于发送调试命令至调试装置以及接收从调试装置返回的信息。其中,传输单元612将调试命令的类型、所要调试的类和源程序的行号、所要监视的对象等信息打包并传输给调试装置。
调试客户端610和调试装置620可以位于同一机器上。或者,如图6所示,调试客户端610和调试装置620可以位于不同的机器上,通过网络连接。在此情况下,调试客户端610的传输单元612还包括命令打包单元6121、数据翻译单元6122和连接管理器6123。连接管理器6123用于发起调试客户端610与调试装置620之间的连接,并且维护该连接。命令打包单元6121按指定的协议将接收的命令打包成信息包进行传输。数据翻译单元6122则根据指定的协议从接收的信息包中翻译出要显示的数据。
优选地,调试客户端610还包括显示单元613,用于根据从调试装置返回的信息向用户显示要观测的对象的值和状态。
调试装置620用于执行调试功能。调试装置620包括命令解释器621和代码交换器622。调试装置620可以是IDE中的一部分,也可以实现为JVM 630的扩展,从而得到增强的JVM。
命令解释器621用于将接收的调试命令解释成执行调试命令的代码。例如,如果调试命令的类型是设断点,则命令解释器将其解释为执行设断点的代码。如果调试命令的类型是单步执行,则将其解释为N次执行设断点的代码,其中N是被调试的代码的行数目。如果调试命令的类型是条件执行,则将其解释为执行条件执行的代码。在上文中已经例举了执行设断点的代码和执行条件执行的代码的例子,在此不再详细描述。在一个优选实施例中,各种执行调试命令的代码,例如执行设断点的代码、执行条件执行的代码等等,可以作为代码块存储在命令解释器621中。命令解释器621根据调试命令的类型找到对应的执行代码。
代码交换器622用于将执行调试代码的代码加入运行中的应用程序中被调试的代码。代码交换器622首先根据调试命令的类型以及所要调试的类、源程序的行号等信息修改类,在指定的类的代码中插入所述执行调试命令的代码,得到修改后的类。例如,对于设断点的命令,代码交换器622在指定的行插入执行设断点的代码。对于单步执行命令,代码交换器622在被调试的每一行插入执行设断点的代码,如要调试N行代码,则插入N次。对于条件执行的命令,代码交换器622在指定的行插入执行条件执行的代码。然后,代码交换器622调用JVM的类装载器重新载入被修改的类以替代运行中的应用程序中的原始的类。优选地,这可以通过RedefineClasses接口来实现。在用户激活调试功能后,可以用信号触发代码交换器622。每次用户修改了特定的类或者方法时,将触发RedefineClasses接口的调用。在RedefineClasses接口的执行过程中,首先在堆(heap)中查找指定的类,找到后,悬挂正在执行的程序,通知类装载器631,从指定的位置重新载入指定的类。应当理解,本发明是通过代码交换器修改应用程序的代码以加入调试命令,其中将修改后的代码加载到虚拟机是通过调用现有的JVM的类装载器实现的。
在一个实施例中,代码交换器622还可以用于在调试模式下,修改运行中的应用程序。在另一实施例中,代码交换器622还可以用于在调试结束后,从运行中的应用程序中删除执行调试命令的代码。应当理解,将执行调试命令的代码加入运行中的应用程序中是通过修改应用程序的代码实现的,因此,其他修改应用程序的代码的情形与该情形类似。在此,不再详细说明。
优选地,调试装置620还包括数据探测器623,用于执行堆数据探测和/或执行运行时堆/栈信息探测。这样,在调试过程中,用户可以输入检查对象的值以及堆/栈的内容的调试命令。数据探测器623根据用户的调试命令检查对象的值以及堆/栈的内容,并将对象的值以及堆/栈的内容返回给调试客户端610。数据探测器623的一个优选实施例可以是利用TI定义的接口实现的代理,即JVM/TI代理。JVM/TI代理在初始化时,跟踪对象生成,对于每一个产生的对象,打入标记ID。当用户想知道某个对象的值和状态的时候,数据探测器623利用ID和TI接口在堆上查找到需要的对象,读出它的值,返回给用户。每次用户需要获得当前运行堆/栈的信息时,JVM/TI代理将调用TI定义的接口获得当前运行堆/栈的信息,返回给用户。应当理解,数据探测器可以通过JVM提供的各种现有的TI接口来访问虚拟机的内部内容。技术人员容易想到可以利用TI接口实现不同的JVM/TI代理,进行各种数据探测功能。
优选地,调试装置620还包括存储库,用于存储被修改的类的原始代码以及/或者被修改的类的列表。优选地,存储库还可以存储在调试过程中修改的各个版本的类及其记录。当用户期望回退时,代码交换器622可以调用JVM的类装载器重新装载所存储的类的版本之一。
优选地,调试装置620还包括控制器,用于控制调试装置的各个单元的执行,例如控制代码交换器和/或数据探测器的激活和停止。
JVM 630用于执行应用程序。应用程序运行在JVM 630上。JVM包括类装载器631、存储代码的常数池632、堆633以及栈634。代码交换器622将执行调试命令的代码插入对应的类,然后调用JVM上的类装载器631装载修改后的类以替换运行中的应用程序中的原始的类。继而,JVM 630将运行带有执行调试命令的代码的应用程序,从而进入调试模式。在调试模式下,用户可以对应用程序进行各种调试。
以上结合JAVA环境对本发明进行了说明。应当理解本发明还可以应用于其他解释性语言环境,例如,Perl、PHP、Ruby、Python等等。以上所描述的实施例是示例性的,而不是限制性的。所例举的各个步骤不是必不可少的,其顺序也不是限制性的。例如,根据实际的需要,可以定制调试方法,增加或删除某些步骤。或者可以以不同的顺序来执行上述步骤,或者可以并行地执行某些步骤。同样,所例举的调试装置和调试客户端还可以包括更多或更少的单元。
图7示意性示出了可以实现根据本发明的实施例的计算设备的结构方框图。图7中所示的计算机系统包括CPU(中央处理单元)701、RAM(随机存取存储器)702、ROM(只读存储器)703、系统总线704,硬盘控制器705、键盘控制器706、串行接口控制器707、并行接口控制器708、显示器控制器709、硬盘710、键盘711、串行外部设备712、并行外部设备713和显示器714。在这些部件中,与系统总线704相连的有CPU 701、RAM 702、ROM 703、硬盘控制器705、键盘控制器706,串行接口控制器707,并行接口控制器708和显示器控制器709。硬盘710与硬盘控制器705相连,键盘711与键盘控制器706相连,串行外部设备712与串行接口控制器707相连,并行外部设备713与并行接口控制器708相连,以及显示器714与显示器控制器709相连。另外需要指出的是,本发明不但可以在个人计算机中实现,而且还应用于大型工作站,或其它带计算功能的设备中实现。
应当注意,为了使本发明更容易理解,上面的描述省略了对于本领域的技术人员来说是公知的、并且对于本发明的实现可能是必需的更具体的一些技术细节。
提供本发明的说明书的目的是为了说明和描述,而不是用来穷举或将本发明限制为所公开的形式。对本领域的普通技术人员而言,许多修改和变更都是显而易见的。本领域技术人员还应该理解,可以通过软件、硬件、固件或者它们的结合的方式,来实现本发明实施例中的方法和装置。硬件部分可以利用专用逻辑来实现;软件部分可以存储在存储器中,由适当的指令执行系统,例如微处理器、个人计算机或大型机来执行。
因此,应该理解,选择并描述实施例是为了更好地解释本发明的原理及其实际应用,并使本领域普通技术人员明白,在不脱离本发明实质的前提下,所有修改和变更均落入由权利要求所限定的本发明的保护范围之内。