Î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-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)"}
Pentru a obține reprezentarea în memorie a IR-ului din exemplu puteți folosi codul din arhiva de sarcini lab5_cpl.zip. După cum se poate observa, pentru a putea folosi API-ul C++ va fi necesară 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:sum_args
sum_args
este un vector ce definește tipurile argumentelor unei funcții: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
:
~/cpl/llvm/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
), Î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 trebuie folosit direct constructorul. Pentru un API uniform e recomandat să folosiți clasa IRBuilder.
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 Value *int32_x_plus_y = Builder.CreateAdd( int32_x, int32_y, "x_plus_y"); Value *int32_x_plus_y_plus_z = Builder.CreateAdd( int32_x_plus_y, int32_z, "x_plus_y_plus_z");
În rezolvarea laboratorului folosiți arhiva de sarcini lab5_cpl.zip.
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 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 de tip int32, 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 un string global care să conțină argumentul de tip format (“sum of %d and %d is %d\n”). (Hint: llvm::IRBuilder::CreateGlobalStringPtr)
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, veti folosi referinta intoarsa de llvm::IRBuilder::CreateGlobalStringPtr, din exercitiul anterior.
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).