韓承鋒,唐云善,楊維永
(南京南瑞信息通信科技有限公司,南京 210003)
源代碼靜態(tài)分析是軟件安全漏洞檢測(cè)的主要方法之一.靜態(tài)分析是指在不運(yùn)行程序情況下,對(duì)軟件源代碼進(jìn)行掃描分析.基于抽象語法樹和程序控制流圖等模型的數(shù)據(jù)流分析則是現(xiàn)階段靜態(tài)檢測(cè)的主要使用技術(shù)[1].使用靜態(tài)檢測(cè)技術(shù)實(shí)現(xiàn)的開源、商用代碼檢測(cè)工具已較為成熟,如 PMD[2]、FindBugs[3]、Fortify SCA[4]等.然而,這些代碼靜態(tài)檢測(cè)工具都是單機(jī)單進(jìn)程運(yùn)行,實(shí)現(xiàn)對(duì)代碼的檢測(cè).由于靜態(tài)分析需要遍歷程序生成的中間語言,所以隨著程序代碼規(guī)模的增大,分析工具所需要的掃描時(shí)間也隨之增加.掃描單元級(jí)代碼需要數(shù)十分鐘甚至一小時(shí),掃描系統(tǒng)級(jí)代碼需要高達(dá)數(shù)十小時(shí),這對(duì)項(xiàng)目開發(fā)進(jìn)度產(chǎn)生較大的影響.
國(guó)家電網(wǎng)南瑞集團(tuán)信通公司已開發(fā)了基于云計(jì)算的分布式代碼檢測(cè)系統(tǒng)QMAP2.0,該系統(tǒng)以檢測(cè)節(jié)點(diǎn)動(dòng)態(tài)擴(kuò)展的方式實(shí)現(xiàn)了多任務(wù)同時(shí)在線檢測(cè),但仍是單任務(wù)單節(jié)點(diǎn)運(yùn)行,依然存在當(dāng)任務(wù)包過大時(shí),檢測(cè)實(shí)現(xiàn)過長(zhǎng)的問題.為解決該問題,在QMAP2.0系統(tǒng)基礎(chǔ)之上,基于依賴性分包的Java分布式靜態(tài)檢測(cè)系統(tǒng)被提出.該系統(tǒng)提出了單任務(wù)多節(jié)點(diǎn)并行檢測(cè)的方法,對(duì)單任務(wù)進(jìn)行程序代碼分包,再進(jìn)行多節(jié)點(diǎn)并行分布式檢測(cè),達(dá)到縮短程序檢測(cè)時(shí)間的目的.
現(xiàn)階段,Leo Pruijt,Christian K?ppe 和 Sjaak Brinkkemper提出了在模塊層級(jí)上的直接結(jié)構(gòu)依賴和間接結(jié)構(gòu)依賴[5];Judith A.Stafford,Debra J.Richardson 和 Alexander L.Wolf提出了軟件系統(tǒng)在結(jié)構(gòu)關(guān)系和行為關(guān)系兩種關(guān)系產(chǎn)生的體系結(jié)構(gòu)層級(jí)上的依賴關(guān)系[6];Xinyi Dong 和 Michael W.Godfrey 提出了一種能夠在系統(tǒng)層級(jí)上高度抽象面向?qū)ο笙到y(tǒng)的模型——高級(jí)對(duì)象依賴關(guān)系圖模型[7];陳樹峰,鄭洪源等人從類層級(jí)上對(duì)面向?qū)ο筌浖蕾囆赃M(jìn)行了分析[8].Java分布式靜態(tài)檢測(cè)系統(tǒng)的需求是對(duì)軟件的源代碼包進(jìn)行文件間的解耦,文獻(xiàn)[5-7]是從更為粗粒度的模塊和系統(tǒng)出發(fā),分析整個(gè)程序的模塊和體系的依賴性;文獻(xiàn)[8]則是從比文件更為細(xì)粒度的類層次,對(duì)程序類間的依賴進(jìn)行分析.Java文件間的依賴分析主要是從源代碼文件層面出發(fā),去分析文件之間的依賴關(guān)系,與模塊層級(jí)相比更為細(xì)粒度,而較之類層級(jí)又稍顯粗粒度;同時(shí),需要深入研究基于文件間依賴性關(guān)系的程序包進(jìn)行解耦拆分方法.
為了實(shí)現(xiàn)文件間解耦功能,本文主要針對(duì)Java分布式靜態(tài)檢測(cè)系統(tǒng)的前端分析模塊——依賴性分析模塊進(jìn)行研究,該模塊主要針對(duì)輸入的程序代碼包進(jìn)行文件間的依賴性分析,給出任務(wù)分包結(jié)果.該模塊分析結(jié)果的準(zhǔn)確性是保證縮短檢測(cè)時(shí)間卻不影響檢測(cè)結(jié)果的重要前提.本文進(jìn)行的主要工作如下:1)闡述依賴性分析模塊的設(shè)計(jì)思路;2)基于對(duì)該模塊設(shè)計(jì)思路,進(jìn)行了模塊的開發(fā)實(shí)現(xiàn);3)針對(duì)實(shí)現(xiàn)后的模塊進(jìn)行準(zhǔn)確性實(shí)驗(yàn).
下面簡(jiǎn)要介紹本文所用的開源工具JavaParser以及源代碼分布式靜態(tài)檢測(cè)系統(tǒng)相關(guān)背景知識(shí).
JavaParser[9]是一個(gè)Github上的開源項(xiàng)目,該庫能夠讓開發(fā)者在Java環(huán)境中以訪客支持的方式使用抽象語法樹(Abstract syntax tree)與Java源代碼進(jìn)行交互.JavaParser把語法樹抽象為CompilationUnit類,每一個(gè)該類的實(shí)例化都記錄了一個(gè)Java源代碼文件的所有信息.圖1給出了一段簡(jiǎn)單代碼在JavaParser工具解析后生成的抽象語法樹形狀(圖中樹的底部的三角形記號(hào)表示這部分已經(jīng)被總結(jié)).如圖1所示,CompilationUnit對(duì)象主要有三大類子樹:包聲明(PackageDeclaration)節(jié)點(diǎn)、導(dǎo)入聲明(ImportDeclaration)節(jié)點(diǎn)、類接口聲明(ClassOrInterfaceDeclaration)節(jié)點(diǎn),類的主要內(nèi)容則在類接口聲明節(jié)點(diǎn)的子樹中.
圖1 JavaParser生成的抽象語法樹
同時(shí),該項(xiàng)目還提供了一個(gè)JavaSymbolSolver庫,該庫能夠解析JavaParser生成的抽象語法樹節(jié)點(diǎn)上各種符號(hào)的引用,為開發(fā)者找到由該符號(hào)表示的變量聲明,參數(shù)聲明或類型聲明提供了便利.
圖2是基于依賴性分包的Java源代碼分布式靜態(tài)檢測(cè)系統(tǒng)總體的工作流程圖,主要分為以下幾個(gè)階段.
圖2 源代碼分布式靜態(tài)檢測(cè)系統(tǒng)工作流程圖
(1)第一階段,系統(tǒng)讀取程序文件,若程序文件不是源代碼文件則退出執(zhí)行;
(2)第二階段,文件依賴性分析.首先針對(duì)輸入程序代碼包,依賴性分析模塊對(duì)源代碼文件進(jìn)行依賴性分析;然后針對(duì)文件依賴性分析的結(jié)果進(jìn)行分析,得到互不依賴文件的集合;再后根據(jù)系統(tǒng)要求,合并互不依賴文件的集合,得到若干個(gè)解耦后相互獨(dú)立的文件集合;
(3)第三階段,編譯器模塊對(duì)整個(gè)工程進(jìn)行編譯,根據(jù)第二階段的分析結(jié)果,進(jìn)行子包拆分;
(4)第四階段,Findbugs檢測(cè)模塊獲取到子任務(wù)包后,對(duì)任務(wù)包進(jìn)行檢測(cè);
(5)第五階段,系統(tǒng)收到所有檢測(cè)模塊檢測(cè)完畢信息后,結(jié)束整個(gè)檢測(cè)過程.
在整個(gè)系統(tǒng)中,最重要的模塊是Java源代碼依賴性分析模塊,該模塊生成的各源文件子包的大小決定了整個(gè)檢測(cè)過程消耗的時(shí)間.同時(shí),各源文件子包之間是否完全解耦也決定了分包前后的檢測(cè)結(jié)果是否受到影響.
一個(gè)Java程序可以分為系統(tǒng)級(jí)、類層次級(jí)、方法級(jí)以及語句級(jí).依賴性分析與之相對(duì)應(yīng)的分層是:包間依賴、類間依賴、方法依賴、語句依賴[10].本系統(tǒng)依賴性分析模塊主要進(jìn)行源文件的文件間依賴進(jìn)行分析.
一個(gè)完整的Java源代碼程序是由一個(gè)或數(shù)個(gè)以Java為后綴名的文本文件組成,每一個(gè)文本文件至多屬于一個(gè)包(package),一個(gè)包中可以含有一個(gè)或者多個(gè)文本文件.在Java語言中,每一個(gè)文本文件都是一個(gè)外部類(public class),該類包含一個(gè)或多個(gè)變量、方法與接口(Interface),接口與類功能類似,但是接口只能實(shí)現(xiàn)(Implenments),不能被實(shí)例化.
依賴性分析模塊主要分析Java源代碼程序每一個(gè).java文本文件之間的依賴性,所以必須從Java程序類間依賴出發(fā),再獲取文件間的依賴關(guān)系.Java程序的類層次依賴主要是由繼承、接口實(shí)現(xiàn)、類創(chuàng)建、接口實(shí)例化、內(nèi)部類、類的靜態(tài)域引用與以及靜態(tài)方法調(diào)用等引起的類之間依賴關(guān)系[8].
Java語言由于追求程序設(shè)計(jì)的靈活性,其存在著動(dòng)態(tài)特性—一個(gè)方法在父類中被定義,若在子孫類中被重新定義,則方法在調(diào)用的時(shí)候回涉及到方法的多態(tài)調(diào)用以及動(dòng)態(tài)綁定.同時(shí)由于Java語言對(duì)對(duì)象類型的綁定時(shí)機(jī)屬于晚綁定,若要精確的分析類的依賴性,需要獲得Java程序的運(yùn)行時(shí)信息[11].由于,靜態(tài)檢測(cè)的目的就是不運(yùn)行程序,所以依賴分析模塊的實(shí)現(xiàn)也是在不運(yùn)行程序的情況下進(jìn)行以依賴性分析,此時(shí)對(duì)Java程序的多態(tài)特性進(jìn)行分析,就是對(duì)對(duì)象的所有可能類型進(jìn)行靜態(tài)確定,這在Java語言上是能夠?qū)崿F(xiàn)的[11].
圖3是依賴性分析模塊的工作流程圖,該模塊的工作流程主要分為以下階段.
圖3 文件依賴分析模塊流程圖
(1)第一階段,掃描該工程下所有文件并記錄文件信息,包括文件名、文件數(shù)量、文件是否已分析完畢等.
(2)第二階段,選取一個(gè)文件,若該文件并未進(jìn)行依賴性分析,則使用JavaParser庫生成該文件的抽象語法樹;若該文件已經(jīng)進(jìn)行過依賴性分析,則跳過該文件,進(jìn)行第五階段檢查.
(3)第三階段,遍歷生成的抽象語法樹,通過下面8個(gè)步驟對(duì)該文件對(duì)其他文件的依賴性進(jìn)行分析:
1)第一步,分析本文件所在的包以及Import產(chǎn)生的包依賴,在接下來的步驟中直接遍歷依賴的包中的類以及 Import依賴的類,縮小遍歷范圍.同時(shí),記錄 Import Static引入的靜態(tài)變量;
2)第二步,類聲明分析.針對(duì)該文件的 public 類以及類的內(nèi)部類進(jìn)行分析,若這些類存在父類或者實(shí)現(xiàn)的接口類,則記錄下來;
3)第三步,變量聲明分析.分析所有變量聲明語句,記錄所有是類對(duì)象變量所屬的類;若這些變量語句是初始化語句,同時(shí)檢查初始化的值是否是類的靜態(tài)成員變量,是則記錄下該成員變量所屬的類;
4)第四步,方法聲明分析.包括類的構(gòu)造方法以及普通方法.分析方法的返回類型、入?yún)㈩愋鸵约皰伋鲱愋?記錄這三處地方存在的類;
5)第五步,特定語句分析.針對(duì)可能存在類符號(hào)的語句進(jìn)行分析,例如賦值語句,類對(duì)象創(chuàng)建語句,for的條件語句,catch的條件語句等.遍歷所感興趣的語句,記錄語句中存在的類或者類的靜態(tài)成員;
6)第六步,方法調(diào)用分析.首先,分析調(diào)用方法的輸入?yún)?shù),若參數(shù)類的靜態(tài)成員變量,則記錄下該變量所屬的類;然后,分析該方法是否是靜態(tài)成員方法,若是則記錄下該成員方法所屬的類;最后,若該方法不是靜態(tài)成員方法,查詢?cè)摲椒ㄊ欠駥儆谠撐募ublic類或者內(nèi)部類,若不是則查詢并記錄下該方法所屬的外部類.
7)第七步,Java 注解 (Annotation)分析.分析文件中注解,若存在自定義注解,則把該注解類記錄下來.
8)第八步,根據(jù)前7步所獲取的該文件依賴的類,定位這些類所在的文件,得到的文件為該文件所依賴的文件.
(4)第四階段,根據(jù)第三階段所得結(jié)果,生成表示該文件對(duì)其他文件依賴的有向圖.
(5)第五階段,檢查所有文件是否已經(jīng)分析完畢.若分析完畢,則合并第四階段所得的每個(gè)文件的依賴有向圖,獲得所有文件之間依賴關(guān)系的有向圖;若尚未分析完畢,則跳回第二階段進(jìn)行迭代.
(6)第六階段,對(duì)所有文件之間依賴關(guān)系的有向圖進(jìn)行分析,獲取不可再分的文件集合;然后,根據(jù)系統(tǒng)需求(比如拆分后文件包大小等),合并不可再分文件集合,得到若干個(gè)相互獨(dú)立的文件集合,這些相互獨(dú)立文件集合為解耦拆分子包的文件集合.
程序源文件的分包結(jié)果對(duì)檢測(cè)的準(zhǔn)確性和檢測(cè)時(shí)間有較大影響,所以依賴性分析模塊除了對(duì)文件依賴性進(jìn)行分析之外,如何根據(jù)生成的文件依賴關(guān)系圖進(jìn)行對(duì)源程序進(jìn)行分包也是該模塊的一個(gè)重要步驟.
圖4是一個(gè)程序源文件依賴圖示例.圖中箭頭指向表示依賴關(guān)系,例如圖中A→G表示表示A文件依賴于G文件,文件D、G、E、F之間形成的依賴環(huán)稱為文件集.在處理圖過程中,所有的文件集被簡(jiǎn)化為一個(gè)獨(dú)立節(jié)點(diǎn)對(duì)外進(jìn)行分析.
圖4 源文件依賴關(guān)系圖
針對(duì)文件集對(duì)關(guān)系圖簡(jiǎn)化后,此時(shí)所有節(jié)點(diǎn)之間都是單向依賴關(guān)系.由文件間依賴性關(guān)系的特點(diǎn)可知,簡(jiǎn)化后的關(guān)系圖必然存在一個(gè)沒有入邊的頂點(diǎn).所以,在對(duì)文件依賴關(guān)系有向圖進(jìn)行簡(jiǎn)化之后,首先尋找圖中所有無入邊頂點(diǎn),如圖4中的A、B、H節(jié)點(diǎn),以這些節(jié)點(diǎn)作為遍歷依賴圖的起始節(jié)點(diǎn).同時(shí),以所有沒有出邊的節(jié)點(diǎn)為終結(jié)節(jié)點(diǎn),遍歷整個(gè)依賴圖,得到的所有節(jié)點(diǎn)集合則為最小不可分的依賴文件集合.如圖4中,以A為起始點(diǎn),遍歷整個(gè)依賴圖,得到所有終結(jié)點(diǎn)文件集1和文件集2,可到一個(gè)不可再分文件集合a(A,文件集1,文件集2);同理,以B為起始點(diǎn)可得到不可再分集合b(B、文件集2),以H為起始點(diǎn)可得到不可再分文件集合c(H、I、J).
得到所有不可再分集合之后,根據(jù)系統(tǒng)實(shí)際要求的分包大小,把不可分集合進(jìn)行合并.具體合并規(guī)則為:1)有交集的兩個(gè)集合先合并;2)多個(gè)集合同時(shí)存在交集,交集大的先合并.
如圖4所得到的3個(gè)不可再分集合,若兩兩相合并之后的大小都滿足系統(tǒng)要求,因?yàn)榧蟖和集合b存在交集文件集2,所以在合并過程中應(yīng)該先合并集合a和集合b,得到獨(dú)立文件集合SetA,若獨(dú)立文件集合SetA和不可再分文件集合c合并后大小超出系統(tǒng)要求,則不可再分文件集合c則被視為獨(dú)立文件集合SetB.把不可再分集合合并之后得到若干獨(dú)立文件集合就是最終程序分包的實(shí)際文件集合,如圖4所得SetA和SetB就是最終所得的兩個(gè)程序子包的文件集合.
為了評(píng)估依賴分析模塊的準(zhǔn)確性,本文通過兩個(gè)步驟進(jìn)行驗(yàn)證:1)針對(duì)自行編寫的簡(jiǎn)單示例程序,驗(yàn)證依賴分析8個(gè)步驟分析結(jié)果的準(zhǔn)確性;2)選擇了三個(gè)Java開源項(xiàng)目作為實(shí)驗(yàn)對(duì)象,驗(yàn)證模塊是否存在普遍準(zhǔn)確性.
實(shí)驗(yàn)軟件環(huán)境:JDK8.實(shí)驗(yàn)對(duì)象為自行編寫的簡(jiǎn)單示例軟件、Apache Commos項(xiàng)目下26個(gè)獨(dú)立組件、JfreeChart開源軟件以及Findgbus檢測(cè)工具.實(shí)驗(yàn)對(duì)象基本情況如表1所示.
表1 實(shí)驗(yàn)軟件基本情況
如表1所示,實(shí)驗(yàn)使用的的示例程序共有7個(gè)類文件,其中代碼規(guī)模和文件關(guān)系最復(fù)雜的就是A.java文件,所以本次實(shí)驗(yàn)主要針對(duì)依賴關(guān)系最復(fù)雜的A文件進(jìn)行分析,驗(yàn)證模塊單步的準(zhǔn)確性.
圖5是主要表示了A.java文件與其他源代碼文件的依賴關(guān)系圖.圖中每一個(gè)小圓圈代表著一個(gè)源代碼文件;虛線框表示源代碼文件所處的包,如A.java文件是處在com.dependencyfiles.package1包中;與圖4類似,箭頭表示著文件的依賴關(guān)系.
如表2所示,每一個(gè)步驟都節(jié)選A.java文件中部分該步驟分析的語句,并給出相應(yīng)結(jié)果,每步分析結(jié)果具體過程如下:
(1)在第1步分析時(shí),模塊首先分析package語句,可得到文件屬于com.dependencyfiles.package1包,同時(shí)通過掃描的方式獲取包下其他文件;然后,模塊分析import語句,如表2中所示,得到文件可能依賴的類為com.dependencyfiles包下A3類的類.
圖5 示例程序的依賴關(guān)系圖
(2)在第2步分析時(shí),模塊需要分析所有類聲明的語句,如表2示例語句所示,獲取到該文件外部類A,有父類FA,實(shí)現(xiàn)了接口IA.
(3)在第3步分析時(shí),模塊需要對(duì)類和類成員方法中變量聲明語句進(jìn)行分析.如表2示例語句所示,該語句是帶初始化的變量聲明語句,模塊分析得到結(jié)果為:變量是屬于 List類,List泛型為 String,變量初始化為ArrayList類.
(4)在第4步分析時(shí),模塊針對(duì)方法聲明的返回類部分、入?yún)⒉糠忠约皰伋鲥e(cuò)誤部分進(jìn)行分析.如表2示例語句所示,分析該方法聲明語句塊可得到:返回類型屬于A1類;形式參數(shù)屬于類Map,Map的泛型為String;拋出錯(cuò)誤類型為AException類.
表2 模塊對(duì) A.java 文件的部分單步分析結(jié)果
(5)在第5步分析時(shí),模塊針對(duì)關(guān)心的興趣點(diǎn)語句進(jìn)行分析,如對(duì)象創(chuàng)建語句、返回語句、for循環(huán)的條件語句、catch塊的條件語句等.如表2中示例語句所示,模塊分析可得for語句塊的條件變量屬于Set類,Set的泛型屬于 String 類;同理,try 語句塊的 catch 條件屬于類AException類.
(6)在第6步分析時(shí),模塊針對(duì)所有的方法調(diào)用進(jìn)行分析.如表2中示例語句所示,模塊通過分析可得,調(diào)用方法屬于變量a2,由上下文查找得到變量a2的初始化語句,分析得屬于類A2;同時(shí),方法調(diào)用的入?yún)轭愋虯3類.
(7)在第 7 步分析時(shí),模塊針對(duì)注解進(jìn)行分析.如表2中示例語句所示,模塊通過分析可得注解屬于AAnnotation類.
(8)在第8步分析時(shí),模塊對(duì)前7步所獲取到的依賴類進(jìn)行定位,確定A.java文件所依賴的源代碼文件.如表2中示例所示,第2步分析所得類FA屬于FA.java文件,接口IA屬于IA.java文件;第3步分析所得類List、String以及ArrayList類均不存在于示例源代碼文件中,所以這些類不產(chǎn)生文件間依賴關(guān)系;第4步分析中,方法聲明中返回類型A1屬于A1.java文件,拋出錯(cuò)誤類型AException屬于AException.java文件;在第6步分析時(shí),方法調(diào)用的入?yún)3類屬于A3.java文件;第7步分析時(shí),注解AAnnotation類屬于AAnnotation.java文件.至此,A.java文件所依賴的其他源代碼文件被找出.
通過對(duì)每一步分析過程的的展示,在示例程序上,依賴分析的最終結(jié)果與圖5實(shí)際結(jié)果相同,模塊的分析結(jié)果的準(zhǔn)確性達(dá)到了實(shí)驗(yàn)的預(yù)期.
依賴分析模塊分析結(jié)果主要是通過使用編譯器對(duì)分包后的源文件集進(jìn)行編譯的方法進(jìn)行準(zhǔn)確性驗(yàn)證.因?yàn)榫幾g器在對(duì)源文件進(jìn)行編譯時(shí),需要對(duì)源文件進(jìn)行語義檢查,若模塊分析模塊對(duì)程序進(jìn)行分包后得到的子包有文件缺失時(shí),編譯器的語義檢查則得不到通過,會(huì)導(dǎo)致編譯無法完成.因此,以把程序分包后得到的不同子包進(jìn)行編譯的方式來驗(yàn)證模塊分包的準(zhǔn)確性是可行的.
因?yàn)橐蕾嚪治瞿K是先獲取所有不可分依賴集之后再進(jìn)行合并,每個(gè)不可分依賴集都是獨(dú)立的,所以驗(yàn)證分析結(jié)果的準(zhǔn)確性時(shí),只需要對(duì)所有的不可分依賴集進(jìn)行驗(yàn)證即可.
經(jīng)過依賴性分析模塊分析之后,Apache Commons組件被拆分為906個(gè)不可分集合,JfreeChar工具被拆分為143個(gè)不可分集、Findbugs工具被拆分為為有290個(gè)不可分集.所有這些不可分集合在使用JavaC編譯器進(jìn)行編譯時(shí)都能順利編譯通過,驗(yàn)證了依賴性分析模塊是可行性的.
本文為滿足Java源代碼分布式靜態(tài)檢測(cè)系統(tǒng)需求,提出了一種適用于該系統(tǒng)的Java源代碼包文件間依賴分析技術(shù).該技術(shù)在文獻(xiàn)[8]的類依賴關(guān)系分析基礎(chǔ)上,使用了類定位文件的方法,完成了源代碼文件間依賴性的分析;同時(shí),提出了用于表達(dá)文件間依賴關(guān)系的有向圖,并基于該圖提出了程序包解耦拆分的方法.最后,本文基于Javaparser開源工具,設(shè)計(jì)和實(shí)現(xiàn)了該系統(tǒng)的Java源文件依賴分析模塊,并針對(duì)該模塊的準(zhǔn)確性進(jìn)行了驗(yàn)證.實(shí)驗(yàn)驗(yàn)證結(jié)果證明,該模塊可以較好的適用于Java源文件程序包的文件間依賴性分析和程序文件間解耦分包,能夠適用于類似代碼靜態(tài)檢測(cè)等程序源代碼文件解耦拆分的場(chǎng)景.