最近在自學符號執行,所以,這篇經典文章(Symbolic Execution for Software Testing: Three Decades Later)[1]做爲入門必讀。git
符號執行 (Symbolic Execution)是一種程序分析技術,它能夠經過分析程序來獲得讓特定代碼區域執行的輸入。顧名思義,使用符號執行分析一個程序時,該程序會使用符號值做爲輸入,而非通常執行程序時使用的具體值。在達到目標代碼時,分析器能夠獲得相應的路徑約束,而後經過約束求解器來獲得能夠觸發目標代碼的具體值。github
軟件測試中的符號執行主要目標是[1]:web
在給定的探索儘量多的、不一樣的程序路徑(program path)。對於每一條程序路徑,
1) 生成一個具體輸入的集合(主要能力);
2) 檢查是否存在各類錯誤(errors,包含assertion violations, uncaught exceptions, security vulnerabilities, and memory corruption)。express
從測試生成的角度,符號執行能夠生成高覆蓋率的測試用例。
從bug finding的角度,符號執行能夠提供一個具體的輸入用於觸發bug(過去經常用於調試bug)。緩存
符號執行的主要思想就是將輸入(input)用符號來表徵而不是具體值,同時將程序變量表徵成符號表達式。所以,程序的輸出就會被表徵成一個程序輸入的函數,即fun(input)。在軟件測試中,符號執行被用於生成執行路徑(execution path)的輸入。
執行路徑(execution path):一個true和false的序列seq={p0,p1,...,pn}。其中,若是是一個條件語句,那麼pi=ture則表示這條條件語句取true,不然取false。
執行樹(execution tree):一個程序的全部執行路徑則可表徵成一棵執行樹。
以下圖所示:
對應的執行樹:
3條不一樣的執行路徑構成了一棵執行樹。三組輸入{x=0,y=1},{x=2,y=1},{x=30,y=15}覆蓋了全部的執行路徑。所以,符號執行的目標就是在給定的時間內,生成一個輸入的集合使得全部的(或讓儘量多的)執行路徑依賴於由符號表徵的輸入。併發
符號狀態(symbolic state):符號執行維護一個符號狀態,它是一個<變量,符號表達式(symbolic expressions)>的mapping。app
符號路徑約束(symbolic path constraint): a quantifier-free first-order formula over symbolic expressions.dom
符號執行後的結果以下圖所示:
分佈式
當代碼中包含循環和遞歸時,若是終止條件是符號的話,那麼符號執行會產生無限數量的執行路徑。例以下圖所示,
ide
這個時候,符號路徑要麼就是一串有限長度的ture
後面跟着一個false
(跳出循環),要麼就是無限長的true
。如圖所示,
符號執行的主要缺點:若是符號路徑約束不可解(不能被solver解決)或者是不能被高效(時間效率)的解,則不能生成input。回到以前的例子(見圖 ~\ref{example-program}),若是把函數twice
改爲(v*v)%50
(非線性):
假設採用的sovler不可解非線性約束,那麼,符號執行將失敗,即沒法生成input。
現代符號執行技術的特色是同時執行精確(Concrete)執行和符號(Symbolic)執行。
當給定若干個具體的輸入時,Concolic testing動態地執行符號執行。Concolic testing會同時維護兩個狀態:
不一樣於傳統的符號執行技術,因爲Concolic testing須要維護程序執行時的整個精確狀態,所以它須要一個精確的初始值。舉例說明,仍是以前這個例子:
表明工具:
EGT在執行每一個操做以前,檢查每一個相關的值是精確的仍是已經符號化了的,而後動態地混合精確執行和符號執行。若是全部的相關值都是一個實際的值(即精確的,concrete),那麼,直接執行原始程序(即,操做,operation)。不然(至少一個值是符號化的),這個操做將會被符號執行。舉例說明,假設以前那個例子,第17行改爲y=10
。那麼,在調用twice時,傳入的參數是concrete的,返回的也是一個concrete value,所以z
也是一個concrete value。第七、8行中的z
和y+10
也會被轉換成concrete vaule。而後再進行符號執行。
表明工具:
因而可知,不管是concolic testing仍是EGT,他們都是動態地mix use concrete and symbolic execution。所以,也被稱做「動態符號執行」。
不精確性:代碼調用了第三方代碼庫(因爲種種緣由沒法進行代碼插裝),假設傳入的參數都是concrete value,那麼就像EGT中的同樣,能夠所有看成concrete execution。即便傳入的參數中包含符號,動態符號執行仍是能夠對符號設一個具體的值。Concolic和EGT有不一樣的解決方法,後面再介紹。除了調用外部代碼,對於難以處理的操做數(如浮點型)或是一些複雜的函數(solver沒法解決或須要耗費大量時間),使用concrete value能夠幫助符號執行環節這種impression。
完整性:動態符號執行經過concrete value能夠簡化約束,從而防止符號執行get stuck。可是這也會帶來一個新問題,就是因爲這種簡化,它可能會致使一些不完整性,即有時候沒法對全部的execution path都能生成input。可是,當遇到不支持的操做或外部調用時,這顯然優於簡單地停止執行。
描述:
首先,要知道,符號執行implicitly過濾兩種路徑:
解決方案:
描述:
約束求解是符號執行的技術瓶頸。所以,對於solver的優化(提升solver的求解能力)成了解決這個技術瓶頸的手段。
解決方案:
(x+y>10)^(z>0)^(y<12)^(z-x=0)
。假設咱們如今想生成知足(x+y>10)^(z>0)^¬(y<12)
,其中咱們想創建對¬(y<12)
(與y有關)的feasibility。那麼,(z>0)
和(z-x=0)
這兩個約束均可以去掉,由於與y不相關。(x+y<10)^(x>5)=>{x=6,y=3}
。對於新的約束,首先判斷這個新約束的搜索空間是緩存里約束的超集仍是子集。若是是新的約束的搜索空間是緩存的約束的子集,那麼,就把緩存中的約束去掉多餘的條件後繼續求解。若是是超集,那麼直接把解代入去驗證。描述:
程序語句轉化成符號約束的精度對符號執行的覆蓋率有很大的影響。例如,內存建模是用一個具體的數值去近似一個固定位數的整數變量可能會頗有效,可是另外一方面,它也會致使imprecision,好比丟失一些符號執行的路徑,或探索一些無用的解。另外一個例子就是指針,一些工具如DART[3]只能處理精確的指針。
解決方案:
precision和scalability是一個trade-off。須要考慮的因素有:
描述:
不少現實世界中的程序是併發的,這也意味着他們不少都是不肯定的(non-deteminism)。儘管如此,符號執行已經被有效地運用在測試併發系統,分佈式系統。
後續,我會記錄一些step by step使用部分符號執行工具的blog。
[1]: Symbolic Execution for Software Testing: Three Decades Later
[2]: DART: Directed Automated Random Testing
[3]: CUTE: A concolic unit testing engine for C
[4]: EXE: Automatically generating inputs of death
[5]: KLEE: Unassisted and automatic generation of high-coverage tests for complex systems programs