This is an old revision of the document!
TO BE UPDATED SOON
Î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.
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ă.
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:
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ă:
; ModuleID = 'test.ll' target datalayout = "e-m:e-p:32:32-f64:32:64-f80:32-n8:16:32-S128" target triple = "i386-pc-linux-gnu" ; Function Attrs: nounwind define i32 @sum(i32 %x, i32 %y) #0 { entry: %add = add nsw i32 %x, %y ret i32 %add } attributes #0 = { nounwind "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" "unsafe-fp-math"="false" "use-soft-float"="false" } !llvm.ident = !{!0} !0 = !{!"clang version 3.6.2 (tags/RELEASE_362/final)"}
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):
std::vector<Type *> FuncTy_0_args; FuncTy_0_args.push_back(IntegerType::get(mod->getContext(), 32)); FuncTy_0_args.push_back(IntegerType::get(mod->getContext(), 32)); FunctionType *FuncTy_0 = FunctionType::get( /*Result=*/IntegerType::get(mod->getContext(), 32), /*Params=*/FuncTy_0_args, /*isVarArg=*/false);
În exemplul de mai sus:
FuncTy_0
este o variabilă ce definește antetul unei funcții cu următoarele proprietăți:FuncTy_0_args
FuncTy_0_args
este un vector ce definește tipurile argumentelor unei funcții:Următorul pas este declararea funcției:
Function *func_sum = mod->getFunction("sum"); if (!func_sum) { func_sum = Function::Create( /*Type=*/FuncTy_0, /*Linkage=*/GlobalValue::ExternalLinkage, /*Name=*/"sum", mod); func_sum->setCallingConv(CallingConv::C); }
În codul de mai sus, se adaugă la modul (dacă nu există deja) funcția sum
cu tipul definit anterior și cu vizibilitate externă (poate fi accesată din alte module). 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 = func_sum->arg_begin(); Value* int32_x = args++; int32_x->setName("x"); Value* 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* label_entry = BasicBlock::Create(mod->getContext(), "entry",func_sum,0);
În cadrul BasicBlock-ului creat anterior se vor adăuga instrucțiunile, așa cum le vedem în reprezentarea fișierului test.ll
:
// Block entry (label_entry) BinaryOperator *int32_add = BinaryOperator::Create( Instruction::Add, int32_x, int32_y, "add", label_entry); ReturnInst::Create(mod->getContext(), int32_add, label_entry);
~/llvm-3.6.2/src/include/llvm
. Pentru acest laborator sunt importante mai ales header-ele din directorul IR
.
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
llvm/include/IR/Instruction.h
, llvm/include/IR/InstrTypes.h
, llvm/include/IR/Instructions.h
) llvm/include/IR/Constant.h
, llvm/include/IR/Constants.h
), Exemple de constante remarcabile sunt
Function
),GlobalVariable
),ConstantInt
, ConstantDataArray
, ConstantStruct
etc), 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);
Pornind de la Makefile-ul din laborator, compilați și rulați exemplul respectiv pentru a obține un fișier .ll
.
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:
#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.
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ă.
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.
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ă.
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
).
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).