RPI assembler tutorial 5

Tot nu voerden onze kleine programmaatjes de instructies na elkaar uit. Het zou kunnen reageren op bestaande voorwaarden die verschillende instructiereeksen nodig hebben. Dit is het doel van de branch instructies

Een speciaal register

In deel 2 heb ik uitgelegd dat de Raspberrypi’s ARM cpu 16 general-purpose integer registers heeft en ook dat sommige speciale rollen hebben in het programma (zoals r0). Nu wordt dit belangrijk.

Zo is register r15 speciaal omdat het ook nog een andere naam heeft: pc. Register r15 wordt dan ook enkel gebruikt met de naam pc omdat het anders te verwarrend is. r15 is wel correct, maar het brengt gewoon verwarring.

pc staat voor program counter. Dit is de naam die stamt uit het begin van de computers. Dit register wordt ook wel de ip of instructie pointer genoemd in andere architecturen zoals i386. Deze bevat het adres van de volgende instructie die zal worden uitgevoerd.

Wanneer de arm processor een instructie uitvoert kunnen er 2 dingen gebeuren aan het eind. Als de instructie de pc niet aanpast (wat voor de meeste instructies het geval is), word het pc-register met 4 opgeteld. Dit is 4 omdat arm instructies 32bit zijn.

Zodra de processor de instructie heeft uitgevoerd, gaat het de waarde in de program counter gebruiken als het adres voor de volgende instructie. Op deze manier zal als een instructie de program counter niet aanpas, gewoon de volgende instructie worden uitgevoerd. Dit is impliciet sequensen: Eens een instructie is uitgevoerd, dan wordt gewoon de volgende uitgevoerd. Maar als een instructie de program counter aanpast, als bijvoorbeeld er een andere waarde dan 4 aan wordt toegevoegd, kunnen we een andere instructie in het programma uitvoeren (een beetje zoals de GOTO-commando in batch). Dit proces wordt ook branchering genoemd. In ARM gebeurt dit met de branch instructie.

Onvoorwaardelijke branches

Je kan de processor vertellen om onvoorwaardelijk te branchen met de instructie b (wat staat voor branch). Zoals in het volgende voorbeeld:

/* -- branch01.s */
.text
.global main
main:
mov r0, #2 /* r0 ← 2 */
b end /* branch to 'end' */
mov r0, #3 /* r0 ← 3 */
end:
bx lr

Als je dit uitvoert dan zal je een error code van 2 zien.

Wat hier gebeurd is dat de instructie ‘b end’ de program counter aanpas naar de instructie met het label ‘end’ en dat is ‘bx lr’, De instructie die wordt uitgevoerd op het einde van het programma. Hierdoor is de instructie ‘mov r0, #3’ nooit uitgevoerd.

op punt is is de b instructie onvoorwaardelijk gebranched. Dit lijkt nutteloos, maar dat is niet het geval. Deze instructie is essentieel in bepaalde gevallen, zeker in combinatie met voorwaardelijke branching.

Voorwaardelijke branches

Als een processor enkel kon branchen zonder voorwaarden dan zou het niet echt handig zijn. Het is veel handiger als er bepaalde voorwaarden voldaan zijn (denk maar aan control-flow tools). Dus een processor moet bepaalde voorwaarden kunnen vergelijken en evalueren.

Voordat we verdergaan moeten we nog een ander register leren kennen: cpsr (Current Program Status Register). Dit register is een beetje speciaal en het direct wijzigingen (met mov etc.) hoort niet bij het idee van branches. Het cpsr register kan bepaalde waarden houden dat kunnen gelezen en aangepast worden tijdens het uitvoeren van een instructie. De waarden van dit register bevatten 4 voorwaarden-codes genaamd: N (negatief), Z (Zero), C (Carry) en V (Overflow). Deze 4 voorwaarden-codes zijn vaak gelezen door branch instructies. Bewerkingsinstructies en speciale test en vergelijkingsinstructies kunnen deze ook aanpassen.

De voorwaarden van deze 4 codes die de cpsr registers aanpassen zijn als volgt:

  1. N: Deze zal worden ingeschakeld als het resultaat van de instructie negatief is, ander wordt hij uitgeschakeld.
  2. Z: Deze wordt ingeschakeld als het resultaat nul (zero) is. En uitgeschakeld als het niet zo is.
  3. C: Deze wordt ingeschakeld als het resultaat een 33ste bit nodig heeft om volledig te zijn. Bijvoorbeeld een optelling die de 32 bit range van integers ‘overflow’. Er is een speciaal geval voor C en aftrekkingen waar niet-lenende aftrekkingen het inschakelen, en anders uitschakelen. Een groot getal aftrekken van een kleiner getal zet C aan, maar het zal uitgeschakeld worden als het omgekeerd is.
  4. V: Deze zal worden ingeschakeld als het resultaat van de instructie niet kan worden weergegeven in 32bits

Dus we hebben alles wat nodig is om voorwaardelijk te branchen. Laat ons starten met het vergelijken van 2 waarden. Hiervoor gebruiken we de instructie cmp.

cmp r1, r2 /* updates cpsr wanneer "r1 - r2", maar als r1 en r2 niet worden aangepast */

Deze instructie verminderd de waarde van het eerste register met de waarde van het 2e register. Voorbeelden van wat er kan gebeuren:

  • Als r2 een waarde had die groter was dan r1, dan zou N worden ingeschakeld.
  • Als r1 en r2 dezelfde waarde hebben, dan zou Z worden ingeschakeld, omdat het resultaat dan 0 is.
  • Als r1 1 was en r2 0, dan zou C worden ingeschakeld.
  • Als r1 het grootste positieve 32bit getal was (2147483647) en r2 -1 was dan zou 2147483648 niet kunnen worden weergegeven in 32 bit, dus zal V worden ingeschakeld.

Hoe kunnen deze opties nuttig zijn:

  • EQ (gelijk): Wanner Z is ingeschakeld
  • NE (niet gelijk): Wanner Z is uitgeschakeld
  • GE (Gelijk of groter dan): Met zowel V als N ingeschakeld of uitgeschakeld (V=N)
  • LT (Kleiner dan): Dit is het tegenovergesteld van GE.
  • GT (Groter dan): Dit is wanneer Z is uitgeschakeld, maar N en V beide zijn ingeschakeld.
  • LE (Kleiner of gelijk aan): Als Z is ingeschakeld, of als N en V niet beide zijn in- of uitgeschakeld.
  • MI (Minus, negatief): Als N is ingeschakeld.
  • PL (Plus, positief): Als N is uitgeschakeld.
  • VS (Overflow Set): Als V is ingeschakeld.
  • VC (Overflow clear): Als V is uitgeschakeld.
  • HI (Groter): Als C is ingeschakeld en Z is uitgeschakeld.
  • LS (Kleiner of hetzelfde): Als C is uit of Z is ingeschakeld.
  • CS/HS (Carry Set/Groter of gelijk): Als C is ingeschakeld.
  • CC/LO (CarryClear of kleiner): Als C is uitgeschakeld.

Deze instructies kunnen gecombineerd worden met onze b instructie om nieuwe instructies te genereren. Op deze manier zal ‘beq’ enkel branchen als Z 1 is. Als deze voorwaarden niet zijn voldaan, dan wordt de branch genegeerd en zal de volgende instructie worden uitgevoerd. Het is de taak van de programmeur om deze goed te gebruiken en dat de voorwaarden goed zijn voordat er gebranched wordt.

/* -- compare01.s */
.text
.global main
main:
    mov r1, #2       /* r1 ← 2 */
    mov r2, #2       /* r2 ← 2 */
    cmp r1, r2       /* update cpsr condition codes with the value of r1-r2 */
    beq case_equal   /* branch to case_equal only if Z = 1 */
case_different :
    mov r0, #2       /* r0 ← 2 */
    b end            /* branch to end */
case_equal:
    mov r0, #1       /* r0 ← 1 */
end:
    bx lr

Als je dit uitvoert zal je een error code van 1 terugkrijgen omdat zowel r1 en r2 dezelfde waarde hebben. Als je r1 naar 3 zet op lijn 5 dan zal je een 2 moeten krijgen. Let op dat bij de case_diffirent: label wel op het einde een onvoorwaardelijke branch naar het end label gaat, omdat het resultaat anders altijd 1 is.

Dit was alles voor deel 5.

RPI Assembler tutorial 4

Als we de fundering leren kennen van ARM assembler, zullen de voorbeelden langer worden. Omdat bij lange programma’s het eenvoudiger word om fouten te maken is het handig dat er gewerkt word met een debugger zoals de GNU Debugger (GDB) om de assembler code te debuggen. Hier zal ik in uitleggen hoe je assembler direct debugt.

GDB

We zullen het voorbeeld store01 van deel 3 gebruiken. Start de GDB met als argumenten het programma dat je gaat debuggen

gdb --args ./store01

Nu zijn we in de interactieve mode van de GDB. Hiermee kan je commando’s uitvoeren om acties binnen de debugger uit te voeren. Er is een help command ingebouwd (gewoon help). met de quit command sluit je de debugger. Als je start typt zal je de het proces starten.

(gdb) start
Temporary breakpoint 1 at 0x8390
Starting program: /home/pi/asm/Deel3/store01

Temporary breakpoint 1, 0x00008390 in main ()

De debugger is gestopt bij de functie main. Dit is handig omdat we de eerste initialisatie stappen hebben overgeslagen. Nu is de debugger aan het wachten aan de eerste instructie van de main functie.

(gdb) disassemble
Dump of assembler code for function main:
=> 0x00008390 :    ldr r1, [pc, #40]   ; 0x83c0 
   0x00008394 :    mov r3, #3
   0x00008398 :    str r3, [r1]
   0x0000839c :    ldr r2, [pc, #32]   ; 0x83c4 
   0x000083a0 :    mov r3, #4
   0x000083a4 :    str r3, [r2]
   0x000083a8 :    ldr r1, [pc, #16]   ; 0x83c0 
   0x000083ac :    ldr r1, [r1]
   0x000083b0 :    ldr r2, [pc, #12]   ; 0x83c4 
   0x000083b4 :    ldr r2, [r2]
   0x000083b8 :    add r0, r1, r2
   0x000083bc :    bx  lr
End of assembler dump.

De instructie die naar het geheugenadres van de variabele gaat zijn anders, dit is nu nog geen probleem. de pijl => wijst naar de instructie die zal worden uitgevoerd. Voordat we deze uitvoeren laat ons de registers controleren.

(gdb) info registers r0 r1 r2 r3
r0             0x1    1
r1             0xbefff744    3204446020
r2             0xbefff74c    3204446028
r3             0x8390    33680

We kunnen de registers aanpassen met de functie p (staat voor print), maar ook de effecten zien:

(gdb) p $r0 = 2
$1 = 2
(gdb) info registers r0 r1 r2 r3
r0             0x2    2
r1             0xbefff744    3204446020
r2             0xbefff74c    3204446028
r3             0x8390    33680

De debugger heeft $1 weergegeven. Dit is het resultaat en we kunnen het gebruiken als we het nodig hebben. Nu is dit nog niet zo handig, maar bij grote programma’s zal dit nuttiger worden. Met deze command zal je de eerste instructie kunnen uitvoeren

(gdb) stepi
0x00008394 in main ()

Hier is niet veel gebeurd. Gebruik dissasemble opnieuw

(gdb) disassemble
Dump of assembler code for function main:
   0x00008390 :    ldr r1, [pc, #40]   ; 0x83c0 
=> 0x00008394 :    mov r3, #3
   0x00008398 :    str r3, [r1]
   0x0000839c :    ldr r2, [pc, #32]   ; 0x83c4 
   0x000083a0 :    mov r3, #4
   0x000083a4 :    str r3, [r2]
   0x000083a8 :    ldr r1, [pc, #16]   ; 0x83c0 
   0x000083ac :    ldr r1, [r1]
   0x000083b0 :    ldr r2, [pc, #12]   ; 0x83c4 
   0x000083b4 :    ldr r2, [r2]
   0x000083b8 :    add r0, r1, r2
   0x000083bc :    bx  lr
End of assembler dump.

Als we kijken wat er in R1 is veranderd zullen we zien dat het het adres is voor myvar1

(gdb) info register r1
r1             0x10564    66916

Als we naar de inhoud zouden kijken van deze variabele zullen we zien dat het 0 is (wat we hadden ingesteld). Volgende stap:

(gdb) stepi
0x00008398 in main ()
(gdb) disas
Dump of assembler code for function main:
   0x00008390 :    ldr r1, [pc, #40]   ; 0x83c0 
   0x00008394 :    mov r3, #3
=> 0x00008398 :    str r3, [r1]
   0x0000839c :    ldr r2, [pc, #32]   ; 0x83c4 
   0x000083a0 :    mov r3, #4
   0x000083a4 :    str r3, [r2]
   0x000083a8 :    ldr r1, [pc, #16]   ; 0x83c0 
   0x000083ac :    ldr r1, [r1]
   0x000083b0 :    ldr r2, [pc, #12]   ; 0x83c4 
   0x000083b4 :    ldr r2, [r2]
   0x000083b8 :    add r0, r1, r2
   0x000083bc :    bx  lr
End of assembler dump.

Als we kijken wat er gebeurd is met r3:

(gdb) info registers r3
r3             0x3    3

Dit is wat we verwacht hebben dus volgende stap:

(gdb) stepi
0x0000839c in main ()
(gdb) disas
Dump of assembler code for function main:
   0x00008390 :    ldr r1, [pc, #40]   ; 0x83c0 
   0x00008394 :    mov r3, #3
   0x00008398 :    str r3, [r1]
=> 0x0000839c :    ldr r2, [pc, #32]   ; 0x83c4 
   0x000083a0 :    mov r3, #4
   0x000083a4 :    str r3, [r2]
   0x000083a8 :    ldr r1, [pc, #16]   ; 0x83c0 
   0x000083ac :    ldr r1, [r1]
   0x000083b0 :    ldr r2, [pc, #12]   ; 0x83c4 
   0x000083b4 :    ldr r2, [r2]
   0x000083b8 :    add r0, r1, r2
   0x000083bc :    bx  lr
End of assembler dump.

Als we nu naar het einde gaan:

(gdb) continue
Continuing.
[Inferior 1 (process 3080) exited with code 07]

Nu zien we dat het heeft gestopt met code 07, wat het resultaat is!

Dit was alles voor deel 4

RPI Assembler tutorial 3

Zoals we zagen in deel 1 en 2 kunnen we data naar de registers verplaatsen (mov) en 2 registers bij elkaar optellen (add). Gelukkig kan een cpu ook met geheugen werken, anders zouden er veel limieten zijn.

Geheugen

Een computer heeft geheugen (hiermee bedoel ik ram) waar de code (.text in assembler) en data worden opgeslagen om beschikbaar te zijn voor de cpu. i386 en x86-64 architecturen kunnen zowel registers en geheugen raadplegen (om zo bijvoorbeeld iets uit het register bij iets in het geheugen toe te voegen). Dit gaat niet in ARM. Hier moet alles gebeuren in de registers. Dit ‘probleem’ is echter op te lossen door eerst data naar een register op te slaan uit het geheugen en achteraf omgekeerd.

Hiervoor zijn er 2 speciale functies. ldr en str, oftewel load en store. er zijn nog andere manieren om dit te doen maar dit zijn de simpelste.

Geheugenadressen

Omdat alle data in het geheugen in feite 1 lange reeks van eenen en nullen is, zullen we ze een naam moeten geven omdat we anders niets kunnen raadplegen. Een computer geeft deze data adressen. Deze adressen zijn nummers en in ARM is dit een 32 bit nummer dat elke byte (8 bits) in het geheugen weergeeft.

Wanneer we data opslaan of lezen uit het geheugen moeten we het adres weten (of berekenen). Dit kan in veel manieren gedan worden. Elke van deze manieren word een ‘adressing mode’ genoemd. ARM heeft er een paar van, maar hier ga ik het doen door middel van een register.

Zoals uitgelegd in deel 2 heeft een register 32bits, wat gelijk is aan de 32 bits voor de adressen voor het geheugen. Dit betekend dat we een adres in een register kunnen opslaan om dan de respecterende data te laden of op te slaan.

Data

In deel 1 zagen we dat de assembler zowel code en data kunnen opslaan. code word duidelijk gemaakt met een label .text. Deze labels zijn eigenlijk symbolische namen naar plaatsen in je programma. Deze plaatsen kunnen zowel data als code zijn. Tot nu toe hebben we het label main gebruikt om de plaats van onze main functie aan te duiden. Een label is enkel de plek, nooit de inhoud.

Ik vertelde in deel 1 dat assembler in feite gewoon een trapje hoger is dan de binaire ode, wel dat ene stapje hoger is ineens wat complexer geworden, omdat het ook verantwoordelijk geworden is om waarde aan deze labels te geven. Zo kunnen we de assembler/compiler dingen doen doen die lijken op magie.

Zo kunnen we bijvoorbeeld de juiste grote aan een variabele geven. Laat ons een 4 byte variabele maken en hem initialiseren naar 3 met de naam myvar1.

.balign 4
myvar1:
    .word 3

er zijn 2 assembler directieven in het voorbeeld hierboven: .balign en .word. Wanner de assembler een .balign directieve tegenkomt, zorgt het ervoor dat het volgende adress zal starten met een 4-byte grote grens. Dit doet niets als het adres al 4 groot was, anders zal de assembler ‘padding bytes’ overslaan.

Nu we het adres van myvar1 hebben vastgelegd weten we dat het 4 byte alligned zal zijn.

Het .word directieve zegt de assembler de waarde als en 4 byte integer nodig is. In dit geval met de waarde 3, maar neemt 4 bytes in beslag.

Data word net zoals de code in het geheugen opgeslagen. Maar word meestal appart gehouden in een data sectie (.data) wat de assembler vertelt dat het om data gaat en niet om code.

Load

Laad het voorbeeld van deel 2 erbij en pas het aan zodat het gebruik maakt van het geheugen. We zullen 2 keer een 4 byte variabele: myvar1 en myvar2 definieren. waarvan de eerste gelijk is aan 3 en de laatste aan 4. We zullen deze waardes laden met ldr en ze dan aan elkaar toevoegen. De uitkomst zal dan weer als een error 7 moeten zijn.

/* -- load01.s */
 
/* -- Data section */
.data
 
/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar1 */
myvar1:
    /* Contents of myvar1 is just 4 bytes containing value '3' */
    .word 3
 
/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar2 */
myvar2:
    /* Contents of myvar2 is just 4 bytes containing value '4' */
    .word 4
 
/* -- Code section */
.text
 
/* Ensure code is 4 byte aligned */
.balign 4
.global main
main:
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    ldr r1, [r1]           /* r1 ← *r1 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    ldr r2, [r2]           /* r2 ← *r2 */
    add r0, r1, r2         /* r0 ← r1 + r2 */
    bx lr
 
/* Labels needed to access data */
addr_of_myvar1 : .word myvar1
addr_of_myvar2 : .word myvar2

De laatste 2 regels bevatten de locaties van myar1 en myvar2. Dit is nodig voor de assembler . Dit is omdat deze in de .data sectie staan en niet in de code sectie.

De assembler compiled het naar binaire code, .word myvar1 zal niet het adres van myvar1 zijn, maar een relocation. Een relocation is de manier waarop de assembler data adressen gebruikt. De exacte waarde is onbekend, maar zal bekend worden zodra het gelinkt word. (de stap waarop de uiteindelijke executable word gemaakt met GCC).

op regel 27 en 28 staat waar de eerste variabele word geladen. Dit is echter enkel het adres. hier word namelijk enkel de inhoud en niet het adres, vandaar dat op ln28 nog een ldr staat om de waarde eruit te halen.

Als je je afvraagt waarom de 2 loads een andere syntax hebben, dat komt omdat de eerste ldr gebruikt word om het symbolische adres van add_of_myvar1 de waarde leest in addressing mode. Dus in het 2e geval gaan we de waarde van r1 als adres gebruiken. In het eerste geval weten we niet welke adressing mode de assembler gebruikt, dus deze word genegeerd.

Als je dit compiled en uitvoert zal je 7 terugzien.

Store

Neem nu het vorige voorbeeld, maar in plaats van de waarden op 3 en 4 te zetten, zetten we ze nu op 0. We zullen dezelfde code gebruiken om de waarden 3 en 4 terug te geven in de assembler

/* -- store01.s */
 
/* -- Data section */
.data
 
/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar1 */
myvar1:
    /* Contents of myvar1 is just '3' */
    .word 0
 
/* Ensure variable is 4-byte aligned */
.balign 4
/* Define storage for myvar2 */
myvar2:
    /* Contents of myvar2 is just '3' */
    .word 0
 
/* -- Code section */
.text
 
/* Ensure function section starts 4 byte aligned */
.balign 4
.global main
main:
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    mov r3, #3             /* r3 ← 3 */
    str r3, [r1]           /* *r1 ← r3 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    mov r3, #4             /* r3 ← 4 */
    str r3, [r2]           /* *r2 ← r3 */
 
    /* Same instructions as above */
    ldr r1, addr_of_myvar1 /* r1 ← &myvar1 */
    ldr r1, [r1]           /* r1 ← *r1 */
    ldr r2, addr_of_myvar2 /* r2 ← &myvar2 */
    ldr r2, [r2]           /* r2 ← *r2 */
    add r0, r1, r2
    bx lr
 
/* Labels needed to access data */
addr_of_myvar1 : .word myvar1
addr_of_myvar2 : .word myvar

Als je dit uitvoert zul je 7 te zien krijgen.

Dat was alles voor deel 3.

RPI Assembler tutorial 2

Een cpu is in eigenlijk een heel krachtig rekentoestel. Deze bewerkingen kunnen enkel uitgevoerd worden met data opgeslagen in kleine opslagplaatsen genaamd registers. De RPI ARM cpu heeft 16 integer (gehele) registers, en 32 floating point (kommagetallen) registers. Een cpu gebruikt deze register om integer en floating-point bewerkingen uit te voeren. Floating point registers zal ik later op terug komen.

De 16 integer registers in ARM hebben de namen r0 tot R15. Deze kunnen 32 bits data houden. Deze kunnen in principe ook andere data bevatten (met een andere encoding) maar we zullen er hier van uit gaan dat er integers in zitten.

Niet alle registers van r0 tot r15 zijn dezelfde, sommige hebben speciale functies (zoals r0 die de error code bevat op het einde). Hier zullen we er van uitgaan dat dit niet uit maakt.

Basisbewerkingen

Bijna elke cpu kan basisbewerkingen doen met integerregisters. Net als ARM cpu’s. Zo kan je 2 register aan elkaar toevoegen (ADD). Laat ons hier het voorbeeld uit deel 1 uit halen. Als we het een beetje aanpassen ziet het er zo uit:

/* -- sum01.s */
.global main
 
main:
    mov r1, #3      /* r1 ← 3 */
    mov r2, #4      /* r2 ← 4 */
    add r0, r1, r2  /* r0 ← r1 + r2 */
    bx lr

In r1 zetten we 3 en in r2 zetten we 4. Deze tellen we op en zetten we in r0. Als we dit compilen (zie deel 1) en uitvoeren (ook, zie deel 1) dan zullen we 7 terugkrijgen zoals verwacht (want 3(r1)+4(r2)=7)

Dit was alles voor deel 2.

RPI Assembler tutorial 1

Het is waarschijnlijk veel nuttiger om een ‘high-level’ programeertaal te leren dan architectuurspecifieke assembler, maar het geeft wel een zeer goed idee in wat er in de processor gebeurt.

ARM op de RPI

ARM is een 32-bit architectuur dat als doel flexibiliteit heeft. Het is handig voor mensen die de vrijheid willen in het ontwerpen van hun eigen hardware, maar niet voor systeemontwikkelaars, die met de verschillen van ARM moeten werken.

Ik gebruik een RPI3B. Er zijn een aantal verschillen met ARM op de raspberrypi en de ‘standaard’ ARM assember. Dit word beter uitgelegd op de ARM website!

Assembler schrijven

Assembler is (voor alle architectuuren) een dunne laag boven de binaire code. binaire code is waarnaartoe je een programma compiled.

Als je assembler schrijft (ARM assembler) kan je computer dat niet uitvoeren. Hiervoor moet je daar de binaire code voor hebben. Dit kunnen we doen met een compiler (voor assembler ook wel assembler genoemd, omdat die de assembler ‘assembleerd’). Die binaire code, wat ook wel een executable wordt genoemd kan dan worden uitgevoerd op een computer (de rpi in dit geval).

Hiervoor kunnen we de GNU assembler gebruiken die in de GCC (Gnu Compiler Colection) zit die standaard op een raspberrypi is geinstaleerd.

Als je een text-editor opent (nano, emacs of vim in een terminal, leafpad of geany in een Desktop omgeving) kan je de bronbestanden (sourcefiles) openen. Deze hebben de extentie ‘.s’ Dat is de standaard voor ARM assembler

Het eerste programma

We moeten ergens beginnen, dus laat ons beginnen met een programmatje dat niets meer doet dan een error-code weergeven. Ik gebruik meestal engelstaalige comments. (die beginnen met /* en eindigen met */ in assembler)

/* -- first.s */
/* This is a comment */
.global main /* 'main' is our entry point and must be global */

main:          /* This is main */
    mov r0, #2 /* Put a 2 inside the register r0 */
    bx lr      /* Return from main */

sla dit op als ‘first.s’

om het te ‘assembleeren’ kun je:

as -o first.o first.s

gebruiken. Nu moet je het nog linken om een executable te krijgen:

gcc -o first first.o

nu kan je het uitvoeren

./first ; echo $?

Je zult normaal error code 2 moeten zien (of gewoon 2). Dat is die #2 die in de op de voorlaatste regel staat.

Alleen is code compilen met zo veel stappen niet echt handig dus kan je hiervoor een makefile gebruiken

# Makefile
all: first
 
first: first.o
    gcc -o $@ $+
 
first.o : first.s
    as -o $@ $<
 
clean:
    rm -vf first *.o

Uitleg bij wat er net gebeurd is

de eerste 2 regels zijn comments. deze worden gebruikt om weer te geven wat een stukje code doet, en worden niet uitgevoerd (de compiler negeert deze)

.global main /* 'main' is our entry point and must be global */

dit is iets wat de compiler vertelt dat het iets speciaals is. Het begint met een punt (.) en word gevolgd door de naam van de functie. .global moet wel, omdat de C runtime de main functie anders niet aanroept.

main:

Dit is een label. Elk label is opgemaakt als: ‘label: instructie’ Dit kan gesplitst worden over 2 regels (zoals hier)

mov r0, #2 /* Put a 2 inside the register r0 */

Dit is de instructie de 2 in register 0 zet. Dit overschrijft wat er in register 0 staat op dat moment.

bx lr      /* Return from main */

Dit betekend Branch Exchange. Dit beindigd ons programma

De error code is wat in r0 is opgeslagen. dit moet r0 zijn.

Dit was alles voor deel 1.