Pe parcursul acestui semestru am invatat sa programam in limbajul de asamblare specific procesoarelor din familia x86. In acest laborator vom studia notiunile de baza ale programarii in limbajul de asamblare specific procesoarelor ARM.
Procesoarele ARM fac parte din categoria RISC. La nivel hardware, aceasta se traduce prin faptul ca necesita mai putine tranzistoare decat arhitecturile CISC, ceea ce le face superioare din punct de vedere al costului de productie, al consumului de energie si al caldurii disipate, sacrificand din puterea de calcul. Aceste caracteristici sunt dezirabile pentru dispozitive de mici dimensiuni, portabile, care se bazeaza pe baterie cum ar fi telefoanele mobile, laptop-uri, tablete, smartwatch-uri, routere, “device”-uri IoT etc. Practic, in ziua de azi interactionati mai des cu dispozitivele ce folosesc un procesor ARM decat cu cele bazate pe x86.
Spre deosebire de arhitectura x86, arhitectura ARM dispune de mai multe registre, numarul fiind variabil in functie de versiune. Cu toate acestea, manualul de referinta ARM specifica 30 de registre de 32 de biti. Primele 16 registre sunt accesibile in “user-mode”, in timp ce restul registrelor sunt disponibile in “privileged-mode”. In acest laborator vom lucra doar cu registrele disponibile in “user-mode”. Aceste 16 registre pot fi impartite in 2 categorii: de uz general si de uz special.
Desi pana la versiunea a 3-a, arhitecturile ARM erau Little-Endian, din acel moment au devenit bi-endian (adica endianness-ul este specificat de catre utilizator); bitul de endianess din CPSR este folosit pentru a specifica acest lucru.
Avand in vedere ca procesoarele ARM sunt folosite pe sisteme embedded unde, de regula, memoria este o resursa foarte limitata, sunt situatii in care dimensiunea programelor este foarte importanta. Astfel, procesoarele arm ofera un subset de instructiuni ce se reprezinta pe 16 biti doar. In felul acesta, daca dorim sa optimizam din punct de vedere al dimensiunii programului, putem sa folosim acest subset. Trecerea de la interpretarea ARM (instructiuni pe 32 de biti) la interpretarea Thumb (16 biti) se poate face in mod dinamic in cadrul unui program, oferind o flexibilitate foarte mare. Pentru ca procesorul sa stie in ce mod trebuie interpretate instructiunile, se foloseste bitul Thumb din CPSR.
In cadrul acestui laborator vom folosi doar date Little-Endian si setul de instructiuni ARM.
Instructiunile ARM au urmatoarea syntaxa generala: MNEMONIC{S}{condition} {Rd}, Operand1, Operand2, unde
Este important sa remarcam cateva lucruri:
Cele mai uzuale instructiuni:
O diferenta majora intre procesoarele ARM si cele x86 este modul in care programatorul poate accesa memoria. Daca pe sistemele x86 putem scrie direct intr-o zona de memorie, pe sistemele ARM suntem obligati sa lucram doar cu registre pe care le putem folosi fie pentru a aduce informatii din memorie, fie pentru a le stoca.
Astfel, exista 4 moduri de adresare a memoriei:
LDR R0, [R1] @ incarca in R0 ce se afla la adresa data de continutul registrului R1 STR R0, [R2] @ stocheaza la adresa data de continutul registrului R2 valoarea registrului R0
STR R2, [R1, #2] @ stocheaza valoarea din R2 la adresa data de R1+2 STR R2, [R1, R2] @ stocheaza valoarea lui R2 la adresa data de R1 + R2 LDR R2, [R1, R2, LSL#2] @ incarca in registrul R2 valoarea de la adresa R1 + R2<<2
STR R2, [R1, #2]! @ stocheaza valoarea din R2 la adresa data de R1 + 2; R1 = R1 + 2 STR R2, [R1, R2]! @ stocheaza valoarea lui R2 la adresa data de R1 + R2; R1 = R1 + R2 LDR R2, [R1, R2, LSL#2]! @ incarca in registrul R2 valoarea de la adresa R1 + R2<<2; R1 = R1 + R2<<2
LDR R3, [R1], #4 @ incarca in R3 valoarea de la adresa de memorie data de R1; R1 = R1 + 4 STR R3, [R2], R4 @ stocheaza la adresa data de valoarea din R2 valoarea lui R3; R2 = R2 + R4 STR R3, [R2], R1, LSL#2 @ stocheaza la adresa data de valoarea lui R2 valoarea lui R3; R2 = R2 + R1<<2
Urmatoarele tipuri de date sunt disponibile pe sistemele ARM:
Registrele ARM nu dispun de subregistre, asa ca daca dorim sa incarcam/stocam din/in memorie un numar mai mic de 4 octeti, va trebui sa folosim sufixele corespunzatoare:
ldr = Load Word ldrh = Load unsigned Half Word ldrsh = Load signed Half Word ldrb = Load unsigned Byte ldrsb = Load signed Bytes str = Store Word strh = Store unsigned Half Word strb = Store unsigned Byte
Pe ARM, stiva functioneaza in acelasi mod ca si pe procesoarele x86. Diferenta, la nivel de instructiuni, este data de faptul ca instructiunile push si pop accepta un numar variabil de registre:
main: mov r0, #2 /* set up r0 */ mov r1, #4 /* set up r1 */ push {r0, r1} /* save r0 and r1 onto the stack */ mov r0, #3 /* overwrite r0 */ mov r1, #3 /* overwrite r1 */ pop {r0, r1} /* restore r0 and r1 to their initial state */ bx lr /* finish the program */
Conventia de apel apel de functie pe procesoarele ARM este usor diferita de cea de pe procesoarele x86 pe 32 de biti. Astfel:
Pentru a intelege mai bine diferentele vom pleca de la exemplul de mai jos:
main: push {r11, lr} /* Start of the prologue. Saving Frame Pointer and LR onto the stack */ add r11, sp, #0 /* Setting up the bottom of the stack frame */ mov r0, #1 /* setting up local variables (a=1). This also serves as setting up the first parameter for the max function */ mov r1, #2 /* setting up local variables (b=2). This also serves as setting up the second parameter for the max function */ bl max /* Calling/branching to function max */ sub sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */ pop {r11, pc} /* End of the epilogue. Restoring Frame pointer from the stack, jumping to previously saved LR via direct load into PC */ max: push {r11} /* Start of the prologue. Saving Frame Pointer onto the stack */ add r11, sp, #0 /* Setting up the bottom of the stack frame */ cmp r0, r1 /* Implementation of if(a<b) */ movlt r0, r1 /* if r0 was lower than r1, store r1 into r0 */ add sp, r11, #0 /* Start of the epilogue. Readjusting the Stack Pointer */ pop {r11} /* restoring frame pointer */ bx lr /* End of the epilogue. Jumping back to main via LR register */
Programul defineste 2 functii: main si max. Sa analizam intai apelul functiei max:
mov r0, #1 mov r1, #2 bl max
Se seteaza parametrii functiei (p1 = 1; p2 = 2) si apoi se foloseste bl (branch with link) pentru a se sari catre label-ul max; se foloseste instructiunea bl pentru a transmite procesorului ca adresa urmatoarei instructiuni sa fie pusa in registrul lr.
In cadrul functiei max putem distinge cele 3 zone specifice unei functii:
1. Prologul:
push {r11} // echivalent cu 'push ebp' add {r11}, esp, #0 // echivalent cu 'mov ebp, esp`
2. Corpul functiei, in care se calculeaza maximul si se pune rezultatul in registrul r0.
cmp r0, r1 movlt r0, r1
3. Epilogul:
add sp, r11, #0 // echivalent cu 'mov esp, ebp' pop r11 // echivalent cu 'pop ebp' bx lr // jump catre adresa aflata in registrul lr
git pull origin master
din interiorul directorului în care se află repository-ul (~/Desktop/iocla
). Recomandarea este să îl actualizați cât mai frecvent, înainte să începeți lucrul, pentru a vă asigura că aveți versiunea cea mai recentă. Dacă doriți să descărcați repository-ul în altă locație, folosiți comanda git clone https://github.com/systems-cs-pub-ro/iocla ${target}
.Pentru mai multe informații despre folosirea utilitarului git
, urmați ghidul de la Git Immersion.
În cadrul acestui laborator vom folosi fișierele din directorul laborator/old/arm
.
Pana acum, pentru a putea rula un program scris in limbaj de asamblare eram nevoiti sa parcurgem 2 pasi:
Calculatoarele din laborator dispun de un procesor din familia Intel (x86). In situatia actuala nu putem rula/compila programe pentru arhitectura ARM. Pentru a depasi acest impas, suntem nevoiti sa emulam sistemul ARM, iar pentru asta avem nevoie de o masina virtuala. Pentru a simplifica procesul, vom folosi o masina virtuala doar pentru rularea executabilului; pentru compilare vom folosi un compilator din suita Linaro.
1-install-toolchain
si rulati comanda make run
. In cazul in care programul compileaza si ruleaza cu succes pachetele sunt deja instalate pe masina voastra, caz in care nu trebuie sa mai refaceti pasii de mai jos.
Asadar, avem nevoie de:
1-install-toolchain
si introduceti comanda:sudo bash install_toolchain.sh
apt-get install qemu
sudo sed -i -e 's/hu.archive.ubuntu.com\|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list sudo sed -i -e 's/archive.ubuntu.com\|security.ubuntu.com/old-releases.ubuntu.com/g' /etc/apt/sources.list sudo apt update
Pentru a va asigura ca ati instalat in modul corespunzator toolchain-ul, rulati comanda:
make run
Dupa rularea programului ar trebui sa aveti afisat mesajul “Hello, World!”.
PATH
. Faceți manual actualizarea folosind
export PATH=/opt/gcc-armv8l/bin:$PATH
Inspectati fisierul Makefile. Identificati principalii pasi descrisi mai sus in obtinerea executabilului.
sudo wget -O "/usr/bin/qemu-arm" "http://elf.cs.pub.ro/asm/res/laboratoare/qemu-arm-static" sudo chmod +x "/usr/bin/qemu-arm"
Verificați că aveți ultima versiune de QEMU:
$ qemu-arm --version qemu-arm version 3.1.50 (v3.1.0-569-ge59dbbac03-dirty) Copyright (c) 2003-2018 Fabrice Bellard and the QEMU Project developers
Pornind de la sursa din 2-mul-sum/mul.asm
, implementati suma patratelor primelor N
numere naturale. Urmariti comentariile din sursa. Pentru compilare si rulare, folositi fisierul Makefile
.
Navigati in directorul 3-mem-addr
. Inspectati sursa mem.asm
. Ce ar trebui sa faca programul? Ce afiseaza? De ce?
nums
.
r2
și r3
pentru calculele voastre. Sunt modificate de funcția printf
.
Navigati in directorul 4-flags
. Inspectati sursa flags.asm
. Ce ar trebui sa faca programul? Ce face? De ce?
Navigati in directorul 5-strings
. In cadrul acestui exercitiu vom implementa o serie de functii utile in lucrul cu siruri:
bool starts_with(char* s1, char* s2)
. Prin conventie, sirul mai mic este transmis al doilea. Functia va intoarce true in situatia in care sirul s1, incepe cu sirul s2, false altfel.void substr(char* s1, char* s2)
. Prin conventie, sirul mai mic este transmis al doilea. Functia va printa pozitiile tuturor aparitiilor sirului s2 in sirul s1.