金 芝,劉 芳,李 戈
1(北京大學 信息科學技術學院,北京 100871)
2(高可信軟件技術教育部重點實驗室(北京大學),北京 100871)
程序理解是軟件工程中的一個經典話題,又叫作軟件理解或系統理解.自軟件出現以來,甚至在軟件工程提出之前,就有了程序理解這一問題.1968年第 1次軟件工程研討會之后,程序理解成為軟件工程中的關鍵活動,在進行軟件重用、維護、遷移、逆向工程以及軟件系統擴展等任務時,都要依賴于對程序的理解[1].比如,源碼逆向工程通過分析目標系統來識別系統的成分及其相互關系,創建系統的更高抽象層次上的表示,包括構建目標系統的概念模型、抽取程序結構和控制信息以及進行數據抽取和系統抽象.程序理解是軟件逆向工程的主要實現手段和行為活動,貫穿整個軟件逆向工程,是決定軟件逆向工程成敗的關鍵[2].在軟件維護和演化中,例如軟件適應(adaptive)、軟件修復(corrective)、軟件復用(reuse)等任務,程序理解也是其首要活動,其他活動都是在程序理解的基礎上進行的.例如:完成軟件適應任務的流程包括理解系統、定義適應需求、制定適應策略、設計、代碼修改、調試、回歸測試;軟件修復任務的流程包括理解系統、提出/評估問題假設、修復代碼、回歸測試.隨著程序設計語言的不斷變化和軟件復雜性的不斷提高,程序理解一直面臨著挑戰,新的研究問題不斷涌現.
什么是“程序理解”呢?首先看看什么是“理解”.在 Wikipedia中,“理解(comprehension或 understanding)”(https://en.wikipedia/wiki/Comprehension)是指與抽象或物理對象相關的心理過程,比如:理解一個人、一件事或一段話,就是建立理解的主體和這個人、這件事或這段話之間的關系,表明這個主體能夠想象出這個對象(具有心理表征),并且/或者能用一些概念恰當地刻畫這個對象(https://en.wikipedia/wiki/Understanding).可以看出:理解是和學習相關的活動,需要建立從理解的對象所屬的概念域到理解的主體所熟悉的概念域的(概念)映射.
從學習和認知的角度,可以把程序理解和自然語言文本理解進行類比.自然語言文本是用自然語言表述的一種認知,閱讀和理解自然語言文本,就是從文本中獲取知識的過程.程序通常看作是用程序設計語言表述的一種認知,因此可以抽象地把程序理解解釋為從程序中獲得關于程序所表達的知識的過程.與自然語言理解的語法、語義和語用等理解階段相比較,程序理解可分為側重文法理解的語法層上的程序分析、側重抽象語義(如信息流、控制流、抽象解釋等)提取的語義層上的程序分析以及側重程序動態語義的程序行為分析.這體現了程序理解技術和結果的由淺入深的層次劃分.
從方法和技術的角度,程序理解以程序分析為基礎.通過對程序進行人工或自動分析,以驗證、確認或發現軟件性質.程序分析貫穿于軟件開發、維護和復用階段:在開發階段,對正在開發的程序進行分析,以快速、高效地開發出高質量的軟件;在維護階段,對已經開發、部署或運行的程序進行分析,以準確理解并維護該程序,從而使其能夠提供更好的服務;在復用階段,對之前開發的程序進行分析,以復用其中有價值的成分.上述不同階段的程序分析過程差異較大,需要的分析技術也各有不同.根據其分析過程是否需要運行程序,可以將程序分析分為靜態分析和動態分析:靜態分析不需要運行程序,直接對程序源代碼進行分析,獲取相關信息[3,4];動態分析則是分析程序運行時性質[5],通過程序的運行獲取程序的輸入/輸出關系或程序內部狀態等,以驗證或發現程序的性質.
從工程應用的角度看,程序理解主要用于軟件開發過程中涉及已有軟件制品及其使用和變更的活動.比如:軟件復用需要將已有軟件制品用于當前軟件開發;軟件逆向工程從已有軟件制品中獲取該軟件制品的高層抽象,所謂“逆向”是針對自頂向下的軟件開發過程而言的;軟件演化則在認識到當前軟件已經不適合新的場景時,需要進行適當的軟件調整和變更.在進行這些軟件工程任務時,需要理解當前軟件制品的含義,包括它所表達的軟件能力及其實現方式,等等.
已有的工作從各自的研究側重點出發,給出了程序理解的不同的說法.一些典型的說法比如:
· 程序理解是根據軟件源代碼去理解程序的過程[6];
· 程序理解的任務是構建軟件的從代碼模型到應用領域問題間各抽象層次上的模型,目的是支持軟件維護、演化和再工程[7];
· 程序理解是利用領域知識以及語法語義知識來理解軟件制品,構建軟件制品與使用場景之間的心理認知模型(區別于物理模型)的過程[8];
· 程序理解是通過研究諸如源代碼和文檔等制品來理解軟件系統內部工作原理,為軟件維護任務提供足夠的信息[9];
· 程序理解是理解整個軟件系統或部分軟件系統如何運行的活動[10];
· 程序理解的任務是解釋軟件行為,通常是通過閱讀源代碼,來判斷它的作用和代碼如何與整個軟件進行交互[11].
當前,云計算和人機物融合應用模式不斷涌現,加速了社會的信息化進程,使軟件成為一種社會基礎設施.軟件的可成長性和可持續演化性成為軟件工程的重要關注點[12].圖靈獎得主Sifaki教授曾指出,(軟件)系統工程的關注點正在發生轉變,體現為:(1) 從小規模集中式不可進化的系統,到大型分布式可演化的系統;(2) 從嚴格控制的系統與外部環境的交互,到不可預測的動態變化環境;(3) 從設計正確性到通過適應性確保正確性[13].軟件具有可成長和可持續演化能力的基本條件,是軟件的可理解性甚至軟件的可自我認知性.另一方面,軟件自適應性和軟件智能化等研究熱點的形成,感知軟件運行狀態和運行環境,以及以此為基礎的運行時決策和系統適應性變更和持續演化,已經成為研究的主要關注點,這更強調了運行時系統的可理解性將成為亟待解決的問題.
因此,軟件(程序)理解,特別是軟件的自理解和自認知,已經被提到了前所未有的重要高度,有必要重新審視軟件理解的內涵,分析目前的技術手段,結合系統工程的發展探索其新的需求,從而展望未來的技術挑戰和趨勢.本文首先通過文獻分析展示長期以來軟件工程領域對程序理解的研究布局,進而分別從學習和認知的角度、方法和技術的角度以及軟件工程應用的角度這 3個問題,綜合分析程序理解研究的發展脈絡和研究進展,最后,根據軟件系統的發展趨勢探討程序理解面臨的挑戰和發展趨勢.
考察研究人員對程序理解研究方向的關注度,有一個維度是查看發表文章的強度.我們用“程序理解”/ “軟件理解”等關鍵詞在 IEEE Xplore Digital Library上檢索近年來發表的文獻.使用程序理解(program comprehension)作為關鍵詞,檢索到 2 124篇文章;使用軟件理解(software comprehension)作為關鍵詞,檢索到2 115篇文章.這兩個關鍵詞的檢索結果有很大部分是重合的.從檢索結果中的文獻來看,1985年之前發表的文章數目較少,因此我們從1985年之后發表的文獻開始統計.對總共4 139個檢索結果進行人工篩選,去掉重合的文獻,并通過人工閱讀文章標題、文章摘要和關鍵詞,選取出與程序理解的具體任務和理解技術相關的文獻共136篇.對它們從文獻發表年代、研究主題分布以及研究主題各年代分布等幾個維度進行了統計,得到如圖1所示的分布結果.可以看出:
(1) 自1985年起,對程序理解研究的關注度一直處于上升趨勢(如圖1(a)所示);
(2) 軟件維護任務和程序理解最相關,研究工作占有很大份額;其次是文檔分析、重構和生成以及逆向工程(如圖1(b)所示);
(3) 機器學習和深度學習技術近年來大量進入程序理解領域(如圖1(c)所示);
(4) 文獻所報道的研究主題完整地覆蓋了程序理解研究的3個方面(如圖1(c)所示).
a.比如數據挖掘、特征提取、機器學習和深度學習等,是具有代表性的程序理解技術.其中,機器學習、特征提取、深度學習的程序理解方法,是最近10年出現的,并正日漸成為前沿熱點;
b.對程序的語法分析、語義理解和程序的動態行為分析,表現了程序理解的認知層次.值得注意的是:程序語法分析的研究在1995年后出現,程序語義理解和運行行為分析的工作在 2000年后出現,表現了程序理解從語法到語義再到語用的認知不斷深入的過程;
c.軟件維護、逆向工程、文檔重構和生成、軟件測試、代碼缺陷檢測和代碼克隆等軟件工程的經典任務,是程序理解的主要應用場景,除了軟件測試和逆向工程外,應用熱度一直處于上升趨勢.
除此之外,本文還根據程序理解相關會議的投稿論文數,展示該領域的研究關注度,包括統計軟件工程頂級會議 ICSE(Int’l Conf.on Software Engineering)近幾年來各個主題提交文章數以及程序理解會議 ICPC(Int’l Conf.on Program Comprehension)近10年來錄用文章數.其中,圖2展示了ICSE 2015~2018各個主題的提交情況,列出了提交文章數排名前十的主題情況(2015年和2017年未設置實證軟件工程這個主題,2017年未設置軟件工程的人為和社會因素這個主題).其中,軟件測試、程序分析、軟件演化與維護、軟件調試、錯誤定位和修復等主題與程序理解密切相關(用藍色色系表示),這幾個主題的文章數目一直占全部文章數的很大份額.可見,程序理解在軟件工程領域中占據著重要的地位.

Fig.1 Statistical analysis of program comprehension literature圖1 程序理解研究論文的統計分析

Fig.2 Top ten topics in the submissions of ICSE from 2015 to 2018圖2 2015年~2018年ICSE提交文章數目前十的主題及其提交文獻數目
ICPC是專門以程序理解為主題的國際會議,該會議平均每年大概錄用40篇文獻,近10年來的錄用文章數如圖3所示,整體呈緩慢上升趨勢.這表明,程序理解相關研究在近10年來一直都是較為熱門的話題,可以推測,程序理解在未來仍然會是活躍的研究主題.

Fig.3 Number of accepted papers of ICPC in recent ten years圖3 ICPC近10年錄用文章數目統計
程序理解本身就是一個學習和認知的過程.程序是知識的編碼,程序理解是指從程序中學習程序所表達的領域知識的過程.根據構建式學習的理論,學習和認知的過程是面向目標或模型驅動的歸納抽象過程:第一,學習是學習者個體從學習材料中通過提取或抽象等手段構建知識體的過程;第二,所構建的知識體除了依賴學習材料,很大程度上還依賴于學習者已有的知識背景和該次學習的目的.
早期很多對程序理解的探討,都是從認知的角度出發進行的,它們大都側重于研究程序員的學習過程和知識的構建過程.從構建式學習理論出發,可以有兩種基本的程序理解策略.
· 自底向上的策略[1]:這種策略將程序理解看作是一個逐層聚合的過程.也就是從閱讀程序源碼開始,將這些源碼中包含的信息逐層組合、抽象成更高層次的信息.例如,一種程序認知框架區分語法和語義兩個層次:語法層關注程序的語句和基本單元;語義層與程序語言無關,是逐層抽象直到所抽象的知識能夠描述應用領域[14].另一種認知框架區分程序模型(program model)和情景模型(situation model):程序模型對程序的控制流進行抽象;情景模型則是封裝了關于數據流抽象和功能抽象的知識,表達了程序的目標層次[15].用傳統的軟件開發方式可以解釋這種策略背后的原因,即,程序理解是為了還原自底向上的軟件開發過程;
· 自頂向下的策略[1]:這種策略將程序理解看作是重新組織程序員自身已有的關于程序應用領域的知識,并建立這些知識到程序源碼上的映射.比如,一個典型的過程是:從關于領域應用的假設出發,根據已有的領域知識進行逐層分解,同時與程序源碼進行比對或驗證,從而建立假設和源代碼之間的關聯.這種方式體現了模型驅動的思想,亦即:如果模型存在,程序理解就是將具體的程序片段關聯到已知的模型上,通過模型的結構和語義去刻畫程序的結構和語義.
對復雜一點的程序而言,僅僅只采用一種策略是不可行的,這兩種策略常常需要結合起來交替地使用,因而產生了集成的策略.
· 集成式程序理解模型[16]:集成式程序理解模型結合了自底向上和自頂向下的策略,它認為:對比較熟悉的代碼,可以采用自頂向下的方式進行理解,直接建立源碼和應用知識之間的關聯;而對不太熟悉的代碼,則需要采用自底向上的策略從底層開始進行逐層抽象,以獲得程序所表達的領域知識.比如,von Mayrhauser等人提出的集成式程序理解模型涉及4個成分.
(1) 自頂向下的策略:從領域知識出發產生假設,并不斷細化假設,直到假設能關聯到熟悉的代碼片段上;
(2) 構建程序模型:從未知代碼出發試圖理解其控制流;
(3) 構建情景模型:從已構建的程序模型出發,發現數據流,得出功能抽象;
(4) 領域知識:包含程序員已經掌握的關于應用領域、應用目標、程序語言等的知識,用于支持其中的推理過程.
這些早期研究結果的獲得,得益于對程序員認知過程的實驗研究.這些實驗性工作的目的是:研究程序員如何理解程序,進而找到能夠幫助程序員理解程序或者提高理解效率的方式.這類實驗工作一般采用諸如內省法(有聲思維法)、記憶測試法等認知心理學的方法.比如,文獻[17]中的有聲思維法的實驗顯示:程序員在遇到熟悉的領域時,首先陳述領域假設(自頂向下法);而當遇到不熟悉的領域時,要結合源碼作推斷(自底向上法).文獻[16]中的有聲思維法實驗也證實:程序員在理解比較大的軟件系統的源碼時會涉及多個不同的策略,用到多種模型,在理解的過程中經常在不同策略和不同模型之間切換(集成式方法).
記憶測試實驗則希望展示源碼理解和源碼記憶之間的關系.文獻[18]的記憶測試實驗顯示:按可執行過程排序的源碼,比亂序源碼更容易記憶.文獻[19]中的實驗發現:有經驗的程序員主要是依靠命名慣例進行理解和記憶,當碰到不熟悉的命名方式時也和新手一樣沒有頭緒.還有實驗表明:當程序語句之間具有邏輯性時,程序員的反應更快[15].這些實驗性研究都表明,源碼的可理解性和可記憶性是正相關的.
這些關于程序可理解性的實驗研究,在一定程度上引出了集成式開發環境的設計.比如,大部分IDE采用的縮進格式、合適的字符間隔寬度、用顏色標定不同類型的語句等策略,它們都是為了提高程序可理解性而引入的編程布局上的考慮[20].
高級語言以及 API和領域特定語言(DSLs)也與這些早期程序理解研究同步發展.高級語言的出現和發展,對不斷提升程序的可理解性起到重要的作用,方便了軟件開發、軟件重用、軟件系統逆向工程和系統演化等.
值得注意的是:沉寂了多年的程序可理解性的實驗研究,由于新的實驗技術的出現,近年來又重新引起了學術界和工業界關于程序理解作為認知過程研究的關注.比如,研究者們利用功能磁共振成像、近紅外光譜學、腦電圖等實驗手段,觀察程序員在理解程序的過程中腦部區域的激活或活躍狀態,試圖建立不同的激活區域與編程任務的難度之間的相關性[21-23].
從程序理解方法的角度,現有工作可以分為基于分析的方法和基于學習的方法.
· 基于分析的方法以是否需要運行軟件為準則,又可以分為靜態分析方法和動態分析方法[24].
? 靜態程序理解直接分析程序源代碼,從中獲取相關信息,分析過程不需要執行程序[3,4].由于其不必運行系統,因而可進行系統的早期分析,在系統開發階段就可以開始,分析結果具有普適性,也可以覆蓋所有可能的執行路徑;
? 動態程序理解主要理解程序運行時性質[5],它通過執行程序,獲取其輸入輸出關系或內部狀態等,提取軟件的性質.與靜態方法相比,其特點是需要運行系統,通常需要具體的輸入數據,因為有具體數據,其分析結果更為精確;但反過來,因為結果的精確是對特定輸入而言的,不能保證分析結果對其他輸入也適用,因此不具有普適性.
· 近年來,數據挖掘和機器學習技術在多個領域獲得成功,特別是深度學習在圖像處理、語音識別、自然語言處理等領域的多項任務上取得的成功.在軟件工程領域,由于開源代碼的大量出現,軟件工程數據越來越容易獲取,基于學習的技術近年來也開始得到關注.許多研究者利用數據挖掘、機器學習等技術進行程序理解,從源代碼和代碼文檔中獲取信息.深度學習技術越來越多地應用于程序理解,成為一種數據驅動的端到端的程序理解方法.
2.2.1 基于靜態分析的程序理解
程序靜態分析由M.E.Fagan于1976年提出[25],主要通過分析程序源碼以發現其中的錯誤.Fagan于1976年發表的文獻[25]提到,使用靜態分析技術發現了 30%~70%的邏輯和編碼錯誤.后來,程序靜態分析成為程序理解的重要方面.
按照理解的層次,程序靜態分析可分為語法分析和語義分析.程序語法分析按照編程語言的語法和詞法規則對程序源碼進行分析,可以檢查程序在語法上是否符合預定義的巴克斯范式(Backus normal form,簡稱BNF),從而發現語法錯誤.程序語言的巴克斯范式一般屬于上下文無關文法,程序語法分析技術則主要包括算符優先分析(自底向上)、遞歸下降分析(自頂向下)和LR分析(自左至右、自底向上)等,含有語法錯誤的程序無法解析為語法樹.程序靜態分析還可以進一步分析所生成的語法樹,以完成程序理解的其他相關任務.例如在程序克隆檢測中,通過對比程序的語法樹之間的相似性來檢測程序克隆[26].
程序靜態分析與自然語言語法分析相似,但程序具有更強的結構性且存在大量的嵌套結構,程序語法樹比自然語言語法樹要復雜得多,分析難度也更大.而且自然語言即使存在語法錯誤,人仍可以根據其上下文來理解它的含義,但程序需要可執行才能體現其價值,含有語法錯誤的程序不能被正確執行,因此程序分析中語法約束更加嚴格.
軟件系統用于求解特定領域問題,軟件系統的語義本質上就是問題的求解過程.比如,軟件系統的控制流從操作的角度表達問題求解過程,它包括過程的細分,表達操作的順序步驟以及包含if-then-else條件、循環和/或分支路徑等操作的控制轉移.軟件系統的數據流從數據傳遞和加工角度表達了系統的邏輯功能,即數據在系統內部的邏輯流向和邏輯變換過程.通過各種程序分析的手段抽象出系統的控制流和/或數據流的過程,可以看作是從問題求解過程視角出發的程序語義理解.
更深一個層次是程序的形式語義,它描述程序執行時所遵循的規約,是關于程序執行性質的抽象.比如通過描述程序(程序語句)的輸入輸出關系,或者解釋程序的執行步驟來創建其計算模型.關于這類語義的刻畫依賴于編程語言的形式語義.編程語言形式語義有 3種:一是操作語義,它通過規約程序在抽象機上的執行過程來描述語言的語義;二是指稱語義,它認為程序(或者程序成分)的語義是執行該程序(或成分)所得到的最終效果,這個最終結果才是程序成分所要表達的含義,它通過構造可描述語句含義的數學概念(稱為指稱)來表達語言的語義;三是公理語義,它使用公理化方法,通過描述程序語句在程序狀態斷言上的效果來定義程序語句的語義.
形式語義精確地表達了程序的執行過程或執行效果,但構造形式語義相當困難,目前只有一些非常有限的輔助手段.比如:符號執行使用抽象符號來表達程序變量的值,用一個解釋器來跟蹤程序,從而確定哪些輸入會引起程序各個部分的執行[27];然后,根據程序中表達式和變量的符號來獲得表達式,根據每個條件分支的可能結果獲得這些符號的約束.由于需要窮舉各種可能執行的路徑,其搜索空間將隨程序規模的擴大呈指數級增長.
2.2.2 基于動態分析的程序理解
程序動態分析用于分析程序運行時性質,分析時需要采集程序運行時信息,通過分析這些運行時信息來獲得程序運行時屬性.程序動態分析的原因包括:一方面,動態鏈接庫被廣泛使用,軟件只有在完成部署之后才能完整地表達程序的功能,這種情況下,傳統的僅依賴源碼分析的靜態分析獲得的結果由于可能缺少涉及動態鏈接庫的信息而不夠精確[28];另一方面,面向對象語言(尤其是 Java)被廣泛使用,這類語言具有動態綁定、多態、線程等特征,這些特性也使靜態分析的結果受到限制.
基于插裝的方法,將監測代碼插裝到程序中,以捕獲程序執行路徑等相關信息[29].監測代碼可以插裝在源代碼、二進制碼以及字節碼中.源代碼插裝在程序編譯前進行,它在需要監測的地方直接加上監測代碼,例如增加輸出信息語句、增加日志語句等.二進制碼插裝需要修改或重寫編譯代碼來添加監測代碼,分靜態插裝和動態插裝:靜態二進制碼插裝直接編寫可執行二進制碼重寫應用程序;動態二進制插裝在程序加載到內存后,在執行之前進行.字節碼插裝在已編譯的代碼中進行追蹤,同樣也可分為靜態的或者動態的:靜態字節碼插裝在程序執行之前離線更改已編譯代碼,創建已插裝的中間代碼的副本;動態字節碼插裝則在程序運行時進行.
基于虛擬機分析的方法通過使用特定虛擬機提供的分析(profiling)和調試(debugging)機制來執行動態分析[30],以深入了解程序的內部操作,特別是與內存和使用堆的相關操作.
從某種意義上說,基于動態分析的程序理解可以類比于自然語言理解中的一種語用性理解,亦即在實際的程序運行過程中捕捉程序的含義,建立程序的執行及輸入輸出之間的依賴關系.
2.2.3 基于學習的程序理解
隨著大量開源代碼的出現,程序相關數據越來越容易獲得.同時,軟件是知識的載體,軟件開發是人類表達知識的行為,對大量代碼進行統計建模來理解程序獲取知識具有合理性.因此,許多研究開始采用機器學習技術,學習代碼數據和代碼文檔數據等其中蘊含的知識[31].這些研究通過運用統計學習和機器學習相關技術來挖掘程序的特征,進行程序理解.
統計語言模型用于學習代碼中的統計特性.N-gram[32]是使用最為廣泛的模型,它基于的假設是:第N個詞的出現只與前N-1個詞相關,因此可以用前N-1個連續的詞來預測下一個詞出現的概率.Hindle等人首先采用N-gram為程序語言建模[32],并用于代碼補全任務.Nguyen等人對N-gram模型進行了擴展[33],在構建模型時考慮了代碼中的語義信息.Tu等人觀察到代碼具有局部性特征[34],因此在N-gram模型的基礎上增加了緩存機制來記錄代碼的局部特性.但這些都屬于語句一級的理解,僅能用于進行特定的編碼和程序分析.
一些經典機器學習算法,如決策樹、條件隨機場等,被用于進行程序理解.其中,條件隨機場(conditional random field,簡稱 CRF)是一類判別式無向概率圖模型[35],用于編碼觀察間的已知關系并構建與其一致的解釋.在程序理解中,該模型可以對多個變量在給定觀測值后的條件概率進行建模,構建代碼結構以及代碼中元素間的依賴關系.比如,文獻[36]利用條件隨機場進行程序元素的屬性預測,包括預測變量名或變量類型.它首先將程序表示為一個依賴網絡(dependency network),以表達程序中各元素間的關系;然后構建條件隨機場模型,從而對程序中元素屬性進行預測.其優點是在預測過程中能夠充分考慮程序中各元素間的依賴關系和程序的結構,可提高預測的準確率.決策樹是基于樹結構進行決策的方法,文獻[37]將學習代碼概率模型的問題看作是基于抽象語法樹學習領域特定語言上的決策樹的問題,從而支持在動態計算上下文中調節程序元素的預測.這些工作支持了程序中跨語句依賴關系的理解.
近年來,人們開始探索深度學習在程序理解上的應用.以往的程序理解研究需要對程序數據進行分析,獲取程序中包含的特征信息,然后根據這些先驗知識人為地制定啟發式規則,這一過程非常耗時耗力.深度學習是一種數據驅動的端到端的方法,將深度學習用于程序理解,目的是希望根據已有程序及其相關數據,自動地學習程序數據中蘊含的特征,取代繁瑣的人工特征提取過程.目前,基于深度學習的程序分析大多是靜態的,即直接對程序源代碼或者其他抽象表示(如抽象語法樹、數據流圖等)進行學習.根據不同表示方式,可以分為如下幾個方面.
· 基于詞素(token)序列的理解:基于詞素序列的理解將程序表示為詞素序列,用深度神經網絡對詞素序列進行建模,學習詞素序列中包含的信息.這樣,當給定部分詞素序列時,模型可以預測下一個最有可能出現的詞素.這樣的模型可用于代碼補全任務中[38,39].這個學習過程也可以獲得整個程序詞素序列的特征向量表示,因此也可以完成程序注釋生成[40]、代碼檢索[41]等任務;
· 基于應用程序接口(API)序列的理解:基于 API序列的程序理解認為程序中用到的API序列可以在一定程度上反映程序的功能特性,它將程序中包含的 API序列作為對象進行建模,或者將 API序列作為額外信息輸入學習網絡.這類模型可用于如代碼注釋生成[42]、代碼檢索[41]等任務;
· 基于抽象語法樹(AST)的理解:抽象語法樹表示了程序的語法結構.利用深度神經網絡對程序語法樹進行建模,可學習到特定程序代碼數據集的語法和結構信息.這類模型可用于包括代碼補全[43]、程序自動生成[44,45]、代碼克隆檢測[46]等任務中;
· 基于圖的理解:程序還可以用基于圖的結構來表示,例如控制流圖和數據依賴圖.把程序的圖表示作為學習模型的輸入,使得模型能更好地捕捉程序中變量間或活動間的依賴關系.這類模型可用于包括程序驗證[47]、程序變量名預測、誤用檢測[48]等任務中;
· 基于程序執行動態軌跡的理解:對程序的動態執行過程中產生的數據進行程序建模,可以捕獲程序運行時的性質,比如程序執行軌跡(execution trace).這些執行過程數據可以包含程序執行時產生的中間結果,如執行過程中調用的子程序及其參數,或程序各變量值的變化.這類研究工作目前主要集中在程序綜合上[49-51].此外,文獻[52]還提出了基于程序執行軌跡構建程序表示模型,獲得程序的特征向量表示,并將此特征向量表示用于對程序錯誤進行分類.
程序理解與軟件工程中的很多任務相關,本小節選擇幾個典型的任務,討論程序理解與它們之間的關系以及程序理解對解決這些問題的支撐作用.
2.3.1 程序理解與軟件維護
軟件維護是指根據需求變化或硬件環境變化對應用程序的部分或全部進行修改.實踐表明,50%~80%的軟件預算消耗在對現有系統的維護上[53].軟件維護包括如下幾個活動:與修改請求相關的軟件理解、修改影響分析和修改實施(包括重構以及修改傳播分析)以及修改后系統調試與測試[54,55].其中,程序理解主要是分析并確定已有系統成分間的關聯關系、分析維護需求與目標代碼間的映射關系等,還可以輔助維護人員理解已有系統代碼.比如,Snelting等人采用程序理解技術進行類與接口層次的識別,以輔助維護人員理解軟件系統的結構[56].一些程序理解工具,如程序切分器、靜態分析器、動態分析器[57]等,可提示程序員選擇最值得關注的程序片段,顯示數據鏈和相關特征,使程序員能夠準確跟蹤更改影響.靜態分析器還能幫助程序員快速提取模塊、過程、變量、數據元素、對象與類、類層次結構等信息.這些工具可以用清晰可讀、可理解的方式組織源代碼,從而把程序員從煩躁的代碼閱讀中解放出來.
程序理解技術不僅能夠提取軟件元素間的依賴關系,還能支持軟件演化性分析.比如,文獻[58]從代碼演化過程中,提取開發過程和演化過程中的模式,從而提高源程序的可理解性;文獻[59]利用基于概念格的程序理解技術,提取出協同修改模式,為軟件演化所需的維護活動提供有效的度量.
2.3.2 程序理解與軟件測試
軟件測試是指在特定條件下對程序進行操作,以發現程序錯誤,衡量軟件質量,并評估其是否滿足設計要求,目的在于檢驗程序是否滿足需求,或弄清預期結果與實際結果間的距離.軟件測試既耗時又耗力,測試成本通常超過軟件首次發布后總成本的50%[60].
無論采用什么測試方法,測試員都需要了解測試對象的功能.軟件設計和實現文檔提供理解軟件的基本功能說明,但沒有提供代碼級別的測試信息,測試員只有閱讀源代碼才能知道函數級或語句級的功能,因此能全面反映程序功能的程序理解技術,可以極大地提升軟件測試的效率和效果.
程序理解技術目前已經用于軟件智能調試、軟件自動測試和軟件安全漏洞發掘等任務.如,IEEE軟件測試標準要求對能獨立測試的軟件成分進行辨識[61],Cellier等人提出融合關聯規則與基于形式概念分析的程序理解技術,幫助進行錯誤定位[62].通過概念格,很容易獲得錯誤的測試和正確的測試間的特征差異,從而實現錯誤定位.Ammons等人將類似的方法應用于調試時態規約[63],大大提高了效率,只需檢查原有軌跡數量的 1/3,就可分類檢查出錯誤與正確的規約.
符號執行和抽象解釋等新方法,可以從外部和內部自動識別程序的運行時特征.與此相關的其他技術,如基于序列[64]和統計的軟件驗證技術、模型驗證技術、可攜帶證明代碼等,都成為當前研究的熱點.
2.3.3 程序理解與軟件重構
軟件重構是指:為了改善軟件的結構,提高軟件的清晰性、可擴展性和可重用性,在不改變軟件功能和外部可見性的情況下,對其進行的改造,使程序的設計模式和架構更趨合理,從而提高軟件的可擴展性和可維護性[65].主要研究包括類層次結構重構、模塊結構、“壞味道”檢測和代碼重構,由低層次語言規范向高層次的語言規范進行重構等.
程序理解在不同層次的軟件重構中都能發揮作用.如,Arévalo等人采用形式概念分析,將對象類結構區分為好的層次設計、壞的層次設計和不正常的層次設計,指導維護人員理解類層次以及修改類層次中存在的“壞味道”[66].Snelting等人提出了基于概念格進行類層次重構的方法[67],經過重構后的類層次更加合理.Bhatti 等人對過程式面向對象代碼進行分析,識別程序代碼中有用的類層次和結構,并進行重構[68].Al-Ekram等人將概念分類、程序切片及形式概念分析結合起來進行重構[69],使重構后的模塊能夠自包含、模塊間有很小的沖突以及較少的代碼復制.Kim等人提出采用面向對象的概念分析方法進行模塊重構[70],該方法除保持了傳統形式概念分析方法的優點外,還可直接粗粒度地識別模塊,適用于大規模軟件的模塊重構.
重構過程中,對軟件進行的修改會對軟件的其他部分造成潛在影響,可能導致軟件不一致.軟件修改影響分析可用于識別這些潛在影響[71].如,Tonella將概念分析與分解切片結合起來,提出一種基于分解切片概念格的程序理解方法[72],用于過程內針對某語句或者變量修改的影響分析.
2.3.4 程序理解與軟件復用
軟件復用是指重復使用已有軟件的各種制品,包括各類文檔和代碼,來開發新的軟件,減少軟件開發和維護的開銷.程序理解技術支持軟件制品的理解,是軟件復用的關鍵技術.
對可復用軟件制品的理解,特別是代碼功能的理解,是進行復用的基礎.如:Tonella等人通過程序理解從源代碼中提取設計模式[73],進而指導程序的演化;Hill等人[74]通過分析與代碼相關的文檔、錯誤報告等,猜測并標注代碼功能;Vinz等人[75]分析代碼中的自然語言注釋,進行軟件代碼的功能分析;Falleri等人[76]分析程序代碼中的變量名、方法名、類名等類自然語言標識符,推測程序代碼的功能;Nguyen等人[77]通過建立程序語言的概率模型(如詞袋模型),進而根據關鍵字或表達式的出現概率對代碼功能進行推測;Shepherd等人發現程序代碼的動作可以用動詞和名詞的組合來表示,因此提出一組規則和算法,根據程序代碼方法中的自定義標識符生成簡短的自然語言描述,作為對代碼功能的表達[78].
更細粒度的理解包括 Robillard等人在特征定位方面的工作[79,80],他們提出基于概念圖的方法,利用概念圖表示代碼不同成分間的關系,從而通過代碼中的成分進行特征定位.
2.3.5 程序理解與需求追蹤
需求跟蹤是指跟蹤需求生命周期全過程,包括建立每個需求和系統元素之間的關聯,比如和其他需求、體系結構、系統構件、源代碼模塊、測試案例之間的關聯等.程序理解在需求追蹤中的作用體現在特征定位方面,即,將自然語言形式的修改請求映射到代碼層的修改上,識別哪些代碼實現需求中的哪些功能(或者非功能)特性[81].如,Zhao等人提出了一種基于分支調用圖(branch reserving call graph)的程序理解方法[82],采用信息檢索技術建立關鍵詞與代碼功能單元之間的關系,利用分支調用圖表示不同功能單元間的關系;Chen等人基于特征定位技術,提出一種基于抽象系統依賴圖(ASDG)的代碼表示方法[83],將函數或全局變量作為點,它們之間的控制依賴關系作為邊,利用ASDG識別代碼的功能;Poshyvanyk等人引入形式概念分析,對代碼中不同對象的屬性進行聚類分析,根據同類對象的共同屬性,建立層次概念模型,找出與指定特征最相關的類或方法,方便維護人員進行特征定位[84].
程序理解技術在非代碼層次的需求分析方面的應用包括:Ivkovic等人利用概念格聚集需求層次的各個模型元素,通過聚集得到的模型元素集合被認為存在依賴關系[85];Niu等人通過形式概念分析來識別軟件演化產品線的模塊化和交互性需求,進而檢查其功能和質量需求[86].
2.3.6 程序理解與逆向工程
逆向工程是指分析目標系統,確定系統的成分及其相互關系,用高層抽象或其他形式來表達目標系統[87].Muller等人將逆向工程,具體地說是信息系統逆向工程,細化為數據抽取、數據分析和概念抽象這3個階段[88]:數據抽取利用適當的抽取機制從目標系統的執行過程中收集原始數據;數據分析從原始數據中發現結構完整語義一致的邏輯數據模型;概念抽象則將數據分析中獲得的邏輯數據模型映射為等價的概念模型.
程序理解在逆向工程的數據抽取和數據分析活動中都能發揮作用.例如:Harandi等人提出一種將代碼中與數據處理相關的部分組織為有層次結構的事件模型[89],其中,底層為數據定義、聲明等,中間層為棧、隊列、樹等數據結構,高層為算法,然后將該事件模型與預定義的事件庫相比對,找出可能與代碼功能相對應的概念;Viljamaa等人基于程序理解技術從軟件框架中提取可重用性接口的規約和代碼[90];Marcus等人提出一種基于潛在語義索引(LSI)的方法[91],對代碼中的自定義標識符進行拆解,用拆解后的字段集合作為對代碼功能的表示,在此基礎上建立關于代碼文本描述的索引,以幫助在特征定位任務中確定代碼段的功能;Carey等人采用支持向量機對人工選擇的代碼特征進行分類[92],將代碼劃分至不同的概念類別,建立代碼與表達功能的概念類別之間的關聯;His等人提出一種基于圖匹配技術的代碼功能表示和識別方法[92],該方法首先建立代碼接口調用關系圖,然后通過接口調用圖的匹配定位實現指定功能的代碼段.
近年來,深度學習方法的引入,使得程序理解逐步趨向于自動化,也延伸了程序理解在軟件開發過程中的作用,基于理解的程序或軟件制品的自動處理成為可能.本節介紹這個方面的一些最新進展.
代碼補全是IDE,例如Eclipse、IntelliJ、Visual Studio等中使用頻率幾乎最高的功能[93],其本質是基于關于代碼的普適知識,預測當前上下文中缺失的代碼.目前,大多數工作通過構建語言模型來學習代碼 token的概率分布,根據已有部分代碼(context)來預測接下來最可能出現的token.前面曾經提到,Hindle等人[32]使用了n-gram的語言模型,在token級別完成代碼補全.隨著深度學習的發展,深度神經網絡模型,例如RNN、CNN等,都被應用于代碼補全任務中,通過構建深度神經網絡模型,學習已有代碼的特征,基于這些特征完成代碼補全任務,包括token級別、AST級別以及各類圖級別的補全.
自動生成給定代碼片段的自然語言描述,是程序理解的一個重要應用場景.早期關于代碼注釋生成的研究,一般用啟發式規則提取代碼關鍵信息,再合成自然語言描述[94,95].深度學習的機制,比如基于 seq2seq框架的模型,可以大大提高代碼注釋生成的能力.比如,Iyer等人[40]采用了seq2seq模型,并引入了注意力機制,完成根據代碼片段生成自然語言描述的任務.Hu等人[42]在基于seq2seq的程序注釋模型的基礎上,進一步集成了代碼所調用的API序列,將API調用序列蘊含的知識輸入到網絡模型中,以輔助注釋的生成,使模型生成的注釋能夠更好地描述代碼的功能.
代碼模式檢測[96]常用于進行錯誤檢測或代碼克隆檢測.Mou等人[96]首次提出了基于樹結構的卷積神經網絡,對程序 AST進行建模,學習程序代碼的結構信息,用于程序分類和代碼模式檢測任務.White等人[97]和 Wei等人[46]通過深度神經網絡學習代碼中隱含的特征,根據得到的特征向量表示來判斷兩個代碼是否屬于一對克隆對,改進了克隆檢測的效果.
利用語言模型對代碼建模,就是對代碼段賦予其概率值.頻繁出現的代碼段獲得較高的概率值,而出現概率很低的代碼段則被認為不太可能出現,很可能是錯誤代碼.一些研究基于這個假設構建語言模型,進行代碼缺陷檢測.Wang等人[98]使用深度信念網絡學習token級別的代碼特征,用于預測代碼中的缺陷.Murali等人[99]結合了主題模型和循環神經網絡,對代碼中調用的API序列進行建模,檢測代碼中是否包含不常用的API序列,以判斷代碼中是否包含錯誤.
除了程序語言自身的語法約束之外,代碼風格也是約束程序的一種方式.例如命名風格等,風格規范的代碼有利于代碼的理解[100].對代碼風格的研究主要包括變量名、函數名和類名的預測(重命名)以及代碼格式修正等.這些工作一般利用深度神經網絡得到代碼的特征向量表示,然后根據這些向量表示完成風格修正任務.Allamanis等人[101]針對程序中的關鍵語句以及層次結構性等特性,構建基于注意力機制的卷積神經網絡,學習程序的這一特性,并用于程序函數名的預測.Allamanis等人[48]采用圖結構來表示程序,對程序中的數據依賴關系進行建模,用于程序變量命名和變量誤用錯誤檢測中.
軟件系統已經成為信息社會的基礎設施,它面臨其軟硬件環境及外部資源不斷變化的挑戰.增強軟件系統的自適應和持續演化能力,使其能夠長期生存并不斷成長,是當前學術界研究的新熱點.前面的技術分析顯示,軟件理解技術將在軟件系統自適應和持續演化中扮演重要的角色,是支撐軟件系統能夠長期生存和不斷成長的不可或缺的基礎技術.
同時,在開放網絡環境下,“萬物互聯”的網絡空間推動了人機物的深度融合,促進應用場景的普適化,軟件系統在規模化、復雜度和異構性上不斷提升,這給軟件系統理解帶來了新的需求和研究挑戰,有必要重新認識現有的程序理解技術,提煉未來軟件系統理解的關鍵需求,從而確定進一步的研究思路和可以突破的方向.我們同樣從技術的角度、工程的角度和認知的角度,分析和討論程序理解的技術需求,目的是希望能把握未來研究選題的方向.
· 在理解的對象上:自然語言理解技術能夠理解的元素粒度從詞到句子再到段落再到篇章.相應地,以從元素到合成物、從微觀向宏觀、從具體關聯到抽象關聯為維度建立譜系,程序理解應該建立在軟件系統完整生命周期上的全譜系理解,包括從詞素和變量及其屬性值等的建模,到程序代碼語句結構理解,再到程序調用關系建模和數據依賴關系建模,需要具備從程序片段的功能理解到程序系統的能力理解.總之,程序理解僅僅局限在源碼級的理解是遠遠不夠的,我們需要能夠理解:(1) 軟件系統構件間的關系(架構具有什么含義?);(2) 特定軟件系統中對應現實世界問題表達的數據類型結構(如何表達問題空間?);(3) 特定軟件系統問題求解過程調用關系的層次(如何求解現實問題?).更具挑戰性的是:在一定抽象層次上建立對大型軟件系統及其軟件體系結構的某個方面(如良構性、魯棒性、健康性等)的系統級量度及其度量方法,支持大型程序的系統級概述的獲得;
· 在理解的技術上:動態分析技術將凸顯其重要性.動態分析根據收集到的軟件系統運行時數據進行軟件系統的分析,它可以揭示軟件系統的真實行為,有可能建立軟件系統的準確的表示.這個表示可以是語句層的調用關系、信息處理的變化過程,甚至體系結構的視圖.隨著軟件系統規模的擴大、復雜度的增長以及軟件系統運行上下文或運行場景不確定性的提高,系統的動態理解技術將越來越重要.同時,在人機物互聯應用環境下,動態理解的范疇有可能不僅需要軟件系統本身的狀態感知,還需要結合互聯環境中的上下文感知和運行場景感知.這種情況可以稱其為程序理解擴大到系統理解;
· 在理解的層次上:從表達知識的角度看,程序理解具有與自然語言理解相似的特性,依據自然語言理解的層次,程序理解也可以遵循從語法分析到語義分析再到語用分析這樣一個由淺入深的過程.目前的技術大部分側重在程序語言語法分析上,部分技術涉及從信息處理視角出發的程序語義分析.但是從軟件系統逆向工程、軟件復用以及程序自適應演化等的需求來看,程序或軟件系統的語用分析的重要性將逐步凸顯,這涉及對軟件系統的運行時上下文、應用場景或軟件系統作用環境的分析,從而決定軟件系統在當前現實應用場景中的適配性,決定在特定現實應用場景下,軟件制品是否適合復用,或在應用場景變化的情況下,軟件系統是否適合繼續使用(即是否需要調整和演化)等;
· 在理解的作用域上:從前面的分析中可以看到,程序理解的方法和理解結果可用于軟件工程的多個方面.目前有很多程序理解的工作,其目標一方面是為了提高程序員理解程序的效率,提取一些具體的程序特征,輔助程序員去理解程序;另一方面是提取出程序中表達的知識,支持逆向工程和知識復用.未來軟件系統越來越需要具有在線適應、自主演化和自我成長的能力,這要求對系統的理解過程要能完全自動化,支持軟件系統的自我認知或自我動態認知能力.關于這方面的一個值得關注的問題與運行時模型相關,軟件系統的自我認知可以看作是它能掌握運行時模型,并確保系統運行狀態和運行時模型的雙向同步.
程序理解是軟件工程中的一個重要活動,在完成軟件重用、維護、遷移、逆向工程以及軟件系統擴展等任務時,都要依賴于對程序的理解.在軟件工程的整個發展過程中,程序理解一直都占據著重要地位.隨著社會信息化進程的不斷加快,程序理解研究的內涵與需求等也發生了新的變化.因此,本文對程序理解進行了重新審視.
本文首先給出了程序理解的定義,分別從學習和認知角度、工程角度以及方法的角度對程序理解進行了解釋.然后,通過文獻分析展示了長期以來軟件工程領域中程序理解的研究布局.分析結果表明:程序理解一直都是軟件工程中的研究熱點,并且隨著軟件工程的發展,程序理解的研究側重點也在隨之發生變化.然后,分別從認知的角度、軟件工程的視角以及理解技術這 3個方面綜合論述程序理解研究的發展脈絡和研究進展.最后,對程序理解的發展趨勢進行了探討.
作為軟件工程領域研究的核心內容之一,程序理解技術正在隨著互聯網各項技術的發展處于不斷推進中,具有廣闊的研究前景和研究價值.隨著近幾年來人工智能和深度學習技術的發展,越來越多的學者開始研究如何運用深度學習的方法實現程序分析的智能化與自動化,程序理解的自動化值得期待.