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