#Klooienmetcomputers

Modulair bouwen

Arnout van Kempen over rommelen in een digitale wereld.

We gaan dit keer een paar kleine verbeteringen aanbrengen in de code van vorige keer. Die kleine verbeteringen hebben behoorlijk wat impact op de code overigens. In de tekst zal ik de gebruikte bestanden aanduiden als main.asm en utils.asm, op GitHub zijn ze terug te vinden met de iets langere namen 110-modulair-*.asm.

Wat gaan we allemaal doen?

1.De code voor het omzetten van een registerwaarde naar een ASCII-gecodeerd getal en het afdrukken daarvan via stdout, gaat naar een subroutine, die we vervolgens vaker kunnen aanroepen. In de originele versie verliest x5 zijn waarde bij het omzetten naar ASCII, dus we gaan in deze routine x5 op de stack plaatsen, om hergebruik mogelijk te maken.

2.We voegen de mogelijkheid toe naast decimale output ook voor hexadecimaal te kiezen en dus de cijfers A - F te gebruiken. Octaal en binair zijn al mogelijk.

3.Om de leesbaarheid te vergroten groeperen we decimale output in drie cijfers en overige talstelsels in vier cijfers. Waarom? Omdat drie cijfers gebruikelijk zijn voor ‘normale’ getallen, maar vier cijfers beter aansluiten op de lengte van bytes en words en dus voor programmeurs logischer zijn.

4.De routine voor het weergeven van een getal zetten we in een apart bestand, utils.asm en het hoofdprogramma komt in main.asm. Dat betekent dat we zonder moeite de routine(s) die we in utils.asm plaatsen kunnen hergebruiken in andere programma's, zonder opnieuw te assembleren, en het houdt main.asm meteen leesbaarder. Overigens zou het dan helemaal netjes zijn om alle lokaal gebruikte registers eerst op de stack te plaatsen, zodat ze ook echt lokaal functioneren, maar ik vond het wel even mooi zo.

5.De operands van het hoofdprogramma, x3 en x4, zijn 64 bits groot, maar kunnen met mov slechts per 16 bits tegelijk gevuld worden. Daarom heb ik in de broncode zowel een voorbeeld met mov opgenomen, als met vier movz/movk opdrachten om een register word voor word te vullen.

6.Voor de terminal kan het ontbreken van een afsluitende newline tot een waarschuwing leiden, daarom voegen we direct voor de exit nog een write met een newline toe.

De complete uitwerking volgt aan het einde en staat op GitHub, nu per punt een aantal aandachtspunten:

Ad 1: Het aanroepen van een subroutine gaat met de instructie bl <label>, de instructie is een afkorting van branch with link. De waarde van <label> wordt net als bij een normale b in de pc geplaatst, maar eerst wordt de huidige waarde+1 van de pc in register x30, ook wel het link register geplaatst. Aan het einde van een subroutine komt de instructie ret, wat staat voor return. Deze instructie haalt de waarde uit het link register en plaatst die in de pc. Effectief betekent dat dat bl een call doet naar een subroutine en ret terugspringt naar de instructie direct na de call. Het betekent overigens ook dat een call vanuit een subroutine naar een andere subroutine even wat aandacht vraagt!

Ad 2: Voor talstelsels van 10 of lager (octaal = 8, binair = 2) werkt de code die hadden al goed. De routine deelt de waarde van x5 door het grondtal, en de rest is het minst significante cijfer. Door bij die rest de ascii-waarde van '0' op te tellen levert dat altijd het goede cijfer op, want de rest kan nooit lager zijn dan 0, en nooit hoger dan 9. Maar wat als we een hexadecimaal talstelsel gebruiken, zoals in de wereld van computers nogal populair? De rest na deling ligt dan tussen de 0 en de 15, en dat gaat mis met ascii. De extra cijfers in hexadecimale notering zijn A voor 10, tot en met F voor 15. Maar in de ascii-codering volgt de A niet op de 9. We voegen dus een test in of de rest groter is dan 9. En als dat zo is, verwijderen we de stap naar '0', voegen een stap naar 'A' toe en verlagen dat geheel weer met 10. Neem als voorbeeld de restwaarde 12. De eerste omzetting naar ascii maakt daar 12+'0' van en dat klopt niet. De correctie wordt 12+'0'-'0'+'A'-10 = 12-10 + 'A' = 2+'A' = 'C' en dat is inderdaad het hexadecimale cijfer voor de decimale waarde 12.

Ad 3: Deze stap lijkt simpel en voor mensen is dat ook zo. Maar om te programmeren is het iets lastiger. Gelukkig werkt groepering van cijfers wel van achter naar voren en dat was ook al de richting waarin we een getal moeten omzetten naar ascii. Extra complicatie is dat we de groepering moeten laten afhangen van de vraag of we converteren naar decimaal (groepen van 3) of overig (groepen van 4). We bereiken het resultaat door een teller mee te laten lopen van het aantal verwerkte cijfers. Iedere keer als die teller deelbaar is door 3 of 4, afhankelijk van het talstelsel, voegen we een spatie toe. Alleen als het hele getal al verwerkt is, komt er geen spatie, aangezien we dan een spatie vóór het getal zouden krijgen, en zo schrijven we nu eenmaal geen getallen. Wie overigens liever een punt invoegt dan een spatie, kan dat natuurlijk doen.

Ad 4: het scheiden van de code die specifiek is voor een programma en de code die herbruikbaar is voor meerdere programma's kennen we al uit C. In assembly werkt het vrijwel hetzelfde. Het hoofdprogramma moet, in Linux althans, een label _start hebben dat voor het gehele programma zichtbaar moet zijn. Dat betekent dat we dat label met de assembler-directive .global moeten definiëren. Hierdoor wordt het zichtbaar voor de linker, in objdump, en in gdb. Andere modules mogen geen label _start bevatten. Maar in andere modules moet wel alles dat zichtbaar moet zijn in andere modules via .global zichtbaar worden gemaakt voor de linker. In ons voorbeeld betekent dat dat het start-label van de routine, to_ascii wel global moet worden gedeclareerd, maar de andere labels, inclusief de databuffer die we gebruiken, niet. Het is essentieel goed te begrijpen dat labels in assembly globaal of lokaal zijn ten opzichte van het source-bestand, net als variabelen in talen als C, maar registers niet. Registers zijn altijd globaal. Wil je registers lokaal gebruiken, dan zal je ze aan het begin van je routine op de stack moeten plaatsen, en aan het eind er weer af moeten halen. De stack in ARM64 werkt regelt vrij weinig zelf. Linux geeft je een startadres voor de stack, maar daarna zal je zelf de stackpointer, sp, moeten verlagen, in stappen van 16 bytes, zelf een of meer registers op de stack plaatsen en na gebruik er weer afhalen en de stankpointer weer correct verhogen.
Overigens, het assembleren en linken met meerdere modules werkt feitelijk precies hetzelfde als met een enkel sourcebestand:

as -o utils.o utils.s          # Assembleer de utils
as -o main.o main.s            # Assembleer het hoofdprogramma
ld -o program main.o utils.o   # Link beide objectbestanden tot een uitvoerbaar bestand

Als je iets verandert aan een van deze bronbestanden, assembleer je alleen dat bronbestand opnieuw, en link je vervolgens weer alle object-bestanden tot een nieuwe executable.

Ad 5: Ik heb dit eerder besproken, dus ik verwijs naar de eerdere aflevering of naar de broncode.

Ad 6: Dit is vrij simpel, bij de exit is een write met een newline toegevoegd.

Hoe zit dat er nu uit? We krijgen dus twee bestanden, hier zonder toelichting, op GitHub staat de toelichting er bij:

main.asm

.section .data

newline: .ascii “\n”

.section .text

.global _start

_start:
     movz x3, 0xFFFF lsl 0
     movk x3, 0xEEEE lsl 16
     movk x3, 0xDDDD lsl 32
     movk x3, 0xCCCC lsl 48

     mov x4, 6000

     add x5, x3, x4

     mov w9, ‘\n’

     mov x7, 10
     bl to_ascii

     mov x7, 16
     bl to_ascii

     mov x7, 8
     bl to_ascii

     mov x7, 2
     bl to_ascii

     mov x0, 1
     ldr x1, =newline
     mov x2, 1
     mov x8, 64
     svc 0

     mov x0, 0
     mov x8, 93
     svc 0

utils.asm

.section .data

buffer: .space 81

.section .text

.global to_ascii

to_ascii:
     sub sp, sp, 16
     str x5, [sp]

     ldr x1, =buffer
     add x1, x1, 80
     strb w9, [x1]
     sub x1, x1, 1
     mov x2, 2
     mov w10, 0

to_ascii_loop:
     udiv x6, x5, x7
     msub x6, x6, x7, x5
     cmp x6, 9
     add x6, x6, ‘0’
     b.le store_digit
     add x6, x6, ‘A’-‘0’-10

store_digit:
     sub x1, x1, 1
     strb w6, [x1]
     add x2, x2, 1
     add w10, w10, 1

     mov w11, 4
     cmp x7, 10
     b.ne group_digits
     mov w11, 3

group_digits:
     udiv w12, w10, w11
     msub w12, w12, w11, w10
     cbnz w12, skip_space

     udiv x12, x5, x7
     cbz x12, skip_space

     sub x1, x1, 1
     mov w6, ‘ ‘
     strb w6, [x1]
     add x2, x2, 1

skip_space:
     udiv x5, x5, x7
     cbnz x5, to_ascii_loop

     mov x0, 1
     mov x8, 64
     svc 0

     ldr x5, [sp]
     add sp, sp, 16

     ret

Wie mee wil doen met #klooienmetcomputers kan dat doen via GitHub. Maak een account op github.com en zoek naar Abmvk/kmc. Het account Abmvk volgen kan ook. Lezers zijn vrij te gebruiken wat ze willen en om zelf zaken toe te voegen of aan te passen, vragen te stellen of commentaar te leveren.

Arnout van Kempen di CCO CISA is directeur compliance & risk bij aaff, de fusieorganisatie van Alfa en ABAB. Hij schrijft op persoonlijke titel.

Gerelateerd

reacties

Reageer op dit artikel

Spelregels debat

    Aanmelden nieuwsbrief

    Ontvang elke werkdag (maandag t/m vrijdag) de laatste nieuwsberichten, opinies en artikelen in uw mailbox.

    Bent u NBA-lid? Dan kunt u zich ook aanmelden via uw ledenprofiel op MijnNBA.nl.