DSA:上下文敏感的指針分析/別名分析

Data Structure Analysis (DSA)

Full Paper: [Data Structure Analysis: A Fast and Scalable Context-Sensitive Heap Analysis (2003)]

DSA算法(DataStructure Analysis的首字母縮寫)是LLVM的發起人Chris Latter在其碩士、博士系列論文中提出的一個上下文感知(context sensitivity)的、過程間(inter-procedure)的數據結構分析算法。這個算法的強大之處在於可以分析像C這樣擁有指針類型的複雜語言,並擁有可觀的效率(在http://llvm.org/pubs/可以找到這篇論文“DataStructure Analysis: An Efficient Context-Sensitive Heap Analysis”)。

DSA算法的實現目前在llvm的poolalloc項目下(poolalloc的源代碼可通過SVN從http://llvm.org/svn/llvm-project/poolalloc/trunk獲取),poolalloc是應用DSA的一個強大的分配池框架。ChrisLatter的論文對此有詳盡的描述。據註釋顯示,DSA的實現尚未穩定,還在劇烈改動中。

DSA算法在llvm的中間表達形式(llvm-IR)的基礎上實現,這個中間表達形式的特點是保存了儘可能多的類型信息。而這是DSA能夠實現的重要條件(llvm-IR的詳盡說明可以參考http://llvm.org/docs/LangRef.html)。

Summary

This paper describes a scalable heap analysis algorithm, Data Structure Analysis, designed to enable analyses and transformations of programs at the level of entire logical data structures. Data Structure Analysis attempts to identify disjoint instances of logical program data structures and their internal and external connectivity properties (without trying to categorize their “shape”). To achieve this, Data Structure Analysis is fully context-sensitive (in the sense that it names memory objects by entire acyclic call paths), is field sensitive, builds an explicit model of the heap, and is robust enough to handle the full generality of C. Despite these aggressive features, the algorithm is both extremely fast (requiring 2-7 seconds for C programs in the range of 100K lines of code) and is scalable in practice. It has three features we believe are novel: (a) it incrementally builds a precise program call graph during the analysis; (b) it distinguishes complete and incomplete information in a manner that simplifies analysis of libraries or other portions of programs; and © it uses speculative field-senstivity in type unsafe programs in order to preserve efficiency and scalability. Finally, it shows that the key to achieving scalability in a fully context-sensitive algorithm is the use of a unification based approach, a combination that has been used before but whose importance has not been clearly articulated.

1. 概要

別名分析在指導傳統的低級內存優化方面取得了很大的成功。別名分析提供了一種消除內存引用對的歧義的轉換過程,並能識別語句的局部和過程間副作用。相比之下,應用於複雜數據結構(如列表、堆或圖)的整個實例的轉換的成功率要低得多。將指針分析/別名分析應用於複雜數據結構還需要一些強大的分析功能的支持:

  • Full Context-Sensitivity: 需要使用分析算法來區分通過程序中不同調用路徑創建的堆對象(即,通過整個非循環調用路徑命名對象),識別數據結構的不相交實例。
  • Field-Sensitivit: 需要區分不同結構字段的屬性點,以識別數據結構的內部連接模式。
  • Explicit Heap Model: 分析堆數據結構需要構造一個顯式堆模型,包括標識別名不直接需要的對象。

由於潛在的成本,實際的別名和指針分析算法尚未嘗試提供上述屬性的組合。相比之下,“Shape Analysis”算法強大到足以提供這些信息和更多信息(例如,足以將特定結構標識爲“鏈表”或“二叉樹”)。然而,到目前爲止,形狀分析在商業優化編譯器中還沒有被證明是實用的。

本文提出的DSA算法是一個上下文敏感(context-sensitivity)的、過程間(inter-procedure)的數據結構分析算法,適用於不相交的邏輯數據結構實例的轉換,它提供了上面列出的三個必需功能。這個算法的強大之處在於可以分析C/C++這類擁有指針類型的複雜語言,並擁有可觀的效率。DSA算法在LLVM的中間表達形式(LLVM-IR)的基礎上實現,LLVM IR的特點是保存了儘可能多的類型信息。而這也是DSA能夠實現的重要條件。

DSA有三個Novel的特徵:

  • 它在分析過程中逐步建立一個精確的程序調用圖。該算法是完全非迭代的,在分析過程中只訪問每條指令和每個調用邊一次。
  • 該算法明確區分了完整信息和不完整信息,使其即使在分析的中間階段也是保守的,並允許它安全地分析程序的某些部分。
  • 該算法通過假設程序中的內存對象在顯示之前是類型安全的,從而提供了推測字段敏感性。這使得該算法對以類型安全方式訪問的對象(常見情況)具有完全的字段敏感性。

2. 數據結構圖 (Data Structure Graph)

程序中的每個函數都會計算出一個數據結構圖(DS圖),圖中總結函數中可訪問的內存對象及其連接模式。每個DS圖節點表示一組(可能是無限的)內存對象,不同的節點表示不相交的對象集,即圖是內存對象的有限靜態分區。所有可以由單個指針變量或字段指向的動態對象都表示爲圖中的單個節點。

一個數據結構圖可以形式化表示爲一個有限有向圖 DSG(F)
DSG(F) = (N, E, E_v, C), 其中

N代表有向圖中頂點的集合,圖中每個頂點都表示一組內存對象;
E代表有向圖中的邊的集合,邊的source和target都是DS node的fields(E的類型是[n_s,f_s]->[n_d,f_d]);
E_v代表vars(f)->[n,f]的函數,其中vars(f)是函數f中虛擬寄存器的集合;E_v(v)是從寄存器v到field[n,f]的邊,它們之間是points-to關係
C是圖中的一組“調用節點”,表示當前函數上下文中未解析的調用位置。每一個調用節點c∈C都是一個k+2元組:(r,f,a_1,...,a_k),f是被調用的函數,r是f的返回值,a_1...a_k是函數f的參數中的pointer-compatible變量

爲了演示DS圖和DSA分析算法,文章使用圖1中的代碼作爲運行示例。本例使用迭代、遞歸、函數指針、指向子對象的指針和全局變量引用創建並遍歷兩個不相交的鏈表。儘管示例很複雜(這個例子太複雜了,筆者看了半天才看懂),但DSA分析能夠證明兩個列表X和Y是不相交的。
在這裏插入圖片描述

本文使用圖2中所示的圖形符號呈現DS圖。
在這裏插入圖片描述

DS圖中的DS節點負責表示與該節點對應的一組內存對象的信息。每個節點n有三條與之關聯的信息:

T(n) 代表由n表示的存儲器對象的Type
G(n) 表示一組(可能是空的)全局對象,即節點n表示的所有對象。
flags(n) 是與節點n相關聯的一組標誌。下面定義了八個可能的標誌(h、s、g、u、m、r、c和o)。標誌(n)中的“H”、“S”、“G”和“U”標誌用於區分四類內存對象:堆分配、堆棧分配、全局(包括函數)和未知對象。特定的內存對象是否在當前的分析範圍內被修改或讀取,這通過“M”和“R”標誌表示。信息完整用“C”標誌表示。

圖3顯示了在應用任何過程間信息之前爲do all和addG函數計算的示例圖。該圖包括一個調用節點的示例,該節點(在本例中)調用FP指向的函數,將L指向的內存對象作爲參數傳遞,並忽略調用的返回值。
在這裏插入圖片描述
這個步驟還沒有執行過程間分析,只是一個過程內分析,因此很多信息都是不全的。例如,在這個函數中,它可以確定L被視爲一個列表對象(構造算法關注指針是如何使用的,而不是它們聲明的類型是什麼),但是它無法知道它爲這個內存對象在更大的範圍內是否正確。爲了處理這種情況,DSA分析計算圖中哪些節點是“完整的”,並用C標誌標記節點。另一種情況是節點上某個對象可能存在類型安全衝突,則假定T(n)=void*,將節點標記爲O,此時節點的field傳出邊被合併爲一個。下面的僞代碼描述了DSA是怎麼處理collapse這種情況的:
在這裏插入圖片描述

3. DS圖的構造算法

DS圖的創建和優化過程一共可以分爲三個步驟:

  • 第一階段,爲程序中的每個函數構造一個DS圖,只使用過程內信息(“局部”圖)。
  • 第二階段,使用“自底向上”的分析消除DS圖中由於函數中的被調用函數而導致的不完整信息,通過將被調用圖中的信息合併到函數調用方的DS圖中(此時第二階段創建的圖稱爲“BU”圖)。
  • 第三階段,使用“自頂向下”的分析將調用者的BU圖合併到被調用者(創建“TD”圖)來消除由於傳入參數導致的不完整信息。BU和TD階段對調用圖中的“已知”強連接組件(scc)進行操作。

3.1 圖的基本操作

該算法對DS圖使用了幾個基本操作,如圖4所示。
在這裏插入圖片描述

這幾個基本操作在算法中的意義簡單解釋一下:

  • mergeCells: 合併兩個<node, field>對,這需要合併類型信息,flags,全局變量,兩個節點的輸出邊緣,並將輸入邊緣移動到結果節點。;
  • cloneGraphInto: 合併兩個節點,並以指定方式對齊字段;
  • resolveCallee: 將被caller的圖內聯到calleee的圖中;
  • resolveCaller: 將被callee的圖內聯到caller的圖中;
  • resolveArguments: 合併參數和返回值以完善函數調用信息(針對全局圖)。

3.2 構建DS圖(局部)

構建局部DS圖階段在無需任何有關Caller和Callee的信息爲每個函數計算一個局部的DS圖。函數F的局部DS圖的構建算法如下圖所示。LocalAnalysis函數首先爲每個指針兼容的虛擬寄存器(在map E_v中輸入它們)創建一個空節點作爲目標,併爲每個全局變量創建一個單獨的節點。然後,分析對函數的指令進行線性掃描在malloc和alloca操作中創建新節點,在賦值和return指令中合併變量的邊,並在特定定的操作中更新類型信息。
在這裏插入圖片描述

一個cell的信息E_v(Y)既<node, field>,僅當實際對Y的進行解引用操作(store或load)時以及在索引到結構或 Y指向的數組時,才被更新,即如下情況:

case X = &Y -> Z:
    updateType(E_v(Y), typeof(*Y))
case X = &Y[idx]:
    updateType(E_v(Y), typeof(*Y))

malloc,alloca和cast操作僅創建一個void類型的節點。結構體字段的訪問調整傳入邊以指向地址字段。忽略對數組對象的索引,即數組被視爲具有單個元素。return指令的處理是通過創建一個特殊的虛擬寄存器用於捕獲返回值

函數調用會將一個新的調用節點添加到DS圖中,並返回函數指針(用於直接調用和間接調用)。例如,圖7(a)顯示了addGTList函數的局部DS圖,其中爲調用的函數do_all創建了調用節點(爲正確合併類型信息,將爲每個條目創建一個空節點,然後使用mergeCells進行合併,因爲參數類型可能與爲正式參數或返回值聲明的類型不匹配)。
在這裏插入圖片描述

局部DS圖構造的最後一步是計算哪些DS節點是Complete的。對於可以從形式參數、全局參數訪問的節點可能不會標記爲C;對於可以傳遞一個參數到函數調用的節點可能不會被標記爲C;對於返回一個值到調用函數的節點可能不會被標記爲C。這反映了局部分析階段沒有任何過程間信息。

3.3 自底向上的分析階段
自底向上(BU)分析階段通過合併來自每個函數被調用者的過程間信息來優化每個函數的局部DS圖。BU分析的結果是每一個函數有一個BU圖,它總結了當沒有上下文信息時調用該函數調用該函數(強制別名和mod/ref信息)的總體效果。它通過將所有已求解的callee的BU圖克隆到caller的局部圖中,合併由相應的形式參數和實際參數指向的節點來計算此圖。

考慮函數F的形式參數f_1…f_n,其中傳遞的實際參數是a_1…a_n。圖4中的函數resolveCallee顯示了在BU階段如何處理形式參數和實際參數的合併。首先爲F複製BU圖,清除所有的堆棧節點標記,因爲被調用方的堆棧對象在調用方中不可合法訪問。然後,我們將指針兼容類型的每個實際參數ai指向的節點與fi指向的節點的副本合併。我們還將合併調用節點中的返回值和來callee的返回值節點的副本。此外,F的BU圖中的所有未解決的節點都將複製到caller的圖中,並且callee的圖中表示未解決的函數調用的參數的所有對象現在也都在caller中表示。

圖8顯示了用於遍歷調用的完整的“自底向上”算法。注意,該階段都不會重新訪問以前訪問的函數,但是該調用節點最終將在自頂向下的階段得到解決。本文針對四種不同情況進行了解釋。
在這裏插入圖片描述

  1. 在最簡單的情況下,僅對非外部函數進行直接調用,沒有遞歸併且沒有函數指針的情況下,每個DS圖中的調用節點都隱式定義了整個調用圖。 BU階段只需按後序遍歷函數調用圖(在調用者之前訪問被調用者),如上所述將callee的BU圖克隆和內聯到caller的圖中即可。
  2. 爲了支持具有函數指針和外部函數(但不遞歸)的程序,僅將後序遍歷限制爲僅在其函數指針指向Complete節點(即,其目標已完全解析)的情況下處理調用站點。如果已知傳遞給函數指針參數的函數的情況下,此類caller纔可能會解析。 例如例子中對FP的調用無法在函數do_all中求解,而是可以在BU圖中針對函數addGToList進行解析,在此我們得出結論,將addG的BU圖內聯到addGToList的圖中,得出的最終圖如圖7(c)所示。其中L指向的節點中的Modified標誌是從addG的節點EV(X)(圖3)獲得的,該標誌與從do all內聯的第二個參數變量節點合併。
  3. 不帶函數指針的遞歸情況(感興趣的讀者可以閱讀原文)
  4. 有函數指針的遞歸情況(帶有間接調用的遞歸函數,感興趣的讀者可以閱讀原文)

在這裏插入圖片描述

圖10的圖顯示了示例的main函數的BU圖。 該圖具有X和Y指向的list的不相交的子圖。 由於我們克隆了該對象,然後爲每個對addGToList()的調用內聯了BU圖,因此證明它們是不相交的。 這顯示了上下文敏感性與克隆的組合如何識別不相交的數據結構,即使涉及複雜的指針操作也是如此。
在這裏插入圖片描述

3.4 自頂向下的分析階段

與自底向上類似,不同的是將caller內聯到calee中

3.5 算法複雜度分析

本文閱讀難度較大,整體晦澀難懂,筆者也沒有對算法複雜度進行更多的思考,就簡單列一下吧。

  • 最壞時間複雜度:O(na(n)+ka(k)e)
  • 最壞空間複雜度:O(fk)

其中n是指令的數量,k是單個過程的數據結構圖的最大大小,e是函數調用圖中邊的數量,f是函數的總數量。實際上k應該非常小,通常是100個或更少,即使在大型程序中也是如此。

4. 實驗評價

作者在35個C程序上對DSA算法進行了評估,結果表明該算法在實際應用中(在性能和內存消耗方面)是非常有效的。這包括包含複雜堆結構、遞歸和函數指針的程序。例如,分析povray31(一個由130000多行代碼組成的程序)只需要不到8秒的分析時間和大約16MB的內存。
在這裏插入圖片描述

5. 總結

本文提出了一種堆分析算法,用於對遞歸數據結構的不相交實例進行分析和轉換。該算法使用了多種技術的組合,以平衡堆分析精度(上下文敏感、克隆、字段敏感和顯式堆模型)和效率(流不敏感、統一和完全非迭代分析)。通過對整個遞歸數據結構進行操作,該算法能夠實現指針密集型代碼的分析和轉換的新方法(用較弱但更有效的方法實現形狀分析的一些目標)。實驗結果表明,該算法在實際應用中速度非常快,佔用的內存非常少。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章