Backend-ul de LLVM primește cod scris în limbajul intermediar și generează asamblare. Binarul este llc.
Pentru a face trecerea de la codul IR la asamblare, este nevoie de 4 transformări:
LLVM IR → SelectionDAG → MachineDAG → MachineInstr → MCInst
Reprezintă un graf în care operațiile sunt reprezentate ca noduri. Majoritatea nodurilor sunt independente de arhitectură, dar pot exista și noduri custom. Fiecare nod are un opcode, care este definit (pentru nodurile independente de arhitectură) în enum-ul ISD::NodeType Dacă este nevoie de noduri specifice pentru o arhitectură, ele se definesc începând cu ISD::BUILTIN_OP_END. Se poate observa ca există mai multe opcode-uri decât instrucțiuni llvm.
Arcele din graf reprezintă dependințe. Ele pot fi:
Valorile din acest graf au tipuri (e.g. i32).
Este un graf foarte asemănător cu SelectionDAG, în care nodurile sunt înlocuite cu instrucțiuni.
MachineDAG-ul este transformat într-o listă de instrucțiuni dependente de arhitectură, cu câteva excepții (e.g. instrucțiunea COPY).
Este o reprezentare simplificată, care conține un opcode și niște operanzi.
Pentru a înregistra un backend, trebuie modificate urmatoarele fișiere:
CMakeLists.txt autoconf/config.sub autoconf/configure.ac configure include/llvm/ADT/Triple.h include/llvm/Object/ELFObjectFile.h include/llvm/Support/ELF.h lib/Support/Triple.cpp lib/Target/LLVMBuild.txt
Hint: Căutați în aceste fișiere apariții corespunzătoare unui target existent (ex Hexagon).
Implementarea se face în directorul lib/Target/X, care trebuie să conțină, printre altele și următoarele fișiere:
Nume | Semnificație |
---|---|
XTargetMachine | Implementarea unei clase care extinde LLVMTargetMachine. Conține XPassConfig care adaugă pașii de transformare. Cel mai important pas este createXISelDag |
XSubtarget | Extinde o clasă generată de tablegen, care definiește un subtarget. |
XISelLowering | Definește modul in care se face lowering la un SelectionDAG legal. |
XISelDAGToDAG | Este responsabil cu trecerea de la SelectionDAG la MachineDAG |
XRegisterInfo | Conține informații despre regiștrii (regștrii rezervați, speciali, salvați de calling convention). Metoda care trebuie implementată este eliminateFrameIndex, care inlocuiește accese abstracte la stivă (e.g. FI -3) cu o adresă concretă (e.g. -20 (SP)) |
XInstrInfo | Informații și transformări pe instrucțiuni. Cele mai importante metode care trebuiesc implementate sunt copyPhysReg, storeRegToStackSlot, loadRegFromStackSlot, expandPostRAPseudo |
XFrameLowering | Responsabil cu calcularea stivei folosite și modificarea ei la intrarea (emitPrologue) și ieșirile (emitEpilogue) din funcție. |
XAsmPrinter | Emiterea codului și datelor. Deși conține Asm în nume, dumparea se face la un stream abstract care poate fi text (pentru asamblare) sau obiect (dacă se generează direct elf). |
Informațiile dependente de arhitectură sunt definite în fișiere speciale cu extensia ``.td`` într-o manieră declarativă.
Aceste fișiere sunt procesate de utilitarul table-gen, care generează cod C++ pe baza acestor fișiere.
Cele 2 concepte importante din limbaj sunt clasele și definițiile.
Clasele sunt folosite pentru a grupa caracteristici comune ale resurselor. O clasă poate să extindă una sau mai multe clase. Cu excepția unor clase de baza (Instruction, Register, CallingConv, CalleeSavedRegs, ș.a.), care definesc semantica unor resurse, clasele nu apar in fișierele C++ generate.
Pentru a defini un registru R0, extindem clasa Register:
// Clasa CplReg extinde clasa Register. // Parametrii sunt transmiși printr-o manieră care seamana cu template-urile din C++. class CplReg<bits<5> num, string n, list<string> alt = [], list<Register> alias = []> : Register<n> { let Namespace = "Cpl"; // Namespace este un câmp din Register în care // vor fi grupate registrele în codul generat. field bits<5> Num; // Field al clasei curente în care salvăm numărul registrului let Aliases = alias; // Listă de alias-uri pentru registru let HWEncoding{4-0} = num; // Codificarea registrului } // Ri - 32-bit integer registers. // Această clasă nu se reflectă în fișierul generat. class Ri<bits<5> num, string n, list<string> alt = []> : CplReg<num, n, alt> { let Num = num; } // Definim R0, R1 și R2 prin intermediul claselor Ri și DwarfRegNum def R0 : Ri<0, "r0">, DwarfRegNum<[0]>; def R1 : Ri<1, "r1">, DwarfRegNum<[1]>; def R2 : Ri<2, "r2">, DwarfRegNum<[2]>; //...
Pentru a defini o instrucțiune, extindem clasa Instruction:
class InstCpl<dag outs, dag ins, string asmstr> : Instruction { let Namespace = "Cpl"; dag OutOperandList = outs; // lista operanzilor destinație dag InOperandList = ins; // lista operanzilor sursă let AsmString = asmstr; // string-ul corespunzător sintaxei let Size = 4; // dimensiunea în octeți (pentru generare obiect) bits<32> Inst = 0; // codificarea binară a instrucțiunii bits<32> SoftFail; } // Instrucțiunea va fi identificată prin enum-ul Cpl::ADD în codul generat. def ADD : InstCpl< (outs IntRegs:$rc), (ins IntRegs:$ra, IntRegs:$rb), "add $rc, $ra, $rb" >;
Generarea de cod se face prin intermediul clasei Pat care mapează noduri din SelectionDAG în MachineDAG:
// Nu avem nevoie de nume pentru pattern, așa că îl definim anonim // add -> ADD def : Pat<(add IntRegs:$ra, IntRegs:$rb), (ADD IntRegs:$ra, IntRegs:$rb)>; // ineg -> // t = EOR ra, ra @ t is zero // res = SUB t, ra @ res is zero - ra def : Pat<(inot IntRegs:$ra), (SUB (EOR IntRegs:$ra, IntRegs:$ra), IntRegs:$ra)>;
Clasa Pat extinde clasa Pattern și ne permite să definim pattern-uri într-un mod mai simplu. In versiunea de LLVM 3.6, aceasta clasa este definita ca:
class Pat<dag pattern, dag result> : Pattern<pattern, [result]>;
Calling convention-ul se definește tot prin intermediul fișierelor .td. Calling convention-ul este un contract prin intermediul căruia se specifică modalitatea de transmitere a parametrilor către funcții și întoarcerea rezultatelor de către acestea (folosind registre, stiva sau ambele).
Se pot defini mai multe calling convention-uri, folosing clasa CallingConvention. Această clasă primește ca parametru o listă de acținui (CCAction) care este folosită pentru a determina modul de transmitere a parametrilor.
Pentru lista tuturor acțiunilor, inspectați fișierul include/llvm/Target/TargetCallingConv.td. Mai jos sunt prezentate câteva din cele mai comune acțiuni, din acest fișier.
/// CCIfType - If the current argument is one of the specified types, apply /// Action A. class CCIfType<list<ValueType> vts, CCAction A> : CCPredicateAction<A> { list<ValueType> VTs = vts; } /// CCPromoteToType - If applied, this promotes the specified current value to /// the specified type. class CCPromoteToType<ValueType destTy> : CCAction { ValueType DestTy = destTy; } /// CCAssignToReg - This action matches if there is a register in the specified /// list that is still available. If so, it assigns the value to the first /// available register and succeeds. class CCAssignToReg<list<Register> regList> : CCAction { list<Register> RegList = regList; } /// CCAssignToStack - This action always matches: it assigns the value to a /// stack slot of the specified size and alignment on the stack. If size is /// zero then the ABI size is used; if align is zero then the ABI alignment /// is used - these may depend on the target or subtarget. class CCAssignToStack<int size, int align> : CCAction { int Size = size; int Align = align; }
Este important de menționat că determinarea modului de transmitere a unui parametru se face parcurgând lista până la prima acțiune de assign care se execută.
Pentru a defini un calling convention care primește toți parametrii pe stivăputem folosi:
def CC_Stack : CallingConv<[ // Promote values to i32 CCIfType<[i1, i8, i16], CCPromoteToType<i32>>, // Use the stack (always works) CCAsignToStack<0, 0> ]>;
În cadrul acestui laborator se va folosi o arhitectură care definește un subset al instrucțiunilor de ARMv3.
Arhitectura dispune de un set de 16 registre de 32 biți (R0-R15) și un registru de stare (CPSR). Registrele R13-R15 sunt rezervate pentru stack pointer SP (R13), link register LR (R14) și program counter PC (R15).
Registrul de stare este scris de către instrucțiunile de comparație și citit de către instrucțiunile de salt condiționat.
Setul de instrucțiuni cuprinde:
Tip | Sintaxă | Efect |
---|---|---|
ALU | add rd, rs1, rs2 | rd = rs1 + rs2 |
add rd, rs1, i8 | rd = rs1 + i8 | |
sub rd, rs1, rs2 | rd = rs1 - rs2 | |
sub rd, rs1, i8 | rd = rs1 - i8 | |
mul rd, rs1, rs2 | rd = rs1 * rs2 | |
and rd, rs1, rs2 | rd = rs1 AND rs2 | |
and rd, rs1, i8 | rd = rs1 AND i8 | |
orr rd, rs1, rs2 | rd = rs1 OR rs2 | |
orr rd, rs1, i8 | rd = rs1 OR i8 | |
eor rd, rs1, rs2 | rd = rs1 XOR rs2 | |
eor rd, rs1, i8 | rd = rs1 XOR i8 | |
mul rd, rs1, rs2 | rd = rs1 * rs2 | |
Transfer | mov rd, rs | rd = rs |
mvn rd, rs | rd = NOT rs | |
Salt | b i24 | pc = address |
Salt condiționat | beq i24 | if eq then pc = address |
bne i24 | if ne then pc = address | |
bge i24 | if ge then pc = address | |
bgt i24 | if gt then pc = address | |
ble i24 | if le then pc = address | |
blt i24 | if lt then pc = address | |
Apel funcție | bl i24 | lr = next_address; pc = address |
Memorie | ldr rd, [ra, i12] | rd = [ra + i12] |
ldrb rd, [ra, i12] | rd = zero_extend [ra + i12] | |
str rs, [ra, i12] | [ra + i12] = rs | |
strb rs, [ra, i12] | [ra + i12] = rs AND 0xFF |
Următoarele pachete sunt necesare:
gcc-arm-linux-gnueabi # cross compiler de arm libc-dev-armel-cross # embedded GNU C library pentru cross-compiling qemu-user # pt qemu-arm android-tools-adb # adb openjdk-7-jre # sau alta varianta de java lib32z1 lib32ncurses5 lib32stdc++6 # lib-uri pe 32 de biti (daca e cazul)
De asemenea, este necesar SDK-ul de Android.
Acesta trebuie dezarhivat, iar calea către directorul obținut în urma dezarhivării trebuie setată în cadrul variabilei de mediu ANDROID_SDK_ROOT.
De asemenea trebuie adăugată în PATH calea către directorul ANDROID_SDK_ROOT/tools.
În cazul în care se folosește un sistem de operare pe 32 biți, este nevoie de forțarea folosirii binarelor pe 32 de biți prin exportarea variabilei de mediu ANDROID_EMULATOR_FORCE_32BIT=true.
=== Qemu ===
<code>
qemu-arm binar # rulati binarul
</code>
===== Exerciții =====
Arhiva laboratorului.
==== Exercițiul 1 ====
Scrieți un program
C care printează
“Hello World!”. Compilați programul și obțineți fișierul .s. Asamblați și link-ați fișierul pentru a obține un fișier ELF. Rulați ELF-ul folosind cele 2 emulatoare (Android și qemu).
==== Exercițiul 2 ====
Completați funcția
sum din fișierul ex2.s pentru a întoarce suma elementelor vectorului dat ca parametru. Adresa vectorului este transmisă prin registrul
R0, iar numărul elementelor este transmis prin registrul
R1. Rezultatul trebuie intors prin registrul
R0. Link-ați ex2.s împreună cu ex2.c și rulați executabilul.<note important>
Funcția nu trebuie sa suprascrie niciun registru rezervat pentru program counter, return address și stack pointer.
</note>
==== Exercițiul 3 ====
Copiați directorul cpl din arhiva în directorul lib/Target al dristribuției de LLVM de mașinile din laborator.
Înregistrați target-ul Cpl în cadrul framework-ului de LLVM.
După ce ați modificat toate fișierele, compilați llc cu suport pentru noul target:
cd ~/packages/llvm-3.8.0/build
cmake -DLLVM_TARGETS_TO_BUILD=Cpl path_to_llvm_src
make -j2 llc
Codul sursa LLVM poate fi descărcat de pe llvm.org
Pentru a testa, compilați fișierul ex3.a.ll folosind opțiunea
-filetype=null pentru llc.
<note warning>
Compilarea llc va dura în jur de 20 minute.
</note>
==== Exercițiul 4 ====
Completați în target-ul de Cpl pentru a genera cod pentru următoarele funcții echivalente în
C.
Funcțiile se află în fișierele
ex4_<no>.ll.
=== f ===
Va trebui să implementați funcția
CplInstPrinter::printOperand.
<code>
void f() {}
</code>
=== ident ===
Va trebui să completați fisierul
CplCallingConv.td
<code>
int ident(int a) {
return a;
}
</code>
=== or ===
Va trebui să adăugați instructiunea
or în fișierul
CplInstrInfo.td și un pattern pentru nodul
or în fișierul
CplPatterns.td
<code>
int or(int a, int b) {
return a | b;
}
</code>
=== second ===
Va trebui să implementați funcția
CplInstrInfo::copyPhysReg''.
int second(int a, int b) { return b; }