În cadrul acestei teme veți implementa un backend în LLVM pentru arhitectura Cpl. Deoarece implementarea unui backend necesită un volum mare de muncă, veți porni tema de la o arhivă de start.
Va trebui să implementați în limbajul C++ porțiuni din cod responsabile cu generarea de generare de cod pentru arhitectura Cpl. Programul vostru va trebui să primească la intrare output-ul temei 3 și să genereze un fișier cu cod în limbaj de asamblare. Output-ul programului realizat de voi poate fi asamblat și link-at cu gcc-arm-linux-gnueabi și rulat cu ajutorul unui emulator (laboratorul 8).
În cadrul acestei teme 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):
Registrul R14 (LR) conține adresa de return și este inițializat la intrarea in funcție. El poate fi folosit de alocator, daca adresa de return este salvată pe stivă sau în alt registru. Registrele R13 (SP) si R15 (PC) sunt rezervate și nu vor fi folosite de alocator.
Registrul de stare este scris de către instrucțiunile de comparație și citit de către instrucțiunile de salt condiționat.
Registrul R15 (program counter) poate fi scris pentru a sări la o anumită adresă. Citind acest registru, obținem adresa instrucțiunii curente + 8 octeți (2 instrucțiuni în față relativ la instrucțiunea curentă). Dacă acest registru nu este scris de o instrucțiune, el este incrementat automat cu 4, pentru a executa instrucțiunea urmatoare.
Parametrii unei funcții sunt transmiși prin intermediul registrelor R0-R3. Restul parametrilor sunt transmiși prin intermediul stivei.
Rezultatul funcției, dacă este cazul, este întors prin registrul R0.
Adresa de return este transmisă prin registrul R14.
Registrele R4-R11 trebuie salvate de funcția apelată.
Vârful stivei este ținut în registrul R13.
Pentru a aloca spațiu pe stivă, acest registru este decrementat.
Similar, pentru a elibera stiva vom incrementa acest registru.
Valorea din R13 reprezintă o locație validă pe stivă (i.e. putem să stocăm folosind instrucțiunea str r14, [r13, #0]
).
Nu există instrucțiuni de tipul push sau pop pentru lucrul cu stiva.
Setul de instrucțiuni cuprinde:
Tip | Sintaxă | Efect | Observații |
---|---|---|---|
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 | rd != rs1 | |
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 | ||
Transfer | mov rd, rs | rd = rs | |
mvn rd, rs | rd = NOT rs | ||
mov rd, i8 | rd = zero_extend i8 | ||
mvn rd, i8 | rd = NOT zero_extend i8 | ||
Transfer condiționat | moveq rd, i8 | if eq then rd = zero_extend i8 | |
movne rd, i8 | if ne then rd = zero_extend i8 | ||
movge rd, i8 | if ge then rd = zero_extend i8 | ||
movgt rd, i8 | if gt then rd = zero_extend i8 | ||
movle rd, i8 | if le then rd = zero_extend i8 | ||
movlt rd, i8 | if lt then rd = zero_extend i8 | ||
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 |
Pentru arhitectura Cpl, program counter-ul este registrul R15.
Deoarece nu există o instrucțiune dedicată de return, intoarcere din funcția curentă se face punând în R15 adresa de return.
La intrarea în funcție, adresa de return este disponibilă în registrul R14.
Valoarea acestui registru este mutată (prin pseudo instrucțiunea COPY) într-un registru virtual.
Ieșirea din funcție se face mutând această valoare din registrul virtual în R15, folosind o instrucțiune specială.
Această instrucțiune trebuie marcată cu isTerminator
pentru a nu fi eliminată de pașii de optimizare.
Pentru o valoare pe 8 biți, aceasta se poate pune într-un registru folosind instrucțiunea MOV
.
Dacă avem o valoare mai mare (sau un simbol), avem nevoie de un alt mecanism de inițializare a registrului.
Recomandăm folosirea unei instrucțiunii de load pentru această inițializare.
Pentru if-uri și bucle putem folosi branch-uri condiționate.
Aceste pattern-uri se pot defini direct.
Pentru o obține rezultatul unei comparații (e.g. return a < b;
) avem nevoie de instrucțiuni de transfer condiționate.
Recomandăm definirea unor pseudo instrucțiuni de comparație care au ca rezultat 0 sau 0xFFFFFFFF, în funcție de operația relațională.
Aceste pseudo instrucțiuni pot fi înlocuite în metoda expandPostRAPseudo
cu o instrucțiune de comparație și 2 transferuri, unul condiționat (sau ambele).
Pentru funcția următoare:
int lt(int a, int b) { return a < b; }
Codul generat ar putea fi următorul:
cmp r0, r1 mvn r0, #0 movge r0, #0 mov r1, #1 and r0, r0, r1 orr r15, r14, r14 @ Return
Adresele de pe stivă sunt reprezentate printr-un operand special, numit Frame Index. Acest operand este un întreg (poate avea și valori negative) care caracterizeaza o intrare pe stiva: offset, dimensiune, aliniament.
În partea de generare de cod (fișiere td), Frame Index-ul este identificat printr-un ComplexPattern numit frame_addr. Pentru instrucțiunile care accesează memoria, acest frame_addr se pune in locul registrului de adresă, iar imediatul este pus pe 0. După pasul de alocare de registre, acești operanzi sunt înlocuiți cu R13 iar offset-ul se pune în locul imediatului.
În prologul și epilogurile funcției se introduc instrucțiuni care modifică stiva cu cantitatea necesară funcției.
Funcționalitatea se va puncta folosind testele din arhivă. Puteți totuși să vă ghidați după următoarele task-uri pentru rezolvarea temei de backend.
Fiecare task este marcat în cod prin intermediul comentariilor cu TODO.
Arhiva de start nu se poate compila.
Definiți instrucțiunile care compun setul de instrucțiuni corespunzător arhitecturii Cpl. Acest lucru se realizează în fișierul CplInstrInfo.td prin instanțierea claselor definite în fișierul CplInstrFormat.td. Instrucțiunile de comparație, salt, apel de funcție și/sau condiționate trebuie marcate corespunzător. Un exemplu în acest sens în constituie instrucțiunea ORR_RET care este marcată ca fiind instrucțiune de întoarcere din funcție prin setarea câmpului isReturn. De asemenea, trebuie să definiți în CplPatterns.td și patternurile care descriu transfomările de la noduri din graful de selecție la instrucțiuni specifice arhitecturii Cpl.
Pentru a putea transmite parametri unor funcții și pentru a primi rezultatul corect este necesar ca toate funcțiile să respecte un calling convention. Descrierea acestui calling convention pentru target-ul Cpl o găsiți în secțiunea de arhitectură.
Trebuie să modificați în fișierul CplCallingConv.td registrele folosite pentru transmiterea parametrilor, rezultatului și registrele salvate de funcția apelată.
Limbajul intermediar LLVM conține o instrucțiune de select. Această instrucțiune primește 3 operanzi, primul de tip i1 și întoarce al 2-lea sau al 3-lea operand în funcție de valoarea primului operand.
Această instrucțiune se poate genera foarte ușor din următorul cod C:
int f(bool c, int a, int b) { return c ? a : b; }
După terminarea pasului de alocare de registre, trebuie alocat spațiu pe stivă. Acest spațiu poate fi necesar pentru spill-uri, pentru parametrii transmiși unor funcții sau pentru variable a căror adresă este folosită explicit. Funcția adjustStackPtr este apelată la intrarea în funcție pentru a aloca spațiu pe stiva și la ieșirile din funcție pentru a elibera acest spațiu. În cadrul acestei metode, trebuie să adăugați cod necesar pentru modificarea stivei (R13).
Tot după alocarea de registre, pot să existe unele transferuri. Acestea pot sa provină din alocare, sau din calling convention. În metoda CplInstrInfo::copyPhysReg, trebuie să adăugațti cod care să copieze valoarea unui registru în alt registru.
Pentru apeluri de funcție indirecte (cu adresa funcției într-un registru) va trebui să generați o secvență de cod care să salveze LR-ul și să modifice PC-ul. În cadrul selecției de instrucțiuni, se generează o pseudo-instrucțiune CALLR, care va trebui înlocuită.
Pentru return-ul unei funcții, se generează o pseudo-instrucțiune RET. Aceasta va trebui înlocuită cu ORR_RET, pentru a transfera LR în PC.
În cadrul acestui task, trebuie sa generați cod pentru secvențe de forma:
int lt(int a, int b) { return a < b; }
Deoarece nu există o instrucțiune care să pună într-un registru valoarea unei comparații, este necesară o secvență de instrucțiuni. Pentru a simplifica generarea de cod mașină, recomandăm să se folosească o pseudo-instrucțiune care sa fie înlocuită cu o secvență validă de instrucțiuni în metoda expandPostRAPseudo. Căutați TODO 6 în codul sursă.
Consultați secțiunea comparații pentru detalii de implementare.
Arhiva care conține tema va avea exact aceleași fișiere și structură ca arhiva de pornire - va conține în plus doar un README.
Nu trebuie să adăugați fișiere noi. Conținutul fișierelor CMakeLists.txt nu trebuie modificate.
Testarea se va face folosind următoarea arhivă
Setați în variabila de mediu CPL_LLC
calea către executabilul de llc, care suportă target-ul cpl.
Testele se rulează folosind comanda:
./run_all.sh
Modul în care este distribuit punctajul pentru această temă este următorul: