程序分析中的 {path,context,flow}-sensitive 問題?
針對不同語言它們的定義,比如有的語言還會提object sensitive;
對不同分析的精度、複雜度、擴展性。。的影響;實際應用的取捨等等。。
題主想要不同語言的定義,我只熟悉OO語言,所以我只解釋OO程序里的概念。不過知道OO語言的之後,C的也基本能觸類旁通,因為它們都是imperative語言。Functional語言的context-sensitivity我猜應該也是相通的,至於functional語言的flow-和path-sensitivity我就不清楚了。
通常所說的xxx-sensitive是指靜態分析中用於降低誤報(false positive)的技術,最常提的xxx正是題主所說的path, context以及flow。要具體解釋這幾個概念,首先我們來看看靜態分析怎麼產生false positive,然後我們再來看看如何用這幾個技術(或者說概念)消除相應的false positive。
我們做靜態分析時,無論是分析源代碼還是目標代碼,我們分析的對象(方法、語句、變數)都只有一份:同一個方法我們只會寫一個方法體(方法體里的語句也就只有一份),同一個變數我們只會聲明一次。然而動態運行程序的時候:- 一個方法可能會被調用N次,每次調用的上下文可以不一樣,不同上下文中這個方法里變數的值會不同;
- 一個方法里,一個變數在不同位置的值也會不一樣。例如一個變數v,在方法執行到第1行和第10行的值會不同;
- 一個方法里同一個位置的變數的值在程序執行不同路徑時也不一樣。例如方法foo第10行要用變數v,第10行之前有一個if-else分支會修改v的值,那麼程序途徑true branch和false branch到達foo第10行時,v的值又不同。
這樣,我們寫的方法、語句、變數在動態運行時彷彿有了「分身」,每個「分身」都自己的值(或者說屬性)。靜態的時候分析工具對於同一個對象只能看到一個實體,如果直接這麼分析,一個變數所有「分身」的相關屬性會全部合併,並且一個變數在的屬性合併了,還會影響其它變數的分析結果,false positive就這麼產生了。要想用靜態分析得到準確的結果,就得為分析的對象模擬動態運行時「分身」。
按照我的理解,xxx-sensitive就是在靜態分析時,按照xxx給程序里的對象(模擬動態運行)創建「分身」(或者說按照xxx區分分析對象):按照上下文區分叫做context-sensitive;按照位置區分叫做flow-sensitive;按照路徑區分叫做path-sensitive。區分之後再分析可以減少false positive。但是靜態不可能完全模擬動態的所有情況,因為一旦有遞歸和循環,理論上你寫下的方法和變數就能產生無窮無盡的「分身」。所以靜態分析只能或多或少地通過xxx-sensitive技術減少false positive,而不可能消除。
寫到這裡我發現,如果要繼續寫下去這個答案會非常非常長……網上有一大堆課件,龍書里也有一些相關內容,所以我要太監了,不好意思。程序的靜態分析有兩個核心思想:abstract interpretation 和approximation。abstract interpretation 就是為了得到準確的靜態信息而去盡量模仿語言本來的語義;但是由於一些限制(比如靜態情況下只有partial input,比如運行太慢)靜態分析又不得不放棄一些原有的語義,這就造成了approximation。這幾個sensitivity 就是在平衡這兩個矛盾時採用的一些策略。
- Path-sensitive
在普通的分析中,當我們遇到if,while 這種分支結構時靜態分析不可能完全知道程序的動態運行路線(path),所以一般都會把不同path 的結果merge 起來(union for may,intersection for must),這樣就有可能會丟失信息。舉個constant propagation 的例子:
if (a &> 0) {
x = 1;
} else {
x = 2;
}
在path-insensitive 分析中,在這段代碼的結束處我們看到x 有可能是1 也有可能是2,所以它不是一個constant 而是一個variable。但是有可能在這段代碼之前我們已經知道了a是一個constant且為 1,所以結束時x 只可能為1(constant),這樣分析的精度就提高了。然而,這種『if 必然執行某一分支』的情況畢竟是少數,所以高級一點的path-sensitive 的做法是同時保留兩個分支的結果而不merge,即在這段代碼的結尾x 的分析結果是『如果a &> 0 則為1,a &<= 0 則為2』。這樣分析的精度就高了很多,但是付出的代價是順著control flow,path 的『分情況討論』數量隨指數增長。當然也有一些優化策略可以在某些情況下降低這個複雜度(比如property simulation)。
- Context-sensitive
在提到context-sensitive 時一般context 專指function call context。Context-insensitive interprocedural analysis 中只是把function 當做一段普通的代碼來看待,它只關心function 之間的數據傳遞(parameters, return value,side-effect)而忽略了同一函數在不同call site 下不同的context,即忽略了call stack。再舉個constant propagation 例子:
function f(x) { //1
var a; //2
a = x + 10; //3
return a; //4
}
……
……
……
a = 10; //5
b = f(a); //6,7
c = f(b); //8,9
我們知道這裡的b 肯定是constant 20,但是在得到這個結果之後我們同時認為function f 中的a和x 也成了一個constant,所以在`c = f(b);` 這一點上我們將b=20 傳入函數之後靜態分析器就認為函數中的x 既可以是10 (from a) 也可以是20 (from b) 進而認為x 是variable,a 也就成了variable,最後c 也成了一個variable,可實際上c 只可能是constant 30. 解決這個問題的核心思想依然是『分情況討論』也就是甜品專家說的為function f 建立『分身』,這樣我們就成功的模擬了call stack 的行為,即用每一次調用生成不同的『分身』來模擬每一次調用都會生成不同的stack frame (代表不同的loacl variables,parameters)。在data flow analysis 中選擇合適的context representation(比如call string)的情況下,context-sensitive 還能解決相當多的其他問題(比如call/return mismatch、scope 混淆等),這些都是在儘力貼近語言原本的語義。除此之外,context-sensitive 在control flow analysis 還肩負這一項重要且極其複雜的任務:表示閉包(Closure)。
- Flow-sensitive
比起上面兩個,flow-sensitive 的概念稍有點混亂,先放個圖直觀感受一下什麼是flow-sensitive:
@彭飛 和 @甜品專家 說得都挺好了,我隨便說說我的想法。
flow-sensitive是說要關注statements之間的先後順序。flow-insensitive的話,其實是把statements當作一個集合來看的。各個statement之間沒有順序,所以control flow statement(if , while)可以直接刪去。
另外一個看待flow-sensitive的角度是先對被分析程序做SSA,然後再perform flow-insensitive analysis。這兩者在沒有alloc / store / load的情況下是嚴格等價的,但是如果有memory object的話,就需要做memory SSA,而memory SSA又依賴於analysis。所以有一個猜想是不停迭代做memory SSA和flow-insensitive pointer analysis,直到一個fixed point,則結果應該等於flow sensitive pointer analysis。這個不是我發明的看法,是某篇老paper [1]裡面提出的。
從SSA的角度來看,path-insensitive是在處理Phi node的時候丟棄分支的信息,只把各個分支來的所有值給merge。path-sensitive是要保留分支信息。
[1] Using static single assignment form to improve flow-insensitive pointer analysis, Rebecca Hasti and Susan Horwitz, PLDI 1998.推薦閱讀:
※Lua 語言的靈活、高擴展性優點體現在哪裡?
※計算機科學與技術(CS)專業學生該如何系統地自學?
※為什麼asp.net沒能比php更流行?
※易語言是圖靈完備的嗎?
※不同編程語言的Delay函數分別是什麼?