Teil 25 „How the computer works“

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

Teil 25 „How the computer works“

Beitrag von bodo » 02.01.2011, 16:13

C für BASIC-Programmierer

Arbeiten mit dem z88dk Cross-Compiler

von Jens Sommerfeld und Bodo Wenzel

Teil 25

Im Kapitel 25 erklärt Steven Vickers „How the computer works“, also wie die einzelnen Schaltkreise im Zeddy zusammen werkeln, um schließlich bei PEEK und POKE zu landen. Wir dagegen münzen das um auf die Software, die ja auch in Teilen geschrieben werden kann.

Wir bringen euch deshalb heute näher, wie ihr ein größeres Programm modularisieren könnt. Dabei wird euch auch klar, wozu es überhaupt einen Linker gibt, den wir in Teil 8 erwähnt haben. Und natürlich zeigen wir, wie ihr PEEK und POKE in C realisiert.

Wozu modularisieren?

Bisher haben wir eher triviale Programme geschrieben, die selten mehr als eine Bildschirmseite hoch waren. Dies ist aber der Sonderfall, denn die meisten „richtigen“ Programme sind deutlich größer.

Wenn ihr an ein ausreichend komplexes Programm denkt, könnt ihr sicher einzelne Teile identifizieren, die thematisch zusammengehören. Dies können z.B. das Einlesen von Benutzereingaben, die Ausgabe auf dem Bildschirm, die Implementierung von Algorithmen, oder Konvertierungsfunktionen sein.

In BASIC-Programmen habt ihr das sicher dadurch gelöst, dass ihr die entsprechenden Unterprogramme gruppiert habt, vielleicht sogar mit derselben Tausenderziffer in der Zeilennummer gekennzeichnet. In C lohnt es sich dagegen, zusammengehörende Funktionen in eine eigene Quelltextdatei zu schreiben. Das so entstandene Modul kann allein compiliert werden, und es kann auch in mehreren unterschiedlichen Programmen benutzt werden.

Für ein Gesamtprogramm wählt ihr dann die Module aus, die gebraucht werden, und „bindet“ sie zusammen. Und genau das ist die Aufgabe eines Linkers: to link = verbinden.

Auf diese Art und Weise schafft ihr euch eine Sammlung nützlicher Module. Genau das ist auch bei der Standardbibliothek von C geschehen. Dort ist häufig sogar jede einzelne Funktion als ein eigenes Modul compiliert worden. Weil aus einer Bibliothek nur die benötigten Funktionen zum Programm gebunden werden, wird ein Programm auch nur so groß wie nötig.

Warum nun aber dieser Zwischenschritt mit dem Compiler? Das stammt aus den Zeiten, als Computer noch klein und langsam waren. Das Compilieren dauert im Gegensatz zum Linken richtig lange, und auch der Quelltext ist meistens größer als der erzeugte Maschinencode. Um Zeit und Speicherplatz zu sparen, war es daher günstiger, im Voraus übersetzte Objektdateien (so heißen die Ergebnisse des Compilers oder Assemblers) zu verwenden.

Übrigens können auch Module, die in anderen Sprachen geschrieben wurden, gelinkt werden, wenn denn das Format der Objektdateien kompatibel ist. Beim z88dk bietet sich das für Module in Assembler an.

Ein Beispiel

Denken wir uns ein kleines Beispiel aus! Das Programm soll zwei Zahlen einlesen, sie addieren und das Ergebnis ausgeben. Zugegeben, das ist trivial, aber wir wollen ja keine Seiten schinden! :-)

Jeden dieser drei Schritte implementieren wir in einem eigenen Modul, und dann brauchen wir noch ein Hauptprogramm, das alles zusammen benutzt.

Dies ist das Modul „CfBASIC_25-1a.c“ zum Einlesen von Zahlen:

Code: Alles auswählen

#include <stdio.h>
#include "CfBASIC_25-1a.h"

int lies_zahl(char *aufforderung) {
    char zeile[10];
    int zahl;

    printf("%s: ", aufforderung);
    gets(zeile);
    puts("");
    sscanf(zeile, "%d", &zahl);

    return zahl;
}
Und so sieht das Modul „CfBASIC_25-1b.c“ zum Addieren aus:

Code: Alles auswählen

#include "CfBASIC_25-1b.h"

int addiere(int summand1, int summand2) {
    return summand1 + summand2;
}
Außerdem brauchen wir eine Ausgabe„CfBASIC_25-1c.c“ :

Code: Alles auswählen

#include <stdio.h>
#include "CfBASIC_25-1c.h"

void gib_zahl_aus(char *erklaerung, int zahl) {
    printf("%s = %d\n", erklaerung, zahl);
}
Schließlich setzen wir das in einem Programm „CfBASIC_25-1m.c“ zusammen:

Code: Alles auswählen

#include "CfBASIC_25-1a.h"
#include "CfBASIC_25-1b.h"
#include "CfBASIC_25-1c.h"

int main(void) {
    int zahl1;
    int zahl2;
    int summe;

    zahl1 = lies_zahl("ERSTE ZAHL");
    zahl2 = lies_zahl("ZWEITE ZAHL");
    summe = addiere(zahl1, zahl2);
    gib_zahl_aus("SUMME IST", summe);

    return 0;
}
Wenn ihr euch die Quelltexte genauer anseht, fallen euch #include-Zeilen auf, die in Anführungszeichen (Warum in Anführungszeichen statt in spitzen Klammern? Siehe Teil 9!) stehen. Und ihr fragt euch, wozu? Nun, zumindest im Fall des Hauptprogramms solltet ihr mit etwas Nachdenken die Antwort finden: die aufgerufenen Funktionen müssen dem Compiler ja vor dem Aufruf bekannt sein. Und das erledigen die eingefügten Headerdateien mit entsprechenden Prototypen:

Für das Einlesemodul enthält die Headerdatei „CfBASIC_25-1a.h“:

Code: Alles auswählen

extern int lies_zahl(char *aufforderung);
Für das Additionsmodul enthält die Headerdatei „CfBASIC_25-1b.h“:

Code: Alles auswählen

extern int addiere(int summand1, int summand2);
Und für das Ausgabemodul enthält die Headerdatei „CfBASIC_25-1c.h“:

Code: Alles auswählen

extern void gib_zahl_aus(char *erklaerung, int zahl);
Interessant ist, dass das z88dk das Schlüsselwort „extern“ unbedingt sehen will – nach dem Standard ist es nicht nötig, schadet aber auch nicht.

Der Grund für das Einfügen in den Modulen selbst ist ein professioneller: der Compiler erhält dann sowohl den Prototypen als auch die Implementierung jeder Funktion zum Übersetzen. Und so wird automatisch gemeckert, wenn der Prototyp in der Headerdatei nicht zur implementierten Funktion passt.

Jedes dieser Module kann jetzt einzeln compiliert werden, wie wir es im Teil 8 gelernt haben:

Code: Alles auswählen

zcc +zx81 -vn -Wall -c CfBASIC_25-1a.c
zcc +zx81 -vn -Wall -c CfBASIC_25-1b.c
zcc +zx81 -vn -Wall -c CfBASIC_25-1c.c
zcc +zx81 -vn -Wall -c CfBASIC_25-1m.c
Dabei entstehen zugehörige Objektdateien, jeweils mit der Dateierweiterung „.o“.

Und alles zusammenlinken!

Jetzt kann das Programm zusammengebunden werden:

Code: Alles auswählen

zcc +zx81 -vn -Wall -create-app -startup=2 CfBASIC_25-1m.o CfBASIC_25-1a.o CfBASIC_25-1b.o CfBASIC_25-1c.o -o CfBASIC_25-1.bin
Das erzeugt auch tatsächlich ein Programm; dummerweise funktioniert es nicht. Der Grund ist, dass beim z88dk jeder Aufruf von „zcc“ die Datei „zcc_opt.def“ im aktuellen Verzeichnis löscht und daher die dort gesammelten Einstellungen verloren gehen, die beim Linken nötig sind. Es gibt zwar die Option „-preserve“, die das Löschen verhindert, aber dann wächst die Datei immer weiter.

Daher könnt ihr euch das einzelne Compilieren sparen und gleich die Quelltexte auf der Kommandozeile angeben:

Code: Alles auswählen

zcc +zx81 -vn -Wall -create-app -startup=2 CfBASIC_25-1m.c CfBASIC_25-1a.c CfBASIC_25-1b.c CfBASIC_25-1c.c -o CfBASIC_25-1.bin
Und damit klappt es jetzt! Unsere heutigen Computer haben ja keine Engpässe, was Speicherplatz und Rechenleistung angeht, daher ist das vorherige Compilieren nicht wirklich zeitsparend.

Programmglobal, modulglobal und funktionslokal

Wir haben schon ab und zu über den Bereich gesprochen, in dem Namen von Funktionen und Variablen sichtbar sind. Dabei können wir drei grundsätzliche Bereiche unterscheiden:
  1. Programm-global: Solche Funktionen und Variablen sind im gesamten Programm bekannt. Ihre Deklarationen werden mit „extern“ gekennzeichnet und dürfen im implementierenden Quelltext kein „static“ haben. Variablen dieser Art können nicht innerhalb von Funktionen definiert werden.
  2. Modul-global: Diese Funktionen und Variablen sind nur in dem Quelltextmodul bekannt, in dem sie definiert werden. Dort allerdings sind sie in jeder Funktion bekannt. Sie werden mit „static“ markiert; solche Variablen müssen außerdem außerhalb der Funktionen definiert sein.
  3. Lokal: Dieser Bereich ist nur für Variablen möglich. Eine lokale Variable ist nur innerhalb des Blocks bekannt, in dem sie definiert wurde.
Ein Beispiel macht das sicher gleich klarer:

Code: Alles auswählen

/* Dies ist nur der Prototyp: */
extern void programm_globale_funktion(void);

/* Diese Funktion ist nur lokal im Modul bekannt. */
static void modul_globale_funktion(void);

/* Hiermit wird noch kein Speicherplatz angelegt! */
extern int programm_globale_variable;

/* Der Wert bleibt während der gesamten Programmlaufzeit erhalten. */
static int modul_lokale_variable;

/* Endlich, Speicher für die obige Variable: */
int programm_globale_variable;

/* Und die Implementierung der globalen Funktion: */
void programm_globale_funktion(void) {
    static int statische_lokale_variable;

    {
        int automatische_lokale_variable;

        modul_lokale_funktion();
    }
}
Durch diese Bereiche seid ihr in der Lage, für eure modul-globalen Funktionen und Variablen und alle lokalen Variablen beliebige Namen zu benutzen. Ihr braucht keine Verrenkungen zu machen, um Konflikte zu vermeiden. Selbst wenn ihr in jeder Funktion eine lokale Variable namens „index“ braucht, sind diese alle unabhängig voneinander!

Und das ist die Moral der Geschichte: reduziert die Sichtbarkeit auf das absolut Nötigste, und ihr werdet die wenigsten Probleme haben.

Bibliotheken nützlicher Funktionen

Jetzt habt ihr euch viele nette und nützliche Module für alle möglichen Zwecke geschrieben, aber es ist zu umständlich, für ein Programm immer genau die richtige Liste anzugeben. Wie wäre es denn, wenn der Linker selbst herauskriegt, welche Module für ein Programm hinzuzubinden sind? Das wäre doch prima, oder?

Und genau das geht auch! Dazu werden alle Module in einer oder mehreren Bibliotheken versammelt, die schließlich vom Linker durchsucht werden. Nichts anderes geschieht bereits seit Beginn des Kurses, wenn z.B. die Ein- und Ausgabefunktionen aus der C-Standardbibliothek verwendet werden.

Nehmen wir an, wir haben außer der Additionsfunktion (Datei „addiere.c“)...

Code: Alles auswählen

#include "CfBASIC_25-2-lib.h"

int addiere(int summand1, int summand2) {
    return summand1 + summand2;
}
... auch noch eine Subtraktionsfunktion (Datei „subtrahiere.c“)...

Code: Alles auswählen

#include "CfBASIC_25-2-lib.h"

int subtrahiere(int minuend, int subtrahend) {
    return minuend - subtrahend;
}
... und eine Multiplikationsfunktion (Datei „multipliziere.c“)...

Code: Alles auswählen

#include "CfBASIC_25-2-lib.h"

int multipliziere(int faktor1, int faktor2) {
    return faktor1 + faktor2;
}
... und schließlich eine Divisionsfunktion (Datei „dividiere.c“)...

Code: Alles auswählen

#include "CfBASIC_25-2-lib.h"

int dividiere(int dividend, int divisor) {
    return dividend / divisor;
}
Ja, schon klar, das Beispiel ist über alle Maßen trivial! Aber es geht ja nur ums Prinzip, eure Module können beliebig komplex sein, und sie dürfen sogar aufeinander aufbauen, also so, dass ihre Funktionen sich untereinander aufrufen.

So sieht die zugehörige Headerdatei „CfBASIC_25-2-lib.h“ aus (Das Schlüsselwort __LIB__ ist für das z88dk nötig...):

Code: Alles auswählen

extern int __LIB__ addiere(int summand1, int summand2);
extern int __LIB__ subtrahiere(int minuend, int subtrahend);
extern int __LIB__ multipliziere(int faktor1, int faktor2);
extern int __LIB__ dividiere(int dividend, int divisor);
Und das ist das Programm„CfBASIC_25-2m.c“, das eine Funktion aus der Bibliothek nutzt. Der Einfachheit halber werden außerdem die Ein- und Ausgabemodule des ersten Beispiels wiederverwendet:

Code: Alles auswählen

#include "CfBASIC_25-1a.h"
#include "CfBASIC_25-2-lib.h"
#include "CfBASIC_25-1c.h"

int main(void) {
    int zahl1;
    int zahl2;
    int differenz;

    zahl1 = lies_zahl("ERSTE ZAHL");
    zahl2 = lies_zahl("ZWEITE ZAHL");
    differenz = subtrahiere(zahl1, zahl2);
    gib_zahl_aus("DIFFERENZ IST", differenz);

    return 0;
}
Gut, damit habt ihr alle Quelltexte vor Augen. Compilieren wir zunächst alle Module, die in die Bibliothek sollen:

Code: Alles auswählen

zcc +zx81 -vn -Wall -make-lib addiere.c
zcc +zx81 -vn -Wall -make-lib subtrahiere.c
zcc +zx81 -vn -Wall -make-lib dividiere.c
zcc +zx81 -vn -Wall -make-lib multipliziere.c
Hier gilt es, eine (weitere!) Besonderheit des z88dk zu beachten: der Name der Quelltextdatei muss dem Namen der darin enthaltenen Funktion entsprechen. Und offenbar darf pro Quelltext auch nur eine programm-globale Funktion enthalten sein.

Für den Bau der Bibliothek rufen wir den Linker (in seiner z88dk-Erscheinung als Assembler) direkt auf; das Programm „zcc“ kennt anscheinend keine Möglichkeit, einen solchen Aufruf bequemer durchzuführen:

Code: Alles auswählen

z80asm -d -ns -nm -Mo -xCfBASIC_25-2-lib.lib addiere.o subtrahiere.o dividiere.o multipliziere.o
Die einzelnen Optionen könnt ihr auch in der Hilfe (durch Aufruf mit „z80asm -h“) nachlesen:
  • „-d“: Übersetzung nur, wenn Quelltext neuer als Objektcode; scheint nötig zu sein, die z88dk-Macher benutzen das auch.
  • „-ns“: Erzeugung einer Symboltabelle abschalten.
  • „-nm“: Erzeugung einer Map-Datei abschalten.
  • „-Mo“: Dateityp von Objektdateien, hier: „*.o“.
  • „-x...“: Eine Bibliothek mit dem angegebenen Namen soll aus den Modulen erzeugt werden.
So, jetzt haben wir eine Bibliothek namens „CfBASIC_25-2-lib.lib“. Dummerweise kann das z88dk kein beliebiges Verzeichnis (auch nicht das aktuelle!) nach Bibliotheken durchsuchen, daher müssen wir die Datei in das z88dk-Verzeichnis für Bibliotheken kopieren. Bei mir (Bodo) ist das „/home/bodo/ZX81/ZX-C/z88dkv1.8/z88dk/lib/clibs/“.

Endlich, jetzt kann das eigentliche Programm compiliert und gelinkt werden! Dies geschieht mit dieser Befehlszeile:

Code: Alles auswählen

zcc +zx81 -vn -Wall -create-app -startup=2 CfBASIC_25-2m.c CfBASIC_25-1a.c CfBASIC_25-1c.c -lCfBASIC_25-2-lib.lib -o CfBASIC_25-2.bin
Ihr seht die zusätzliche Option „-lCfBASIC_25-2-lib.lib“, nicht wahr?

Für ein einziges Programm ist dieser Aufwand natürlich höher als beim „normalen“ Verfahren, und deshalb lohnt es sich auch nicht, wenn ihr nur ein Programm so entwickelt. Aber es beginnt sich zu lohnen, wenn ihr eine solche Bibliothek in mehreren Programmen benutzt. Oder wenn ihr sie an andere weitergebt. Zum Beispiel kann ich mir vorstellen, dass es eine alternative HRG-Bibliothek geben kann, oder eine Bibliothek zum Zugriff auf SD-Karten, oder eine Bibliothek für einen UART oder ein Modem; der Fantasie sind keine Grenzen gesetzt!

PEEK und POKE in C

Zum Abschluss kommen wir wieder inhaltlich auf Vickers zurück. Und auch im Forum tauchten früh Fragen nach PEEK und POKE auf.

In C läuft der Zugriff auf beliebige Speicherstellen eigentlich ganz einfach, nämlich mit Zeigern. Dazu definiert ihr einen Zeiger auf den passenden Datentyp. Ihr könnt ihn sogar mit der Adresse initialisieren, wenn er nicht für verschiedene Adressen verwendet werden soll:

Code: Alles auswählen

    unsigned char *zeiger = 0x4009;

    gelesen = *zeiger; /* PEEK */
    *zeiger = zu_schreiben; /* POKE */
Aber wenn's euch Spaß macht, könnt ihr euch natürlich auch Funktionen für PEEK und POKE schreiben:

Code: Alles auswählen

unsigned char PEEK(unsigned char *adresse) {
    return *adresse;
}

void POKE(unsigned char *adresse, unsigned char wert) {
    *adresse = wert;
}
C bietet euch sogar die Möglichkeit, gleich mit dem richtigen Datentyp auf den Speicher zuzugreifen. Bei Datentypen mit mehr als einem Byte Größe müssen die einzelnen Bytes natürlich in der richtigen Reihenfolge im Speicher liegen!

Aber diese Ideen hatten die Macher des z88dk auch schon, deshalb enthält die Standardbibliothek folgende Funktionen, die in der „stdlib.h“ deklariert sind:

Code: Alles auswählen

void bpoke(void *addr, unsigned char byte);
void wpoke(void *addr, unsigned int word);
unsigned char bpeek(void *addr);
unsigned int wpeek(void *addr);
So, dieser Teil ist ja 'mal wieder extrem lang geworden! Im nächsten Teil geht es genauso interessant weiter, da werden wir uns mit der Schnittstelle zur Maschinensprache beschäftigen...
B0D0: Real programmers do it in hex.

Antworten