所謂的呼叫慣例是指一個規範,這個規範描述了函式的參數如何傳遞、返回值式怎麼返回等等的議題。不同的處理器架構通常會有不同的呼叫慣例。比如: 在 x86 的架構中,函式的參數傳遞會以堆疊來完成。但在 Arm 的架構中,有一些參數會放在寄存器中,有一些參數則可能放在堆疊中。
由於不同的處理器可能存在不同的呼叫慣例,LLVM 框架提供了一些機制來讓後端開發者可以實做自家處理器的呼叫慣例。這些程式碼主要集中在 <Arch>ISelLowering.cpp 與 <Arch>CallingConv.td 中。本文會以 Sparc 處理器為例來介紹 LLVM (本文是採用 LLVM 3.2 的版本) 是如何處理整數型態的呼叫慣例。
原始碼分析
我們先來看 SparcCallingConv.td 這份檔案。這份檔案定義了兩個變量: RetCC_Sparc32 與 CC_Sparc32。其中,RetCC_Sparc32 描述了返回值如何返回,而 CC_Sparc32 則描述了函式的參數如何傳遞。以 CC_Sparc32 為例,它描述了如果函式參數的型態為 i32 ,則前 6 個參數會依序放入 I0 到 I5 這六個寄存器中,而從第7個之後的參數則會被放到堆疊中。
// Sparc 32-bit C return-value convention. def RetCC_Sparc32 : CallingConv<[ CCIfType<[i32], CCAssignToReg<[I0, I1, I2, I3, I4, I5]>>, ...
// Alternatively, they are assigned to the stack in 4-byte aligned units. CCAssignToStack<4, 4> ]>; // Sparc 32-bit C Calling convention. def CC_Sparc32 : CallingConv<[ ... // i32 f32 arguments get passed in integer registers if there is space. CCIfType<[i32, f32], CCAssignToReg<[I0, I1, I2, I3, I4, I5]>>, ... ]>;
接著,我們來分析 SparcISelLowering.cpp。其中有 3 個重要的函式
- LowerFormalArguments(): 處理被呼叫端(callee)的參數傳遞
- LowerReturn(): 處理被呼叫端(callee)的返回值
- LowerCall(): 處理呼叫端(caller)的參數傳遞與返回值
本文會先針對 LowerFormalArguments() 這個函式來進行探討。
我們約略將 LowerFormalArguments() 分成兩個步驟:
- 取得參數擺放位置的資訊:
此步驟會以下面這段程式碼完成:
CCInfo.AnalyzeFormalArguments(Ins, CC_Sparc32);
其中,Ins 裡面會存放著每一個參數的資訊,而 CC_Sparc32 是 LLVM 根據 SparcCallingConv.td 中的 CC_Sparc32 所生成的一個函式,會定義在 SparcGenCallingConv.inc 中。AnalyzeFormalArguments() 所做的事就是掃描 Ins 中的每一個參數,再根據 CC_Sparc32() 中的資訊來判斷每一個參數要擺放在什麼地方(寄存器 or 堆疊),而這些資訊會存放在 ArgLocs 這個容器中。
- 生成 DAG 節點將參數從寄存器或堆疊中取出
此步驟會走訪 ArgLocs 容器中的每一個參數,並依據參數擺放的位置來生成相對應的節點。主要又分成兩種情況
- VA.isRegLoc() 為真: 代表參數放在寄存器中
會生成 CopyFromReg 的 DAG 節點,將寄存器中的參數搬移置虛擬寄存器中。
- VA.isMemLoc() 為真: 代表參數放在堆疊中
首先會計算出此參數在堆疊中的 FrameIndex ,再生成 Load 的 DAG 節點,將此 FrameIndex 所指到的位置中的參數載入到虛擬寄存器中。
這裡補充一下,這裡的 Load 節點是將 FrameIndex 所指到的位置的值載入,但是根據 Sparc 的呼叫慣例,被呼叫端應該從 %fp + 92, %fp + 96, ... 等位置載入這些參數。因此為了將這些 FrameIndex 轉換為在堆疊中的真實位置,LLVM 提供了另一個 eliminateFrameIndex() 的 hook function 來讓後端開發者實現這部分的轉換。有關 eliminateFrameIndex() 的機制,之後有機會會再介紹。
以下我們直接透過實驗來觀察這些由 LowerFormalArguments() 生成的 DAG 節點。
實驗觀察
首先,為了方便觀察,我們先偷偷的將 CC_Sparc32 中的寄存器數量從6個減少到1個 (也就是只剩一個 I0)。
def CC_Sparc32 : CallingConv<[ ... // i32 f32 arguments get passed in integer registers if there is space. CCIfType<[i32, f32], CCAssignToReg<[I0]>>, ... ]>;
接著,準備一份具有函式呼叫的 LLVM IR,且這個函式必須有2個參數。
define i32 @foo(i32 %m, i32 %n) nounwind uwtable { entry: %m.addr = alloca i32, align 4 %n.addr = alloca i32, align 4 store i32 %m, i32* %m.addr, align 4 store i32 %n, i32* %n.addr, align 4 ret i32 0 }
然後我們透過 llc 來編譯這份 LLVM IR,並將其 DAG 節點的資訊印出,如下圖所示
在這張圖中用紅色框框標出來的即是 LowerFormalArguments() 所生成的節點。我們可以看到,第一個參數 %m 會透過 CopyFromReg 節點 (也就是左邊的紅框) 來傳遞; 而第二個參數 %n 則會透過 Load 節點 (右邊的紅框) 來傳遞,此結果符合上述我們對於 LowerFormalArguments() 的分析。
結語
本文介紹了 LLVM 是如何支援不同處理器架構的呼叫慣例。並藉由探討 Sparc 處理器的原始碼來了解其運作的機制。由於篇幅的關係,剩下的兩個函式 LowerReturn() 和 LowerCall() 會留在下一篇文章介紹。
沒有留言:
張貼留言