Table of Contents

05. LLVM IR - C++ API

Î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 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:

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”, “llvm IRBuilder” 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ă

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

Î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");

Exerciții de laborator (10p)

În rezolvarea laboratorului folosiți arhiva de sarcini lab5_cpl.zip.

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 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 de tip int32, 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 - string global (2p)

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)

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, veti folosi referinta intoarsa de llvm::IRBuilder::CreateGlobalStringPtr, din exercitiul anterior.

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).