「5.User Manual」

ODB是通过保持状态变化进行调试的概念,并及时向后“导航”以找到问题。ODB是概念验证实现,是用Java编写的。它既没有优化也没有整合。无论如何,我相信你会发现它是你使用过的最有效的调试器。

ODB基于在程序中的每个“兴趣点”收集“时间戳”的想法,然后允许程序员使用这些时间戳来浏览程序运行的历史。“兴趣点”包括:设置变量值(本地,实例,数组元素,静态)、进行方法调用、抛出/捕获异常、创建新对象。调试器将代码插入到类文件中以收集这些“时间戳”。当程序运行时,时间戳将被记录下来。这写功能是通过BCEL库实现的。

ODB不需要任何预处理,或任何特殊的存储库。对正在调试的程序没有限制,甚至没有必要提供源代码。安装后(见下文,安装包括一个Unix脚本),程序员通过调用以下命令运行调试器:

# debug TargetProgram arg1 arg2 arg3

调试器将显示一个小小的”控制窗口“(我们称之为”Debugger Controller“)并启动程序。当程序调用exit()或程序员在控制窗口中按下“Stop Recording”按钮时,”调试窗口“将弹出。程序员将能够浏览时间戳以找到感兴趣的事件。下面是控制窗口的截图:

(图一)

一旦调试窗口启动,即使程序仍在运行,所有记录都将被关闭。你可以使用控制窗口继续录制。下面是调试窗口的截图:

(图二)

导航(Navigation)

在传统的调试器中,导航功能从来都不是问题,因为没有太多想法。你点击继续,程序继续运行,直到它到达下一个断点,就是这样。ODB需要更正式一点,因为有很多方法可以从这里到那里,然后再回来。

ODB提供了多种用于浏览程序运行的方法。它们都旨在使程序员尽可能简单地在程序中获得“感兴趣”的点,并查看它处于什么状态。DODB实现了最“明显”的方法,具有足够的空间添加其他。

主要的导航模式如下:

  • 在「Method Traces」窗口中选择一行:这将使调试器恢复到该方法记录的第一个时间戳的时间。如果该方法未被检测,则调试器将恢复到它所调用的行。你可以通过关闭Trace->Go to first line in method来使调试器始终到达调用行。
  • 在「Code」窗口中选择一行:这会将调试器恢复为该行的时间戳(见下文)。
  • 按下其中一个按钮:这将根据当前选中的对象或该窗格中的行来将调试器恢复到对应的时间戳。(请参阅下面的导航按钮章节。)
  • 在「Stack」窗格中选择一行:这不会更改当前时间戳的行,但会显示堆栈中的方法从何处调用(恢复「Code」窗格)以及本地变量的值。(请参阅「Stack」窗格。)
  • 选择一个线程:该线程将恢复到该线程中最接近的时间戳,早于当前首选的时间。(所有的被检测方法都是同步的,所以时间戳是严格按照顺序的。)
  • 在「TTY Output」窗口中选择一行:将恢复为新选择的行被打印的时间。

在「Code」窗格中选择哪一个时间戳可以通过Code菜单中的一组选项来控制。如果新选择的行在先前选择的行之上,则正常方向是及时倒退。如果不是,则向前。向前走,你会选择一行上的第一个TS(连续集合,例如,呼叫/返回);向后退,你会选择最后一个。通常,时间戳必须位于当前线程中。你也可以将选择限制在同一个方法调用中。(这里很难确定最好的表现,我试过很多想法。)

导航按钮(Navigation Buttons)

按钮对不同的窗格尽可能均匀地工作。四个箭头按钮将恢复为按钮所属窗格中所选对象的第一个、上一个、下一个、最后一个时间。所有的按钮都有工具提示信息。

你可以列出一个变量所有值的列表,并选择其中的一个。命令Object->Select IV Value为实例变量选择一个值,Object-> Select Local Value从局部变量中选择一个值。ODB将恢复到该变量首次取得该值的时间。

下面是导航按钮的截图:

细节(Now for the Details)

如果你刚开始使用ODB,先跳到“安装调试器”部分,并花一些时间操作一下DEMO程序。

小型缓冲区(Minibuffer)

在EMACS之后建模,扩展命令(例如表达式求值和搜索)显示在此处,同样也显示给程序员的消息。

搜索(Searches)

你可以通过【Method Traces】窗格进行增量文本搜索。按下<Ctrl-S>将开始搜索并且MiniBuffer将会获取焦点。键入字符将扩展搜索字符串。再次点击<Ctrl-S>将前进到下一个匹配项目。<Ctrl-R>将反向搜索。<Return>将结束搜索。<Ctrl-G>将中止任何命令。

搜索不区分大小写,并查找两种:屏幕上的确切字符(包括用于很长的名称的省略号(“..”))以及单个对象中的字符(即长名称和字符串的完整名称)。TAB键作为本身输入(不是<Ctrl-I>,也不是“\t”),换行符输入为“\n”。在搜索过程中,双击某个对象会将该对象添加到搜索字符串中。

“事件”搜索通过“Fget”命令(<Ctrl-F>)提供。它们是基于键入的模式作为增量搜索执行的(另一个<Ctrl-F>将查找下一个匹配项,而<Ctrl-R>将在相反方向上搜索)。典型的模式是(注意“Prolog风格”):

port = call & to = THIS & arg0 = <MyObj_5>

它将搜索方法调用(“port = call”),并且其“this”对象将被分配给变量THIS(“to = THIS”。注意:大写的符号表示它是变量),并且其第一个参数(“ arg0“)是<MyObj_5>。

port必须是以下之一:

  • call
  • return
  • enter:方法的第一行;
  • exit:方法中的最后一行;
  • civ:更改实例变量;
  • clv:更改本地变量;
  • chgArray:更改数组元素;

所有的port都定义了“to”(此对象),“toc”(此对象类),“mn”(方法名称),“thr”(线程),“pN”(参数0-9)。

call都定义了”argN” (参数0-9)和”cmn” (调用方法名)。

returns定义了“rv”,表示返回值。

changes(civ\clv\chngArray)定义了“nv”,表示新的值。

字符串需要使用双引号,类对象需要以’#’作为前缀,所有对象都使用’=’和’ !=’比较,整数值可以与’>’和'<‘比较。如下示例:

port = enter & mn = “sort” & toc = #fr.insa.Thing & p0 > 3

将匹配fr.insa.Thing类中名为“sort”的任何方法中的第一行,其中第一个参数的int值且大于3。

“Trace”菜单上有两个命令可以为你创建查询,匹配当前跟踪行(即当前方法调用)或当前源码行。这些将作为“前一个”FGET查询存储,以便键入<Ctrl-F> <Ctrl-F>将显示它们。

在FGET查询中键入<Ctrl-T>将显示程序整个运行过程中的总匹配数(从当前选定的时间开始)。

标记兴趣点(Marking Points of Interest)

ODB为时间戳保留“标记链”。你可以用<Ctrl-SPACE>在“标记链”上添加一个标记。然后你可以使用<Ctrl-X>恢复该标记。“标记链”是圆形的,发就是说如果有多个标记,<Ctrl-X>将循环穿过圆环。

锁和等待集(Locks and Wait Sets)

当一个线程阻塞、等待一个锁、调用wait()时,它所阻塞的对象将显示在「Threads」窗格旁边(例如,<Thread-7>在类对象DemoWait被阻塞)。如果线程在那里显示,伪实例变量_blockedOn将被添加到线程并显示在「Objects」窗格中。三个伪实例变量(_waiters,_owner,_sleepers)将被添加到该对象。如果一个线程阻塞未被检测的代码,ODB将不会注意到。

过滤器(Filters)

如果你有大量不特别感兴趣的行,可以使用Filter菜单选项将其滤除。你可以过滤出个别方法(隐藏它们及其内部),过滤掉比某个限制更深的方法调用,或者过滤出一种方法(隐藏其他所有内容)。而且你可能会不过滤所有的东西。你可以将过滤器保存到.debuggerDefaults文件(参阅options),下次你对这些类和方法进行测试时,它们既不会被检测也不会被记录。

可能最有用的过滤器选项是“滤出”一种方法,将所有方法调用隐藏在所选方法之外。

所有窗格都反映当前的时间戳(使用「Stack」导航时除外)。代码行是由编译器定义的行,所以在一行代码中可能会有很多时间戳。每个方法调用都会生成两个时间戳 : 一个用于调用;一个用于返回。如果检测到该方法,该方法还会记录执行的第一行和最后一行。如果不直接调用return,Sun编译器将隐式返回指令分配给方法的第一行(有点奇怪)。这一行:

t = first(tl.getNext());

会生成五个时间戳,两个调用,两个返回和一个赋值。逻辑、控制流、运算操作不会生成时间戳。这个:

if ( ((a + 71) > (b * c)) || (this == that) ) break;

不会生成时间戳。

交互式的评估表达式(Evaluating Expressions Interactively)

可随时进行任意方法调用。你可以随时将调试器恢复到任意时间,然后使用记录对象的当前值评估方法调用。所以如果你回到时间1234,那么当<Demo_0> .array元素16和17分别是454和123时,调用<Demo_0> .quick(16,17)会将这两个排序。如果你提前到1330,当16和17已经是123和454,那么排序他们不会改变任何东西。

为了保持一致性,这些方法将在不同的“时间线”中执行。基本上,当前状态将被复制到仅用一个时间戳初始化的新时间线中,然后运行该方法(记录自动开启和关闭),填充这个新的时间线。你可以自由地在两个时间线之间来回切换。进一步的方法调用将在运行之前清除辅助时间线。

在辅助时间线中,你可以在运行之前更改实例变量的值。这些命令都在主菜单上,并带有键盘快捷键。<Tab>将自动完成对象和方法名称。<Return>将执行该命令(运行该方法或设置实例变量值)。

这些命令是ODB中最不健壮的部分。交互式的“评估表达式”只接受四个参数。参数必须是对象或类打印字符串(例如Demo,<Demo_0>),字符串,整数,true,false,null。你不能改变最终变量的值。在每个“,”以及其他地方都需要一个空格。必须在MiniBuffer的末尾键入<Return>和<Tab>。你只能更改Object、int、boolean类型的变量的值。锁定状态和等待是无法设置的,因此它们的显示将被设置为“未锁定”和“空”。

垃圾回收(Garbage Collection)

可以记录的时间戳的数量是有限制的。每个简单的时间标记需要三个字(32位系统上的12个字节),每个方法调用/返回大约需要25个字,另外25个字用于JList显示它。然后你的程序可能也需要使用一些内存。

ODB查看MEMORY环境变量(或.debuggerDefaults中的MaxTimeStamps)的值,​​以确定要将多少内存用于时间戳。目前它允许最多MEMORY/200个时间戳。(你会注意到aliases中默认为400MB,允许两百万个时间戳。)超过该数字时,ODB的“垃圾收集器”会踢入并丢弃一些早期时间戳。缺省设置是在命令行中的设置的。例如debug别名:

# java -Xms400100100 -Xmx400100100 -DMEMORY=400100100 -cp $HOME/Debugger/debugger.jar:$CLASSPATH $USER_FLAGS com.lambda.Debugger.Debugger

标志-Xmx为程序设置最大内存,-Xms设置初始内存大小。我们希望避免无用的早期JVM垃圾回收,因此它们被设置为相同的大小。根据你的程序,你可能希望增加这些数字,同时保持MEMORY的值低。这会给你的程序更多的空间。

当然这些都不是垃圾。所以ODB试图保留最可能感兴趣的时间戳(对象改变),同时丢弃那些不是的时间戳。在第一种模式下,ODB丢弃超过实体的50%的本地变量和方法调用的所有时间戳。。如果不够用(收集到的不足10%),则第二种模式将启动并收集对象的实体。

在每次调用时,收集器都会打印出类似如下的一行:

ODB: Collected 244783 out of 326382 stamps and 47268 TraceLines

虽然非常聪明,但并不清楚GC是否真的有用。如果关闭它,收集将会在存储空间填满时停止。(请参阅环境变量GC_OFF。)

ODB的目标(Objective of the ODB)

我最初写了一篇文章描述了这个,以便其他人可以编写适当的调试器/ IDE,以便我可以使用它。然后,我认为如果我能证明这实际上有用,这将是一件好事。所以我写了一个小模型,演变成了这个完整的概念验证。我声称ODB足以证明“万能调试”的思想是有效的并且工作得很好。

因此,有很多关于什么数据最有用的猜测,应该如何显示数据,导航命令是有意义的,等等。大部分IDE的周边部分已经由其他人完成。因此,对于编译,编辑,浏览类等,我简单地假设ODB将被正确地整合到那些效果最好的位中。我用Java写了ODB,因为它很容易。ODB可以同样适用于C,C ++,Eifel等。

优化与限制(Optimizations and Limitations)

除了下面“bug”部分中提到的任何异常外,ODB显示的数据是100%正确的,并且可以放心地在任何Java 1.3、1.4、1.5上运行。(ODB1.3用于JDK1.3,ODB1.4用于JDK1.4,ODB1.5用于JDK1.5)。除非还有什么未知的错误。

如果你不需要什么花样,现在停下来。你可能永远不会使用下面的东西。

尽管ODB可以绝对记录所有内容,但它相当昂贵且不太有用。对于可信任的类(例如JCF类,库类和你自己经过良好测试的类),可能不会对它们进行检测。事实上,默认情况下只是在主方法的包中检查和记录这些类。调试后的代码运行速度更快,调试器不会因无趣的数据而混乱。有一些限制…

这些限制包括:对非监测代码中实例变量(例如obj.iv = value;)的任何直接更改都不会被记录(直接设置实例变量是不好的OOP实践!你应该使用getter和setter );从非监测代码(例如来自事件循环)的调用返回到监测代码将看起来很有趣(调用链将不可见,这些“没有上级”的调用在「Method Traces」窗格中在调用之前显示“**”);在非检测代码中调用System.exit()中的将退出该进程。

你可以清除历史记录并从运行的应用程序记录新数据(例如,在调试某个按钮点击时),或者重新启动它们。你可以在程序中选择一行,并告诉调试器在那里打开/关闭记录。你可以为同一目而选择一行输出。

在已监测代码之外创建的对象只有在监测代码访问后才能识别。实际的创建时间因此是未知的,它们的实例变量值将不被知道。它们将在「Objects」窗格中标有“@”。

有些类只是为了特殊目的而喊叫,手动监测。例如,Vectors真的希望被显示,就好像它们恰好是恰好大小的数组一样。这是通过在监测使用期间将Vector替换为MyVector完成的。

显然,监测ODB使用的类(例如Swing)将不起作用。如果你正在处理这样的代码,你将不得不为你正在使用的代码更改包。例如,要调试ODB,需要完整地复制源代码树,并将“com.lambda.Debugger”包替换为“lambda.Debugger”。在调试Swing本身时,我会做同样的事情。

显示非常大的对象很尴尬。默认情况下,具有超过1000个实例变量(即数组)的对象将仅显示前1000个,并仅打印(Objects>Print)前10000。

选项(Options)

ODB通常在通过java.lang.ClassLoader的子类加载类时插入检测。这是默认为main方法的包中的类完成的。因此,如果你使用Apache的crimson软件包,除非你明确要求它(请参阅.debuggerDefaults),否则它将不会被检测到。从哪里加载文件并不重要。通常,ODB将在目标程序的主线程返回或调用代码调用System.exit()后立即启动主调试器窗口。(exit()方法被调用引发的DebuggerExit异常所取代。)程序中的任何其他线程在下次调用监测代码时将被暂停。

对于软件包中的程序,必须正确设置CLASSPATH(在运行时,aliase会获取该值),因此必须从命令行启动调试器。对于来自jar文件的代码也是如此 – 你必须将jar文件放入CLASSPATH,并从命令行启动调试器。(Microsoft Windows的细节如下。)

% setenv CLASSPATH /home/your_directory/your_code.jar

% debug com.your_company.CoolProgram arg1 arg2

你可以通过将其类和方法名称放入当前目录中的.debuggerDefaults文件中,以此来要求某些方法不被检测或不被记录。

例如,StringBuffer和StringWriter在默认情况下不会被记录下来,因此去掉了「Method Traces」窗格中那些来自于类似于下面代码的行:

“Teach “+ n +”dogs new tricks!”

上面的代码实际上它涉及一个new StringBuffer,两个appends(),一个valueOf()调用。默认未监测的方法是:toString()和valueOf()。那些没有记录的是:toString(),valueOf(),<cinit>(类构造函数 – “初始化静态”),所有的StringBuffer方法。

如果你真的想记录/监测的一切(建议你不要这么做),你可以从默认文件中删除它们。你可以使用“Filter”菜单为其添加方法或手动编辑它(请参阅下面的Defaults File部分)。

你可以手动地在不同目录中监测特定的类:

% debugify MyObject1.class MyObject2.class …

如果你想运行调试器,并让它不处理任何文件,你可以传递DONT_INSTRUMENT标志。(推测你已经手工调试了一些文件。)这可以通过alias/ bat文件完成:

% debugdi MyProgram arg0 arg1…

如果你启动ODB时,没有附带参数或直接从文件管理器中启动,将弹出一个文件选择器并允许你选择一个程序并设置这些选项。这种方法相当有限。有问题的程序必须在默认包中。

Microsoft Windows

除了使用bat替代alias之外,所有的工作方式与上述相同。假设你将调试器放在目录c:\Debugger中,并且你将解压bat文件而不是别名。把它们包括在你的path中,你就可以开始了! 如果你选择其他位置,请适当编辑bat文件。

% jar xf debugger.jar Microsoft

查找源代码(Finding the Source Code)

ODB将在它所加载类文件的同一目录中查找源代码。如果那里没有.java文件,那么ODB将在当前目录中查找。如果它没有找到任何东西,它会弹出一个文件选择器,以便你可以告诉ODB源文件的位置。你添加的任何目录将被写入当前目录中的.debuggerDefaults文件中,并在随后的运行中加载。

未来增强功能(Future Enhancements)

人们可以想象各种各样的事情。列表中最高的是:改变命令行参数,一些更精细的数据显示选项,将调试会话保存到磁盘,与IDE集成,重新编译的代码的动态重新加载,收集的手动检测,新的导航命令,窗格 中显示所有I/O,另一个显示窗口,当然还有性能优化。建议欢迎。

性能(Performance)

这是一个未经优化的概念验证实现。我知道ODB在时间和空间上都有显着的提高。其他实现可能会做得更好。那么你能指望什么?在我的110MHz,128MB SS4上调试调试器时,它工作正常。在具有80MB最大堆大小的SS4上,我可以合理地创建和导航40,000个“典型”方法调用(其中包含10个时间戳)。当你超出可用物理内存量时,性能会有所降低。即使在非常大的虚拟空间中,ODB也运行得很好 – 直到你必须进行垃圾收集。你的程序可能不会。

一个程序可以创建比ODB更多的时间戳记录。你当然可以将最大堆扩展到31位限制(2GB)。我已经成功收集了1亿张时间戳,并能够浏览它们,但它显然比我想要的要慢。收集这么多时间戳未必是有用的,因为通过这么多数据很难找到出路。选择性监测,明智地使用Start和Stop,以及明智的缩小代码到bug中几乎肯定是足够的。

安装ODB(Installing the Debugger)

将jar文件下载到$HOME/Debugger中,然后提取aliases文件(请参阅上面的.BAT文件),并在SHELL中引入该文件。如果你想添加命令行属性(例如:-DYOUR_PROPERTY),你可以设置变量USER_FLAGS,命令别名(debug, debugn, debugp)就可以正常使用了。操作如下:

# cd ~/Debugger

# jar xf debugger.jar aliases

# source aliases

# setenv USER_FLAGS -DYOUR_PROPERTY

# debug YourProgram

或者在Window中:

% cd \Debugger

% jar xf debugger.jar Microsoft

将Microsoft/*.BAT文件放到可以找到的地方。

% set USER_FLAGS=-DYOUR_PROPERTY

% debug YourProgram

运行演示程序(Running the Demo)

启动ODB后,点击“Demo”按钮将会运行一个演示程序(一个多线程的快速排序),然后会弹出调试器,你可以浏览程序。

参考文献

Omniscient Debugger User Manual