趙鴻昌
(無錫城市職業(yè)技術(shù)學(xué)院 物聯(lián)網(wǎng)工程學(xué)院,江蘇 無錫214153)
充分利用信息技術(shù)手段激發(fā)學(xué)生的創(chuàng)新熱情、提高各個階段的教學(xué)質(zhì)量和教學(xué)效率已成為國家教育事業(yè)發(fā)展“十三五”規(guī)劃的重點[1],因此,未來信息技術(shù)課程的教學(xué)質(zhì)量尤其重要,承載著國家教育信息化的愿景,直接決定了能否實現(xiàn)信息技術(shù)與教育過程的深度融合。作為經(jīng)典的信息技術(shù)課程,Java不僅僅是一門程序設(shè)計課程,更是當(dāng)今諸多實用軟硬件技術(shù)環(huán)境的代名詞。一方面,Java與Linux腳本相結(jié)合演化而來的Scala語言同時是大數(shù)據(jù)人工智能軟件Spark和開源處理器RISCV的設(shè)計語言,代表了程序設(shè)計的發(fā)展方向。另一方面,Java技術(shù)生態(tài)圈中的Java虛擬機(Java Virtual Machine,JVM)提供了越來越多的先進技術(shù),而這些先進技術(shù)已經(jīng)很難在常規(guī)程序設(shè)計課程中教學(xué),問題的難度和廣度遠超出了課堂容量。例如,Java調(diào)試接口 (Java Debugging Interface,JDI)技術(shù)用于遠程調(diào)試服務(wù)器的出錯線程[2];JVM反射技術(shù)用以實現(xiàn)類的動態(tài)加載和服務(wù)器軟件的容器內(nèi)熱部署;再如,實踐中需要十分綜合的軟件背景和硬件知識,而不是孤立的編程語言課程或硬件電路設(shè)計課程,才能理解JVM反射應(yīng)用于開源RISC-V處理器生成環(huán)境的連線生成過程[3]。
眾所周知,調(diào)試是精通程序設(shè)計的必要手段,但對于Java程序,不論經(jīng)典的jdb命令行調(diào)試工具還是Eclipse等圖形界面調(diào)試工具,都難以從宏觀整體上把握程序的運行過程,很難直觀理解靜態(tài)的程序與動態(tài)的進程之間的關(guān)系??梢姡谶@樣一個信息技術(shù)日新月異的時代,Java程序設(shè)計課程作為教育信息化戰(zhàn)場的排頭兵,亟待在Java調(diào)試方法上取得突破。理想的Java調(diào)試方法要與信息化教學(xué)工具合二為一,不僅要能多維度地啟發(fā)學(xué)生的思考與提問,實現(xiàn)學(xué)習(xí)、應(yīng)用與創(chuàng)新的密切結(jié)合從而成為素質(zhì)教育的平臺,還要盡可能地將學(xué)習(xí)內(nèi)容拓展到軟硬件融合創(chuàng)新的層面,最終實現(xiàn)軟硬件綜合人才多學(xué)科協(xié)同創(chuàng)新能力的培養(yǎng),全面提升國家教育信息化從業(yè)人員的技術(shù)水平。
反觀硬件設(shè)計和芯片仿真領(lǐng)域,其中有很多成熟的電子設(shè)計自動化工具可以借鑒,通過發(fā)掘軟件問題與硬件問題的相似性,往往可以用極低的投入整合出巧妙的軟件功能。例如,芯片仿真驗證用到的System Verilog語言具有生成約束隨機激勵的功能,如果將硬件激勵的每一位與試題庫中的每道題目相對應(yīng),就能類比地解決約束隨機抽取題目的自動組卷問題[4]。因此,以成熟的芯片仿真驗證軟件為基礎(chǔ)作二次開發(fā)能有效地加速教育信息化軟件的快速原型化。本文基于芯片仿真領(lǐng)域的波形變化值存儲(Value Change Dump,VCD)格式保存Java程序執(zhí)行過程中的所有變量值的變化軌跡,并利用成熟的VCD查看軟件輔助分析程序執(zhí)行的過程。整個項目的實現(xiàn)過程十分巧妙,對于軟件的自動化測試和硬件的查錯兩方面均具有很強的參考價值。
調(diào)試是精通程序設(shè)計的必由之路,更是從原理上理解數(shù)據(jù)結(jié)構(gòu)與算法的有力工具。縱觀當(dāng)前以jdb為代表的命令行調(diào)試工具和Eclipse等圖形界面調(diào)試工具,本質(zhì)上常用的調(diào)試方法主要分兩類:
一是日志記錄法。通過人工加入若干關(guān)鍵變量的打印語句,在程序運行后分析日志中的變量變化情況以查找程序中的問題。如此就要插入大量的打印語句,對于多源碼項目顯然是一件費力的工作,而且源碼的改動也要再耗時編譯。
二是單步調(diào)試法,即利用調(diào)試工具加斷點,然后在斷點處單步調(diào)試查看變量值的變化情況是否符合預(yù)期。盡管Eclipse、IntelliJ IDEA等工具已經(jīng)提供了圖形化的用戶調(diào)試界面,但是斷點的增刪仍需要手工設(shè)置,而且調(diào)試過程中默認只顯示當(dāng)前運行堆棧中的局部變量,想看到其他對象中的變量值也只能手工切換堆?;蚴止ぜ颖O(jiān)視器。此外,限于調(diào)試者的經(jīng)驗和精力,單步調(diào)試時每個時刻無法關(guān)注所有變量的值的變化情況,常常需要手工記錄若干關(guān)鍵變量值的變化軌跡,調(diào)試中常常顧此失彼,調(diào)試手段上缺乏整體性的把握。
調(diào)試實踐中通常綜合使用上述兩種方法。先用日志記錄法查看程序的整體運行情況,再分析出可疑的出錯位置,然后在可疑位置上設(shè)置斷點并用單步調(diào)試法查看相關(guān)變量值的變化軌跡。對于稍微復(fù)雜的調(diào)試過程,一旦人工操作有疏忽從而越過了可疑的出錯位置,就只能再重新調(diào)試找到可疑的出錯位置,這是因為jdb工具缺少gdb工具的checkpoint保存回退功能??梢?,Java程序的調(diào)試效率往往比較低下,對程序的整體運行情況也缺乏宏觀上的把握,因此,理想的Java程序調(diào)試器要能記錄所有變量值的變化軌跡,并且能簡潔直觀地顯示,還要盡量降低手工的工作量,理想情況是Java程序一經(jīng)運行完畢立即顯示出所有變量值的變化軌跡。此外,理想的Java程序調(diào)試器還要能盡量拓展出硬件設(shè)計相關(guān)的知識,從而對于軟件課程來說得以展示出軟硬件知識足夠豐富的細節(jié)場景,以激發(fā)出學(xué)生千差萬別的問題和自主探究的熱情。
本文在上述背景下提出了Java程序自動調(diào)試方法,能利用芯片仿真驗證領(lǐng)域的波形展示工具顯示出所有變量值的變化軌跡。整個技術(shù)方案分兩步,首先基于JDI技術(shù)設(shè)計自動斷點調(diào)試策略,然后打印出所有變量的即時值,免除了所有手工的調(diào)試操作。其次,通過分析芯片仿真驗證領(lǐng)域的VCD波形格式與軟件變量值變化的聯(lián)系與異同,確立了用VCD文件保存軟件變量值的可行性。
因為Java程序運行在JVM之上,所以JVM擁有程序運行過程中的所有控制權(quán)限。本質(zhì)上,各種Java程序的調(diào)試工具都是利用JDI向JVM發(fā)送命令,從而控制程序的整體運行過程,包括設(shè)置斷點、查看斷點處的變量值等等。
一般地,發(fā)出JDI調(diào)試命令的Java程序稱作控制程序,調(diào)試時的作用完全類似Eclipse工具??刂瞥绦蛑惺褂肑DI提供的事件管理器EventRequestManager,能控制行斷點BreakpointEvent及單步斷點StepEvent的啟停,通過偵聽JVM的事件隊列EventQueue可以實現(xiàn)斷點的處理函數(shù),即相當(dāng)于設(shè)定了斷點調(diào)試策略。最終,被控制Java程序便在控制程序的指揮下自動地執(zhí)行了多次斷點處理函數(shù),實現(xiàn)了自動調(diào)試。
VCD文件格式是芯片仿真軟件的標準波形數(shù)據(jù)存儲格式[5],開源的波形展示軟件GTKWave和商業(yè)的Cadence SimVision軟件均可支持。VCD文件格式組織形式十分簡單,包括時間單位定義、連線定義和連線變值三個區(qū)域,形式如下:
其中,以$timescale標明的前三行區(qū)域設(shè)置時間單位是1ps,即每一步變化的歷時,此數(shù)值對于軟件調(diào)試來說可任意設(shè)置,因為軟件只關(guān)心所有變量的值有改動的單步操作,至于這一“步”歷時1ps或是1ns均無關(guān)緊要。
由$scope和$upscope之間標明的區(qū)域則給出連線的定義,形式為:
其中,位寬決定了該連線所存儲的變量的取值范圍,例如4位即表示無符號數(shù)只能取0到15之間。連線的名字分長短的原因是VCD旨在減少文件尺寸,芯片仿真時動輒若干GB的波形文件要在連線變值區(qū)域中用短名取代完整名,當(dāng)然存儲量不關(guān)鍵的場合長名與短名可以相同,短名甚至亦可長于長名。連線的名字可以包含“[]”之外的特殊符號,例如可以用點號表示類中的函數(shù)或是函數(shù)中的局部變量這種歸屬關(guān)系,第5行到第7行就定義了類zdut中的函數(shù)goodSort()中的局部數(shù)組變量 arr[0]到 arr[2]。
連線變值區(qū)域又分成若干子區(qū)域,每個子區(qū)域?qū)?yīng)一個時刻所有變量的值的改動情況,例如第12行和第16行開始的4行分別表示時刻1ps和2ps時變量zdut.main.arr_1_等的值有變動。對于值沒有變化的連線可寫可不寫,例如第17行就是冗余的,VCD文件格式能大幅度減少存儲量的原因即在于只記錄了變化的波形值。
VCD格式中的$scope和$upscope支持多層嵌套,以反映硬件設(shè)計中的模塊與子模塊的包含關(guān)系。但對應(yīng)到軟件的類和函數(shù)的情況時,這種硬件的包含關(guān)系便不再關(guān)鍵,完全可以組織成打平成一層的$scope和$upscope形式,只要從變量名上區(qū)分變量的從屬關(guān)系即可,例如使用“類名.函數(shù)名.變量名”的格式。經(jīng)上述分析可見,如果把程序中的函數(shù)和類當(dāng)作硬件設(shè)計中的模塊,把函數(shù)和類中的變量當(dāng)作硬件設(shè)計中最基礎(chǔ)的連線變量,那么將所有軟件變量值的變化軌跡記錄到VCD文件中是完全可行的。而且如此的優(yōu)點是可以使用芯片仿真領(lǐng)域成熟的波形展示軟件,當(dāng)程序一次運行后便能全方位地分析它的運行過程。
Java程序的自動調(diào)試包括兩個組件,控制程序和被控制程序??刂瞥绦蛟O(shè)定自動調(diào)試的策略,令被控制程序在指定的斷點處執(zhí)行相應(yīng)的斷點處理函數(shù),打印出變量的值,而被控制程序則是一般的Java程序。兩者的運行順序也有嚴格的時序關(guān)系,因此涉及到一定的運行技巧,而變量的值保存為VCD文件的過程也需要Linux腳本的技巧。
被控制程序的核心代碼如下,其中main函數(shù)中的變量arr以值傳遞和引用傳遞的方式分別執(zhí)行了一次冒泡排序。又為了展示由波形查錯的直觀性,badSort()函數(shù)設(shè)計中人為加了一個bug:即數(shù)值1無法排序。
值得一提的是,代碼中第3行的延遲3秒是出于實際運行的考慮。因為被控制程序必須要先于控制程序運行,否則JDI接口找不到zdut類,從而無法加斷點即無法接管被控制程序的運行;另一方面,被控制程序執(zhí)行的時間一般很短,為保證在控制程序運行前仍未運行結(jié)束,就必須人為地加入適當(dāng)?shù)难舆t,具體數(shù)值以方便調(diào)試為原則。
控制程序的代碼軟件較多,核心功能是設(shè)定調(diào)試策略。通常情況下很少關(guān)心Java庫代碼的執(zhí)行過程,因此可設(shè)定用戶代碼的調(diào)試方式為單步進入,而Java庫代碼采用單步跳出的切換調(diào)試策略,相關(guān)偽代碼摘錄如下:
被控制程序編譯時要設(shè)置javac-g選項,確保生成局部變量的調(diào)試信息,而控制程序編譯時的路徑還要包含JDK目錄下的tools.jar文件。運行時,被控制程序要先于控制程序,并以服務(wù)的形式運行:java-Xdebug-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 zdut。如此,被控制程序就處于等待JDI命令的狀態(tài),當(dāng)然如果沒有控制程序發(fā)來JDI命令,被控制程序就正常執(zhí)行直到結(jié)束。根據(jù)被控制程序中延遲3秒的設(shè)計可知,控制程序必須在被控制程序啟動后的3秒內(nèi)發(fā)出第一個JDI調(diào)試命令。
在被控制程序單步運行的過程中,控制程序打印出的日志文件摘錄如下:
對比此日志文件格式與VCD文件格式中的時間單位定義、連線定義和連線變值三個區(qū)域,可見日志文件轉(zhuǎn)成VCD文件只涉及兩個關(guān)鍵步驟,即連線定義區(qū)域的生成和連線變值區(qū)域的生成。完整的Linux腳本如下,其中連線定義的生成包括三步:取出所有有變值的行、連線名去重和生成$var格式的連線,依次見第5、6、8行。
連線變值區(qū)域的生成則包括四步:取出所有含有變值的行以及含有“now to”的行(第12行腳本),變量值由10進制數(shù)值化為2進制(第13行腳本),且邏輯型變量true/false化為1/0(第11行腳本)。 而含有“now to”的行則使用 awk 遞增替換為“#1”“#2”“#3”的形式(第 14、15行腳本)以記錄VCD文件中的時刻變化。
用開源軟件GTKWave打開VCD波形文件,被控制程序中所有變量值的變化軌跡圖展示于圖1。由圖1可見,程序執(zhí)行的全過程中所有變量的變化情況一目了然,這是傳統(tǒng)的軟件調(diào)試工具jdb或Eclipse等所不具備的。相比傳統(tǒng)調(diào)試工具,圖1更能有效激發(fā)學(xué)生的自主發(fā)現(xiàn)、提問與學(xué)習(xí)的熱情,能生動地化解傳統(tǒng)Java程序的教學(xué)難點。
圖1 程序中所有變量值的變化軌跡圖
VCD文件格式規(guī)定未賦值變量的初始值顯示作“x”或“xxx”,因此圖1中波形展示的程序變量的賦值順序為 main().arr、goodSort().arr、main().arrOutClone、badSort().arr、main().arrOutNonClone,與Java程序的設(shè)計完全吻合。學(xué)生由此很容易建立起程序與進程兩個概念動靜態(tài)的直觀印象,為深入理解軟件的工作機理打下良好的基礎(chǔ)。而且,由goodSort()函數(shù)中的數(shù)據(jù)波形的變化規(guī)律,能直觀地看到數(shù)值“1”和“2”向上逐級跳躍,形象地展示了冒泡排序算法的原理,而badSort()函數(shù)中的排序算法存在一個bug,數(shù)值“1”未按期望向上逐級跳躍,數(shù)據(jù)波形亦展示得很清晰。
按值傳遞或是按引用傳遞是Java函數(shù)調(diào)用時實參的教學(xué)難點,學(xué)生一般難以從書本上獲得直觀的認識,而圖1中的波形不但形象地展示出二者的不同,而且還能揭示出更一般的原理。例如,goodSort()函數(shù)的實參使用了值傳遞,因此161ps時刻其返回值改變了主函數(shù)中的變量main().arrOutClone,但傳入的參數(shù)main().arr并未改變,相比之下,badSort()函數(shù)使用了引用傳遞的方式,所以它的返回值同時改變了主函數(shù)中的變量main().arrOutNonClone和傳入變量main().arr。
至此,細心的學(xué)生還會發(fā)現(xiàn)變量main().arrOutNon-Clone和main().arr的數(shù)據(jù)批量改變的時刻不同,分別是303ps和302ps。兩者相差1ps實則反映出Java程序運行環(huán)境中對傳入?yún)?shù)的鎖定規(guī)則和堆棧中的局部變量返回的處理細節(jié),而這些細節(jié)知識只有通過學(xué)生自主發(fā)現(xiàn)才能激發(fā)其學(xué)習(xí)的興趣,才能自然地深入Java程序的編譯原理和JVM運行時環(huán)境的相關(guān)知識。必須要指出,傳統(tǒng)的純軟件調(diào)試工具很難展示出這些細節(jié)知識,而基于VCD波形的調(diào)試手段記錄了每個軟件變量每次值的變化情況,提供了全方位的數(shù)據(jù)變化信息,給學(xué)生挖掘程序執(zhí)行過程中的細節(jié)問題提供了平臺。此外,Gtkwave軟件提供了上百個菜單功能項,本身即是軟件專業(yè)的學(xué)生拓展硬件知識的探索環(huán)境。
在軟件開發(fā)與測試過程中,為了保證設(shè)計的正確性,通常由兩個或多個設(shè)計者獨立設(shè)計復(fù)雜的軟件模塊,然后插入大量的打印語句以實現(xiàn)運行后的輸出變量或中間變量異同的自動比較,工作過程耗時費力。用VCD波形文件自動展示所有變量值的變化軌跡十分適合作自動對比,商用VCD波形展示軟件Cadence SimVision提供了波形比較功能[6],通過自動對比關(guān)鍵的控制信號 goodSort().needSwap、goodSort().tmp和 badSort().needSwap、badSort().tmp的異同很容易發(fā)現(xiàn)程序的bug所在。這種來源于硬件仿真的調(diào)試技巧是傳統(tǒng)軟件調(diào)試領(lǐng)域從未觸及的,能開闊眼界且拓展思維,于軟件專業(yè)和硬件專業(yè)的教學(xué)均互補互益,更是培養(yǎng)學(xué)生軟件與硬件多學(xué)科協(xié)同創(chuàng)新能力的切入點。
條件斷點是調(diào)試中的有力手段,而jdb工具沒有提供原生的條件斷點支持,只能人為加入判斷代碼以實現(xiàn)條件斷點的調(diào)試功能,工作過程費力且調(diào)試效率相對低下。相比之下,VCD展示軟件大多數(shù)都能提供波形的搜索功能,直接由給定條件定位到波形時刻,一次運行后可以靜態(tài)地、多維度、多時刻分析變量值的各種變化情況。此外,VCD文件格式的開放性允許用戶定制Linux腳本以實現(xiàn)復(fù)雜條件的組合,大大提高了調(diào)試查錯的效率,學(xué)習(xí)使用過程中如果遇到更復(fù)雜的條件斷點,還會激發(fā)能力更強的學(xué)生修改開源軟件GTKWave的熱情。可以說,用VCD波形格式記錄變量值的變化軌跡為各個層次的學(xué)生提供了發(fā)現(xiàn)問題、解決問題以及施展創(chuàng)造力的平臺。
信息技術(shù)的綜合運用是未來教育信息化研究與應(yīng)用的方向,只有軟件技術(shù)和硬件技術(shù)相貫通,才能在底層問題上尋找相似性,才能促進學(xué)生多學(xué)科協(xié)同創(chuàng)新能力的培養(yǎng)。這方面加州大學(xué)伯克利分校同時用Scala語言開發(fā)開源大數(shù)據(jù)處理軟件Spark和開源RISC-V系列處理器便是一個有力的佐證。
本文基于與純軟件不相關(guān)的、芯片仿真驗證用到的VCD波形格式保存Java程序自動調(diào)試過程中軟件變量的值的變化情況,整個項目的實現(xiàn)過程巧妙,是一個綜合運用軟件硬件技術(shù)開發(fā)的教學(xué)案例,對于軟件的自動化測試和硬件的查錯均具有很強的參考價值。所用到的實現(xiàn)技巧在信息化教育教學(xué)領(lǐng)域具有很強的實用性及推廣價值,利于軟硬件綜合人才多學(xué)科協(xié)同創(chuàng)新能力的培養(yǎng)。
本文所述的調(diào)試方法尚有幾點不足:一是凡是涉及到Java反射機制的程序還需要深入研究其JDI自動調(diào)試方法,尤其是基于Java編譯器的Scala程序,其類型擦除操作涉及Java編譯原理和JVM運行時環(huán)境的機制,變量值的獲取方法有待研究,斷點策略上也要涉及STEP_INTO、STEP_OUT、STEP_OVER 及 STEP_MIN 這幾種方式的切換以實現(xiàn)更復(fù)雜的JVM進程控制技術(shù)。二是有符號數(shù)、浮點數(shù)以及復(fù)雜數(shù)據(jù)結(jié)構(gòu)在VCD文件中的存儲格式尚不完善,很可能需要修改GTKWave的源碼才能正確展示。最后,如果能將大數(shù)據(jù)展示軟件如ECharts、MATLAB/Simulink中的Stateflow圖以及組態(tài)軟件中真值表和流程圖與VCD文件相結(jié)合,會得到樣式更多、形式更生動的大型程序如AVS2視頻編碼程序等運行過程中變量值的變化情況,也將涉及更多學(xué)科知識的學(xué)習(xí)、應(yīng)用與創(chuàng)新融合,為實現(xiàn)未來編程語言高效率高質(zhì)量的素質(zhì)教育提供更多的實踐研究成果。