#Klooienmetcomputers

Een echt project!

Arnout van Kempen over rommelen in een digitale wereld.

Dat programmaatje van vorige keer werkte wel, maar het was wel slordig. Een app hoort een exitcode mee te geven waarmee het aanroepende programma, vaak Linux zelf, kan zien of het goed is gegaan. Dus je geeft een 0 mee, of een errorcode. Wij misbruikten die exitcode om de uitkomst van een optelling door te geven. Dat moet natuurlijk anders kunnen.

Om dat te doen gaan we om te beginnen wat serieuzer nadenken over ons programma, voordat we aan het coderen slaan. In de praktijk werkt dat vrijwel altijd beter dan wat proberen, bijschaven en kijken of het werkt.
We maken ons nog niet druk om de functionaliteit van ons programma. Het blijft dus even bij optellen van twee vooraf vastgestelde getallen. Dat is vrij simpel, lastiger is hoe we dat getal in beeld krijgen op een nette manier. Linux geeft ons geen directe toegang tot hardware, dus we zullen een system call, svc 0, moeten doen om Linux onze resultaten op het scherm te laten zetten. De Linux call daarvoor is 64, met in X0 het bestand waar we naar willen schrijven, in X1 het adres van de buffer waarin de te schrijven bytes staan en in X2 het aantal te schrijven bytes.

We zagen al eerder dat in Linux alles een bestand is, dus het scherm ook. Willen we naar het scherm schrijven, dan is veelal de beste keuze om naar STDOUT te schrijven, zodat piping en redirection ook eenvoudig zijn. In X0 komt dus de waarde 1, want dat is de filedescriptor van STDOUT. We hebben nu alleen nog een buffer nodig en een lengte van het weer te geven getal. Klein probleem is wel dat Linux geen getallen kan weergeven, alleen bytes. Als we onze uitkomst willen weergeven, zullen we die dus eerst moeten omzetten naar ascii-characters. Zolang we ons beperken tot een decimale weergave is omzetting van een cijfer naar een karakter eenvoudig. We tellen simpelweg de waarde‘0’op bij de decimale waarde van ons cijfer. Aangezien de cijfers in ascii-code achter elkaar staan, levert dat dus de correcte ascii-code op.
Maar dan. Onze uitkomst kan natuurlijk groter zijn dan één cijfer. Dat betekent dat we de uitkomst cijfer voor cijfer moeten omzetten in ascii-characters en deze achter elkaar moeten plaatsen in een buffer, die vervolgens via system call 64 aan Linux kan worden aangeboden.

Hoe splits je een decimaal getal op de cijfers waar het uit bestaat? Je deelt het getal door tien en de rest is het rechter cijfer. Bijvoorbeeld: 123/10 =12 rest 3. Als je dit herhaalt, krijg je vervolgens 12/10 =1 rest 2, 1/10 =0 rest 1. Met die laatste nul weet je dat je alle cijfers hebt gehad.

De ARM heeft hiervoor twee nuttige instructies, waarvan één wellicht wat onverwacht complex:

udiv doel, teller, noemer     unsigned integer deling
msub doel, n1, n2, n3         doel = (n1 * n2) - n3

Die tweede instructie is in feite een efficiënte uitvoering van twee losse instructies. Opvallend wellicht is, dat de instructie eerst vermenigvuldigt en daarvan de derde operand aftrekt. In ons voorbeeld levert dat de eerste keer (12*10)-123=-3 op en niet de gewenste 3. Maar omdat we een udiv gebruikten, weet de CPU dat we met unsigned waarden werken en wordt het minteken genegeerd. De uitkomst is dus toch de gewenste 3.

We kunnen nu een loop maken waarin een register steeds door 10 gedeeld wordt en we de rest wegschrijven als ascii-character in een buffer. De loop herhaalt tot de uitkomst van de deling nul is. Dan schrijven we nog eenmaal de rest weg en zijn we klaar.
Maar let op, als we een buffer zo vullen van begin tot eind, dan wordt onze uitkomst achterstevoren weggeschreven. Immers, het delen door 10 zorgt dat het minst significante cijfer, ofwel het meest rechter cijfer, in de rest terecht komt. Dat cijfer moet dus ook weer achterin de buffer komen, waarna de pointer naar het volgende cijfer zal moeten worden verlaagd.

Hoe groot moet de buffer eigenlijk zijn? Als we de inhoud van een Xn-register willen kunnen weergeven, dan is het grootst mogelijke getal een unsigned 64-bits integer, dus maximaal 2^64-1=18.446.744.073.709.551.615.
Onze buffer moet dus twintig decimale cijfers kunnen bevatten. Dat is nu wellicht wat veel, maar als we in de toekomst deze code willen hergebruiken, kan het geen kwaad de zaak alvast ruim op te zetten. Op een later moment gaan we daartoe een bestand maken met veel gebruikte basisfuncties, vergelijkbaar met de eigen header-file die we ooit in C maakten.

Wat moeten we nu verder nog bedenken? De CPU geeft ons een flinke set registers, daar moeten we nog wat keuzes in maken. Het is daarbij wel handig om achteraan te beginnen, dus bij de system call.

Voor de exit call hebben we x8 nodig met de waarde 93, en x0 met een exit-code. De  write call gebruikt x8 ook om de call aan te duiden, dit keer met 64. Dan hebben we nog x0 voor de file-descriptor (1 voor STDOUT), x1 voor het adres van de buffer en x2 voor het aantal bytes.

Dat betekent dat het handig zou zijn als we bij het vullen van de buffer ook x1 gebruiken en dat we x0, x2 en x8 verder niet gebruiken als dat niet nodig is. Dan hebben we nog registers nodig voor onze oorspronkelijke berekening, dus twee operands en een uitkomst. Willen we het omzetten van een getal naar ascii-characters flexibel maken, dan kunnen we beter niet met een immediate waarde voor het talstelsel werken, maar hier ook een register voor gebruiken. Dat zou bijvoorbeeld kunnen betekenen:

x0   reserveren voor de write en exit calls
x1   bufferpointer
x2   teller bij verlagen van de bufferpointer, dus bufferlengte
x3   operand 1
x4   operand 2
x5   resultaat van berekening(x3, x4)
x6   hulpvariabele voor berekening rest (x5/10)
x7   talstelsel
x8   code voor system call

Een programma dat deze opzet volgt, zou er dan zo uit kunnen zien:

.section .data
buffer: .space 21        // 21 bytes voor de ASCII-buffer

.section .text
.global _start

_start:
    // 1. Initialiseer operanden
    mov x3, 10           // Operand 1 (x3)
    mov x4, 20           // Operand 2 (x4) 

    // 2. Bereken de som
    add x5, x3, x4       // x5 = x3 + x4 (Resultaat)

    // 3. Zet bufferpointer naar einde van buffer
    ldr x1, =buffer      // Laad bufferadres in x1
    add x1, x1, #20      // Zet x1 naar het einde van de buffer
    mov w6, #’\n’        // sluit getal af met newline
    strb w6, [x1]

    // 4. Zet resultaat om naar ASCII (achterwaarts vullen)
    mov x2, #1           // x2 houdt de lengte van de string bij
    mov x7, #10               // Talstelsel = 10

to_ascii:
    udiv x6, x5, x7      // x6 = x5 / x7 (Quotient)
    msub x6, x6, x7, x5  // x6 = x5 - (x6 * x7) (Rest)
    add x6, x6, #'0'          // Zet rest om naar ASCII ('0' = 48)
    sub x1, x1, #1       // Verplaats bufferpointer naar links
    strb w6, [x1]        // Sla ASCII-karakter op in buffer
    add x2, x2, #1       // Verhoog stringlengte
    udiv x5, x5, x7           // Update x5 naar quotiënt
    cbnz x5, to_ascii    // Herhaal zolang quotiënt niet 0 is

    // 5. Schrijf naar stdout
    mov x0, 1            // File descriptor (stdout)
    mov x8, 64           // System call nummer voor write
    svc 0                // Voer system call uit

    // 6. Sluit programma af
    mov x0, 0            // Exit code (0 = succesvol)
    mov x8, 93           // System call nummer voor exit
    svc 0                // Beëindig programma

Bovenstaand programma staat, met enkele kleine aanpassingen, op GitHub. De liefhebbers kunnen nog op zoek naar een verklaring van de verschillen.

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.