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.
Gerelateerd

Einde van deze rubriek
Arnout van Kempen besluit met aflevering 112 voorlopig zijn reeks van wekelijkse bijdragen over 'rommelen in een digitale wereld': het zelf leren programmeren, verkennen...

Variabele input
Arnout van Kempen over rommelen in een digitale wereld.

Tools voor debugging
Arnout van Kempen over rommelen in een digitale wereld.

Een echt project!
Arnout van Kempen over rommelen in een digitale wereld.

Een eerste toepassing
Arnout van Kempen over rommelen in een digitale wereld.