Teil 26 „Using machine code“

Der C-Kurs
Antworten
Benutzeravatar
bodo
User
Beiträge: 319
Registriert: 14.02.2007, 17:21
Kontaktdaten:

Teil 26 „Using machine code“

Beitrag von bodo » 02.01.2011, 16:57

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 26

Dieser Teil ist sicher nach dem Geschmack der ganzen Bitpfriemler, die sich mit Assembler auskennen: Kapitel 26 heißt bei Steven Vickers „Using machine code“. Und wir werden hier halbwegs ausloten, wie wir Assembler und C miteinander benutzen können.

Inline-Assembler

Das z88dk bietet uns zwei Alternativen für Assembler direkt im C-Quelltext an:
  • Zwischen den Präprozessoranweisungen „#asm“ und „#endasm“ können wir beliebigen Assemblerquelltext schreiben. Dieser wird sogar noch von den Optimierern behandelt – wenn ihr das nicht wollt, müsst ihr die unten beschriebene dritte Variante über ein eigenes Modul wählen.
  • Der Assemblerquelltext wird als String an asm() übergeben. Dies ist kein wirklicher Aufruf einer Funktion, sondern wohl eher eine Art Makro des Präprozessors. Der String kann auch mehrere Zeilen enthalten, die dann aber auch korrekt mit '\t' (Tabulator) und '\n' (Zeilenende) formatiert sein sollten!
Ein simples Beispiel zeigt die Anwendung. Das Programm setzt und liest das I-Register, das bekanntermaßen für die Bildschirmausgabe benutzt wird. Nach dem Setzen auf 0x0E wird auf dem Bildschirm also „Schrott“ erscheinen, erschreckt bitte nicht!

Code: Alles auswählen

#include <stdio.h>
#include <stdlib.h>

static unsigned char lies_i(void) {
#asm
    ld  a,i
    ld  h,0
    ld  l,a
#endasm
}

static void setze_i(unsigned char wert) {
#asm
    ld  hl,2
    add hl,sp
    ld  a,(hl)
    ld  i,a
#endasm
}

int main(void) {
    unsigned char old_i;

    old_i = asm("ld\ta,i\nld\th,0\nld\tl,a\n");
    printf("I ENTHAELT %02x\n", old_i);

    asm("ld\ta,0x0E\nld\ti,a\n");

    sleep(5);

    printf("I ENTHAELT %02x\n", lies_i());
    setze_i(old_i);

    return 0;
}
Etwas befremdlich ist die Warnung, die der Aufruf von setze_i() auslöst:

Code: Alles auswählen

L:28 Warning:#33:Call to function without prototype
Aber das fertige Programm ist korrekt...

Die Pedanten werden jetzt anmerken, dass beim Lesen des I-Registers ein 16-Bit-Wert erzeugt wird. Nun, hier scheint die sogenannte Integer-Promotion (C erweitert alle Ganzzahlberechnungen grundsätzlich auf die Breite eines int. Gekürzt wird allerdings nicht, das wäre bei long-Berechnungen ja kontraproduktiv.) von z88dk nicht ganz korrekt zu sein. Jedenfalls müssen wir die oberen 8 Bits wirklich selbst löschen...

Eingabe und Ausgabe

Natürlich können wir auf diese Art auch Funktionen oder Makros für die Z80-Befehle zur Ein- und Ausgabe schreiben. Aber das haben die z88dk-Macher bereits für uns erledigt, in der „stdlib.h“ sind folgende Funktionen deklariert:

Code: Alles auswählen

unsigned int inp(unsigned int port);

void outp(unsigned int port, unsigned char byte);
Die Makrovarianten dazu heißen M_INP und M_OUTP und werden genauso angewendet. Sie haben den Vorteil, dass kein Funktionsaufruf erzeugt wird, sondern der Maschinencode direkt an der Stelle des Aufrufs eingefügt wird. Allerdings müssen die Parameter („port“ und „byte“) dazu Konstanten sein!

Die Portadressen sind übrigens 16 Bits breit, wie der Datentyp bereits anzeigt.

Module in Assembler

Es gibt noch eine dritte Variante, Assembler und C miteinander zu verbinden, indem wir ein Modul komplett in Assembler schreiben. Dann müssen wir aber verschiedene Konventionen einhalten.

Zunächst müssen die Namen der globalen Funktionen mit XDEF exportiert werden. Dabei müssen sie mit einem Unterstrich beginnen, der vom C-Compiler verwendet wird, um keinen Konflikt mit reservierten Namen zu bekommen.

Wenn wir eine C-Bibliotheksfunktion aus dem Assemblermodul heraus aufrufen, müssen wir deren Namen auch mit LIB deklarieren. Solche Funktionen besitzen einen Namen ohne zusätzlichen Unterstrich – tja, so ist das z88dk nun einmal...

Wenn wir Parameter in Registern übergeben haben möchten statt auf dem Stack, muss der Funktionsprototyp auch das Schlüsselwort __FASTCALL__ erhalten.

So sieht ein solches Modul beispielsweise aus, die Funktion mirror() soll den übergebenen Parameter bitweise spiegeln:

Code: Alles auswählen

XDEF    _mirror         ;Startadresse bekanntgeben

LIB     puts            ;als Bibliotheksfunktion deklarieren

; unsigned short __FASTCALL__ mirror(unsigned short);

_mirror:
        ld b,#8
loop1:
        add hl,hl
        rra
        djnz    loop1   ;die ersten 8 Bits spiegeln

        ld      l,a     ;halbes Ergebnis in L retten

        ld      b,#8
loop2:
        add hl,hl
        rra
        djnz    loop2   ;die zweiten 8 Bits spiegeln

        ld      l,h
        ld      h,a     ;ganzes Ergebnis zusammensetzen

        push hl

        ld      hl,text
        push    hl
        call    puts
        pop     hl      ;Fertigmeldung ausgeben

        pop     hl
        ret

text:
        defm    "FERTIG.",0
Natürlich schreiben wir den Funktionsprototypen in eine Headerdatei, die dann vom benutzenden C-Programm per #include eingefügt werden kann:

Code: Alles auswählen

extern unsigned short __FASTCALL__ mirror(unsigned short value);
Das Beispiel wäre nicht vollständig ohne das Testprogramm:

Code: Alles auswählen

#include <stdio.h>
#include "CfBASIC_26-2a.h"

int main(void) {
    printf("%x GESPIEGELT = %x\n", 0x1248, mirror(0x1248));
    printf("%x GESPIEGELT = %x\n", 0x3DE6, mirror(0x3DE6));

    return 0;
}
Erweiterungen gegenüber C

Um Assemblerfunktionen noch schöner mit C-Funktionen zu mischen, haben die Autoren des z88dk sich drei neue Schlüsselworte einfallen lassen:
  • return_c Arbeitet wie return, setzt aber das Carry-Flag.
  • return_nc Arbeitet wie return, löscht aber das Carry-Flag.
  • iferror Dies ist am besten vergleichbar mit „if (Carry-Flag)“.
Auch hier hilft sicher ein kleines Beispiel, das sogar gar keinen Assemblerquelltext besitzt, es geht auch sehr schön ohne.

Code: Alles auswählen

#include <stdio.h>

static void ist_null(int wert) {
    if (wert == 0) {
        return_c;
    } else {
        return_nc;
    }
}

static void test(int wert) {
    ist_null(wert);
    iferror {
        printf("%d IST NULL.\n", wert);
    } else {
        printf("%d IST NICHT NULL.\n", wert);
    }
}

int main(void) {
    test(0);
    test(81);

    return 0;
}
Wenn ihr euch den erzeugten Maschinencode anseht (Option „-a“, siehe Teil 8), werdet ihr feststellen, dass diese Erweiterungen sogar schnelleren Code erzeugen als die übliche Variante mit einer kleinen Ganzzahl als boolschem Wert.
B0D0: Real programmers do it in hex.

Antworten