喬文利 孔祥營(yíng)
江蘇自動(dòng)化研究所,江蘇連云港 222006
軟件調(diào)試是軟件開發(fā)必備的過(guò)程,是為了能在開發(fā)階段及時(shí)發(fā)現(xiàn)并排除程序中的錯(cuò)誤。據(jù)統(tǒng)計(jì),調(diào)試時(shí)間大約占開發(fā)總時(shí)間的50%,甚至更多[1]。而集成開發(fā)環(huán)境便會(huì)嚴(yán)重影響軟件的調(diào)試效率。Eclipse 是一個(gè)開源的、可擴(kuò)展的、用Java 語(yǔ)言實(shí)現(xiàn)的開發(fā)平臺(tái),它不是一個(gè)一整塊的程序,而是一個(gè)包含了插件載入器,被數(shù)百個(gè)甚至更多的插件所包圍的小內(nèi)核[2]。CDT(C/C ++ Development Toolkit)是Eclipse 平臺(tái)下一組用來(lái)開發(fā)、調(diào)試C/C++程序的插件[3],但它在使用過(guò)程中存在以下兩點(diǎn)不足:(1)當(dāng)程序出現(xiàn)錯(cuò)誤時(shí),需要程序員根據(jù)經(jīng)驗(yàn)分析設(shè)置可疑斷點(diǎn),并從斷點(diǎn)處執(zhí)行,而當(dāng)程序錯(cuò)誤出現(xiàn)在斷點(diǎn)之前,則需重新設(shè)置斷點(diǎn)并運(yùn)行,程序繁瑣且大大降低了軟件調(diào)試效率;(2)開源的Eclipse 集成開發(fā)環(huán)境只支持X86 平臺(tái),不支持國(guó)產(chǎn)平臺(tái),而國(guó)產(chǎn)系統(tǒng)只提供他們各自系統(tǒng)廠商所提供的集成開發(fā)環(huán)境,對(duì)其他不同的國(guó)產(chǎn)平臺(tái)不適用,即在通用性方面無(wú)法滿足要求[4]。
針對(duì)CDT 插件的不足,設(shè)計(jì)開發(fā)了一個(gè)支持國(guó)產(chǎn)平臺(tái)并提供反向調(diào)試功能的Eclipse 插件,并且為了方便用戶使用,實(shí)現(xiàn)了插件的可視化。該插件增強(qiáng)了面向國(guó)產(chǎn)平臺(tái)的軟件調(diào)試功能,使得調(diào)試過(guò)程方便快捷,提高了軟件調(diào)試效率。
當(dāng)前的主流調(diào)試器gdb 在gdb7 版本之后提供了反向調(diào)試。反向調(diào)試技術(shù)是一種軟件開發(fā)技術(shù),可有效地幫助修復(fù)由于不確定的程序行為而發(fā)生的錯(cuò)誤。反向調(diào)試與普通調(diào)試的跟蹤方向相反,能夠使被調(diào)試的程序產(chǎn)生一種時(shí)光倒流的效果,可以有效提高調(diào)試效率。在串行、多核、多線程等多維場(chǎng)景下,反向調(diào)試應(yīng)具備兩種功能:
一是在單次調(diào)試過(guò)程中讓被調(diào)試程序能夠從當(dāng)前的狀態(tài)回退到上一個(gè)狀態(tài),方便問(wèn)題定位;
二是在調(diào)試模式下對(duì)部分代碼進(jìn)行自動(dòng)執(zhí)行操作時(shí),當(dāng)自動(dòng)執(zhí)行的代碼出現(xiàn)錯(cuò)誤后,能夠記錄本次錯(cuò)誤異常信息,同時(shí)自動(dòng)將系統(tǒng)回退至上一個(gè)正常狀態(tài)(包括恢復(fù)堆棧狀態(tài)、寄存器的值等),并自動(dòng)定位到出現(xiàn)錯(cuò)誤的代碼行,方便開發(fā)人員對(duì)此錯(cuò)誤進(jìn)行調(diào)試,避免了排查代碼、設(shè)置斷點(diǎn)、重啟調(diào)試等一系列繁雜的操作,提高了故障定位的精確性和調(diào)試效率。
圖1 所示為gdb 反向調(diào)試示意圖。由圖可見(jiàn),gdb向各指令添加了一個(gè)斷點(diǎn),在斷點(diǎn)發(fā)生時(shí)以差分方式保存正在調(diào)試的應(yīng)用程序的內(nèi)存狀態(tài)和寄存器狀態(tài)。鑒于每條指令都添加了一個(gè)斷點(diǎn),且程序的運(yùn)行狀態(tài)保存在該斷點(diǎn)處,能夠使得歷史調(diào)試精度得到最高保證[4]。
Eclipse 是以開放服務(wù)網(wǎng)關(guān)協(xié)議(OSGI)框架[5]為基礎(chǔ),實(shí)現(xiàn)了一個(gè)小內(nèi)核并通過(guò)集成大量插件所共同形成的開發(fā)環(huán)境。OSGI 是JAVA 動(dòng)態(tài)化模塊化系統(tǒng)的一系列規(guī)范,插件是由遵循一系列特定規(guī)范的應(yīng)用程序接口編程而得,每個(gè)插件之間是通過(guò)聲明擴(kuò)展點(diǎn)和擴(kuò)展其它插件中的擴(kuò)展點(diǎn)實(shí)現(xiàn)連接的[1][6]。 構(gòu)成Eclipse 平臺(tái)的子系統(tǒng)以插件的形式實(shí)現(xiàn), 圖2 為各子系統(tǒng)的組成形式示意圖[7]。
首先需要確定,應(yīng)用程序的狀態(tài)僅由應(yīng)用程序的寄存器及其虛擬地址空間內(nèi)的所有內(nèi)存內(nèi)容所確定,不管外界環(huán)境如何變化,一旦能確定應(yīng)用程序所有的寄存器值(包括指令寄存器等),及其所有內(nèi)存的值,即確定了應(yīng)用程序的狀態(tài)。
在此基礎(chǔ)上,將進(jìn)程內(nèi)用戶態(tài)指令的執(zhí)行與內(nèi)核態(tài)指令的執(zhí)行分開,由于用戶態(tài)指令不會(huì)對(duì)系統(tǒng)環(huán)境造成影響,也不會(huì)被系統(tǒng)環(huán)境影響,其執(zhí)行過(guò)程完全是確定性的(對(duì)于上述應(yīng)用程序的狀態(tài)而言),具有無(wú)限可重復(fù)性。因此,在進(jìn)行程序狀態(tài)記錄時(shí),可以完全不對(duì)用戶態(tài)的指令進(jìn)行跟蹤,僅需要對(duì)外界對(duì)應(yīng)用程序狀態(tài)影響的操作進(jìn)行跟蹤即可。在Linux操作系統(tǒng)下,主要對(duì)內(nèi)核態(tài)指令與事件進(jìn)行跟蹤即可。在Linux 操作系統(tǒng)下,內(nèi)核態(tài)指令與事件主要包括系統(tǒng)調(diào)用(syscall)、信號(hào)(signal)與進(jìn)程間通信機(jī)制(如共享內(nèi)存)等。
Linux 操作系統(tǒng)下有數(shù)百個(gè)系統(tǒng)調(diào)用,而且每個(gè)架構(gòu)支持的系統(tǒng)調(diào)用、系統(tǒng)調(diào)用號(hào)與具體實(shí)現(xiàn)不完全一致。為了對(duì)系統(tǒng)調(diào)用進(jìn)行跟蹤,需要對(duì)每個(gè)系統(tǒng)調(diào)用進(jìn)行分析,根據(jù)其語(yǔ)義、參數(shù)、返回值與對(duì)應(yīng)架構(gòu)下的函數(shù)調(diào)用規(guī)約(例如使用哪幾個(gè)寄存器傳參、是否使用棧傳參、返回值存放在哪個(gè)寄存器或者棧上)得到每個(gè)系統(tǒng)調(diào)用具體會(huì)讀取以及寫入的用戶態(tài)進(jìn)程空間的寄存器與內(nèi)存區(qū)域,進(jìn)而才能對(duì)系統(tǒng)調(diào)用進(jìn)行跟蹤,并將系統(tǒng)調(diào)用對(duì)進(jìn)程狀態(tài)的改變記錄下來(lái)。
在Linux 操作系統(tǒng)下,進(jìn)程運(yùn)行的過(guò)程中可能會(huì)收到內(nèi)核發(fā)出的信號(hào),導(dǎo)致進(jìn)程對(duì)應(yīng)的信號(hào)處理函數(shù)被調(diào)用,或者進(jìn)程狀態(tài)發(fā)生改變,例如暫停、退出等。為了精確跟蹤進(jìn)程收到信號(hào)的事件,以及對(duì)信號(hào)的處理,需要記錄進(jìn)程對(duì)信號(hào)設(shè)置的信號(hào)處理掩碼(sigprocmask),并基于ptrace 對(duì)被調(diào)試進(jìn)程收到信號(hào)的事件進(jìn)行監(jiān)聽處理,繼而通過(guò)單步的方式跟蹤內(nèi)核為信號(hào)處理函數(shù)設(shè)置的寄存器與棧幀,將其記錄下來(lái),以便進(jìn)行回放調(diào)試時(shí)的狀態(tài)回放。
由于調(diào)試器僅對(duì)內(nèi)核態(tài)指令與事件進(jìn)行跟蹤,對(duì)用戶態(tài)指令采取自動(dòng)運(yùn)行的方式進(jìn)行處理。因此,在回放時(shí),調(diào)試器需要以用戶態(tài)指令序列為回放的時(shí)間戳,以支持精確的指令序列回放。在此處使用絕對(duì)時(shí)間或者相對(duì)時(shí)間都是不行的,因?yàn)橛涗洉r(shí)的時(shí)間無(wú)法與回放時(shí)候的時(shí)間完全一致(即使是同一臺(tái)機(jī)器,由于調(diào)試的時(shí)候需要進(jìn)行人工操作,也必然導(dǎo)致時(shí)間間隔的不同)。因此,必須記錄每個(gè)內(nèi)核態(tài)指令與事件相對(duì)于用戶態(tài)指令發(fā)生的計(jì)數(shù)進(jìn)行記錄,并在回放的時(shí)候以此計(jì)數(shù)為基準(zhǔn)定位原內(nèi)核態(tài)指令與事件的發(fā)生時(shí)機(jī),進(jìn)行執(zhí)行歷史的回放。
圖3 給出了X86 平臺(tái)下單步反向調(diào)試的示意圖。由 圖中可以看到與gdb 不同,本調(diào)試器使用ptrace 跟蹤系統(tǒng)調(diào)用(步驟1),在運(yùn)行用戶態(tài)指令(上述的for 循環(huán)代碼)時(shí)不打斷被調(diào)試程序,但是一旦發(fā)生系統(tǒng)調(diào)用(步驟2),內(nèi)核就將暫停被調(diào)試程序,同時(shí)通知調(diào)試器(步驟3),調(diào)試器此時(shí)將保存系統(tǒng)調(diào)用信息至記錄文件(步驟4)。其實(shí)在系統(tǒng)調(diào)用結(jié)束后還有一次通知,那時(shí)調(diào)試器將把系統(tǒng)調(diào)用的結(jié)果記錄在文件中。
鑒于在國(guó)產(chǎn)平臺(tái)實(shí)施gdb 單指令反向調(diào)試與確定性反向調(diào)試實(shí)施存在的困難,參考微軟Visual Studio 企業(yè)版的IntelliTrace 功能[8],將以事件級(jí)反向調(diào)試取代指令級(jí)單步反向調(diào)試。
在此方案中,調(diào)試器在程序運(yùn)行過(guò)程中對(duì)程序的運(yùn)行狀態(tài)進(jìn)行快照,提供快照管理。下面的問(wèn)題是在什么時(shí)候進(jìn)行被調(diào)試應(yīng)用程序的快照,以及如何對(duì)被調(diào)試程序進(jìn)行快照。
類似于IntelliTrace,將程序運(yùn)行過(guò)程中發(fā)生的外部事件進(jìn)行分類,對(duì)其進(jìn)行記錄。這些外部事件主要是系統(tǒng)調(diào)用與信號(hào),這是因?yàn)槌绦虻倪\(yùn)行過(guò)程在用戶態(tài)指令中都是確定的,可以通過(guò)傳統(tǒng)的方法進(jìn)行調(diào)試并重現(xiàn)問(wèn)題(除非是執(zhí)行了非確定性指令,例如RTDSC 或者RDRAND等),因此,難以調(diào)試的故障均是與系統(tǒng)環(huán)境相關(guān)的故障。
而在Linux 操作系統(tǒng)中,主要是通過(guò)系統(tǒng)調(diào)用和信號(hào)來(lái)完成應(yīng)用程序與系統(tǒng)環(huán)境之間的交互。它們通過(guò)超出應(yīng)用程序指令預(yù)期的方式改變程序的行為,信號(hào)對(duì)應(yīng)于大多數(shù)程序運(yùn)行時(shí)錯(cuò)誤[4]。因此,通過(guò)記錄相應(yīng)的事件,可以回放和調(diào)試大量難以調(diào)試的軟件故障,從而有助于快速定位和解決故障。
在基于底層單步反向調(diào)試(X86 平臺(tái)下)與基于事件的反向調(diào)試的基礎(chǔ)上,將基于自主可控環(huán)境下主流的集成開發(fā)環(huán)境Eclipse 提供圖形化的反向調(diào)試界面,此界面將基于Eclipse 插件接口進(jìn)行開發(fā)。
此外,圖形化用戶界面還將提供事件管理功能,這些事件包括系統(tǒng)調(diào)用與信號(hào)等分類,一旦用戶設(shè)定了相應(yīng)的事件,在記錄反向調(diào)試信息時(shí)即可進(jìn)行過(guò)濾,從而可以減少事件記錄的壓力,提高反向調(diào)試的性能。
圖形界面插件與Eclipse 之間的接口為插件接口,與底層調(diào)試模塊的接口與RSP 調(diào)試協(xié)議接口。后臺(tái)調(diào)試器通過(guò)RSP 協(xié)議與集成開發(fā)環(huán)境的前端界面通信,接收前端界面?zhèn)鬟f的調(diào)試指令,會(huì)在處理完畢后發(fā)回相應(yīng)的響應(yīng)。
圖5 給出了開發(fā)人員、集成開發(fā)環(huán)境、圖形界面插件、調(diào)試器、被調(diào)試程序等幾者之間的互動(dòng)關(guān)系。
本文在龍芯平臺(tái)上實(shí)現(xiàn)了上述反向調(diào)試技術(shù)方案,龍芯運(yùn)行環(huán)境配置如表1 所示。
表1 龍芯運(yùn)行環(huán)境配置
用戶通過(guò)集成開發(fā)環(huán)境Eclipse 中本系統(tǒng)提供的插件,通過(guò)圖形界面的按鈕、快捷鍵與窗口運(yùn)行調(diào)試程序,記錄程序發(fā)生的事件以及事件的記錄與回放。記錄程序在運(yùn)行過(guò)程中的狀態(tài),將被分析程序執(zhí)行中發(fā)生的事件記錄下來(lái),再通過(guò)本系統(tǒng)的圖形界面插件對(duì)其進(jìn)行記錄、調(diào)試與可視化。
圖6 是集成開發(fā)環(huán)境反向調(diào)試插件的圖形界面示意圖。
在此界面上提供如下多個(gè)聯(lián)動(dòng)的窗口:
·隨著被調(diào)試程序的執(zhí)行,時(shí)間軸窗口將不斷推進(jìn),展示執(zhí)行過(guò)程中發(fā)生的事件;
·在時(shí)間軸窗口中開發(fā)人員可以對(duì)執(zhí)行過(guò)程中發(fā)生的時(shí)間進(jìn)行穿梭,在移動(dòng)時(shí)間軸時(shí),插件將讀取相應(yīng)的事件記錄,將其顯示在事件列表窗口中;
·事件列表窗口將載入每個(gè)事件的細(xì)節(jié),包括參數(shù)(如open 系統(tǒng)調(diào)用的文件名、打開標(biāo)識(shí)等參數(shù))與返回值、事件起止時(shí)間、源碼位置等;
·單擊事件窗口中的每條事件,除了顯示上述的事件相關(guān)信息外,還將在調(diào)用棧窗口中顯示對(duì)應(yīng)的調(diào)用棧信息,在局部變量窗口顯示局部變量信息;
·雙擊事件窗口中的每條事件,會(huì)在編輯窗口中載入并顯示相應(yīng)的源碼文件的對(duì)應(yīng)行。
本文分析了CDT 調(diào)試插件在Linux 操作系統(tǒng)中使用存在的不足,并通過(guò)分析X86 平臺(tái)下單步反向調(diào)試技術(shù)以及在國(guó)產(chǎn)平臺(tái)下實(shí)施的困難,提出了基于事件的反向調(diào)試技術(shù),并最終實(shí)現(xiàn)了插件的可視化,提高了嵌入式軟件的調(diào)試效率。
本文提出的反向調(diào)試插件界面友好,從功能上簡(jiǎn)化了用戶的操作,使開發(fā)人員的調(diào)試工作更加簡(jiǎn)捷直觀,軟件調(diào)試效率得到了提高,但仍然存在一些不足,如目前只能適用于國(guó)產(chǎn)龍芯平臺(tái),對(duì)于其他國(guó)產(chǎn)平臺(tái)還不支持等,這些問(wèn)題需要在下一步工作中繼續(xù)改進(jìn)。