2008年10月18日星期六

Javascript调试工具

Javascript作为一门脚本语言,最近受到人们越来越多的关注,例如Chrome推出V8、Mozilla推出TracMonky、WebKit推出SquirrelFish等等Javascript引擎,使得Javascript实现的性能不断的得到提升,甚至更有Microsoft高级开发人员认为Silverlight的对手竟然是Javascript,而不是Flash,还有诸如Gears、prototype、dojo、jquery等JS库的大力发展,可见Javascript的应用前景相当光明。

但是其复杂性也会越来越高,Web开发人员遇到大量的Javascript往往会觉得头痛,甚至觉得Javascript比较诡异而又神奇,从而难以彻底掌握应用好。不过要掌握好Javascript的应用,其调试是个不得不提的话题,很早以前Microsoft就推出了Script Debugger工具,但Web开发人员使用起来还不是特别的方便,作为一个Web开发应用平台的推动者,Mozilla社区却为我们提供了更优秀的Javascript Debugger工具Venkman、Firebug,它们的出现极大的方便了Javascript的调试,让开发人员爱不释手,将它们比着Web开发的瑞士军刀一点都不为过。

今天我们从Venkman、Firebug的使用入手,进而分析其实现原理,同时了解了解WebKit内核的Javascript调试工具及其实现原理。

一、安装使用Venkman、Firebug
如果你还没使用过它们,现在就该动手了。赶快去Mozilla Addon中心下载JavaScript Debugger VenkmanFirebug吧。作为Firefox的extension,Venkman、Firebug不管是下载、安装、更新及使用,都已经与Firefox进行有机结合,使用起来非常的方便。

作为调试工具,其基本的功能不外乎设置断点、Step-in、Step-out、Step-over、Continue、查看参数及调用堆栈等。操作上与用VS调试C/C++代码几乎一致,但考虑各自特点的不同,下面总结一下个人对这两个工具的使用感受:

1、Firebug非常适合普通的Web开发人员,它可以非常方便、直观的查看html、js、dom等详细内容,查看js执行效率以及网络获取状态等,更值得称赞的是调试时动态更改html、js、dom的内容后,浏览效果会及时显现,从而大大的提供修改、调试效率。

网上一篇《初识Firebug 全文 — firebug的使用》很值得刚接触者参考参考。
有关Firebug更加详细的内容请参考Firebug网站

2、Venkman非常适合与Firefox开发相关的开发者如extension开发者等,虽然它的使用没有Firebug那么的直观方便动态更改内容,但其提供了完整的命令行调试模式,这一点与python的pdb很类似;更为突出的是它可以调试Firefox本身以及各种extensions(即chrome源文件,这一点目前Firebug还难以方便的处理),同时提供方便的过滤功能。有了Venkman,开发研究Firefox(当然包括各种有特色的extension及js库等)也就得心应手。

其中Learning the JavaScript debugger Venkman对Venkman有非常详细的介绍,很值得好好阅读阅读。

有关Venkman更加详细的使用请参考其HomePage

3、使用Venkman、Firebug调试Javascript有时会出现一些异常,没有象用Vs、gdb调试C/C++那样的稳定,也许与其本身的实现有些相关,毕竟是调试脚本语言。下面了解一下其实现也许能解除你的一些困惑。

二、Venkman实现
Venkman作为一个extension,其实现完全基于js+xul等相关技术实现。

1、UI
考虑所调试内容的多样性,Venkman UI方面的实现充分的利用了Gecko中的Overlay、XBL等技术要素,同时其显示的项目及其菜单等都是动态生成,界面风格类似于VS,各个显示区域可动态调整、组装。

可以具体关注一下Venkman-bindings.xml、Venkman.xul、Venkman-overlay.xul等实现;

2、调试逻辑控制
整个Gecko内核调试逻辑控制由一个实现了接口Components.interfaces.jsdIDebuggerService,CTRID为 "@mozilla.org/js/jsd/debugger-service;1"的XPCOM组件,其提供的主要接口如下:
interface jsdIDebuggerService : nsISupports
{
...........................................
/**
* Called when an error or warning occurs.
*/
attribute jsdIErrorHook errorHook;
/**
* Called when a jsdIScript is created or destroyed.
*/
attribute jsdIScriptHook scriptHook;
/**
* Called when the engine encounters a breakpoint.
*/
attribute jsdIExecutionHook breakpointHook;
/**
* Called when the engine encounters the debugger keyword.
*/
attribute jsdIExecutionHook debuggerHook;
/**
* Called when the errorHook returns false.
*/
attribute jsdIExecutionHook debugHook;
/**
* Called before the next PC is executed.
*/
attribute jsdIExecutionHook interruptHook;
/**
* Called when an exception is thrown (even if it will be caught.)
*/
attribute jsdIExecutionHook throwHook;
/**
* Called before and after a toplevel script is evaluated.
*/
attribute jsdICallHook topLevelHook;
/**
* Called before and after a function is called.
*/
attribute jsdICallHook functionHook;
attribute unsigned long flags;
attribute boolean initAtStartup;
readonly attribute boolean isOn;
/**
* Turn on the debugger.
*/
void on ();
[noscript] void onForRuntime (in JSRuntime rt);
void off ();
readonly attribute unsigned long pauseDepth;
unsigned long pause();
unsigned long unPause();
/**
* Force the engine to perform garbage collection.
*/
void GC();
void DumpHeap(in string fileName);
void clearProfileData();
/**
* Adds an execution hook filter. */
void insertFilter (in jsdIFilter filter, in jsdIFilter after);
/**
* Same as insertFilter, except always add to the end of the list.
*/
void appendFilter (in jsdIFilter filter);
....................................................
void enumerateContexts (in jsdIContextEnumerator enumerator);
void enumerateScripts (in jsdIScriptEnumerator enumerator);
void clearAllBreakpoints ();
jsdIValue wrapValue (/*in jsvalue value*/);

/**
* Push a new network queue, and enter a new UI event loop.
*/
unsigned long enterNestedEventLoop (in jsdINestCallback callback);
/**
* Exit the current nested event loop after the current iteration completes,
* and pop the network event queue.
*/
unsigned long exitNestedEventLoop ();
};

调试过程主要控制流程如下:
1、由Debugger(如Venkman)发起调试开始,调用DebuggerService.on方法,DebuggerService创建JSDContext,与当前JSRuntimer建立联系,并将DebuggerService中方法设置为当前JSRuntimer的globalDebugHooks对应元素的Hook(即设置回调函数指针);

2、Debugger将DebuggerService中的errorHook、scriptHook、breakpointHook、debuggerHook、debugHook、interruptHook、throwHook等Hook设置成Debugger提供的方法;

3、当JS引擎执行解析、调用JS脚本时,发现其globalDebugHooks中有对应回调Hook,如每解析完一个JS Function时就会调用newScriptHook,进而由DebuggerService调用Debugger提供的scriptHook,Debugger的scriptHook会根据JS引擎提供的文件名、函数名、行号等,读出对应文件显示出来;

4、当在指定文件对应行设置一个断点时,Debugger由DebuggerService调用JS引擎向指定的函数调用Frame中设置Trap操作码;

5、一旦JS引擎执行到Trap操作码,则会调用其globalDebugHooks中对应的回调Hook,进而调用DebuggerService breakpointHook,从而调用Debugger的debugTrap,其中会根据当时的调用上下文调用DebuggerService的DebuggerService的EnterNestedEventLoop方法,它往往会维护一个嵌套计数,然后作事件处理循环,其主要代码如下:
jsdService::EnterNestedEventLoop (jsdINestCallback *callback, PRUint32 *_rval)
{
// Nesting event queues is a thing of the past. Now, we just spin the
// current event loop.
nsresult rv;
nsCOMPtr
stack(do_GetService("@mozilla.org/js/xpc/ContextStack;1", &rv));
if (NS_FAILED(rv)) return rv;
PRUint32 nestLevel = ++mNestedLoopLevel;
nsCOMPtr thread = do_GetCurrentThread();
if (NS_SUCCEEDED(stack->Push(nsnull))) {
if (callback) {
Pause(nsnull);
rv = callback->OnNest();
UnPause(nsnull);
}
while (NS_SUCCEEDED(rv) && mNestedLoopLevel >= nestLevel) {
if (!NS_ProcessNextEvent(thread))
rv = NS_ERROR_UNEXPECTED;
}
JSContext* cx;
stack->Pop(&cx);
NS_ASSERTION(cx == nsnull, "JSContextStack mismatch");
}
else rv = NS_ERROR_FAILURE;
NS_ASSERTION (mNestedLoopLevel <= nestLevel,
"nested event didn't unwind properly");
if (mNestedLoopLevel == nestLevel)
--mNestedLoopLevel;
*_rval = mNestedLoopLevel;
return rv;
}

其中的事件处理循环很关键,在处理事件时被调试的JS脚本无法继续进行执行(即Block),而Debugger仍然可以处理UI事件,如显示当前CallFrame、查看变量值等;

6、当遇到断点时,用户通过Step-in或Step-out等触发JS引擎调用CallHook等,进而根据嵌套计数跳出上面的处理循环,而进入另一个循环或调试结束。

说明:整个过程与传统的windbg、gdb与OS、硬件协同完成Debug过程的实现非常类似,只不过这里的JS引擎相当与OS,它们都非常清楚源文件与具体执行点之间的对应和当时的上下文信息;一样由debugger来提供一些Hook给JS引擎或OS,当JS引擎或OS遇到Trap指令或异常等,它们会及时调用debugger提供的Hook,以由debugger给出下一步执行的决定。

但它们之间有个最大的不同在于JS debugger与运行的被debug的JS脚本都处于同一浏览器进程中;而windbg、gdb调试程序时debugger与被debug的程序往往处于不同的进程。

这里就导致JS debugger中需要过滤的问题,因为不可能在同一进程中调试自己(如用Venkman调试Venkman或用Firebug调试Firebug),这个过滤的动作往往由debugger来决定什么样的源文件可以设置断点、可对其函数进行Step-in、Step-out等,Venkman与Firebug的过滤机制不一样,导致Firebug不能调试chrome类的源文件。

Firebug有关调试方面的实现与Venkman很类似,一样需要利用DebuggerService来实现交互,有时间可具体参考参考。

三、WebKit Javascript调试实现
其实现逻辑与Gecko差不多,首先由Javascript实现提供一个Debugger接口类(相关于Gecko中的DebugHook),然后由一个实现了该Debugger接口的JavaScriptDebugServer来控制调试窗口与被调试页面之间的关系。

一旦JS引擎遇到解析完JS函数、设置的断点、开始函数调用、结束函数调用等则通知(或回调)调试窗口,以控制下一步的操作。

Drosera-Webkit-Trac对其中的设计有一定的说明。

四、总结
对一个开发人员来讲,不管是Web开发、还是Java、C/C++开发,乃至内核、驱动开发等,调试是一门必须掌握的技术,因为它不仅能让你提高工作效率,同时能让你更加深入的了解掌握计算机技术;如果能更一步的了解调试工作原理,则会让你如虎添翼。有空可以看看《软件调试》、《How debuggers work》等。。。

五、参考资源
Venkman HomePage
Learning the JavaScript debugger Venkman
Firebug HomePage
Drosera-Webkit-Trac

Wiki Debugger
Wiki GNU_Debugger(GDB)
Wiki WinDbg
Advanced Debugging(高端调试)