莫培弘,衷璐潔
(首都師范大學 信息工程學院,北京 100048)
現代軟件系統的復雜性越來越突出,程序的規模也越來越大,難以直觀地了解程序的編碼邏輯結構。過程間信息以及過程內信息能夠反映軟件系統中的程序編碼邏輯,在對程序的理解和分析、軟件的測試、調試和維護、編譯優化、錯誤定位、bug查找、過程間數據流分析、回溯測試等軟件工程領域中都有應用[1-3],完整的過程間信息和過程內信息更好地輔助程序驗證和程序調試[4,5],提高程序分析的質量,增強對程序的理解。
程序分析方法分為動態分析方法和靜態分析方法。其中,靜態分析方法不需要運行待分析的程序就能獲取程序可能執行的狀態,有利于對程序進行自動化和快速分析。在大型項目中存在大量的過程間信息和復雜的過程內信息,通過靜態分析可以快速地獲取過程間信息和過程內信息。精確的過程內信息更好地提高靜態分析的精度[6,7]。函數指針指向信息的準確獲取對過程間信息分析的精度有著重要的作用[8,9]。完整的過程內信息和精確的過程間信息有助于代碼閱讀和分析人員在閱讀和分析大工程或者開源代碼時快速地了解程序的過程內結構和過程間的層次結構。
目前,對程序靜態分析工作的研究比較活躍。針對過程間信息提取的問題,牟等[10]在函數調用路徑的基礎上獲取測試用例優先級排序的問題,通過獲取函數調用的路徑,并利用調整算法實現動態調整測試用例的優先級排序。孫等[11]針對操作系統內核等大型軟件的函數調用問題,通過RTL工具對源碼生成的中間信息提取函數調用信息,生成函數調用圖。王等[12]針對多語言函數調用圖的構建工具重用率低和實現復雜的問題,通過GNU編譯器集合(GCC)的插件在GCC中間表示層上提取函數調用關系并轉化成圖形描述語言,獲取函數調用圖。
現有的過程間分析工具存在函數調用信息提取不夠準確和處理庫函數調用信息不夠完善的問題。
Source Insight[13]是一個項目向導的程序編輯器和代碼瀏覽器,具有對引用樹(reference tree),類繼承圖(class inheritance diagram)和調用樹(call trees)等程序分析信息的可視化支持,并可以生成函數調用圖。
CodeViz[14]是一個C源碼靜態分析工具,針對C程序生成可視化的函數調用圖,通過給GCC打補丁,在編譯源文件時dump出函數調用信息,再通過Perl腳本提取函數調用信息。
Cflow[15]是一個C源碼程序靜態分析工具,它可以產生前向和反向的兩種函數調用圖,直接對源碼進行分析,生成一個C程序的函數調用信息的外部引用集合。
CallTree[16]是一個C源碼靜態調用樹生成器,通過分析C源碼,提取函數調用信息。
但是上述文獻[10-12]中均不能獲取函數指針指向的信息,存在函數調用信息獲取不夠完善的問題;Source Insight和CodeViz不能獲取函數指針指向的信息;CallTree不能獲取函數指針指向的真實信息;CodeViz、CallTree以及Cflow不能完善地處理庫函數調用信息。綜上,在現有過程間信息獲取的工作中存在的主要問題有以下兩點:
(1)存在不能獲取函數指針指向的真實信息;
(2)存在不能完善地處理庫函數的調用信息。
LLVM(low level virtual machine)是一個被廣泛使用的開源編譯器框架,它基于靜態單賦值(static single assignment,SSA)的編譯策略,能同時支持靜態和動態的任意編程語言的編譯目標。LLVM有著豐富的工具鏈和用戶可自由定義的LLVM Pass等,是一個模塊化、可重復使用的編譯器和工具技術的集合。它由不同的子項目組成,許多項目已經在各種商業和開源項目中得到廣泛應用,并被應用于學術研究[17,18]。
IR是LLVM的中間表示(intermediate representation,IR),是LLVM編譯框架的一個重要組成部分。IR中包含豐富的程序分析信息[19],由模塊、全局變量、函數以及連接類型等信息組成。其中,這些程序信息中包含過程內信息和過程間信息。
LLVM自定義了一組獨特的指令模式,并為用戶提供了大量的API接口和可復用的庫,通過編寫LLVM Pass可以為用戶從IR中獲取程序分析信息帶來方便。針對上述函數指針指向信息獲取不夠準確和庫函數調用信息處理不完善的問題,提出一種LLVM中靜態程序信息的過程間分析方法(control-flow inter-procedural call-graph,CFICG)。
LLVM IR文件由LLVM指令組成,本文獲取過程內信息和過程間信息,需要對ret、br、switch,call,store以及load等LLVM指令等進行解析,進而提取本文需要的靜態程序分析信息。表1是代表性的LLVM指令,如終結指令:通過獲取br指令,可以獲取當前基本塊的后繼信息;通過switch指令獲取基本塊的分支信息,解決控制流的分支問題。

表1 代表性LLVM指令
LLVM IR文件中包含指針、數據流、過程內以及過程間等的信息。通過提取本文需要的過程內和過程間的信息,獲取CFICG信息。CFICG表示為
CFICG=
其中,cf
關于過程內信息的提取,每一個函數都由一個或若干個基本塊組成,一個基本塊又由一條或者若干條語句組成,在LLVM IR中以switch、ret、br等終結指令區分過程內基本塊的信息,如圖1所示。

圖1 過程內信息分析
以上述圖1(a)中的main函數為例進行說明(Li中,L表示行,i表示第幾行)。main函數有4個基本塊,分別如圖1(a)中4個實線方框所示;main函數中有兩條過程內路徑,分別是L7->L9->L14和L7->L11,L12->L14。
圖1(b)為圖1(a)對應的LLVM IR,源碼中的基本塊語句轉化成LLVM中的指令,如圖1中虛線箭頭對應的方框,在圖1(b)中實線方框前的虛線方框即為當前基本塊的名稱,圖1(b)中main函數分別有entry,if.then,if.else和if.end這4個基本塊,其過程內的路徑分別為entry->if.else->if.end和entry ->if.then ->if.end。
每一個基本塊由多條LLVM指令組成,并以br,ret等終結指令劃分基本塊。如圖1(b)中main的entry由{L9,L10,L11,L12}組成,L12中實線箭頭表示為圖2,entry以br指令結束一個基本塊并由label指令指明當前基本塊的后繼基本塊的個數和名稱,一個label指令對應一個后繼基本塊,entry基本塊的br指令中存在兩個label指令,即存在兩個后繼的基本塊。通過準確地獲取過程內信息并組成與源碼對應的過程內信息,解決過程內信息的復雜性與多樣性地問題。

圖2 基本塊信息獲取
函數調用是一個有向圖,這里先將其表示為CallGraph(V,E),V表示函數調用中所有函數節點的集合,E表示節點之間的邊,并滿足E?V×V。
表示如下:

其中,VE表示函數調用之間的關系集合;functionSet表示函數的集合;F(a,b)表示函數a到函數b之間有一條路徑。
過程間信息提取需要區分直接函數調用信息和函數指針指向信息,在LLVM 的指令中直接函數調用與函數指針都以call指令的形式表示[19],如圖3所示。為區分直接函數調用和函數指針,需要做進一步的分析,即通過函數所在的文件、函數名、參數類型和參數個數并結合定值-引用(def-use)方法和指針分析方法(load和store指令)來確定函數指針指向的信息。

圖3 過程間信息分析
圖3(a)中假設EditFunc函數與FileFunc函數已事先定義好,由函數指針*funcptr1,*funcptr2和4個非函數指針的函數FileFunc,EditFunc,foo和main組成,圖中實線箭頭表示函數調用的過程,虛線箭頭表示函數指針指向的真實函數,main函數的調用路徑,分別是main->foo->funcptr1和main->funcptr2,這兩條調用路徑上都有函數指針。
獲取函數指針指向的真實信息需要獲取函數指針指向的真實函數,即funcptr1指向的真實函數是FileFunc,funcptr2指向的真實函數是EditFunc, 得出main中的兩條真實函數調用路徑,分別為main-> foo->FileFunc和main->EditFunc。
圖3(a)源碼轉化成圖3(b)的LLVM 指令,對圖3(b)中main的過程間信息分析,存在兩條調用路徑,即圖中細虛線箭頭表示的調用路徑和細實線箭頭表示的調用路徑。細虛線箭頭調用信息表示為圖4(a)所示,main通過call指令調用foo函數,foo通過call調用 FileFunc函數。

圖4 main調用信息的路徑
第二條函數調用信息的路徑,即圖中細實線箭頭所示,表示為圖4(b),main函數通過call指令調用“%1”,“%1”的形成過程,首先,store指令將EditFunc函數存儲到函數指針funcptr2中,然后load指令將funcptr2加載到“%1”,最后main函數通過call指令調用“%1”,最終獲取函數指針指向的真實函數EditFunc。
上述的兩條函數調用路徑中都存在函數指針信息,獲取準確的過程間信息需要做以下分析:
(1)區分直接函數調用信息與函數指針指向信息;
(2)分析函數指針指向的信息存在的兩種情況:①只存在call指令形式的函數指針信息;②存在store、load和call指令的函數指針信息。
直接函數調用和函數指針都以call指令表示,通過對函數所在的文件、函數名、參數類型和參數個數等進行分析獲取函數指針指向的信息。針對函數指針的第二種情況,需要對store和load進行指針分析,再對call進行指向分析獲取函數指針指向的信息。
最后,結合獲取的過程內信息和過程間信息,組成CFICG信息,如圖5所示。

圖5 main的CFICG信息
針對上述過程間存在不能獲取函數指針指向的真實信息和不能完善地處理庫函數調用信息的問題,提出一種在LLVM 中提取靜態程序信息的過程間分析方法(CFICG),其結構框架如圖6所示。首先輸入LLVM IR文件,通過上述過程間信息提取的分析方式,編寫LLVM Pass提取過程內信息和過程間信息,形成程序執行的所有可能過程,并解析過程內信息和過程間信息,最后生成過程內信息和過程間信息的文本。解決過程間的函數指針指向信息不夠準確和庫函數調用信息處理不完善的問題以及過程內信息的復雜性與多樣性的問題。

圖6 CFICG框架
為了獲取過程內信息和過程間信息,提出在LLVM中靜態程序信息的過程間分析方法(CFICG),CFICG算法表示為getProInfo(),其由3部分組成,分別是過程內信息提取算法(getIntra()),直接函數調用信息提取算法(getCusCall())和函數指針指向信息提取算法(getPtrCall()),它們的具體算法描述分別如下所示。
CFICG的算法描述如下:
CFICG的算法描述
輸入:IR
輸出:過程內信息、過程間信息
getProInfo(IR)
(1)getIntra(IR)//提取過程內信息算法
(2)getCusCall(IR)//提取直接函數調用信息算法
(3)getPtrCall(IR)//提取函數指針指向信息算法
CFICG算法包括3部分:過程內信息提取算法、直接函數調用信息提取算法和函數指針指向信息提取算法,分別是算法1、算法2和算法3,算法1、算法2和算法3的輸入都是LLVM的IR,輸出分別是過程內信息、直接函數調用信息和函數指針指向信息。
首先獲取每一個函數的每一個基本塊,然后獲取每一個基本塊的終結指令,再根據終結指令確定基本塊的后繼,重復上述步驟,依次獲取,直到每一個函數執行完畢。其算法如下:
算法1:過程內信息提取的算法描述
輸入:IR
輸出:過程內信息
getIntra(IR)
(1)forbbin func do //遍歷函數獲取基本塊
(2)br←branch(bb); //獲取br指令
(3) if unconditional_branch(br) then
(4)sets←successors(bb);//無后繼的基本塊
(5) else
(6)sets_n←successor(bb);//有后繼的基本塊
(7) end if
(8)end for
本文的過程間信息分為直接函數調用信息和函數指針指向信息,其中直接函數調用信息包括庫函數調用信息和非庫函數調用信息。
4.3.1 直接函數調用信息提取的算法描述
從LLVM指令中獲取call指令,并通過call指令獲取直接函數調用信息,對獲取的函數進行庫函數和非庫函數的區分,最后得到直接函數調用信息。其算法如下:
算法2:直接函數調用信息提取的算法描述
輸入:IR
輸出:直接函數調用信息
getCusCall(IR)
(1)forfuncin mod do //遍歷模塊獲取函數
(2) forbbin func do //遍歷函數獲取基本塊
(3) foriin basicbk do //遍歷基本塊獲取指令
(4)setinstrc←i;
(5) if(call←instrc) //獲取call指令
(6)setfunct←call; //獲取函數調用信息
(7)setfunct←funct; //獲取非庫函數調用信息
(8) end if
(9) end for
(10) end for
(11) end for
4.3.2 函數指針指向信息提取的算法描述
通過解析call指令和call的函數,若不是函數指針則不做進一步分析,直接獲取調用的函數信息;若是函數指針則進一步分析,通過函數參數傳遞的個數和函數傳遞的實參獲取函數指針指向的信息;若是存在store和load指令則進行指針分析,獲取調用的函數信息,然后通過對函數的形參和實參分析,獲取函數指針指向的真實信息;當一個函數調用分析完后定值-引用(def-use)分析下一個被調用的函數,再繼續重復上述的執行過程,直到所有的函數都分析完畢為止。其算法描述如下:
算法3:函數指針信息提取的算法描述
輸入:IR
輸出:函數指針的指向信息
getPtrCall(IR)
(1)forbbin func do
(2) foriin basicbk do
(3)cur_inst=i;
(4) if(!call←cur_inst)
(5)setfunc←call;//獲取函數調用信息
(6) else
(7)setnum←getNumArg;//參數個數
(8)setAgrOperand←num;//實參
(9) end if

(11)setfunc←store;//獲取函數調用信息
(12)setfunc←load;//獲取函數調用信息
(13) end if
(14) forargin func do //遍歷函數獲取參數
(15) if(arg==ArgOperand)//獲取形參
(16)setfunct←call;//函數指針信息
(17) end if
(18) end for
(19) getFuncChain();//函數調用鏈信息獲取
(20) end for
(21)end for
算法3的重要部分在對函數指針指向信息的分析和對過程間信息的def-use分析[20,21]上。在分析函數指針調用信息的部分,通過分析確定函數指針,再通過函數指針傳遞的實參與形參確定函數調用信息指向的真實函數;def-use分析,通過函數的定值,再由函數調用來確定哪個函數被哪個函數引用。其表示形式如下:
(1)def(func2)={func1|函數func1定值了函數func2}
表示函數func2的定值函數集def(func2)由定值函數func2的所有函數組成。
(2)use(func2)={func1|函數func1引用了函數func2}
函數func2的引用函數集use(func2)由引用函數func2的所有函數組成。
(3)use(func2)={func1|函數func1引用了函數func2}
函數func2的定值-引用函數集def-use(func2)由定值函數func2的所有引用的函數組成。
如:main=foo1->foo2->foo3->foo4;
則:def-use(main)={foo1,foo2,foo3,foo4}。
針對過程間信息的處理,首先根據函數的定值,然后通過確定main函數調用時引用了哪個函數,再判斷引用的函數是否為函數指針;如果是,則獲取函數指針變量的信息,通過分析函數指針的實參(稱為定值)被哪個函數指針指向的函數引用了,最終獲取函數指針指向的真實函數信息。即獲取函數調用的定值-引用算法表示為getFuncChain()。
getFuncChain()算法描述的流程具體如下:
算法4:函數調用鏈算法的描述
輸入:IR中的函數
輸出:函數調用鏈
getFuncChain(func1,func2){
(1)從main函數的func1函數開始分析.
(2)分析函數列表中的每一個函數fun;
(3) if(fun是函數變量)
(4) 則def(fun),use(fun)中的函數,def(func)|函數變量func∈use(fun)中的函數是func2中函數.
(5) else if((f為函數自變量|f∈def(fun)或f∈(def(def(fun)…)或f∈use(fun))
(6) 則def(fun),use(fun)中的函數是func2中的函數.
(7) getFuncChain(func11,func22)遞歸獲取下一個引用的函數.
(8) 生成函數調用鏈.
(9) }
本文在LLVM下靜態分析提取C/C++程序的過程內信息和過程間信息,首先通過與現有工具Source Insight、CallTree以及Cflow的對比來驗證本方法過程間信息的提取精度;再通過開源碼進行實驗驗證本方法的實用性。
本文的實驗環境為,硬件環境:Intel(R) Core(TM) i7-4790 CPU @ 3.60GHz,3GB內存;軟件環境:Ubuntu 15.04,LLVM 3.7.0;開發工具:Sublime Text 3,vim;編程語言:C/C++,python。
本文的實驗驗證主要從兩方面進行驗證:
(1)驗證本文提出的過程間分析方法對過程間信息提取的精度;
(2)驗證本文提出的過程間分析方法的實用性。
5.2.1 驗證CFICG提取過程間信息的精度
圖7(a)中函數add_ptr、mul_ptr以及test1_all為函數指針,本文利用graphviz[22]工具繪制函數調用圖,圖7(b)是過程間分析方法(CFICG)與現有分析工具的對比結果。根據圖7的實驗結果可知,Source Insight可以獲取直接函數調用信息,不能獲取函數指針指向的信息;CallTree可以獲取函數調用信息,無法獲取函數指針指向的真實信息;Cflow可以獲取函數指針指向的真實信息,不能解析、區分非庫函數調用與庫函數調用的信息(如scanf函數);本文提出的過程間分析方法獲取了函數指針指向的真實信息以及識別和區分非庫函數和庫函數調用的信息并可以直觀地觀察在哪個基本塊內發生了函數調用,如圖7(b)的實驗結果對比。
圖7(b)中CFICG的函數add、mul、test1和test2都只有1個基本塊entry;all有4個基本塊分別是entry、if_then、if_else以及if_end,在if_then內調用了test1(函數指針test1_all指向test1),在if_else內調用了test2;函數main有5個基本塊分別為entry、sw_bb、sw_bb2、sw_bb3以及sw_epilog,即sw_bb對應源程序的第1個case,并調用add(函數指針add_ptr指向add),sw_bb2對應源程序的第2個case,并調用mul(函數指針mul_ptr指向mul),sw_bb3對應源程序的第3個case,并調用了all,再以一個sw_epilog作為最后1個基本塊結束整個函數。

圖7 CFICG與現有工具實驗結果
表2用于統計過程間分析方法(CFICG)和現有工具對圖7的C源碼獲取程序中實際被調用函數與實驗獲取被調用函數的個數進行對比,統計出圖8的函數調用信息提取的覆蓋率((覆蓋率=(實驗獲取被調用函數個數/程序中實際被調用函數個數)×100%)),表2中reCal表示程序中實際被調用函數個數,ExO表示實驗獲取被調用函數個數,IdCo表示函數調用提取的覆蓋率,統計結果見表2。

表2 圖7中示例代碼函數調用信息的統計結果

圖8 函數調用信息獲取的覆蓋率
5.2.2 驗證CFICG的實用性
通過測試開源碼,來驗證本文提出的過程間分析方法(CFICG)的實用性。實驗結果見表3。
由于處理源碼的規模較大,為了體現過程間分析方法(CFICG)的檢測速度,進行了時間開銷的統計。如圖9所示。
同時,以aget開源碼為示例給出實驗結果的圖示,并放大一小部分以便了解,如圖10所示。
實驗結果表明,本文提出的過程間分析方法(CFICG)提取了過程內信息的路徑流向,在過程間信息提取中函數指針指向信息提取更為精確和庫函數調用信息處理更為完善,并且能結合過程內信息分析出函數調用信息發生在哪個基本塊內,比上述工具更準確地體現出函數調用發生的位置。更精確地獲取函數指針指向的真實信息和更完善地處理庫函數調用的信息,提高了靜態程序分析過程間的精度;對開源碼進行了實驗,驗證本文提出的方法的實用性并且在時間開銷上也不是很大;同時,有助于代碼閱讀和程序分析人員更清晰、更好地理解程序結構以及程序的設計流程。

表3 源碼實驗結果

圖9 開源碼獲取函數信息的時間開銷
本文針對C/C++靜態程序分析,提出一種在LLVM平臺下靜態程序信息的過程間分析方法(CFICG),通過對過程內信息和過程間信息的提取,解決了過程間信息獲取中函數指針指向信息不夠準確的問題以及庫函數調用信息處理不完善的問題。實驗結果表明,本文提出的靜態程序分析方法(CFICG)支持C/C++程序過程內和過程間的信息提取,更好地處理庫函數調用信息并更為準確地獲取函數指針指向的真實信息,并且具有實用性。

圖10 aget實驗結果
參考文獻:
[1]Ohmann P,Liblit B.Lightweight control-flow instrumentation and postmortem analysis in support of debugging[C]//IEEE/ACM 28th International Conference on Automated Software Engineering,2013:378-388.
[2]ZONG Fangfang,HUANG Hongyun,DING Zuohua.Software fault location based on double-times-locating strategy[J].Journal of Software,2016,27(8):1993-2007(in Chinese).[宗芳芳,黃鴻云,丁佐華.基于二次定位策略的軟件故障定位[J].軟件學報,2016,27(8):1993-2007.]
[3]LI Zhoujun,ZHANG Junxian,LIAO Xiangke,et al.Survey of software vulnerability detection techniques[J].Chinese Journal of Computers,2015,38(4):717-732(in Chinese).[李舟軍,張俊賢,廖湘科,等.軟件安全漏洞檢測技術[J].計算機學報,2015,38(4):717-732.]
[4]Fittkau F,Finke S,Hasselbring W,et al.Comparing trace visualizations for program comprehension through controlled experiments[C]//IEEE 23rd International Conference on Program Comprehension,2015:266-276.
[5]Sui Y,Xue J.SVF:Interprocedural static value-flow analysis in LLVM[C]//Proceedings of the 25th International Confe-rence on Compiler Construction.New York:ACM,2016:265-266.
[6]Tice C,Roeder T,Collingbourne P,et al.Enforcing forwar-dedge control-flow integrity in GCC & LLVM[C]//23rd USENIX Security Symposium,2014:941-955.
[7]Niu B,Tan G.Modular control-flow integrity[J].ACM SIGPLAN Notices,2014,49(6):577-587.
[8]Wang P,Yang J,Tan L,et al.Generating precise dependencies for large software[C]//4th International Workshop on Managing Technical Debt.IEEE,2013:47-50.
[9]Padhye R,Khedker U P.Interprocedural data flow analysis in soot using value contexts[C]//Proceedings of the 2nd ACM SIGPLAN International Workshop on State of the Art in Java Program Analysis,2013:31-36.
[10]MOU Yongmin,LI Huili.Test case prioritization based on function calling paths[J].Computer Engineering,2014,40(7):242-246(in Chinese).[牟永敏,李慧麗.基于函數調用路徑的測試用例優先級排序[J].計算機工程,2014,40(7):242-246.]
[11]SUN Weizhen,DU Xiangyan,XIANG Yong,et al.CG-RTL:A RTL-based function call graph generator[J].Journal of Chinese Computer Systems,2014,35(3):555-559(in Chinese).[孫衛真,杜香燕,向勇,等.基于RTL的函數調用圖生成工具CG-RTL[J].小型微型計算機系統,2014,35(3):555-559.]
[12]WANG Yagang,XU Chenghua.Construction of function calls relationship graph for multi-language source code[J].Journal of Xi’an University of Posts and Telecommunications,2013,18(6):75-79(in Chinese).[王亞剛,徐成華.多語言源程序函數調用關系圖的生成方法[J].西安郵電大學學報,2013,18(6):75-79.]
[13]Source Dynamics Inc:Source insight version 3.5[EB/OL].[2016-06-07].https://www.sourceinsight.com/doc/v3/ManualTOC.htm.
[14]Petersenna:CodeViz:A CallGraph visualiser [EB/OL].[2015-07-23].https://github.com/petersenna/codeviz.
[15]Ruda:GNU cflow[EB/OL].[2015-10-23].http://rudix.org/packages/cflow.html.
[16]Czaber:callTree[EB/OL].[2016-07-26].https://sourceforge.net/projects/calltree/.
[17]LLVM Team.The LLVM compiler infrastructure[EB/OL].[2017-03-13].http://llvm.org/.
[18]Lopes B C,Auler R.Getting started with LLVM core libra-ries[M].Birmingham:Packt Publishing Ltd,2014:219-284.
[19]LLVM Team.LLVM language reference manual[EB/OL].[2015-09-01].http://releases.llvm.org/3.7.0/docs/LangRef.html.
[20]Wei F,Roy S,Ou X.Amandroid:A precise and general inter-component data flow analysis framework for security vetting of android apps[C]//Proceedings of the ACM SIGSAC Conference on Computer and Communications Security.New York:ACM,2014:1329-1341.
[21]Jin W,Cohen C,Gennari J,et al.Recovering c++ objects from binaries using inter-procedural data-flow analysis[C]//Proceedings of ACM SIGPLAN on Program Protection and Reverse Engineering Workshop.New York:ACM,2014:1.
[22]John Ellson:Graph visualization tools[EB/OL].[2017-01-04].https://github.com/ellson/graphviz/releases.