一种基于芯片仿真VCD波形格式的Java程序自动调试方法

    

    摘 要:为提高调试的效率,作者提出一种Java程序自动调试方法。基于JDI技术实现了Java程序的自动调试,设计了用户代码的单步进入和Java库代码的单步跳出的切换调试策略,最终得到程序运行过程中所有变量值的变化轨迹,并利用Linux脚本将其保存成芯片仿真领域波形数据翻转的通用VCD存储格式。开源VCD文件查看器所展示的效果说明这种Java程序自动调试方法有助于学生理解程序运行的全局过程,能激发学生提出细节问题,远胜于当前纯软件的人工单步调试方法。此外,文章所述的调试方法作为综合运用软硬件技术的教学案例,适合培养软硬件综合人才的多学科协同创新能力,对教育信息化软件的内涵提升提供了借鉴。

    关键词:Java程序;自动调试;芯片仿真VCD格式;软硬件协同创新

    中图分类号:TP312 ? ? ? ? 文献标志码:B ? ? ? ? ?文章编号:1673-8454(2019)12-0091-06一、引言

    充分利用信息技术手段激发学生的创新热情、提高各个阶段的教学质量和教学效率已成为国家教育事业发展“十三五”规划的重点[1],因此,未来信息技术课程的教学质量尤其重要,承载着国家教育信息化的愿景,直接决定了能否实现信息技术与教育过程的深度融合。作为经典的信息技术课程, Java不仅仅是一门程序设计课程,更是当今诸多实用软硬件技术环境的代名词。一方面,Java与Linux脚本相结合演化而来的Scala语言同时是大数据人工智能软件Spark和开源处理器RISC-V的设计语言,代表了程序设计的发展方向。另一方面,Java技术生态圈中的Java虚拟机(Java Virtual Machine, JVM)提供了越来越多的先进技术,而这些先进技术已经很难在常规程序设计课程中教学,问题的难度和广度远超出了课堂容量。例如,Java调试接口(Java Debugging Interface, JDI)技术用于远程调试服务器的出错线程[2];JVM反射技术用以实现类的动态加载和服务器软件的容器内热部署;再如,实践中需要十分综合的软件背景和硬件知识,而不是孤立的编程语言课程或硬件电路设计课程,才能理解JVM反射应用于开源RISC-V处理器生成环境的连线生成过程[3]。

    众所周知,调试是精通程序设计的必要手段,但对于Java程序,不论经典的jdb命令行调试工具还是Eclipse等图形界面调试工具,都难以从宏观整体上把握程序的运行过程,很难直观理解静态的程序与动态的进程之间的关系。可见,在这样一个信息技术日新月异的时代,Java程序设计课程作为教育信息化战场的排头兵,亟待在Java调试方法上取得突破。理想的Java调试方法要与信息化教学工具合二为一,不仅要能多维度地启发学生的思考与提问,实现学习、应用与创新的密切结合从而成为素质教育的平台,还要尽可能地将学习内容拓展到软硬件融合创新的层面,最终实现软硬件综合人才多学科协同创新能力的培养,全面提升国家教育信息化从业人员的技术水平。

    反观硬件设计和芯片仿真领域,其中有很多成熟的电子设计自动化工具可以借鉴,通过发掘软件问题与硬件问题的相似性,往往可以用极低的投入整合出巧妙的软件功能。例如,芯片仿真验证用到的System Verilog语言具有生成约束随机激励的功能,如果将硬件激励的每一位与试题库中的每道题目相对应,就能类比地解决约束随机抽取题目的自动组卷问题[4]。因此,以成熟的芯片仿真验证软件为基础作二次开发能有效地加速教育信息化软件的快速原型化。本文基于芯片仿真领域的波形变化值存储(Value Change Dump, VCD)格式保存Java程序执行过程中的所有变量值的变化轨迹,并利用成熟的VCD查看软件辅助分析程序执行的过程。整个项目的实现过程十分巧妙,对于软件的自动化测试和硬件的查错两方面均具有很强的参考价值。二、Java程序自动调试环境的要求

    调试是精通程序设计的必由之路,更是从原理上理解数据结构与算法的有力工具。纵观当前以jdb为代表的命令行调试工具和Eclipse等图形界面调试工具,本质上常用的调试方法主要分两类:

    一是日志记录法。通过人工加入若干关键变量的打印语句,在程序运行后分析日志中的变量变化情况以查找程序中的问题。如此就要插入大量的打印语句,对于多源码项目显然是一件费力的工作,而且源碼的改动也要再耗时编译。

    二是单步调试法,即利用调试工具加断点,然后在断点处单步调试查看变量值的变化情况是否符合预期。尽管Eclipse、IntelliJ IDEA等工具已经提供了图形化的用户调试界面,但是断点的增删仍需要手工设置,而且调试过程中默认只显示当前运行堆栈中的局部变量,想看到其他对象中的变量值也只能手工切换堆栈或手工加监视器。此外,限于调试者的经验和精力,单步调试时每个时刻无法关注所有变量的值的变化情况,常常需要手工记录若干关键变量值的变化轨迹,调试中常常顾此失彼,调试手段上缺乏整体性的把握。

    调试实践中通常综合使用上述两种方法。先用日志记录法查看程序的整体运行情况,再分析出可疑的出错位置,然后在可疑位置上设置断点并用单步调试法查看相关变量值的变化轨迹。对于稍微复杂的调试过程,一旦人工操作有疏忽从而越过了可疑的出错位置,就只能再重新调试找到可疑的出错位置,这是因为jdb工具缺少gdb工具的checkpoint保存回退功能。可见,Java程序的调试效率往往比较低下,对程序的整体运行情况也缺乏宏观上的把握,因此,理想的Java程序调试器要能记录所有变量值的变化轨迹,并且能简洁直观地显示,还要尽量降低手工的工作量,理想情况是Java程序一经运行完毕立即显示出所有变量值的变化轨迹。此外,理想的Java程序调试器还要能尽量拓展出硬件设计相关的知识,从而对于软件课程来说得以展示出软硬件知识足够丰富的细节场景,以激发出学生千差万别的问题和自主探究的热情。

    三、技术方案与可行性分析

    本文在上述背景下提出了Java程序自动调试方法,能利用芯片仿真验证领域的波形展示工具显示出所有变量值的变化轨迹。整个技术方案分两步,首先基于JDI技术设计自动断点调试策略,然后打印出所有变量的即时值,免除了所有手工的调试操作。其次,通过分析芯片仿真验证领域的VCD波形格式与软件变量值变化的联系与异同,确立了用VCD文件保存软件变量值的可行性。1.JDI技术

    因为Java程序运行在JVM之上,所以JVM拥有程序运行过程中的所有控制权限。本质上,各种Java程序的调试工具都是利用JDI向JVM发送命令,从而控制程序的整体运行过程,包括设置断点、查看断点处的变量值等等。

    一般地,发出JDI调试命令的Java程序称作控制程序,调试时的作用完全类似Eclipse工具。控制程序中使用JDI提供的事件管理器EventRequestManager,能控制行断点BreakpointEvent及单步断点StepEvent的启停,通过侦听JVM的事件队列EventQueue可以实现断点的处理函数,即相当于设定了断点调试策略。最终,被控制Java程序便在控制程序的指挥下自动地执行了多次断点处理函数,实现了自动调试。2.VCD文件格式

    VCD文件格式是芯片仿真软件的标准波形数据存储格式[5],开源的波形展示软件GTKWave和商业的Cadence SimVision软件均可支持。VCD文件格式组织形式十分简单,包括时间单位定义、连线定义和连线变值三个区域,形式如下:

    1 $timescale

    2 ?1 ps

    3 $end

    4 $scope module zdut $end

    5 $var wire 4 zdut.goodSort.arr_0_ ?zdut.goodSort.arr_0_ $end

    6 $var wire 4 zdut.goodSort.arr_1_ ?zdut.goodSort.arr_1_ $end

    7 $var wire 4 zdut.goodSort.arr_2_ ?zdut.goodSort.arr_2_ $end

    8 $var wire 4 zdut.main.arr_0_ ?zdut.main.arr_0_ $end

    9 $var wire 4 zdut.main.arr_0_ ?zdut.main.arr_1_ $end

    10 $var wire 4 zdut.main.arr_0_ ?zdut.main.arr_2_ $end

    11 $upscope $end

    12 #1

    13 b100 zdut.main.arr_0_

    14 b011 zdut.main.arr_1_

    15 b111 zdut.main.arr_2_

    16 #2

    17 b100 zdut.main.arr_0_

    18 b111 zdut.main.arr_1_

    19 b011 zdut.main.arr_2_

    其中,以$timescale标明的前三行区域设置时间单位是1ps,即每一步变化的历时,此数值对于软件调试来说可任意设置,因为软件只关心所有变量的值有改动的单步操作,至于这一“步”历时1ps或是1ns均无关紧要。

    由$scope和$upscope之间标明的区域则给出连线的定义,形式为:

    $var wire 位宽 短名 长名 $end

    其中,位宽决定了该连线所存储的变量的取值范围,例如4位即表示无符号数只能取0到15之间。连线的名字分长短的原因是VCD旨在减少文件尺寸,芯片仿真时动辄若干GB的波形文件要在连线变值区域中用短名取代完整名,当然存储量不关键的场合长名与短名可以相同,短名甚至亦可长于长名。连线的名字可以包含“[]”之外的特殊符号,例如可以用点号表示类中的函数或是函数中的局部变量这种归属关系,第5行到第7行就定义了类zdut中的函数goodSort()中的局部数组变量arr[0]到arr[2]。

    連线变值区域又分成若干子区域,每个子区域对应一个时刻所有变量的值的改动情况,例如第12行和第16行开始的4行分别表示时刻1ps和2ps时变量zdut.main.arr_1_等的值有变动。对于值没有变化的连线可写可不写,例如第17行就是冗余的,VCD文件格式能大幅度减少存储量的原因即在于只记录了变化的波形值。3.技术可行性分析

    VCD格式中的$scope和$upscope支持多层嵌套,以反映硬件设计中的模块与子模块的包含关系。但对应到软件的类和函数的情况时,这种硬件的包含关系便不再关键,完全可以组织成打平成一层的$scope和$upscope形式,只要从变量名上区分变量的从属关系即可,例如使用“类名.函数名.变量名”的格式。经上述分析可见,如果把程序中的函数和类当作硬件设计中的模块,把函数和类中的变量当作硬件设计中最基础的连线变量,那么将所有软件变量值的变化轨迹记录到VCD文件中是完全可行的。而且如此的优点是可以使用芯片仿真领域成熟的波形展示软件,当程序一次运行后便能全方位地分析它的运行过程。四、技术实现与细节问题

    Java程序的自动调试包括两个组件,控制程序和被控制程序。控制程序设定自动调试的策略,令被控制程序在指定的断点处执行相应的断点处理函数,打印出变量的值,而被控制程序则是一般的Java程序。两者的运行顺序也有严格的时序关系,因此涉及到一定的运行技巧,而变量的值保存为VCD文件的过程也需要Linux脚本的技巧。

    1.被控制程序

    被控制程序的核心代码如下,其中main函数中的变量arr以值传递和引用传递的方式分别执行了一次冒泡排序。又为了展示由波形查错的直观性,badSort()函数设计中人为加了一个bug:即数值1无法排序。

    1 public class zdut {

    2 public static void main(String[] args) {

    3 ? ?Thread.sleep(3000);

    4 ? ?int[] arr={4,3,7,2,11,1};

    5 ? ?for(int num:arrOutNonClone) print(num+ " ");

    6 ? ?int[] arrOutClone = goodSort(arr.clone());

    7 ? ?for(int num:arrOutClone) print(num+ " ");

    8 ? ?int[] arrOutNonClone = badSort(arr);

    9 ? ?for(int num:arrOutNonClone) print(num+ " ");

    10 }

    11 public static int[] goodSort(int[] arr) {

    12 ? ?boolean needSwap;

    13 ? ?int tmp;

    14 ? ?for(int i=0;i

    15 ? ? ?for(int j=0;j

    16 ? ? ?{ needSwap = arr[j] > arr[j+1];

    17 ? ? ? ?if(needSwap){

    18 ? ? ? ? ?tmp=arr[j];

    19 ? ? ? ? ?arr[j]=arr[j+1];

    20 ? ? ? ? ?arr[j+1]=tmp;

    21 ? ? ? ?}

    22 ? ? ?}

    23 ? ? ?}

    24 ? ?}

    25 ? ?return arr;

    26 ?}

    27 public static int[] badSort(int[] arr) {

    函數体与goodSort()相同,除第17行改作: if(needSwap && (1!=arr[j+1])){

    28 }

    值得一提的是,代码中第3行的延迟3秒是出于实际运行的考虑。因为被控制程序必须要先于控制程序运行,否则JDI接口找不到zdut类,从而无法加断点即无法接管被控制程序的运行;另一方面,被控制程序执行的时间一般很短,为保证在控制程序运行前仍未运行结束,就必须人为地加入适当的延迟,具体数值以方便调试为原则。2.控制程序与调试策略

    控制程序的代码软件较多,核心功能是设定调试策略。通常情况下很少关心Java库代码的执行过程,因此可设定用户代码的调试方式为单步进入,而Java库代码采用单步跳出的切换调试策略,相关伪代码摘录如下:

    public static void main(String[] args) {

    连接被控制程序;

    在类zdut的第4行加BreakpointRequest类型的断点;

    循环调用execute()函数处理JVM的事件eventQueue;

    }

    private static void execute(Event event) {

    if (event ?instanceof ?BreakpointEvent)

    在当前线程上设置STEP_INTO 类型的断点

    else if (event ?instanceof ?StepEvent) {

    打印“Step hit:”

    打印当前堆栈StackFrame.visibleVariables()中的所有变量值

    if(event.location().sourcePath()).indexOf("sun/") == -1 &&

    event.location().sourcePath()).indexOf("java/") == -1)

    在当前线程上设置STEP_INTO 类型的断点

    打印“now to STEP_INTO”

    else

    在当前线程上设置STEP_OUT 类型的断点

    打印“now to STEP_OUT”

    else ;

    }3.编译与运行技巧

    被控制程序编译时要设置javac -g选项,确保生成局部变量的调试信息,而控制程序编译时的路径还要包含JDK目录下的tools.jar文件。运行时,被控制程序要先于控制程序,并以服务的形式运行:java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 ?zdut。如此,被控制程序就处于等待JDI命令的状态,当然如果没有控制程序发来JDI命令,被控制程序就正常执行直到结束。根据被控制程序中延迟3秒的设计可知,控制程序必须在被控制程序启动后的3秒内发出第一个JDI调试命令。4.Linux脚本与VCD文件生成

    在被控制程序单步运行的过程中,控制程序打印出的日志文件摘录如下:

    Step hit:

    in Method: java.lang.StringBuilder.toString()

    now to STEP_OUT

    Step hit:

    in Method: zdut.main(java.lang.String[])

    zdut.main(java.lang.String[]).arr[0]: 2

    zdut.main(java.lang.String[]).arr[1]: 3

    zdut.main(java.lang.String[]).arr[2]: 4

    zdut.main(java.lang.String[]).arr[3]: 7

    zdut.main(java.lang.String[]).arr[4]: 11

    zdut.main(java.lang.String[]).arr[5]: 1

    zdut.main(java.lang.String[]).arrOutClone[0]: 1

    zdut.main(java.lang.String[]).arrOutClone[1]: 2

    zdut.main(java.lang.String[]).arrOutClone[2]: 3

    zdut.main(java.lang.String[]).arrOutClone[3]: 4

    zdut.main(java.lang.String[]).arrOutClone[4]: 7

    zdut.main(java.lang.String[]).arrOutClone[5]: 11

    now to STEP_INTO

    對比此日志文件格式与VCD文件格式中的时间单位定义、连线定义和连线变值三个区域,可见日志文件转成VCD文件只涉及两个关键步骤,即连线定义区域的生成和连线变值区域的生成。完整的Linux脚本如下,其中连线定义的生成包括三步:取出所有有变值的行、连线名去重和生成$var格式的连线,依次见第5、6、8 行。

    1 echo "\$timescale"

    2 echo " 1 ps"

    3 echo "\$end"

    4 echo "\$scope module zdut \$end"

    5 cat 1.log | grep ^zdut | sed 's/:.*//g' ?\

    6 ? | sort | uniq \

    7 ? | sed 's/(.*)//g' | grep "\." \

    8 ? | sed 's/^\(.*\)/$var wire 4 \1 \1 $end/g'

    9 echo "\$upscope \$end"

    10

    11 cat 1.log | sed 's/ true/ 1/g' | sed 's/ false/ 0/g' \

    12 ? ? ? ? | grep "^zdut\|now to" \

    13 ? ? ? ? | awk -F ": " ?'{ printf("%s", zD2B($2));} ?print " "$1 }' \

    14 ? ? ? ? | sed 's/.*now to.*/#www/g' | sed 's/(.*)//g' \

    15 ? ? ? ? | awk -v RS="www" '{n+=1;printf $0n}'

    连线变值区域的生成则包括四步:取出所有含有变值的行以及含有“now to”的行(第12行脚本),变量值由10进制数值化为2进制(第13行脚本),且逻辑型变量true/false化为1/0(第11行脚本)。而含有“now to”的行则使用awk递增替换为“#1”“#2”“#3”的形式(第14、15行脚本)以记录VCD文件中的时刻变化。五、结果展示

    用开源软件GTKWave打开VCD波形文件,被控制程序中所有变量值的变化轨迹图展示于图1。由图1可见,程序执行的全过程中所有变量的变化情况一目了然,这是传统的软件调试工具jdb或Eclipse等所不具备的。相比传统调试工具,图1更能有效激发学生的自主发现、提问与学习的热情,能生动地化解传统Java程序的教学难点。

    

1.程序执行整体过程的理解

    VCD文件格式规定未赋值变量的初始值显示作“x”或“xxx”,因此图1中波形展示的程序变量的赋值顺序为main().arr、goodSort().arr、main().arrOutClone、badSort().arr、main().arrOutNonClone,与Java程序的设计完全吻合。学生由此很容易建立起程序与进程两个概念动静态的直观印象,为深入理解软件的工作机理打下良好的基础。而且,由goodSort()函数中的数据波形的变化规律,能直观地看到数值“1”和“2”向上逐级跳跃,形象地展示了冒泡排序算法的原理,而badSort()函数中的排序算法存在一个bug,数值“1”未按期望向上逐级跳跃,数据波形亦展示得很清晰。2.细节问题的自主发现

    按值传递或是按引用传递是Java函数调用时实参的教学难点,学生一般难以从书本上获得直观的认识,而图1中的波形不但形象地展示出二者的不同,而且还能揭示出更一般的原理。例如,goodSort()函数的实参使用了值传递,因此161ps时刻其返回值改变了主函数中的变量main().arrOutClone,但传入的参数main().arr并未改变,相比之下,badSort()函数使用了引用传递的方式,所以它的返回值同时改变了主函数中的变量main().arrOutNonClone和传入变量main().arr。

    至此,细心的学生还会发现变量main().arrOutNonClone和main().arr的数据批量改变的时刻不同,分别是303ps和302ps。两者相差1ps实则反映出Java程序运行环境中对传入参数的锁定规则和堆栈中的局部变量返回的处理细节,而这些细节知识只有通过学生自主发现才能激发其学习的兴趣,才能自然地深入Java程序的编译原理和JVM运行时环境的相关知识。必须要指出,传统的纯软件调试工具很难展示出这些细节知识,而基于VCD波形的调试手段记录了每个软件变量每次值的变化情况,提供了全方位的数据变化信息,给学生挖掘程序执行过程中的细节问题提供了平台。此外,Gtkwave软件提供了上百个菜单功能项,本身即是软件专业的学生拓展硬件知识的探索环境。3.波形对比功能与自动查错

    在软件开发与测试过程中,为了保证设计的正确性,通常由两个或多个设计者独立设计复杂的软件模块,然后插入大量的打印语句以实现运行后的输出变量或中间变量异同的自动比较,工作过程耗时费力。用VCD波形文件自动展示所有变量值的变化轨迹十分适合作自动对比,商用VCD波形展示软件Cadence SimVision提供了波形比较功能[6],通过自动对比关键的控制信号goodSort().needSwap、goodSort().tmp和badSort().needSwap、badSort().tmp的异同很容易发现程序的bug所在。这种来源于硬件仿真的调试技巧是传统软件调试领域从未触及的,能开阔眼界且拓展思维,于软件专业和硬件专业的教学均互补互益,更是培养学生软件与硬件多学科协同创新能力的切入点。4.条件断点与创造力培养

    条件断点是调试中的有力手段,而jdb工具没有提供原生的条件断点支持,只能人为加入判断代码以实现条件断点的调试功能,工作过程费力且调试效率相对低下。相比之下,VCD展示软件大多数都能提供波形的搜索功能,直接由给定条件定位到波形时刻,一次运行后可以静态地、多维度、多时刻分析变量值的各种变化情况。此外,VCD文件格式的开放性允许用户定制Linux脚本以实现复杂条件的组合,大大提高了调试查错的效率,学习使用过程中如果遇到更复杂的条件断点,还会激发能力更强的学生修改开源软件GTKWave的热情。可以说,用VCD波形格式记录变量值的变化轨迹为各个层次的学生提供了发现问题、解决问题以及施展创造力的平台。六、结束语

    信息技术的综合运用是未来教育信息化研究与应用的方向,只有软件技术和硬件技术相贯通,才能在底层问题上寻找相似性,才能促进学生多学科协同创新能力的培养。这方面加州大学伯克利分校同时用Scala语言开发开源大数据处理软件Spark和开源RISC-V系列处理器便是一个有力的佐证。

    本文基于与纯软件不相关的、芯片仿真验证用到的VCD波形格式保存Java程序自动调试过程中软件变量的值的变化情况,整个项目的实现过程巧妙,是一个综合运用软件硬件技术开发的教学案例,对于软件的自动化测试和硬件的查错均具有很强的参考价值。所用到的实现技巧在信息化教育教学领域具有很强的实用性及推广价值,利于软硬件综合人才多学科协同创新能力的培养。

    本文所述的调试方法尚有几点不足:一是凡是涉及到Java反射机制的程序还需要深入研究其JDI自动调试方法,尤其是基于Java编译器的Scala程序,其类型擦除操作涉及Java编译原理和JVM运行时环境的机制,变量值的获取方法有待研究,断点策略上也要涉及STEP_INTO、STEP_OUT、STEP_OVER及STEP_MIN这几种方式的切换以实现更复杂的JVM进程控制技术。二是有符号数、浮点数以及复杂数据结构在VCD文件中的存储格式尚不完善,很可能需要修改GTKWave的源码才能正确展示。最后,如果能将大数据展示软件如ECharts、MATLAB/Simulink中的Stateflow 图以及组态软件中真值表和流程图与VCD文件相结合,会得到样式更多、形式更生动的大型程序如AVS2视频编码程序等运行过程中变量值的变化情况,也将涉及更多学科知识的学习、应用与创新融合,为实现未来编程语言高效率高质量的素质教育提供更多的实践研究成果。参考文献:

    [1]国发[2017]4号.国务院关于印发国家教育事业发展“十三五”规划的通知[Z].

    [2]钱毅,蔡小川.使用Java Debug Interface(JDI)调试多线程应用程序:开发定制的多线程分析器[EB/OL]. https://www.ibm.com/developerworks/cn/java/j-lo-jdi/index.html.

    [3]Krste Asanovi c' ,Rimas Avizienis,Jonathan Bachrach,et al.The Rocket Chip Generator[R].California(Berkeley):University of California,2016.

    [4]赵鸿昌.基于芯片仿真验证软件实现的一种试题库与试卷[J].中国教育信息化,2018(14):89-92.

    [5]Lambert M. Surhone, Mariam T. Tennoe, Susan F. Value Change Dump[M]. Mauritius:Betascript Publishing,2010.

    [6]Cadence Design Systems.SimVision User Guide 8.2版本[R].California(San Jose):Cadence Design Systems,2009.

    (編辑:鲁利瑞)