LLVM Backend技術


LLVM Backend技術

LLVM編譯器介紹

LLVM 是一個編譯器基礎設施的設計與實現,具有獨特的多階段優化系統。廣泛支持過程間和配置文件驅動的優化,同時對於商業編譯器系統也十分高效。

LLVM 虛擬指令集把系統作為一個整體關聯到一起,使用高級的類型信息,來做低級的系統表象,支持鏈接時和鏈接后積極的過程間優化,運行時和系統空閑期都可進行優化處理。

鏈接時和鏈接后的過程間優化,充分考慮真實的使用環境與習慣,把局部性最大化

高級類型信息 high-level type information 都有那些

低級表現 low-level representation 都有那些?緊湊的表現、各種轉換等等。

當代編程語言與實踐,想通過提高軟件開發的生產效率和為編譯器提供高級的語意信息,以支持更加可靠、模塊化和動態的應用軟件。但這些特性有時,反而會降低已編譯應用的運行時性能。與此同時,微處理器仍在以驚人的速度發展,管道越來越深、緩存的級別越來越深、內存訪問時間(相對於CPU)越來越長。  

為了解決軟硬件發展的不協調問題,硬件設計人員暴露了更多的並行執行資源和集成特性,如寄存器重命名引擎和緩存重排序等原編譯器開發人員才需要關注的領域。編譯器介於當代編程語言和硬件架構之間,要盡可能地優化應用程序:消除程序中非必要開銷,高效使用處理器暴露的資源。

LLVM 是如何處理的?
LLVM 系統中的靜態編譯器,會把源代碼編譯成,帶有高級類型信息的低級別表現:LLVM 虛擬指令集。靜態編譯器便可使用它的產物,執行大量的編譯期優化,同時能夠把高級信息傳遞給鏈接器。

在鏈接時,程序被組合成唯一的 LLVM 虛擬指令集代碼單元,並且執行過程間優化(可參考第四章中幾個高級過程間優化的例子)。程序完全優化后,便可生成機器碼,生成本機可以執行代碼,同時包含了后續優化階段需要的程序 LLVM 字節碼的拷貝。

LLVM 運行時優化器,簡單的監視程序執行,收集配置信息。當運行時優化器確定,可以通過轉變提升應用性能時,可能通過兩種方式實現程序的優化:

  • 直接修改已經被優化的機器碼
  • 根據附加的 LLVM 字節碼生成新的代碼

無論采用那種方式,LLVM 字節碼都提供重要的高級別控制流、數據流和類型信息,以便進行積極的運行時優化。

Program Work Flow

如果要編譯opencl/cuda代碼,並已經有了一些opencl/cuda kernel,以及在主機端運行的代碼。主機端代碼調用kernel。如下圖所示:

 

圖1

 

在主機端,執行clang編譯主機代碼,這和正常編譯類似,但是要鏈接opencl/cuda runtime。對kernel代碼, 也會執行clang編譯kernel 代碼,生成LLVM IR。為了支持kernel built-in function,這里使用built-in lib,比如對opencl,一般使用的built-in lib是libclc。

主機代碼調用runtime api,比如opencl的clCreateProgramWithBinary,生成kernel。主機端代碼也可以調用其它runtime call來為相應設備創建comandQueue,或者得到設備或平台信息。

Pipeline

LLVM backend的主要功能是code gen,也就是代碼生成,其中包括若干個code gen分析換轉化pass將LLVM IR轉換成特定目標架構的機器代碼。當然希望這個機器 代碼是最優化的機器代碼。LLVM backend具有流水線結構,如下圖所示。指令經過各個階段,從LLVM IR到SelectionDAG,再到MachineDAG,再到MachineInstr,最后到MCInst。有些資料中不將MachineDAG不做為單獨的一種指令格式,因為其基本格式仍然是SelectionDAG,只不過其中的指令是目標相關指令。這其中經過的各個階段實際是不同的pass,包括Instruction selection(指令選擇)、Register allocation(寄存器分配),instruction scheduling已經code emission。不同的target的backend根據需要對不同pass做customization。

 

圖2

 

下面來具體看看每個pass和步驟的功能。

  1. 首先,SelectionDAGBuilder遍歷LLVM IR中的每一個function以及function中的每一個basic block,將其中的指令轉成SDNode,整個function或basic block轉成SelectionDAG。這時DAG中每個node的內容仍是LLVM IR 指令。
  2. SelectionDAG經過legalization和其它optimizations,DAG節點被映射到目標指令。這個映射過程是指令選擇。這時的DAG中的LLVM IR節點轉換成了目標架構節點,也就是將LLVM IR 指令轉換成了機器 指令.所以這時候的DAG又稱為machineDAG。
  3. 在machineDAG已經是機器 指令,可以用來執行basic block中的運算。所以可以在machineDAG上做instruction scheduling確定basic block中指令的執行順序。指令調度分為寄存器分配前的指令調度,和寄存器分配后的指令調度。寄存器分配前的指令調度器實際有2個,作用於SelectionDAG,發射線性序列指令。主要考慮指令級的平行性。經過這個scheduling后的指令轉換成了MachineInstr三地址表示。指令調度器有三種類型:list scheduling algo, fast algo, vliew.
  4. 寄存器分配為virtual Register分配physical Register並優化Register分配過成使溢出最小化。

virtual Register到physical Register的映射有2中方式:直接映射和間接映射。直接映射利用TargetRegisterInfo和MachineOperand類獲取load/store指令插入位置,以及從內容去除和存入的值。間接映射利用VirtRegMap類處理load/store指令。寄存器分配算法有4種:Basic Register Allocator、Fast Register Allocator、PBQP Register Allocato、Greedy Register Allocator。

  1. 寄存器分配后的指令調度器作用於機器指令,也就是MachineInstr。這時能得到physical寄存器信息,可以結合physical Register的安全性和執行效率,對指令順序做調整。
  2. Code emission階段將機器 指令轉成MCInstr,並發射匯編或二進制代碼。

Initial Selection Construction

SelectionDAG類用一個DAG表示一個basic block。SelectionDAG的創建是個基本的窺孔算法。LLVM IR經過SelectionDAGBuilder的處理后轉換成SelectionDAG。下圖是c代碼實現除法,只有一個function,一個basic block。

 

圖3

 

IR經過多個優化pass做進一步優化后由SelectionDAGBuilder類產生Selection DAG節點。當SelectionDAGBuilder遇到IR指令,調用相應的visit()方法,比如,如果是sdiv操作,就調用visitSDiv()方法將兩個操作數保存到SDValue,從DAG中得到一個SDNode節點並以ISD::SDIV作為其操作符。在該IR中,操作數0為%add,操作數1為%c。其它計算也做類似處理。處理完所有IR指令后,IR被轉為如圖所示的SelectionDAG。每一個DAG表示一個基本塊中的計算,不同的基本塊與不同的DAG關聯。節點表示計算,邊有不同含義。將IR轉化為DAG很重要,因為這可以讓代碼生成器使用基於樹的模式匹配指令選擇算法。此時的SelectionDAG還與目標設備無關,但對於具體目標設備來說,有些指令可能不合法,因為不同目標設備支持的指令集不同,指令集中的指令與IR指令可能沒有對應關系。例如,x86不支持sdiv而支持sdivrem。

下面詳細介紹DAG圖中不同符號的含義。DAG中的每個節點SDNode會維護一個記錄,其中記錄了本節點對其它節點的各種依賴關系,這些依賴關系可能是數據依賴(本節點使用了被其它節點定義的值),也可能是控制流依賴(本節點的指令必須在其它節點的指令執行后才能執行,或稱為chain)。這種依賴關系通過SDValue對象表示,對象中封裝了指向關聯節點的指針和被影響結果的序列號。也可以說,DAG中的操作順序通過DAG邊的use-def關系確定。如果圖中的sdiv節點有一個輸出的邊連到add節點,這意味着add節點定義define了一個值,這個值會被sdiv節點使用。因此,add操作必須在sdiv節點之前執行。

  • 黑色箭頭表示數據流依賴。數據流依賴表示當前節點依賴前一節點的結果。
  • 虛線藍色箭頭表示非數據流鏈依賴。鏈依賴防止副作用節點,確定兩個不相關指令的順序。比如,load和store指令如果訪問相同的內存位置,就必須和他們在原程序中的順序保持一致。從圖中的藍色箭頭可知,copytoreg操作必須在ret_flag之前發生,因為他們之間是鏈依賴。
  • 紅色箭頭表示glue依賴。Glue是用來防止兩個指令在scheduling時分開,即他們中間不能插入其它指令。

每個節點的類型,可以是實際的數據類型,如i32,i64等,也可以是chain類型,表示chain values,或者是glue類型,表示glue。

SelectionDAG對象有一個特殊的EntryToken來標記basic block的入口。EntryToken的類型是ch,允許被鏈接的節點以這個第一個token作為起始。

在這張圖中,目標無關和目標相關的節點共存。Copyfromreg、copytoreg、add等是目標無關節點。Register %EAX、TargetConstant是和ret_flag是目標相關節點。

  • CopyFromReg:copy當前basic block外的register,用在當前環境,這里用於copy函數參數。
  • CopyToReg:copy一個值給特定寄存器而不提供任何實際值給其它節點消費。然而,這個節點產生一個chain value被其它節點鏈接,這些其它節點不產生實際值。比如為了使用寫到EAX的值,ret_flag節點使用EAX寄存器提供的i32結果,並消費CopyToReg節點產生的chain,這樣保證EAX隨着CopyToReg更新,因為chain會使得CopyToReg在ret_flag之前被調度。

Legalization

從SelectionDAGBuilder輸出的SelectionDAG還不能做指令選擇,在SelectionDAG機制從DAG節點產生機器指令之前,DAG節點還要經過幾個轉化階段,其中合法化是最重要的階段,如下圖所示。做合法化的原因是SelectionDAGBuilder構造的SDNode中的指令操作數類型和操作不一定能被目標平台支持。

 

圖4

 

SDNode的合法化涉及類型和操作的合法化。首先介紹操作的合法化。

目標平台一般不可能為所有支持的數據提供IR中所具有的全部指令,x86上沒有條件賦值(conditional moves)指令,PowerPC也不支持從一個16-bit的內存上以符號擴展的方式讀取整數。因此,合法化 階段要將這些不支持的指令按三種方式轉換成平台支持的操作:擴展(Expansion) ,用一組操作來模擬一條操作; 提升(promotion)將數據轉換成更大的類型來支持操作; 定制(Custom),通過目標平台相關的Hook實現合法化。

下圖以x86除法指令為例說明操作的合法化。LLVM IR的sdiv只計算商,而x86除法指令計算得到商和余數,分別保存在兩個寄存器中。因為指令選擇會區分SDIVREM和sdiv,因此當目標平台不支持SDIV時需要在合法化階段將sdiv擴展到sdivrem指令。

 

圖5

 

目標平台相關的信息通過TargetLowering接口傳遞給SelectionDAG。目標設備會實現這個接口以描述如何將LLVM IR指令用合法的SelectionDAG操作實現。在x86的TargetLowering構造函數中會通過”expanded”這個label來標識。當SelectionDAGLegalize::LegalizeOp看到SDIV節點的Expand標志會用SDIVREM替換。類似的,目標平台相關的合並方法會識別一組節點組合的模式,並決定是否合並某些節點組合以提高指令選擇質量。

類型合法化pass保證后續的指令選擇只需要處理合法數據類型。合法數據類型是目標平台原生支持的數據類型,例如,目標平台的td文件中會定義每一種數據類型關聯的寄存器類。例如,

def FPRegs : RegisterClass<"SP", [f32], 32, (sequence "F%u", 0,31)>;

這里定義了一組32個從F0-F31 單精度浮點類型的寄存器。

def DFPRegs : RegisterClass<"SP", [f64], 64, (add D0, D1, D2, D3, D4, D5, D6, D7, D8, D9, D10, D11, D12, D13, D14, D15)>;

定義了一組16個從D0-D5 雙精度浮點類型的寄存器。

如果平台的td文件的寄存器類定義沒有相應的數據類型,那對平台來說就是非法數據類型。非法的類型必須被刪除或做相應處理。根據非法數據類型不同,處理方式分為兩種情況。第一種是標量。標量可以被promoted(將較小的類型轉成較大的類型,比如平台只支持i32,那么i1/i8/i16都要提升到i32),expanded(將較大的類型拆分成多個小的類型,如果目標只支持i32,加法的i64操作數就是非法的。這種情況下,類型合法化通過integer expansion將一個i64操作數分解成2個i32操作數,並產生相應的節點);第二種是矢量。LLVM IR中有目標平台無法支持的矢量,LLVM也會有兩種轉換方案, 加寬(widening),即將大vector拆分成多個可以被平台支持的小vector,不足一個vector的部分補齊成一個vector;以及標量化(scalarizing),即在不支持SIMD指令的平台上,將矢量拆成多個標量進行運算。

目標平台也可以實現自己的類型合法化方法。類型合法化方法會運行兩次,一次是在第一次DAG combine之后,另一次是在矢量合法化之后。無論怎樣,最終都要保證轉換后的指令與原始的IR在行為上完全一致。

在做合法化時,還有其它可能的情況要考慮。比如,某種目標平台寄存器類支持某種向量類型,但某個特定的操作不支持這個向量類型。舉個例子,x86支持v4i32類型,但沒有x86指令支持v4i32類型的ISD::OR操作,只支持v2i64。這樣,向量合法化pass就要promote使用v2i64類型。

DAG Combine pass是將一組節點用更簡單結構的節點代替。比如一組節點表示(add(Register X), (constant 0))將寄存器X中的值和常數0相加,這種情況可以簡化成(Register X),和常數0相加無效,被優化掉了。

setTargetDAGCombine() 方法表示哪些節點可以被組合,例如:

setTargetDAGCombine(ISD::ADD);
setTargetDAGCombine(ISD::AND);
setTargetDAGCombine(ISD::FADD);

Combine pass在legalization后執行,可以最小化SelectionDAG的冗余節點。

Instruction Lowering

SelectionDAG中已經有和目標相關的節點。為什么會這樣?為了理解這個問題,首先看圖1。這里面包括了Instruction selection之前的所有步驟,以LLVM IR起始。首先,SelectionDAGBuilder遍歷LLVM IR中的每一個basic block,生成SelectionDAG。在這個過程中,某些指令,如call和ret已經需要目標相關信息。比如如何傳遞調用參數,如何從函數返回。

為了解決這個問題,TargetLowering類中的算法會在這里被首次使用。不同目標的backend要實現TargetLowering抽象接口。只有一小部分節點會在這里轉成目標相關節點,大部分節點都在instruction selection中在pattern match后被替換成機器指令。

Instruction selection

指令選擇是backend中的一個重要階段。從耗時方面來說,指令選擇占用了backend總耗時的一半。指令選擇通過節點模式匹配完成DAG到DAG的轉換,將SelectionDAG節點轉換成表示目標指令的節點。這一階段的輸入是經過合法化的SelectionDAG,如下圖所示。

 

圖6

 

LLVM的指令選擇需要tablegen輔助來產生一種基於表的指令選擇機制。目標平台的backend可以在SelectionDAGISel::Select方法中實現定制代碼手工處理某些指令。其它指令通過SelectCode由默認的指令選擇過程處理。例如在x86 backend中,對於經過合法化的SDIVREM操作就是手動方法做指令選擇的。Select方法的輸入SDNode節點如果是SDIVREM,會選擇對應的x86指令opcode,也就是IDIV32r,並生成一個MachineSD節點。MachineSD節點是SDNode的子集,其中的內容是平台機器指令,但仍然以DAG node的形式表示。其中CopyToReg,CopyFromReg和Register節點在寄存器分配階段之前保持不變。有三種類型的指令表達會在同一個DAG中共存:一般LLVM ISD節點,如ISD::ADD,平台相關ISD節點,如X86ISD::RET_FLAG,和平台指令,如X86::ADD32ri8.

在Select方法最后會調用Selectcode方法,如下圖所示。這個方法是tablegen為目標平台生成的。主要的作用就是將ISD和平台ISD映射到機器指令節點。這種映射通過Matcher table實現。

 

圖7

 

Instruction scheduling

指令選擇完成后的MachineDAG內容雖然是機器指令,但仍然是以DAG形式存在,CPU/GPU不能執行DAG,只能執行指令的線性序列。寄存器分配前的指令調度的目的就是通過給DAG節點指定執行順序將DAG線性化。最簡單的辦法就是將DAG按拓撲結構排序,但LLVM backend用更智能的方法調度指令使其運行效率更高。

調度器會調用InstrEmitter::EmitMachineNode發射一系列指令到MachineBasicBlock,指令的形式是MachineInstr(MI),DAG表示形式不再使用,可以銷毀。指令調度會根據性能需要做優化,特別是考慮寄存器footprint。

 

圖8

 

Register Allocation

經過指令選擇階段產生的代碼是SSA形式的,即靜態單賦值。這些代碼假定可以使用無限多的虛擬寄存器。這當然是不可能的。因此接下來要執行寄存器分配,將無限的虛擬寄存器替換成有限的物理寄存器。如果物理寄存器數量不夠用,虛擬寄存器就會被assing到內存,也就是spill slot。但也有例外,比如在x86的除法指令中,輸入要保存在EDX和EAX寄存器中。指令選擇階段就已經知道這個限制,因此在那個時候,也就是select方法中就會為除法指令分配物理寄存器而不必等到寄存器分配階段。

寄存器分配過程依賴幾個pass的分析結果,包括Register coalescer和virtual register rewrite,如下圖所示。Coalescer的目的主要是消除冗余的copy指令,在RegisterCoalescer類中實現,這是一個machine function pass。Machine function pass是按function為單位作用在機器指令而不是IR指令上。在coalescing時,joinAllIntervals方法遍歷copy操作列表,joinCopy方法從copy機器指令中生成Coalescerpair,並將copy合並。

 

圖9

 

Interval是程序的一對起點和終點。從起點開始,某個值被產生並在被某個臨時位置持有,一直到這個值被使用和銷毀。下面分析在bc程序上運行coalescer情況,如下圖所示。

 

圖10

 

其中的序號0B,16B…是每條MI的序號,也稱為slot index,每個序號對應一個live range slot。字母B對應Block,用於live range的進入/離開一個basic block的邊界。在這個例子中,index都帶B,因為這是默認的slot。不同的slot,字母r,會出現在interval中,表示寄存器,用於標號一個普通的寄存器use/def slot。

從示例中的指令可以看出,%vreg0,%vreg1,%vreg2,%vreg3是需要分配物理寄存器的虛擬寄存器。因此在這段代碼中會用掉4個物理寄存器,另外還有兩個&I0和&I1。這兩個是已經使用的物理寄存器,因為有遵守ABI調用規則。需要這些物理寄存器傳遞函數參數。

在coalescing之前會運行活動變量(live variable)分析pass,因此實例代碼中會有活動變量信息,顯式每個寄存器在哪個時刻被定義和銷毀。***
INTERVALS **之后的信息就是活動變量分析結果,** MACHINEINSTRS **之后的信息是機器指令。活動變量信息對分析寄存器的相互影響很有用,如果在同一時間有幾個虛擬寄存器存活,就需要分配不同的物理寄存器。

前面提到,coalescing只是尋找寄存器copy。在寄存器到寄存器的copy中,coalescer會嘗試將源寄存器的interval和目的寄存器的interval連起來,使源和目的寄存器公用一個物理寄存器,這樣就可以減少一個copy指令,16B-32B就是這種情況。Coalescer需要知道每個虛擬寄存器存活的interval以便知道應該將哪些interval合並。例如,從示例中可以看到,虛擬寄存器%vreg0的interval是[32r:48r:0)。這表示%vreg0在32定義,在64被銷毀。48后的0是一個代碼,表示第一次定義這個interval的地方。0的含義在0@32r中表示,說明0就在32r,其實已經知道這個含義了。但如果interval在后面被分裂了,這個定義可以用來跟蹤interval的原始定義。

從示例中的interval可以發現,%I0的interval是[0B, 32r :0],%vreg0的interval是[32r , 48r : 0]。在32處有一個copy指令將%I0 copy到%vreg0。這是就可以做coalesce,將[0B, 32r : 0] [32r , 48r :0]這兩個interval合並,並為%I0和%vreg0分配同一個物理寄存器,過程如下圖所示。但遺憾的是,物理寄存器(例如%I0)的interval必須被保留,也就是物理寄存器不能被分配到其它的live range存活范圍。所以coalescer會放棄這個機會,因為擔心將%I0分配給整個range可能會對整體運行性能有影響,所以讓后續的寄存器分配階段來決定。

 

圖11

 

寄存器分配pass為每個虛擬寄存器分配物理寄存器后,VirtRegMap會保存寄存器分配結果,實際是一張從虛擬到物理寄存器的映射表,如下圖所示。接下來,虛擬寄存器rewrite pass會根據映射表將指令中的虛擬寄存器替換成物理寄存器,並刪除相同寄存器之間的copy操作。所以,coalesce不能刪除的copy指令,寄存器分配可以通過給兩段live range分配相同的物理寄存器,將冗余copy操作刪除。

 

圖12

調用約定

調用約定定義描述了怎樣向函數傳值,以及如何從函數返回值。參數傳遞方式和目標平台高度相關。td格式文件中定義的調用約定由一系列條件和預定義action組成。為了支持平台特定的調用約定,在<target>CallingConv.td定義了目標平台返回值調用約定和基本C調用約定,文件中使用CCIfType和CCAssignToReg等接口指定:

  • 函數參數分配順序
  • 函數參數和返回值是放在寄存器中還是放在stack
  • 使用哪個寄存器
  • caller或callee是否對棧回溯

從下圖的例子可以看出CCIfType和CCAssignToReg接口的用法。如果CCIfType預測為真,也就是說,如果當前參數類型是f32或i32,就會執行CCAssignToReg這個動作action,也就是將參數值賦值給I0-I5的第一個可用寄存器。

 

圖13

其中,RetCC_Sparc32指明了對某種標量返回類型應該用哪個寄存器。例如,單精度浮點返回到F0,雙精度浮點返回到D0,32位整型返回到I0/I1。CC_Sparc32還用到了CCAssignToStack,這個action會以指定的大小和對齊方式將值賦給stack slot。在這個例子中,第一個參數4表示slot的大小為4byte,第二個參數4表示stack是4字節對齊。如果參數為0,則沿用ABI中的定義。

函數參數會首先嘗試放入I0-I5中的可用寄存器。如果I0-I5全都被之前的函數調用占用,就會采用stack model的調用約定。將函數參數值放入一個4字節大小、4字節對齊的slot。

還有一種比較常用的接口是CCDelegateTo,該接口用於查找特定的子調用約定。例子中,當前函數參數值賦值給ST0/1后,就會執行RetCC_X86Common。CCDelegateTo經過tblgen處理后會變成函數調用。CCIfCC接口用於將參數1中給定的名稱與當前調用約定匹配。如果參數1的名稱與當前調用約定相同,參數2指定的action會被執行。例子中,如果正在使用Fast CallingConv(”CallingConv::Fast”),RetCC_X86_32_Fast這個action就會被執行。

def RetCC_X86_32_C : CallingConv<[
  CCIfType<[f32], CCAssignToReg<[ST0, ST1]>>,
  CCIfType<[f64], CCAssignToReg<[ST0, ST1]>>,
  CCDelegateTo<RetCC_X86Common>
]>;

def RetCC_X86_32 : CallingConv<[
  CCIfCC<"CallingConv::Fast", CCDelegateTo<RetCC_X86_32_Fast>>,
  CCIfCC<"CallingConv::X86_SSECall", CCDelegateTo<RetCC_X86_32_SSE>>,
  CCDelegateTo<RetCC_X86_32_C>
]>;

Code emission

在介紹code emission之前首先介紹MC framework。MC framework的主要作用是對function和指令做底層處理。和其它后端模塊相比,MC framework設計的目的是用來輔助產生基於LLVM的匯編器和反匯編器。之前的NVPTX backend沒有集成的匯編器,編譯過程只能進行到發射PTX為止,產生PTX作為輸出,然后依賴外部工具,比如PTXAS完成其余的編譯步驟,如Register allocation和code emission。在code emission的早期階段會用機器碼指令(MCInstr)取代機器指令(MachineInstr)。和MI相比,MCInst攜帶的程序信息較少。

Code emission階段發生在所有post-register allocation之后。Code emission在asmprinter這個pass中開始。下圖是從MI指令到MCInstr,再到匯編或二進制指令要經過的步驟:

 

 

 圖14

 

  1. AsmPrinter是一個machine function pass.

AsmPrinter首先發射function header,然后遍歷所有基本塊,一次分發一條MI指令到EmitInstruction()方法做后續處理。自定義backend都會提供一個AsmPrinter子類重載這個方法。

  1. 目標設備的EmitInstruction()收到MI指令輸入后通過MCInstLowering interface將其轉換為MCInst instance。自定義backend會提供MCInstLowering接口子類,並由其中的定制代碼產生MCInst實例。
  2. 此時有兩個選項:發射匯編或二進制指令。MCStreamer類處理MCInst指令流,通過兩種類MCAsmStreamer和MCObjectStreamer將其發射到選定的輸出。MCAsmStreamer子類將MCInst轉換為匯編指令,MCObjectStreamer子類將MCInst轉換為二進制指令。
  3. 如果生成匯編代碼,MCAsmStreamer::EmitInstruction()被調用,並使用自定義backend提供的MCInstPrinter subclass將匯編指令打印到文件中。
  4. 如果生成二進制指令,MCObjectStreamer::EmitInstruction()被調用LLVM object code assembler,並使用自定義backend提供的MCCod

 

參考鏈接:

https://juejin.cn/post/6875498613750333453

https://zhuanlan.zhihu.com/p/52724656

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM