This is an old revision of the document!


05. LLVM IR - C++ API

TO BE UPDATED SOON

Exemplele de mai jos folosesc LLVM 3.6 - codul va fi ușor diferit pentru a folosi noile facilități din 3.8

În laboratorul trecut ne-am folosit de reprezentarea textuală a codului intermediar LLVM pentru a înțelege mai bine forma și structura acestuia. Laboratorul curent își propune să vă familiarizeze cu API-ul C++ cu care lucrează bibliotecile LLVM-ului pentru a reprezenta în memorie instrucțiuni.

Reprezentarea în memorie a IR-ului este utilă (printre altele) pentru a putea utiliza engine-ul de optimizări independente de platformă și limbaj, LLVM Passes. Acest framework va fi folosit în laboratoarele următoare și nu face subiectul laboratorului curent. Cunoașterea API-ului de C++ ne va ajuta pentru a putea implementa mai ușor astfel de optimizări, dar și pentru realizarea temei de casă nr. 2, cea de generare de cod intermediar.

Organizarea IR-ului

Pentru mai multe detalii despre tipurile de instrucțiuni și semnificația acestora, revedeți secțiunea Organizarea IR-ului din laboratorul trecut sau puteți consulta documentația oficială.

Generarea de cod LLVM IR

Pentru a înțelege mai ușor API-ul de C++ vom începe prin a explica pașii necesari generării în memorie a IR-ului corespunzător unei funcții simple, sum, ce calculează suma a două numere întregi:

test.c
int sum(int x, int y)
{
	return x + y;
}

Mai întâi, să observăm cum arată fișierul test.ll corespunzător fișierului sursă:

test.ll
; ModuleID = 'test.ll'
target datalayout = "e-m:e-i64:64-f80:128-n8:16:32:64-S128"
target triple = "x86_64-pc-linux-gnu"
 
; Function Attrs: nounwind uwtable
define i32 @sum(i32 %x, i32 %y) #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 %x, i32* %1, align 4
  store i32 %y, i32* %2, align 4
  %3 = load i32, i32* %1, align 4
  %4 = load i32, i32* %2, align 4
  %5 = add nsw i32 %3, %4
  ret i32 %5
}
 
attributes #0 = { nounwind uwtable "disable-tail-calls"="false" "less-precise-fpmad"="false" "no-frame-pointer-elim"="true" "no-frame-pointer-elim-non-leaf" "no-infs-fp-math"="false" "no-nans-fp-math"="false" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+fxsr,+mmx,+sse,+sse2" "unsafe-fp-math"="false" "use-soft-float"="false" }
 
!llvm.ident = !{!0}
 
!0 = !{!"clang version 3.8.1-12ubuntu1 (tags/RELEASE_381/final)"}

Introducere în API-ul de C++

Pentru a obține reprezentarea în memorie a IR-ului din exemplu puteți folosi codul de aici cu acest Makefile. După cum se poate observa, pentru a putea folosi API-ul C++ vor fi necesare includerea mai multor headere. În plus, ar fi bine să folosiți și namespace-ul llvm:

using namespace llvm;

Pentru construcția modulului primul pas este inițializarea unui obiect de acest tip și setarea:

Module* mod = new Module("test.ll", getGlobalContext());
mod->setDataLayout("e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128");
mod->setTargetTriple("i386-pc-linux-gnu");

Înainte de a declara o funcție, trebuie definit tipul acesteia (adică antetul funcției):

//Create IntType
auto IntType = llvm::IntegerType::getInt32Ty(getGlobalContext());
 
//Create sum's function prototype    
std::vector<Type*> sum_args = { IntType, IntType };
FunctionType* sum_func_type = FunctionType::get(
/*Return Type*/ IntType,
/*Params*/ sum_args,
/*isVarArg*/ false);

În exemplul de mai sus:

  • sum_func_type este o variabilă ce definește antetul unei funcții cu următoarele proprietăți:
    • rezultatul funcției este un întreg pe 32 de biți
    • parametrii funcției sunt specificați prin vectorul sum_args
    • nu are număr variabil de argumente
  • sum_args este un vector ce definește tipurile argumentelor unei funcții:
    • primul parametru este un întreg pe 32 de biți
    • al doilea parametru este un întreg pe 32 de biți

Următorul pas este declararea funcției:

//Declare sum
Function* sum_func = llvm::cast<llvm::Function>(Mod->getOrInsertFunction("sum", sum_func_type));;
sum_func->setCallingConv(CallingConv::C);

În codul de mai sus, se adaugă la modul (dacă nu există deja) funcția sum cu tipul definit anterior. Pe obiectul de tip Function returnat se pot apela diverse metode, de exemplu cea pentru setarea calling convention-ului.

Urmează definirea efectivă a funcției. Primul pas este adăugarea argumentelor:

Function::arg_iterator args = sum_func->arg_begin();
Argument* int32_x = &(*args++);
int32_x->setName("x");
Argument* int32_y = &(*args++);
int32_y->setName("y");

Următorul pas este adăugarea primului BasicBlock din cadrul funcției. În mod uzual acesta va purta numele entry, însă poate avea și un alt nume.

BasicBlock* entry = BasicBlock::Create(getGlobalContext(), "entry", sum_func);

În cadrul BasicBlock-ului creat anterior se vor adăuga instrucțiunile, așa cum le vedem în reprezentarea fișierului test.ll:

Documentația pentru cea mai recentă versiune de LLVM se găsește pe net (căutați “llvm instruction” sau orice altă clasă vă interesează). La laborator folosim versiunea 3.8, așa că pot exista schimbări de API față de ce găsiți online, dar puteți măcar să vă orientați in linii mari. Pe mașina virtuală de la laborator găsiți header-ele LLVM-ului în ~/cpl/llvm/src/include/llvm. Pentru acest laborator sunt importante mai ales header-ele din directorul IR.

}

Tipuri importante pentru lucrul cu API-ul de C++

La baza reprezentării în memorie a IR-ului de LLVM stau două ierarhii de clase: una pentru tipuri și una pentru valori.

Ierarhia pentru tipuri are la bază clasa ''Type'' (include/llvm/IR/Type.h) din care derivă

  • IntegerType,
  • FunctionType,
  • PointerType,
  • StructType etc (include/llvm/IR/DerivedTypes.h).

După cum probabil ați observat deja din exemplele de mai sus, instanțe ale acestora se pot obține folosind metodele statice get corespunzătoare fiecărei clase.

Ierarhia pentru valori are la bază clasa ''Value'' (include/llvm/IR/Value.h). Aceasta are o multitudine de subtipuri, incluzând în primul rând

  • toate tipurile de instrucțiuni de IR (llvm/include/IR/Instruction.h, llvm/include/IR/InstrTypes.h, llvm/include/IR/Instructions.h)
  • toate tipurile de constante (llvm/include/IR/Constant.h, llvm/include/IR/Constants.h),
  • argumentele funcțiilor,
  • basic block-urile etc.

{{:cpl:labs:classllvm_1_1ilist_node_with_parent_inherit_graph.png?200|}

Exemple de constante remarcabile sunt

  • funcțiile (Function),
  • variabilele globale (GlobalVariable),
  • literalii de diverse feluri (ConstantInt, ConstantDataArray, ConstantStruct etc),
  • expresiile constante (BinaryConstantExpr, GetElementPtrConstantExpr etc).

În general, expresiile constante sunt folosite pentru valori care vor fi constante în executabilul final, dar nu pot fi calculate încă (de exemplu pentru că se bazează pe adresa unei variabile globale, care este stabilită mult mai târziu în cadrul procesului de compilare).

De regulă subclasele lui Value pun la dispoziție una sau mai multe metode statice Create care se pot folosi pentru a obține obiecte de acel tip. Există însă și excepții, pentru care va trebui să folosiți direct constructorul. În general e recomandat să folosiți ceva care primește ca parametru fie o instrucțiune InsertBefore fie un basic block InsertAtEnd, pentru a vă asigura că instrucțiunea a fost inserată acolo unde doriți. Aveți grijă mai ales la instrucțiunile care derivă din TerminatorInst, care trebuie întotdeauna să se afle la sfârșitul basic block-ului din care fac parte, și la instrucțiunile PHINode, care trebuie întotdeauna să se afle la începutul basic block-ului din care fac parte (dacă basic block-ul conține deja cel puțin o instrucțiune non-phi, puteți insera noduri phi înaintea instrucțiunii întoarse de metoda getFirstNonPHI a basic block-ului).

Ierarhia de valori ușurează lucrul cu orice combinații de instrucțiuni sau constante. De exemplu, o instrucțiune de adunare poate folosi ca operanzi fie un argument al funcției și o constantă, fie o constantă și rezultatul unei instrucțiuni anterioare, fie rezultatele a două instrucțiuni anterioare, și așa mai departe. Pentru a folosi rezultatul unei instrucțiuni ca operand, pur și simplu folosiți instanța acelei instrucțiuni:

// Add 3 numbers
BinaryOperator *int32_x_plus_y = BinaryOperator::Create(
  Instruction::Add, int32_x, int32_y, "add", label_entry);
BinaryOperator *int32_x_plus_y_plus_z = BinaryOperator::Create(
  Instruction::Add, int32_x_plus_y, int32_z, "add", label_entry);

Exerciții de laborator (10p)

Exercițiul 1 - first dump (1p)

Pornind de la Makefile-ul din laborator, compilați și rulați exemplul respectiv pentru a obține un fișier .ll.

Exercițiul 2 - main (1p)

La exercițiul anterior am construit un program care generează instrucțiunile corespunzătoare obținerii funcției sum. Pentru următoarele exerciții vom construi un program care să genereze întreg .ll-ul necesar pentru a apela funcția sum și a-i printa rezultatul:

main.c
#include <stdlib.h>
#include <stdio.h>
 
int sum(int x, int y)
{
  return x + y;
}
 
int main(void)
{
  int a = 10, b = 23;
 
  printf("sum of %d and %d is %d\n", a, b, sum(a, b));
 
  return 0;
}

Puteți să generați fișierul în forma textuală folosind clang, pentru a înțelege exact ce trebuie să generați:

clang -S -emit-llvm main.c -o - | opt -S -mem2reg -o main.ll

Ca prim pas, generați o funcție main care întoarce 0.

Exercițiul 3 - function call (2p)

Adăugați în corpul funcției main un apel către funcția sum, cu argumentele din exemplu. Pentru simplitate, puteți folosi direct constante (ConstantInt), nu e nevoie să alocați spațiu pe stivă.

Exercițiul 4 - printf (2p)

Adăugați declarația funcției printf. Aceasta primește un parametru de tip pointer la un întreg pe 8 biți, și are număr variabil de parametri (isVarArgs). Întrucat este o funcție de bibliotecă, nu trebuie definită de către voi.

Exercițiul 5 - variabile globale (2p)

Pentru apelarea functiei printf, avem nevoie de o variabilă globală care să conțină argumentul de tip format. Pentru aceasta va trebui să generați un tip de date de tip array de i8 cu dimensiunea egală cu numărul de caractere din șir + 1, o constantă cu acest tip și cu valoarea “sum of %d and %d is %d\x0A” (ultimul caracter este reprezentarea în hexa a sfârșitului de linie), și o variabilă globală inițializată cu constanta precedentă.

Exercițiul 6 - apel printf (2p)

Apelul funcției printf va primi 4 argumente: formatul + cele 3 valori care trebuie printate (2 constante și rezultatului apelului functiei sum). Pentru a transmite ca parametru formatul, va trebui să îi calculați adresa folosind o constantă de tip GEP (ConstantExpr::getGetElementPtr).

BONUS

contains

Adăugați o funcție contains care primește ca parametri un șir de caractere și un caracter, și întoarce true dacă șirul conține caracterul dat (și false în caz contrar).

cpl/labs/05.1477727975.txt.gz · Last modified: 2016/10/29 10:59 by bogdan.nitulescu
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0