Uvod u assemblersko programiranje s ARM 32-bitnom porodicom procesora
1. Uvod
Assemblerski jezici se koriste za programiranje programiranje računala na razini koja je dovoljno blizu hardveru kako bi mogli dobiti značajan uvid u stvarni hardver i njegove
performanse. Kao informatičari, mi smo zainteresirani za razumijevanje asembler programiranja
uglavnom zato što nam daje bolje razumijevanje načina na kojoj su implementriani viši programnski jezici kao C/C++, što se podrazumijeva kada govorimo o pozivu
funkcije, for petlji ili pristupu nizu brojeva. Kako takve složene strukture mogu biti izražene, kad imamo i dostupni su nam registri, memorija i jednostavne instrukcije
sa tri operanda?
Baš kao i na razina viši programskih jezika i asemblerski programi se kompajli raju na razinu strojnog koda. Ovaj proces se zove „asembliranje“, a softverski alat koji
ovo izvodi se zove assembler.
1.2 ARM arhitektura
ARM arhitektura predstavlja relativno jednostavna implementaciju load / store arhitekture, odnosno arhitekture gdje se pristup glavnoj memoriji (RAM) obavlja preko Load i Store instrukcija, dok se sve računske operacije provode na vrijednostima koje se drže u registrima.
Na primjer instrukcija, ADD R4, R1, R2
će pohraniti u registar R4 zbroj
vrijednosti koje se čuvaju u registrima R1 i R2. ARM arhitektura nudi 16 registara opće namjene, označenih od R0 do R15, od koji je posljednja tri imaju posebnu važnost. . R13, također poznat kao SP, je stack pointer (pokazivač stoga) koji drži
adresu gornjeg elementa na programskom stogu. Ona se automatski koristi u PUSH i POP instrukcijama za upravljanje pohrane i oporavak registara u stogu.
R14, također poznat kao LR je link registar, i drži povratnu adresu, koja
je adresa prve instrukcije koja se provodi nakon povratka iz trenutna funkcije. funkcije.
R15, također poznat kao PC je programsko brojilo, i čuva adresu slijedeće
instrukcije koja se treba izvršiti. O ovim registrima ćemo detaljnije kasnije u tekstu.
2. Embest IDE i programiranje u assembleru
U našim vježbama za razvojno okruženje koristiti ćemo Embest IDE. Napisati ćemo i analizirati jednostavni program zapisan preko Embest okruženja.
C Kod
Assembler
int x=4;
_start:
int y=7;
MOV r0, #0x4
z=x*y;
MOV r1, #0x7 MUL r2, r1, r0 stop: B .end
stop
Nakon instalacije i nakon pokretanja EmbestIDE programa dobijamo slijedeće korisničko sučelje.
Prvo ćemo kreirati novi radni prostor (workspace ) te novi projekt.
(File->New Workspace) Otvori se dijaloški okvir gdje zadajemo lokaciju u datotečnom s ustavu i naziv
projekta
Nakon što kliknemo u dijaloškom okviru OK u
programu će se pojaviti radni prostor i projekt sa nekoliko datoteka sa desne strane.
Potom moramo ići na File-> New gdje dobijemo tekstualni okvir za upis našeg assemblerskog koda . Upišemo kod iz tablice.
Nakon što upišemo tekst, spasimo sa File -> Save u direktorij sa našim projektom pod nazivom zad1.asm
Nakon toga potrebno je ubaciti naš u spremljenu datoteku zad1.asm u projekt na način prikazan na slici (desni klik miš na Pr oject Source File)
Sada kada ima mo ubačenu asemblersku datoteku moramo konfigurirati način i gdje će se izvesti sami kod ( File ->Project->Settings)
Otvori se dijaloški okvir u koje postavljamo slijedeće stavke kao na slikama.
U ovom trenutku potrebno je spasiti promjene kliknuti OK, te na glavnom meniju ići na Build->Compile, te potom na Build-> Build Vjezba 1.
Nakon što se uvjerimo da nema grešaka pri kompajliranju i stvaranju zadatka
(Build) potrebno je se opet vratiti na Project->Settings te u tabu Debug u kategoriji General pronaći elf datoteku u debug direktoriju našeg projekta kao na slici te
postaviti na Auto Download. U kategoriji Download također pronaći elf datoteku postaviti adresu preuzimanja 0x00008000 te označiti stavku Progra m Entry Point
Nakon svih predradnji ponovno kompajliramo i „buildamo“ projekt te se spojima na ploču sa ARM procesorom sa naredbom Debug ->Remote Connect. Sada možemo izvoditi naše asemblerske programe.
3. Assembler Jezik
Kao što je prikazano u prvom primjeru, izjave u assembleru su strukturirane na slijedeći način:
label: instruction
Komentari se mogu umetnuti u C stilu (razgraničeno s / * komentar* / ). Oznaka (label) je opcijska – moguće je definirati instrukcije i bez oznake. Oznaka se koristi tako da čuva adresu instrukcije u kodu - ovo je potrebno ako moramo napraviti grananje ( branch) u kodu tzv. skok( jump) instrukcije. Za labele se mogu koristiti bilo koje alfanumeričke sekvence. Mogu se koristiti ('_') kao i
znak ('$') . Labele ne mogu započinjati sa brojevima.
Instrukcije se mogu podijeliti u tri različite skupine:
Aritmetički-Logike instrukcije izvode matematičke operacije nad podacima:
to može biti aritmetika (zbroj, oduzimanje, množenje), logika (boolean operacije), ili relacijska (usporedba dvije vrijednosti) operacija.
Branch instrukcije ili grananja mijenjaju tijek
izvođenja programa,
mijenjanjem vrijednosti programskog brojila (R15). One su potrebne za provedbu uvjetnih izjava, petlji i poziva funkcija.
Load / Store instrukcije premještaju podatke u i iz glavne memorije. Budući sve druge operacije rade samo s neposrednim konstantnim vrijednostima (kodirane u samoj instrukciji) ili s vrijednostima iz registara, load / store instrukcije su potrebne da se bave sa svim podacima (osim najmanjim skupovima podataka gdje koristimo naredbu MOV koja zaprima vrijednosti do 255)
U ostatku ovog odjeljka, mi ćemo predstaviti podskup ARM seta naredbi
koji se mogu koristiti za pisanje cjelovitih programa. Ovaj podskup je mnogo manji nego cijeli skup Arm naredbi.
3.1 Aritmetičko-Logičke instrukcije
Aritmetičke instrukcije uključuju sljedeće naziv instrukcije te registre nad kojim
operiramo.
Rd- odredišni registar
Rd- prvi registar sa podacima
Rm – drugi registar sa podacima
#imm-umjesto drugog registra možemo korisiti 8 bitne neposredne vrijednosti (npr, #5)
Sintaksa
Semantika
dodatno objasnjenje
ADD Rd Rn RM / # IMM
Rd = Rn + Rm / # IMM
Zbrajanje
SUB Rd Rn Rm / # IMM
Rd = Rn - Rm / # IMM
Oduzimanje
MUL Rd Rn Rm Rd
Rd = Rn* Rm / # IMM
Množenje
AND Rd Rn Rm/#imm
Rd=Rn & Rm/#imm
(bitwise and)
EOR Rd Rn Rm/#imm
Rd = Rn ^ Rm/#imm
(bitwise
exclusive
or) ORR Rd Rn Rm/#imm
Rd = Rn | Rm/#imm
(bitwise or)
MVN Rd Rm/#imm
Rd = ~ Rm/#imm
(bitwise negacija)
CMP Rn Rm/#imm
Status usporedba (<, >, ==)
MOV Rd Rm/#imm
Rd = Rm/#imm
Premještanje
jednog registar
u
iz drugi
Većina aritmetičkih naredbi je poprilično jasna: sadržaj registra Rd su zamjenjeni s
rezultatima operacija koje se izvode na vrijednostima Rm ili Rn (odnosno konstantnom #imm). Na primjer, instrukcija SUB R3 R2 #12 smanjuje vrijednost registra R2 za 12. Potom se vrijednost registra R2 sprema u registar R3. Također, lako je prevesti C jezik u ARM instrukcije sve dok su sve promjenjive
vrijednosti pohranjene u registrima. Ra zmotrimo slijedeći fragment u C-u
int i =0 , j =2 , k =4; i = k | j ; /* Bitwise or */
Pod pretpostavkom da su vrijednosti tri varijable pohranjeni u registre R4, R5, i R6, tada se može prevesti u fragment kako slijedi:
MOV R4 #0
/* i =0; */
MOV R5 #2
/* j =2; */
MOV R6 #4
/* k =4; */
ORR R4 R5 R6
/* i = k | j ; */
Važno je napomenuti da je množenje teže implementirati u hardveru od drugih
naredbi. Za razliku od većine drugih instrukcija, u MUL instrukciji registri Rd i Rm ne mogu biti u istom registru.
Instrukcija
usporedbe, CMP, ne
sprema
svoj
rezultat
u
opće namjenski
registar. Umjesto toga ona koristi posebni (ne izravno dostupan prema programeru) registar, koji se zove Processor Status Word. Rezultat usporedbe govori procesoru da li je vrijednost u Rn je veća, manja ili jednaka registru Rm (ili konstantnom operandu). Te se informacije mogu se koristiti da odlučite hoćete li obavljati uzastopne uvjetne instrukcije grananja ili ne.
3.2 Branch Instrukcije Osnovna funkcionalnost Branch instrukcija je promijeniti vrijednost programskog brojila postavljanjem na novu vrijednost. Set instrukcija grananja uključuje sljedeće:
Sintaksa
Semantika
B label
Skoči na label bezuvjetno
BEQ label
Skoči na label a ko je prethodna usporedba jednaka
BNE label
Skoči na label ako je prethodna usporedba nije jednaka
BGT label
Skoči na label ako je prethodna usporedba Rn >Rm/#imm
BGE label
Skoči na label ako je prethodna usporedba Rn >=Rm/#imm
BLT label
Skoči na label ako je prethodna usporedba Rn
BLE label
Skoči na label ako je prethodna usporedba Rn <=Rm/#imm
BL label
Funkcijski poziv (label je ime funckije /ulazna točka)
BX Rd
Povratak iz funkcije (uvijek BX lr)
B label je osnovno grananje. Mijenja se tok programam kako bi se izvela
instrukciju označena sa label BL label obavlja poziv funkcije , što znači da skače kao u B label, ali i sprema
vrijednost programskog brojila te Link Registar (slično kao MOV LR PC). Povratak iz funkciju obavlja
BX LR.
To je gotovo isto kao i
MOV
PC LR, ali sa
PC ne treba manipulirati izričito od strane programera, osim ako postoji jako dobar razlog za to. Sve ostale instrukcije su uvjetne varijante osnovnog grananja. Oni se izvode kao B label, ako je određen specifičan uvjet. Npr., razmislite sli jedeći ulomak:
CMP R0 #0 BGT label ADD R0 R0 #1
Ključna naredba je CMP koja mijenja stanje u registru CPSR.
Najznačajniji bitovi u registru CPSR s krajnji N,Z,C iV .
N - određuje da li je broj negativan ili je manji od onog s kojim uspoređujemo Z- određuje da li je broj 0 odnosno da li su dva broja jednaka C – Da li je došlo do preljeva, prenošenja ili posuđivanja kod računskih operacija V – Preljev i prekoračenje kod brojeva sa predznakom
Na osnovu stanja u CPSR registru naredba BGT može izvršiti uvjetno grananj e.
Prvo smo usporediti R0 sa 0, onda granamo na label ( ako i samo ako) je vrijednost sadržana u R0 veća od nule. S druge strane ako nije veća ide se na
instrukciju 3.
3.2 Prevođenje uvjetne naredbe
Ključno pitanje u razumijevanju assemblera je razumijevanje kako naredbe za uvjetno izvođenje koda iz viših jezika mogu biti implementir ani u asemblerskom
jeziku niskog nivoa. Prvi korak je naučiti kako predstaviti u assembler „if – theelse“ konstrukt iz C-a.
Pretpostavimo da su x i y sačuvani u R4 i R5, možemo prevesti C fragment na slijedeći način:
C Kod
Assembler
int x , y ;
.equ x, 1
...
_start:
if(x >0) {
MOV r4, #x CMP r4, #x
y =1;
BLE else
} else {
MOV r5, #1
y =2; }
end:
return y ;
B end else: MOV R5,#2 B end
Možemo vidjeti da u liniji 3 i 4 u assembleru postavljamo uvjetno grananje koje dopušta da preskočimo „then“ dio koda (y=1; u našem primjeru) ako se uvjet (x>0)
ne izvrši. S druge strane koristi se i provodi „then “ dio. U kasnijem slučaju moramo osigurati da s e „else“ ne izvršava u originalnom C konstruktu , dva dijela su uzajmno isključiva : samo se jedna izvršava kada se uvjet is punjava. Na taj način u liniji 8 dodajemo bezuvjetno grananje B kako bi preskočili else dio i
pomakli se direktno na kraj preko labele end. C kod
Assembl er
int res;
.equ x, 0
.Lt_mx:
int x=0;
.equ
mx, 4
cmp r0,r1
int mx=4
.equ
mn, 7
bge stop
int mn =7
_start:
add r3,r0,r2
if(x>=mx) res=mn+x-mx; else if(x
MOV r0, #x
sub r0,r3,r1
MOV r1, #mn
b stop
MOV r2, #mx cmp r0,r2
.end
blt .Lt_mx add r3,r0,r1 sub r0,r3,r2 stop: B
stop
Prevođenje While i Do -While instrukcija
Počnimo sa while petljom i prevedimo slijedeći C frgament koda.
C kod
Assembler
int x,y; ...... x=1; while(x>0) { y*=x; x--; }
.equ x, 5 .equ y, 1 _start: MOV r5, #y MOV r4, #x
loop: CMP r4, #0 BLE end MUL r6, r5, r4 MOV r5, r6 SUB r4, r4, #1 B loop end: MOV r0, r5 stop: B stop
Možemo vidjeti u liniji 6 i 7 u asemblerskom kodu da postavljamo i izvršavamo uvjetno grananje koje dopušta da izađemo iz petlje kada uvjet u izvornom kodu
(x>0) nije osiguran.. Nakon kraja tijela petlje moramo vratiti se i provjeriti uvjet kako bi započeli s a iteracijom petlji. U liniji 12 dodajemo bezuvjetno grananje B kako bi se vratili na početak petlje označene sa oznakom labelom loop.
Sa do-while petljom možemo izvesti sličan prijevod. Ovdje je C fragment koji računa istu funkciju faktorijel osim što je x==0, povratna vrijednost je 0 radije nego
1. C Kod
Assembler
int x , y ;
.equ x, 5 .equ y, 1
x=5;y =1;
_start:
do {
MOV r5, #y
y *= x ;
MOV r4, #x
x - -; } while (x >0) ;
loop: MUL r6, r5, r4 MOV r5, r6 SUB r4, r4, #1 CMP r4, #0 BGT loop MOV r0, r5 stop: B stop
return y ;
Jedina uočljiva razlika je u odnosu na while verziju u tome što sada se granamo n a početak koji je uvjetan i
trebamo samo jednu granu budući nemamo kontrole
provjere x>0 na početku.
Može se činiti da prevođenje for petlje predstavljaju je teže budući je kosntrukcija
petlje kompleksnija. Ipak, nije tako budući se for petja se može odma h prevesti u C while petlju. Napr imjer razmotrimo slijedeći fragment C koda:
int x , y ; ... for ( y =1; x >0; x - -) y *= x ; return y ;
Lako je vidjeti da petlja u liniji 3 ima isto ponašanje kao while petlja. Generalno , C
for petlja ima slije deću strukturu: for(inicijalizacija; uvjet; korak petlje) tijelo petlje. Petlju for lako možemo prevesti u while petlju na slije deći način:
Inicijalizacija while(uvjet) { Tijelo-petlje Korak_petlje }
3.3 Load / Store Instrukcije
Posljednje, ali ne i najmanje važno, load i store instrukcije obavljaju prijenos
podataka iz memorije prema registrima i obrnuto. Sljedeće instrukcije omogućuju nam rad s podacima veličine riječ (32 bita) i veličine byte (8 bita)
Sintaksa
Semantika
LDR Rd [Rn, Rm/#imm]
Rd = mem[Rn+Rm/#imm] (32 bit copy)
STR Rd [Rn, Rm/#imm]
mem[Rn+Rm/#imm] = Rd (32 bit copy)
LDRB Rd [Rn, Rm/#imm]
Rd = mem[Rn+Rm/#imm] (8 bit copy)
STRB Rd [Rn, Rm/#imm]
mem[Rn+Rm/#imm] = Rd (8 bit copy)
PUSH { register list }
Push registers onto stack
POP { register list }
Pop registers from stack
Load instrukcijama vrijednosti se čitaju iz određene memorijske adrese i učitavaju u registar Rd, dok su store instrukcije Rd
izvori iz kojih se podaci čitaju ,a
vrijednosti spremaju u memo riji. Memorijska adresa se u oba slučaja računa kao suma baze Rn i odmaka (Rm ili kosntante). Uobičajeno programer koristi ove načine da pristupe poljima podataka. Razmotrimo s lijedeći C kod:
1. char AA [10] , BB [10]; 2. ... 3. AB [ i ]= BB [ i ];
Gdje su AA i BB polja. Ako se baza adrese AA i BB čuvaj u u R4 i R5, a indeks i se čuva u R1, tada se liniji 3 može prevest kao
LDRB R6 R5 R1 STRB R6 R4 R1
Budući je memorija adresirana po byte-ovima (svaka adresa odgovara specifičnom byte-u) kada se pristupa polju integera moramo modificirati indeks polja kako bi inkrementirali po koraku 4 umjesto po koraku 1 jer integer zauzima 4 byte-a.
int AA [10] , BB [10]; ... 3 i =2; 4 AA [ i ]= AB [ i ];
Odgovarajući ARM prijevod koristi istu pohranu za adrese i varijable MOV R1 #8
/* 2*4: addressing in bytes ! */
LDR R6 R5 R1 STR R6 R4 R1
LDR se koristit kada se služimo riječima (32 bita) dok LDRB kada imamo byte ove(8 bita). Slično je i za STR.
Push i Pop instrukcije se koristi da upravljaju programskim stogom koje se kolekcija zapisa za sve funkcije aktivne u is to vrijeme kao rezultat ugniježđenih poziva funkcija.
Globalna podatkovna sekcija
Svaki program vidi memorijski prostor koji je rezerviran za njega. Memorijski prostor se može koristiti za čuvanje podataka: npr, možemo čuvati globalne
skalarne varijable, stringove , polja ili matrice. Kako bi rezervirali prostor namijenjen globalnoj podatkovnoj memoriji moramo dati dovoljno informacija o veličini prostora potrebnog za memoriju kao i
inicijalizacijske vrijednosti. Direktive .int, .byte, .float, .string respektivno rezerviraju memoriju za 32-bitni integer, 8-bitni integer 32- bitni floating point broj i zero terminated string (veli čina stringa je jedan byte po zn aku plus jedan dodatna znak za kraj stringa) Ovim podacima možemo pristupiti sa load i store instrukcijama
Zadatak 7. Pronađimo najveći zajednički djelitelj između dva zadana broja preko
niza brojeva
C kod
Assembler
while(a!=b)
arr:
{ if(a>b) a=a-b; else b=b-a; }
.byte 100,64
gcd: cmp r0, r1
eoa:
beq stop
.align
blt less
_start:
sub r0, r0, r1
ldr r3, =arr
bal gcd
ldr r4, =eoa
less:
ldrb r0, [r3], #1
sub r1, r1, r0
ldrb r1, [r3 ], #1
bal gcd stop: B
stop
U slučaju da koristimo .byte ili polu riječi tip podataka moramo ko ristiti direktiva .align . Budući instrukcije zauzimaju 4 byte-a, adresa prvog byte mora biti djeljiva sa 4 pa stoga umećemo dodatne byte -ove kako bi slijedeća adresa instrukcije bila višekratnik broja 4.
Zadatak 8. Zbrojite elemente niza C kod
Assembler
char arr[]={10,20,25}
start:
bne loop stop: b stop
int sum=0; int i=0;
ldr r0, =eoa
arr:
do
ldr r1, =arr
eoa:
{
mov r3, #0 sum=sum+char[i];
loop:
i++;
ldrb r2, [r1], #1
}while(i<3);
add r3, r2, r3
.byte 10, 20, 25
.align
cmp r1, r0
Zadatka 9. Napišite program u assembleru kojim ćete pronaći duljinu niza znakova.
str:
.asciz "Hello World“
ldrb r2, [r0], #1
.equ nul, 0
add r1, r1, #1
.align
cmp r2, #nul
start:
bne loop
ldr r0, =str mov r1, #0
sub r1, r1, #1 stop: b stop
loop:
Direktiva .asciz prihvata string znakove kao argument. String znakovi se smješta ju
u susjedne i kompaktne memorijske lokacije.
Stog Stog je podatkovna struktura (način na koji organziramo podatke). Stog se sastoji od tri tipične operacije: pristup vrhu stoga, postavljanje na vrh (push), skidanje sa
vrha (pop). U ovisnosti o kontesk tu možemo samo imati pristup vrhu stoga. Kada govorimo o funkcijama to je dio memorije koju posjeduje funkcija, a koji se dinamički aktivira kada se pozove funkcija. Za kontrolu stoga služimo se pokazivačem stoga (stack pointer) odnosno registrom sp . U registru se samo nalazi
pohranjen vrh stoga. Područje memorije koje se stvara kada se pozove funkcija nazivamo lokalnom memorijom funkcije i sastoji se od početne vrijednost sp – a
kada je funkcija pozvana i trenutne vrijednost pokazivača stoga (sp -a). Područ je se se povećava kada dodajemo elemente, a smanjujemo kada skidamo elemente sa
stoga. Stavke se skidaju obrnutima redoslijedom u kojem su dodavane (LIFO).
.equ x, 45
/* definiramo varijablu x i dodajmo 45 */
.equ y, 64
/* definiramo varijablu y i dodajmo 64 */
.equ stack_top, 0x1000
/* definiramo vrh stoga 0x1000*/
.global _start
.text
/* simbol početka programa */
_start:
/* dodaj vrijednost x u registar R0 */
MOV
sp, #stack_top
MOV
r0, #x
STR
r0, [sp]
/* spasi vrijednost R0 na stog*/
MOV
r0, #y
/* dodaj vrijednost y na R0 */
LDR
r1, [sp]
ADD
r0, r0, r1
STR
r0, [sp]
/* preuzmi vrijednost iz stoga na R1 */
stop: B .end
stop
/* program završava i ulazi u beskonačnu petlju*/
.equ stack_top, 0x1000
/*stavi na vrh stoga 0x1000*/
.global _start .text
_start:
/*ulazna točka*/
MOV sp, #stack_top
/* postavlja se stack pointer na vrh stoga */
adr r4, pod /* stavlja se u r4 adresa podataka */
LDR r0,[r4],#4 LDR r1,[r4],#4
/* u r0 prvi podatak iz memorije 0x000F */ /* u r1 drugi podatak iz memorije 0x00FF */
ADD r2,r0,r1 LDR r0,[r4],#4 MUL r3,r2,r0 STR r3,[r4] STR r3,[sp] stop: B
stop
pod: .word .end
0x000f, 0x00ff, 0x0fff, 0xffff
/*zaustavljanje grananja*/