The Often Misunderstood GEP Instruction 經常被誤解的GetElementPtr(GEP)指令

原文:http://llvm.org/docs/GetElementPtr.html


介紹

         本文旨在消除圍繞LLVM的GetElementPtr(GEP)指令的神祕和困惑。 一旦開發人員開始使用LLVM進行編碼,關於狡猾的GEP指令的問題可能是最常出現的問題。 在這裏,我們列出了混淆的來源,並表明GEP指令非常簡單。

地址計算

        當人們第一次面對GEP指令時,他們傾向於將其與其他編程範例中的已知概念聯繫起來,最明顯的是C數組索引和字段選擇。 GEP非常類似於數組c的索引和字段選擇,但是它有點不同,這將導致以下問題。

GEP指令的第一個索引是什麼?

        快速回答:索引單步執行第二個操作數。 與第一索引的困惑通常源於思考GetElementPtr指令,如果它作爲一個C索引操作。 他們不一樣。 例如,當我們寫“C”時:

AType *Foo;
...
X = &Foo->F;

很自然地認爲只有一個索引,即字段F的選擇。但是,在這個例子中,Foo是一個指針。 必須在LLVM中顯式索引該指針。 另一方面,C透明地通過它進行索引。 要獲得與C代碼相同的地址位置,您將爲GEP指令提供兩個索引操作數。 第一個操作數通過指針索引; 第二個操作數索引結構的字段F,就像你寫的那樣:

X = &Foo[0].F;

有時這個問題被改爲:
                                    爲什麼可以索引第一個指針,但後續的指針不會被解引用?

       答案很簡單,因爲不必訪問內存來執行計算。 GEP指令的第二個操作數必須是指針類型的值。 指針的值作爲操作數直接提供給GEP指令,無需訪問存儲器。 因此必須將其編入索引並需要索引操作數。 考慮下面這個例子:

struct munger_struct {
  int f1;
  int f2;
};
void munge(struct munger_struct *P) {
  P[0].f1 = P[1].f1 + P[2].f2;
}
...
munger_struct Array[3];
...
munge(Array);

        在這個“C”示例中,前端編譯器(Clang)將通過賦值語句中的“P”爲三個索引生成三個GEP指令。 函數參數P將是這些GEP指令中的每一個的第二個操作數。 第三個操作數通過該指針索引。 對於 f1 或 f2 字段,第四個操作數將是struct munger_struct類型的字段偏移量。 因此,在LLVM程序集中,munge函數如下所示:

void %munge(%struct.munger_struct* %P) {
entry:
  %tmp = getelementptr %struct.munger_struct, %struct.munger_struct* %P, i32 1, i32 0
  %tmp = load i32* %tmp
  %tmp6 = getelementptr %struct.munger_struct, %struct.munger_struct* %P, i32 2, i32 1
  %tmp7 = load i32* %tmp6
  %tmp8 = add i32 %tmp7, %tmp
  %tmp9 = getelementptr %struct.munger_struct, %struct.munger_struct* %P, i32 0, i32 0
  store i32 %tmp8, i32* %tmp9
  ret void
}

       在每種情況下,第二個操作數是GEP指令開始的指針。 無論第二個操作數是參數,分配的內存還是全局變量,都是如此。 爲了清楚說明,讓我們考慮一個更爲愚鈍的例子:

%MyVar = uninitialized global i32
...
%idx1 = getelementptr i32, i32* %MyVar, i64 0
%idx2 = getelementptr i32, i32* %MyVar, i64 1
%idx3 = getelementptr i32, i32* %MyVar, i64 2

這些GEP指令只是從MyVar的基地址進行地址計算。它們計算如下(使用C語法):

idx1 = (char*) &MyVar + 0
idx2 = (char*) &MyVar + 4
idx3 = (char*) &MyVar + 8

  由於已知類型i32是四個字節長,因此索引0, 1 和 2 分別轉換爲 0 , 4 和 8 的存儲器偏移。 由於%MyVar的地址直接傳遞給GEP指令,因此沒有訪問內存來進行這些計算。此示例的遲鈍的部分是%idx2和%idx3。 它們導致計算指向內存超過%MyVar全局結尾的地址,這只是一個i32長,而不是三個i32長。 雖然這在LLVM中是合法的,但是它是不可取的,因爲具有由這些GEP指令產生的指針的任何加載或存儲將產生未定義的結果。

爲什麼需要額外的0索引?

  快速回答:沒有多餘的索引。 當GEP指令應用於始終爲指針類型的全局變量時,最常出現此問題。例如,考慮一下:

%MyStruct = uninitialized global { float*, i32 }
...
%idx = getelementptr { float*, i32 }, { float*, i32 }* %MyStruct, i64 0, i32 1

上面的GEP通過索引結構 %MyStruct 的 i32 類型字段來產生 i32 *。 當人們第一次看到它時,他們想知道爲什麼需要 i64 0索引。 然而,仔細檢查全局和GEP的工作方式可以發現是需要的。 瞭解以下事實將消除這種困惑:

  1. %MyStruct的類型不是 {float *,i32},而是 {float *,i32} *。 也就是說,%MyStruct 是一個指向結構的指針,該結構包含指向 float 和 i32 的指針。
  2. 通過注意GEP指令的第二個操作數的類型(%MyStruct)來證明點#1是 {float *,i32} * 。
  3. 第一個索引 i64 0 需要跨越全局變量 %MyStruct。 由於GEP指令的第二個參數必須始終是指針類型的值,因此第一個索引會逐步執行該指針。 值 0 表示從該指針偏移 0 個元素。
  4. 第二個索引 i32 1 選擇結構的第二個字段(i32)。

GEP取消引用了什麼?

  快速回答:沒什麼。 GetElementPtr指令不引用任何內容。 也就是說,它不會以任何方式訪問內存。 這就是加載和存儲指令的用途。 GEP僅涉及地址的計算。 例如,考慮一下:

%MyVar = uninitialized global { [40 x i32 ]* }
...
%idx = getelementptr { [40 x i32]* }, { [40 x i32]* }* %MyVar, i64 0, i32 0, i64 0, i64 17

在這個例子中,我們有一個全局變量 %MyVar,它是一個指向結構的指針,該結構包含一個指向 40 個 int 的數組的指針。 GEP指令似乎正在訪問結構的整數數組的第18個整數。 但是,這實際上是非法的GEP指令。 它不會編譯。 原因是必須取消引用結構中的指針才能索引到40個整數的數組。 由於GEP指令永遠不會訪問內存,因此它是非法的。 要訪問數組中的第18個整數,您需要執行以下操作:

%idx = getelementptr { [40 x i32]* }, { [40 x i32]* }* %, i64 0, i32 0
%arr = load [40 x i32]** %idx
%idx = getelementptr [40 x i32], [40 x i32]* %arr, i64 0, i64 17

在這種情況下,我們必須先使用加載指令在結構中加載指針,然後才能索引數組。如果示例更改爲:

%MyVar = uninitialized global { [40 x i32 ] }
...
%idx = getelementptr { [40 x i32] }, { [40 x i32] }*, i64 0, i32 0, i64 17

一切正常。在這種情況下,結構不包含指針,GEP指令可以通過全局變量索引到結構的第一個字段並訪問數組中的第18個i32。

爲什麼不用 GEP x, 0, 0, 1 和 GEP x, 1 個別名?

  快速回答:他們計算不同的地址位置。 如果查看這些GEP指令中的第一個索引,您會發現它們不同( 0 和 1 ),因此地址計算與該索引不同。考慮這個例子:

%MyVar = global { [10 x i32] }
%idx1 = getelementptr { [10 x i32] }, { [10 x i32] }* %MyVar, i64 0, i32 0, i64 1
%idx2 = getelementptr { [10 x i32] }, { [10 x i32] }* %MyVar, i64 1

  在此示例中,idx1 計算 %MyVar 結構中數組中第二個整數的地址,即 MyVar + 4。 idx1 的類型是 i32 *。 但是,idx2 計算 %MyVar之後的下一個結構的地址。 idx2 的類型是 { [10 x i32] } *,它的值等於 MyVar + 40,因爲它索引超過 MyVar中的10個4字節整數。 顯然,在這種情況下,指針不會別名。

爲什麼 GEP x, 1, 0, 0 和 GEP x, 1個別名

  快速回答:他們計算相同的地址位置。 這兩個GEP指令將計算相同的地址,因爲通過第0個元素的索引不會改變地址。但是,它確實改變了類型。考慮這個例子:

%MyVar = global { [10 x i32] }
%idx1 = getelementptr { [10 x i32] }, { [10 x i32] }* %MyVar, i64 1, i32 0, i64 0
%idx2 = getelementptr { [10 x i32] }, { [10 x i32] }* %MyVar, i64 1

  在此示例中,%idx1 的值爲 %MyVar + 40,其類型爲 i32 *。 %idx2 的值也是 MyVar + 40,但其類型爲 { [10 x i32] } *。

GEP可以將索引轉換爲向量元素嗎?

  然不建議這樣做,但並不總是被強制禁止。它導致優化器中的特殊情況的尷尬,並且IR中的基本不一致。在未來,它可能會被完全禁止。

GEP與ptrtoint,arithmetic和inttoptr有何不同?

  它非常相似; 只有微妙的差異。
  使用ptrtoint,您必須選擇整數類型。 一種方法是選擇 i64; 這對LLVM支持的一切都是安全的(LLVM內部假設指針在許多地方從不寬於64位),優化器實際上將 i64 算法縮小到不支持64位算術的目標上的實際指針大小。 大多數情況下。 但是,在某些情況下,它不會這樣做。 使用GEP可以避免此問題。 此外,GEP還帶有額外的指針別名規則。 從一個對象獲取GEP,將地址轉換爲另一個單獨分配的對象並取消引用它是無效的。 IR生產者(front-ends前端)必須遵循這一規則,消費者(優化者,特別是別名分析)可以從中依賴它。 有關詳細信息,請參閱規則部分。 並且,GEP在常見情況下更簡潔。 但是,對於隱含的基礎整數計算,沒有區別。

我正在寫一個目標的後端,這需要自定義降低爲GEP。我該怎麼做呢?

  你不知道。 GEP隱含的整數計算與目標無關。 通常,您需要做的是使您的後端模式匹配表達式樹涉及ADD,MUL等,這是GEP降低的內容。 這樣做的好處是可以讓代碼在更多情況下正常工作。 GEP確實使用與目標相關的參數來確定數據類型的大小和佈局,目標可以自定義。 如果您需要支持非 8 位尋址單元,則需要在後端修復大量代碼,GEP降低只是整個畫面的一小部分。

VLA尋址如何與GEP合作?

  GEP本身不支持VLA。 LLVM的類型系統完全是靜態的,GEP地址計算由LLVM類型引導。

VLA指數可以實現爲線性化索引。 例如,像 X [a] [b] [c] 這樣的表達式必須有效地 降低爲類似 X [a * m + b * n + c] 的形式,因此它在GEP中看起來像是一維的 數組引用。

這意味着如果您想編寫一個理解數組索引並希望支持VLA的分析,您的代碼必須準備好對線性化進行反向工程。 解決此問題的一種方法是使用ScalarEvolution庫,它始終以相同的方式呈現VLA和非VLA索引。

規則

如果數組索引超出範圍會發生什麼?

  有兩種情況,數組索引可以超出範圍。

  首先,數組類型來自GEP的第一個操作數的(靜態)類型。 大於相應靜態數組類型中元素數的索引是有效的。 在這個意義上,出界索引沒有問題。 索引到數組只取決於數組元素的大小,而不是元素的數量。
  如何使用它的常見示例是不知道大小的數組。 通常使用長度爲零的數組類型來表示這些。 靜態類型表示零元素的事實是無關緊要的; 計算任意元素索引是完全有效的,因爲計算僅取決於數組元素的大小,而不取決於元素的數量。 請注意,零大小的數組在這裏不是特殊情況。
       這種感覺與inbounds關鍵字無關。 inbounds關鍵字旨在描述低級指針算術溢出條件,而不是高級數組索引規則。
       希望理解數組索引的分析過程不應該假設遵守靜態數組類型邊界。
       超出界限的第二個意義是計算超出實際底層分配對象的地址。
       使用inbounds關鍵字,如果地址在實際的底層分配對象之外而不是一個接一個地址的地址,則GEP的結果值是未定義的。
       如果沒有inbounds關鍵字,則對計算越界地址沒有限制。 顯然,執行加載或存儲需要分配和充分對齊的存儲器的地址。 但GEP本身只關心計算地址。

數組索引可以爲負數嗎?

       是的。這基本上是數組索引超出範圍的特殊情況。

我可以比較用GEP計算的兩個值嗎?

        是的。如果兩個地址都在同一個分配的對象中,或者一個接一個地,那麼您將獲得預期的比較結果。如果其中任何一個在其外部,則可能發生整數算術包裝,因此比較可能沒有意義。

我可以使用與基礎對象類型不同的指針類型來執行GEP嗎?

        是的。 將指針值連接到任意指針類型沒有限制。 GEP中的類型僅用於定義基礎整數計算的參數。 它們不需要與底層對象的實際類型相對應。

       此外,加載和存儲不必使用與基礎對象的類型相同的類型。 此上下文中的類型僅用於指定內存大小和對齊方式。 除此之外,優化器只有一個提示,指示如何使用該值。

我可以將對象的地址轉換爲整數並將其添加到null嗎?

        您可以通過這種方式計算地址,但是如果使用GEP進行添加,則不能使用該指針實際訪問該對象,除非該對象是在LLVM之外進行管理的。

        底層整數計算已充分定義; null 有一個定義的值 - 零 - 你可以添加你想要的任何值。

        但是,使用這樣的指針訪問(load from 或store to)具有LLVM感知的對象是無效的。這包括GlobalVariables,Allocas和noalias指針指向的對象。

        如果您確實需要此功能,可以使用顯式整數指令進行算術運算,並使用inttoptr將結果轉換爲地址。 大多數GEP的特殊別名規則不適用於從ptrtoint,arithmetic和inttoptr序列計算的指針。

我可以計算兩個對象之間的距離,並將該值添加到一個地址以計算另一個地址嗎?

        與null上的算術一樣,您可以使用GEP以這種方式計算地址,但是如果您這樣做,則不能使用該指針實際訪問該對象,除非該對象在LLVM之外進行管理。

        同樣如上所述,ptrtoint和inttoptr提供了另一種沒有此限制的方法。

我可以在LLVM IR上進行基於類型的別名分析嗎?

        您不能使用LLVM的內置類型系統進行基於類型的別名分析,因爲LLVM對尋址,加載或存儲中的混合類型沒有限制。

LLVM基於類型的別名分析過程使用元數據來描述不同類型的系統(例如C類型系統),並在此基礎上執行基於類型的別名。進一步的細節在語言參考

如果GEP計算溢出會發生什麼?

       如果 GEP 缺少 inbounds 關鍵字,則該值是評估隱式的二進制補碼整數計算的結果。 但是,由於無法保證在地址空間中分配對象的位置,因此這些值的含義有限。

        如果GEP具有 inbounds 關鍵字,則如果 GEP 溢出(即在包裹地址空間的末尾),則結果值是未定義的(“陷阱值”)。

        因此,對於入口GEP存在一些這樣的分支:由數組 / 向量 / 指針索引 隱含的比例總是已知爲“nsw”,因爲它們是由元素大小縮放的有符號值。 這些值也允許爲負(例如“gep i32 *%P,i32 -1”),但指針本身在邏輯上被視爲無符號值。 這意味着GEP在指針基(被視爲無符號)和應用於它的偏移(被視爲有符號)之間具有不對稱關係。 偏移計算中的加法結果不能有符號溢出,但是當應用於基指針時,可能存在有符號溢出。

如何判斷我的前端是否遵守規則?

        目前沒有getelementptr規則的檢查器。 目前,唯一的方法是手動檢查前端中創建GetElementPtr運算符的每個位置。 編寫一個可以靜態查找所有規則違規的檢查程序是不可能的。 通過動態檢查來編寫代碼,可以編寫一個檢查器。 或者,可以編寫一個靜態檢查器來捕獲可能出現問題的子集。 但是,今天不存在這樣的檢查器。

 

解釋

爲什麼GEP以這種方式設計?

GEP的設計具有以下目標,粗略的非正式優先順序:

  • 支持C,C類語言和可以在概念上降低到C語言的語言(這涵蓋了很多)。
  • 支持優化,例如C編譯器中常見的優化。特別是,GEP是LLVM指針別名模型的基石。
  • 提供一致的計算地址的方法,以便地址計算不需要成爲IR中加載和存儲指令的一部分。
  • 支持非C語言,只要它不干擾其他目標。
  • 最大限度地減少IR中的特定目標信息。

爲什麼struct成員索引總是使用i32?

特定類型 i32 可能只是一個歷史文物,但是它足夠寬,可用於所有實際目的,因此無需更改它。 它不一定意味着i32地址算術; 它只是一個標識結構中字段的標識符。 要求所有結構索引相同會減少兩個GEP實際上相同但具有不同操作數類型的情況的可能性範圍。

什麼是醜陋的?

一些 LLVM optimizers通過在內部將它們降級爲更原始的整數表達式來操作GEP,這允許它們與其他整數表達式組合和/或拆分成多個單獨的整數表達式。 如果它們進行了非平凡的更改,則轉換回 LLVM IR 可能涉及對尋址結構進行反向工程,以使其適合原始第一個操作數的靜態類型。 並不總是完全重建這種結構; 有時底層尋址根本不符合靜態類型。 在這種情況下,optimizers(優化器)將發出一個GEP,其基本指針使用名稱“uglygep”轉換爲簡單的地址單元指針。 這不是很好,但它同樣有效,並且足以保留GEP提供的指針別名保證。

總結:

總之,這裏有一些關於GetElementPtr指令的常識:

  1. GEP指令從不訪問內存,它只提供指針計算。
  2. GEP指令的第二個操作數始終是指針,必須將其編入索引。
  3. GEP指令沒有多餘的索引。
  4. 尾隨零索引對於指針別名是多餘的,但對於指針的類型則不是。
  5. 對於指針別名和指針的類型,前導零索引不是多餘的。

(轉載請註明出處)

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