Programmiersprachen sind eines der zentralsten Themen, mit denen sich Programmierer auseinandersetzen müssen. Trotzdem werden sie von vielen als magische Kiste, entwickelt von Spezialisten angesehen. In der Tat ist es ganz und gar nicht einfach einen guten Compiler zu entwickeln, vor allem sobald Optimierungen ins Spiel kommen. Die Grundlagen sind jedoch gar nicht so schwer. Dieser Artikel soll eine recht praxisnahe Einführung in das Thema Compilerbau darstellen und richtet sich an jeden der C++ kann und Grundkenntnisse in x86-Assemblerprogrammierung hat. Auf theoretische Konstrukte wie Grammatiken wird vereinzelt hingewiesen, jedoch braucht man sie nicht zu kennen, um folgen zu können.
Es muss irgendein GCC installiert sein, um die Beispiele ausführen zu können. Ich werde nämlich dessen Assembler und Linker einsetzen.
2 Backtracking
Bei einem Compiler handelt es sich um ein Programm, das eine Textdatei einliest und dann übersetzt. Es sollte offensichtlich sein, dass das Einlesen der einzelnen Zeichen ein wichtiger Baustein ist. Es muss möglich sein, in der Eingabe zurückzuspulen, da ein Übersetzer oft einfach ein paar Sachen durchprobieren muss, um herauszufinden, was ein gewisser Abschnitt darstellt. Zum Beispiel könnte ein Compiler an einer Stelle eine Logik ähnlich zu folgender anwenden: Wenn es keine Zahl ist dann wird es wohl ein String sein und wenn es dies auch nicht ist, dann handelt es sich um einen Fehler im Programmcode. Wenn es aber keine Zahl ist, so müssen aber alle Zeichen die nötig waren, um festzustellen, dass es keine Zahl ist, erneut interpretiert werden, um festzustellen ob es sich vielleicht um einen String handelt. Bei einer Zahl kann man es bereits beim ersten Buchstaben feststellen, jedoch gibt es auch komplexere Beispiele wo dies nicht so einfach ist. Hier benötigt man irgendeinen Weg, zurückspulen zu können.
Es gibt viele Wege, dies nun zu realisieren. Zum Beispiel kann man Zeichenströme schreiben, welche immer nur das absolute Minimum von Zeichen im Speicher halten. Man muss ja nicht überall hin spulen können. Es reicht an ein paar bekannte Stellen zurück spulen zu können. Dies ermöglicht es selbst wahnsinnig große Quelldateien zu übersetzen und doch nur ein paar Byte Speicher zu benötigen.
In diesem Artikel wollen wir es allerdings nicht zu kompliziert machen und kopieren die Datei kurzerhand in den Speicher. Eine effiziente Implementierung eines Zeichenstroms wäre ein Thema, das in einen anderen Artikel gehört. Unser primitiver Strom wird aus zwei Zeigern bestehen. Einer zeigt auf das Zeichen, welches als nächstes verarbeitet werden soll und der andere auf das erste Zeichen nach dem Ende des Puffers. Es entspricht also einem Iteratorenpaar im Sinn der STL.
Die meisten Funktionen im Übersetzer werden der folgenden ähnlich sehen:
Der erste Zeiger wird per Referenz übergeben, damit auch der Aufrufer weiß, bis wohin gelesen wurde. end dagegen ist immer gleich und muss deswegen nicht per Referenz übergeben werden. Der Rückgabewert gibt an, ob "es" geklappt hat. Was das nun im Detail heißt, hängt natürlich von der Funktion ab.
Da wir im Fehlerfall backtracken müssen und eine Ausnahme einen solchen Fall darstellt, werde ich Scopeguards einsetzen, um den Code ausnahmesicher und lesbar zu halten. Wir werden zwar keine Ausnahmen einsetzen, allerdings sollte man doch dafür sorgen, dass der Code ausnahmefreundlich ist.
In vielen Compilern sind diese Zeichenströme auch dafür verantwortlich, die Position in der Datei zu merken. Das heißt konkret, welche Zeile und das wie vielte Zeichen in dieser. Dies ist zum Beispiel nötig, um sinnvolle Fehlermeldungen ausgeben zu können. Wir werden es uns hier aber einfach machen und dieses Problem einfach ignorieren: Wir geben keine Positionsinformationen mit den Fehlern aus.
3 Einfachstes Program
Um anzufangen wollen wir einen Compiler schreiben, welche nur eine sehr einfache Sprache übersetzen kann, eine die so einfach ist, dass es schon an Spott grenzt, sie als Sprache zu bezeichnen. Ein Programm besteht aus einer einzigen Ziffer und diese muss eine '1' sein. Ein korrektes (und sogar das einzig korrekte) Programm wäre also:
Code:
1
Code:
1
Code:
1
Man sollte beachten, dass der Einfachheit zuliebe keine Leerzeichen erlaubt sind.
Das "1" soll bewirken, dass "1" auf der Konsole ausgegeben wird. Der Compiler hierfür sieht wie folgt aus:
if(parse_one(pos, end, out)){
if(pos != end)
cerr<<"Unexpected trailing characters at end of program"<<endl;
else
out<<"ret"<<endl;
}
}
Auch wenn dieses Programm nicht allzu kompliziert sein dürfte, reicht es aber um die Grundstruktur zu verdeutlichen. Source code wird eingelesen, dann verarbeitet und der Assemblercode wird in eine Datei namens "out.s" geschrieben. Wo etwas fehlschlagen kann und warum, sollte durch die Fehlermeldungen deutlich werden.
Um eine ausführbare Datei zu erzeugen muss "out.s" gegen eine C-Sourcecode-Datei gelinkt werden, welche folgendermaßen aussieht:
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void prog();
int main(){
prog();
}
void print_int(int n){
printf("%d", n);
}
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void prog();
int main(){
prog();
}
void print_int(int n){
printf("%d", n);
}
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void prog();
int main(){
prog();
}
void print_int(int n){
printf("%d", n);
}
Dieser Schnipsel C-Code stellt sozusagen die Laufzeitbibliothek unserer Sprache dar. Darum nenne ich die Datei "rt.c" (steht für runtime). Übersetzen kann man das ganze recht einfach.
Code:
gcc out.s rt.c -o out.exe
Code:
gcc out.s rt.c -o out.exe
Code:
gcc out.s rt.c -o out.exe
Manche GCC-Inkarnationen hängen nicht automatisch einen Unterstrich vor C-Funktionsnamen. Wenn die angegebene Kommandozeile einen Linkerfehler produziert, dann wird das wahrscheinlich der Grund sein. Ändern Sie in diesem Fall die Datei "rt.c" zu:
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void _prog();
int main(){
_prog();
}
void _print_int(int n){
printf("%d", n);
}
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void _prog();
int main(){
_prog();
}
void _print_int(int n){
printf("%d", n);
}
C/C++ Code:
1 2 3 4 5 6 7 8 9 10 11
#include <stdio.h>
void _prog();
int main(){
_prog();
}
void _print_int(int n){
printf("%d", n);
}
Nun sollte es keine Probleme mehr geben.
4 Immer noch einfach
Man braucht kein Hellseher zu sein, um zu bemerken, dass unsere Sprache noch nicht zu viel zu gebrauchen ist. Dies wird vorerst aber leider auch so bleiben. Zuerst arbeiten wir nämlich an der Syntax unserer Sprache.
Es wäre doch gut, wenn man mehr Freiheiten beim Setzen der Leerzeichen hätten. Hierfür schreiben wir uns eine Funktion, welche Leerzeichen überspringt.
if(parse_one(pos, end, out)){
skip_spaces(pos, end); // Ich auch (2) if(pos != end)
cerr<<"Unexpected trailing characters at end of file"<<endl;
else
out<<"ret"<<endl;
}
}
if(parse_one(pos, end, out)){
skip_spaces(pos, end); // Ich auch (2) if(pos != end)
cerr<<"Unexpected trailing characters at end of file"<<endl;
else
out<<"ret"<<endl;
}
}
if(parse_one(pos, end, out)){
skip_spaces(pos, end); // Ich auch (2) if(pos != end)
cerr<<"Unexpected trailing characters at end of file"<<endl;
else
out<<"ret"<<endl;
}
}
Wozu die erste neue Zeile dient, sollte selbsterklärend sein. Die zweite sorgt dafür, dass Leerzeichen sich auch am Ende des Programms befinden können.
Als nächstes werden wir es möglich machen, die 1 in Buchstaben auszuschreiben. Dazu schreiben wir eine Funktion welche überprüft, ob wir uns vor einem Wort befinden oder nicht. Falls ja wird dieses auch gleich übersprungen.
Wer genau hinschaut, bemerkt, dass sich in parse_one kein skip_spaces mehr befindet. Dadurch, dass wir in ignore einen Aufruf platziert haben, werden beinahe alle anderen überflüssig und damit ist der Code schon viel weniger fehleranfällig und lesbarer.
Und schon können wir Programme wie folgendes übersetzen.
Code:
one
Code:
one
Code:
one
Jedoch hat sich bereits der erste Bug eingeschlichen. Betrachten wir mal:
Code:
onehundred
Code:
onehundred
Code:
onehundred
Das Programm wird zwar wie erwünscht abgelehnt, aber nicht aus dem richtigen Grund. Der Compiler liest nur bis zum Ende von "one", übersetzt dies dann auch und das Programm wird nur abgelehnt, weil der Compiler nichts mit "hundred" hinter dem Ende des erkannten Programms anzufangen weiß. Das heißt, der Compiler reißt Wörter ohne Gewissensbisse entzwei. Es hängt natürlich von der Sprache ab, ob dies ein Fehler ist oder nicht, allerdings nimmt ein C-Compiler kein "staticintfoo" an, sondern verlangt, dass diese schön mit Leerzeichen getrennt werden. Dies hat auch seinen guten Grund, denn dadurch werden Zweideutigkeiten vermieden. Allerdings übersetzt ein C-Compiler auch "1+2" obwohl zwischen der "1" und dem "+" kein Leerzeichen ist. Man könnte nun für Zahlen und Operatoren eigene Funktionen schreiben. Die eine würde zusätzlich noch überprüfen, ob sich ein Leerzeichen hinter dem Wort befindet.
Streng genommen ist dies zwar ein Hack, aber er macht doch vieles einfacher und angenehmer.
5 Schon etwas komplizierter
So, nun wollen wir auch mal eine Sprache verwenden, welche ansatzweise eine Verwendung hat. Ziel ist es, einen Compiler zu schreiben, der Programme wie folgendes annimmt:
Code:
print 1
print 2 print 3
print 4 print
5
Code:
print 1
print 2 print 3
print 4 print
5
Code:
print 1
print 2 print 3
print 4 print
5
Das Programm besteht aus einer Reihe von "print" Befehlen, welche jeweils eine positive Ganzzahl ausgeben.
Als erstes ersetzen wir parse_one (und auch den Aufruf in main) durch folgende Funktion:
Wie bereits erwähnt, besteht ein Programm aus einer Reihe von Befehlen und darum sollte die Funktion, die ein Programm liest, auch nur dies tun. parse_program gehen die Details eines Befehls nichts an. Dadurch halten wir die Funktionen klein und den Compiler übersichtlich und verständlich.
Um das Lesen eines Befehls kümmert sich die Funktion parse_command, welche dann wieder auf eine Reihe von Unterfunktionen zurückgreift.
bool parse_command(const char*&pos, const char*end, ostream&out){
BacktrackGuard guard(pos);
if(ignore(pos, end, "print")){
int n;
if(read_number(pos, end, n)){
out
<<"pushl $"<<n<<endl
<<"call _print_int"<<endl
<<"addl $4, %esp"<<endl;
guard.no_backtrack();
return true;
}else
cerr<<"Argument to print is missing"<<endl;
}
return false;
}
bool read_number(const char*&pos, const char*end, int&n){
skip_spaces(pos, end);
if(pos != end && isdigit(*pos)){
n = 0;
do{
n *= 10;
n += *pos - '0';
++pos;
}while(pos != end && isdigit(*pos));
return true;
}else
return false;
}
read_number könnte man nun natürlich auch das Lesen von Hexadezimal- oder Oktalzahlen beibringen. Wir wollen uns aber mit einfachen Dezimalzahlen begnügen, um nicht den Rahmen zu sprengen.
In welche und wie viele Unterfunktionen man aufbrechen soll, hängt von der eigenen Erfahrung ab. Hier kann ich höchstens als Faustregel angeben, dass man alles auslagern soll, für was man einen passenden Funktionsnamen findet.
6 Taschenrechner
Als nächstes nehmen wir uns vor, komplexere Ausdrücke zu unterstützen. Also zum Beispiel folgendes Programm.
Code:
print 1+1
print 5*8
print 4 + 8*7
Code:
print 1+1
print 5*8
print 4 + 8*7
Code:
print 1+1
print 5*8
print 4 + 8*7
Jeder, der mit dem Überladen von Operatoren in C++ vertraut ist, der weiß, dass man Operatoren auch als Funktionsaufrufe ansehen kann. Den Code oben könnte man auch wie folgend umschreiben.
Auf diese Weise wollen wir unsere Operatoren auch umsetzen, als inline-Minifunktionen. Wer schon einmal x86-Assembler geschrieben hat, der weiß, dass man das Register eax für Rückgabewerte nutzt. Dies wollen wir auch hier tun. Jede Minifunktion schreibt den Rückgabewert in eax. Eine Funktion mit mehreren Argumenten wertet die Argumente nacheinander aus und nutzt den Stack um Werte zwischenzuspeichern.
Allerdings, bevor wir uns um die Generierung des Assemblercodes kümmern, müssen wir zuerst dafür sorgen, dass wir den Ausdruck richtig lesen können. Hier hilft es, sich eine Reihe von Fragen zu stellen:
Woraus besteht ein Program? Aus Befehlen
Woraus besteht ein Befehl? Aus "print" gefolgt von einem Ausdruck
Woraus besteht ein Ausdruck? Aus einer Summe oder Differenz von Termen
Woraus besteht ein Term? Aus einem Produkt, Quotient oder Modulo von Faktoren
Woraus besteht ein Faktor? Aus einer Zahl mit vielleicht einem Vorzeichen
Wer sich für den theoretischen Hintergrund dieser Fragen interessiert, der kann mal nach EBNF suchen. In diesem Artikel werde ich aber nicht weiter darauf eingehen.
Nun wissen wir schon, welche Funktionen gebraucht werden. Einige der Funktionen sind bereits implementiert und darum werde ich sie nicht wiederholen.
Diese Funktion sieht auf den ersten Blick bedrohlich aus, allerdings ist sie an sich recht einfach. Die einzelnen Terme der Summe werden nacheinander ausgewertet und zusammengezählt beziehungsweise voneinander abgezogen.
Als nächstes wollen wir geklammerte Ausdrücke unterstützen. Fragen wir uns erneut, woraus ein Faktor bestehen kann. Wirklich nur aus einer Zahl? Nein, unter Umständen auch aus einem geklammerten Ausdruck. Ein Ausdruck besteht - über Umwege - aber wieder aus einem Faktor. Es mag auf den ersten Blick nicht offensichtlich sein, allerdings stellen solche rekursiven Definitionen an sich kein Problem dar. Man muss die entsprechende Funktion lediglich rekursiv aufrufen.
Das nächste, was jeder Compiler beherrschen sollte, sind Variablen. Wir wollen uns hier auf lokale Variablen beschränken. Das heißt, alle werden auf den Stack gepackt und keine kommen in statischen Speicher. Um die Sache noch weiter zu vereinfachen, sind alle Variablen vom Typ int. Dies ist eine ziemlich starke Einschränkung, aber irgendwo muss ich ja streichen damit dieser Artikel nicht zu groß wird.
Das erste was benötigt wird, ist eine Datenstruktur welche die Variablen verwaltet. Hierfür definiere ich eine Klasse.
Auf die Implementierungsdetails werde ich nicht eingehen. Eine fertige Implementierung findet man hier.
Es soll möglich sein, in höheren Scopes Variablen zu definieren, welche Variablen aus den unteren Scopes verdecken. Dieses Verhalten ist natürlich sprachabhängig allerdings wollen wir uns hier einfach an die C-Regeln halten. Mit ihnen dürfte jeder Leser vertraut sein.
create_var erzeugt eine Variable im obersten Scope mit einem gewissen Namen. Falls es bereits eine solche Variable gibt, so wird false zurückgegeben, andernfalls wird true zurückgegeben.
Mit is_var_defined kann man ermitteln, ob es eine Variable bereits gibt oder nicht. Hier sollte darauf hingewiesen werden, dass diese Funktion nichts über die Erfolgsaussichten von create_var aussagt, da Variablen verdeckt werden können.
get_var_address gibt die Stackadresse der Variable aus. Adressen werden von ebp aus gezählt. Wir müssen also auch die main Funktion anpassen damit ebp einen brauchbaren Wert enthält.
push_scope sollte aufgerufen werden, wenn sich der Compiler in einen neuen Scope begibt.
Beim Verlassen eines Scopes sollte pop_scope aufgerufen werden. Alle Variablen dieses Scopes werden zerstört.
Der out-Parameter ist der Zeichenstrom der den Assemblercode ausgibt. Methoden mit ihm können Code generieren.
Nun müssen wir noch dafür sorgen, dass VarManager überall zugänglich ist. Eine Möglichkeit wäre, eine Instanz global anzulegen oder ein Singleton zu basteln. Dies ist aber nicht wirklich die feine Art und führt zu Problemen, sobald man den Code parallelisieren möchte. Eine andere Möglichkeit ist eine Instanz in main anzulegen und dann eine Referenz auf diese von Funktion zu Funktion weiter zu geben. Diese Lösung ist viel flexibler, allerdings sind Variablen nicht das einzige, was verwaltet werden will.
Wer sich ein bisschen mit Schleifen auskennt, der weiß, dass dort Sprungadressen verwaltet werden müssen. Wenn man Variablen in Register packen will, so müssen diese auch verwaltet werden. Wenn wir jetzt auf alles eine Referenz übergeben, dann verlieren wir uns bald in endlosen Parameterlisten. Ein noch viel schlimmeres Problem ist, dass man keine neue Struktur einführen kann ohne den ganzen Code umzubauen.
Die Lösung ist recht einfach. Man baut eine Hilfsstruktur, welche diese Referenzen verwaltet. Dies sieht dann etwa folgendermaßen aus:
C/C++ Code:
class Env{
public:
explicit Env(VarManager&var):
var(var){}
VarManager&var;
};
C/C++ Code:
class Env{
public:
explicit Env(VarManager&var):
var(var){}
VarManager&var;
};
C/C++ Code:
class Env{
public:
explicit Env(VarManager&var):
var(var){}
VarManager&var;
};
Eine Referenz auf diese Klasse wird dann von Funktion zu Funktion übergeben. Wenn eine neue Verwaltungsstruktur hinzugefügt wird, so braucht man nur Env und main zu verändern. Der gewählte Name steht für Environment. Als Environment verstehe ich die Zusammenfassung aller Verwaltungsstrukturen.
Man kann die Ausgabe auch als Teil von Env ansehen.
C/C++ Code:
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
class Env{
public:
explicit Env(VarManager&var, ostream&out):
var(var), out(out){}
VarManager&var;
ostream&out;
};
C/C++ Code:
1 2 3 4 5 6 7 8
class Env{
public:
explicit Env(VarManager&var, ostream&out):
var(var), out(out){}
VarManager&var;
ostream&out;
};
C/C++ Code:
1 2 3 4 5 6 7 8
class Env{
public:
explicit Env(VarManager&var, ostream&out):
var(var), out(out){}
VarManager&var;
ostream&out;
};
Die main-Funktion wird also wie folgend abgeändert:
if(parse_program(env, pos, end)){
skip_spaces(pos, end);
if(pos != end)
cerr<<"Unexpected trailing characters at end of file"<<endl;
else
out
<<"pop %ebp"<<endl
<<"ret"<<endl;
}
}
parse_command muss nun auch geändert werden, da unsere Sprache ja nun zusammengesetzte Befehle mittels Scopes unterstützt. Wir werden hier wie in C Akkoladen verwenden um Scopes zu begrenzen. Um die Funktion klein zu halten, lagere ich Teile der Funktion in eine neue Funktion namens parse_block aus.
bool parse_command(Env&env, const char*&pos, const char*end){
BacktrackGuard guard(pos);
if(ignore(pos, end, "print")){
if(parse_expression(env, pos, end)){
env.out
<<"pushl %eax"<<endl
<<"call _print_int"<<endl
<<"addl $4, %esp"<<endl;
guard.no_backtrack();
return true;
}else{
cerr<<"Argument to print is missing"<<endl;
return false;
}
}else if(ignore(pos, end, "var")){
string identifier;
if(read_identifier(pos, end, identifier)){
if(env.var.create_var(identifier, env.out)){
guard.no_backtrack();
return true;
}else
cerr<<"Variable redefined"<<endl;
}else
cerr<<"var must be followed by a variable name"<<endl;
return false;
}else{
string identifier;
if(read_identifier(pos, end, identifier)){
if(ignore(pos, end, "=")){
if(parse_expression(env, pos, end)){
env.out<<"movl %eax, "
<<env.var.get_var_address(identifier)<<endl;
guard.no_backtrack();
return true;
}else
cerr<<"Missing right operant of ="<<endl;
}else
cerr<<"Missing = in assignment"<<endl;
}
return false;
}
}
Jetzt brauchen wir nur noch eine Möglichkeit, lesend auf eine Variable zuzugreifen. Dazu verändere ich parse_factor. Hier lagere ich auch wieder einen Teil der Funktion aus.
Langsam aber sicher entwickelt sich unsere Sprache weiter. Ein Programm sieht jetzt schon aus, als könnte es für irgendetwas zu gebrauchen sein.
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13
1 2 3 4 5 6 7 8 9 10 11 12 13
{
var a
var b
a = 5
b = a + 5
{
a = a-1
var c
c = b
print c
}
print a+b
}
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13
{
var a
var b
a = 5
b = a + 5
{
a = a-1
var c
c = b
print c
}
print a+b
}
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13
{
var a
var b
a = 5
b = a + 5
{
a = a-1
var c
c = b
print c
}
print a+b
}
9 Hello World!
Im vorherigen Kapitel habe ich gesagt, alle Variablen wären vom Typ int. Daran werde ich jetzt auch nichts ändern, allerdings benötigt jede Sprache wenigstens eine minimale Unterstützung für Strings. Ohne kann man noch nicht einmal ordentliche Beispielprogramme schreiben. Noch nicht einmal ein "Hello world!"-Programm ist möglich.
Ziel dieses Kapitels ist es, den print-Befehl ein wenig aufzumotzen. Er soll auch Stringliterale als Parameter übernehmen können und mehrere durch Kommas getrennte Parameter sollen auch unterstützt werden. Er wird aber einzigartig bleiben in dem Sinn, dass nur er Strings als Argument nehmen kann.
Als erstes benötigen wir eine Funktion, welche Stringliterale parsen kann.
Als nächstes brauchen wir eine Möglichkeit, Zeichenketten in den Code einzufügen. Diese werden wir in einer Data-Sektion unterbringen. Wir brauchen also eine weitere Klasse, welche die Stringliterale verwaltet.
Auch hier werde ich auf eine fertige Implementierung verweisen. Es gibt den offensichtlichen Weg, diese Klasse zu implementieren oder man kann versuchen, ein paar Bytes zu sparen, indem man versucht, die Strings so anzuordnen, dass sie sich überlappen. Dies geht natürlich nur, solange die originalen Zeichenketten in der Data-Sektion nicht verändert werden dürfen.
Mit add_string_data kann man einen String ablegen. Es wird ein Nullbyte angefügt und Label wird so gesetzt, dass es auf das erste Byte zeigt.
write_data_section macht das, was der Name sagt.
Da DataSection ein Label braucht, brauchen wir auch jemanden, der diese verwaltet.
C/C++ Code:
class LabelManager{
public:
string make_unique();
};
C/C++ Code:
class LabelManager{
public:
string make_unique();
};
C/C++ Code:
class LabelManager{
public:
string make_unique();
};
make_unique erzeugt einen eindeutigen Labenamen und gibt diesen zurück.
printf will ich nicht benutzen, da man bei dieser Funktion auf Prozentzeichen achten muss. Durch die Benutzung von fwrite gehe ich allen möglichen Problemen aus dem Weg, selbst denen die durch Printf-Format-Erweiterungen entstehen könnten.
Da der print-Befehl komplizierter geworden ist, lagere ich ihn aus.
bool parse_command(Env&env, const char*&pos, const char*end){
BacktrackGuard guard(pos);
if(parse_print(env, pos, end)){
guard.no_backtrack();
return true;
}else if(ignore(pos, end, "var")){
string identifier;
if(read_identifier(pos, end, identifier)){
if(env.var.create_var(identifier, env.out)){
guard.no_backtrack();
return true;
}else
cerr<<"Variable redefined"<<endl;
}else
cerr<<"var must be followed by a variable name"<<endl;
return false;
}else{
string identifier;
if(read_identifier(pos, end, identifier)){
if(ignore(pos, end, "=")){
if(parse_expression(env, pos, end)){
env.out<<"movl %eax, "
<<env.var.get_var_address(identifier)<<endl;
guard.no_backtrack();
return true;
}else
cerr<<"Missing right operant of ="<<endl;
}else
cerr<<"Missing = in assignment"<<endl;
}
return false;
}
}
Nun endlich können wir ein "Hello world"-Programm schreiben.
Code:
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
{
print "Hello" , " world!\n"
var a
var b
a = 3
b = 5
print a, " + ", b, " = ", a+b
}
Code:
1 2 3 4 5 6 7 8
{
print "Hello" , " world!\n"
var a
var b
a = 3
b = 5
print a, " + ", b, " = ", a+b
}
Code:
1 2 3 4 5 6 7 8
{
print "Hello" , " world!\n"
var a
var b
a = 3
b = 5
print a, " + ", b, " = ", a+b
}
10 If und Schleifen
Was jetzt noch fehlt, sind Abfragen und Schleifen. Diese sind auch nicht sonderlich schwer. Zuerst benötigen wir eine Funktion, um ein if einzulesen. Der Einfachheit halber werden wir nicht versuchen, die Werte im Statusregister zu halten und wenn möglich wiederzuverwenden. If nutzt einfach wie überall eax und nimmt natürlich auch ein int als Bedingung. Dies ist recht ineffizient, allerdings macht es die Sache doch wesentlich einfacher.
An sich war es das schon, allerdings ist ein Vergleich, ob denn nun zwei Werte gleich sind, noch ziemlich haarig.
Code:
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8
{
var a
var b
a = 4
b = 3
if a - b
print "nicht gleich"
}
Code:
1 2 3 4 5 6 7 8
{
var a
var b
a = 4
b = 3
if a - b
print "nicht gleich"
}
Code:
1 2 3 4 5 6 7 8
{
var a
var b
a = 4
b = 3
if a - b
print "nicht gleich"
}
Deswegen muss parse_expression verändert werden. Als erste nenne ich parse_expression in parse_numeric_expression um und schreibe ein paar weitere Funktionen. Diese kümmern sich um das Einlesen der Vergleichsoperatoren und der logischen Verknüpfungen. Dadurch werden die Funktionen nicht endlos lang.
Man sollte beachten, dass die Reihenfolge der Abfragen in parse_compare_expression nicht egal ist. Wenn mehrere Operatoren mit dem gleichen Zeichen anfangen, so muss man zuerst auf den längsten testen, ansonsten wird der Operator entzweigerissen. Aus "<=" würde ein "<", gefolgt von einem "=" und dies würde zu einem Fehler führen.
In parse_if wird natürlich parse_boolean_expression verwendet und nicht parse_numeric_expression. Dadurch ist die doppelte Bedeutung von "=" auch nicht zweideutig. Aus dem Kontext heraus kann bestimmt werden, ob es ein Vergleich oder eine Zuweisung sein soll.
Nun fehlt nur noch ein else. Dafür müssen wir parse_if anpassen.
Schleifen sind auch nicht sonderlich verschieden von einem if. Als erstes schreibe ich eine parse_while-Funktion und die wird dann in parse_command genauso wie parse_if aufgerufen.
{
print "Between 1 and 100 the following prime numbers exist : 2"
var p
p = 3
while p < 100{
var d
d = 2
var is_prime
is_prime = 1
while d<p{
if p%d = 0{
is_prime = 0
}
d = d+1
}
if is_prime != 0{
print ", ", p
}
p = p+1
}
}
{
print "Between 1 and 100 the following prime numbers exist : 2"
var p
p = 3
while p < 100{
var d
d = 2
var is_prime
is_prime = 1
while d<p{
if p%d = 0{
is_prime = 0
}
d = d+1
}
if is_prime != 0{
print ", ", p
}
p = p+1
}
}
{
print "Between 1 and 100 the following prime numbers exist : 2"
var p
p = 3
while p < 100{
var d
d = 2
var is_prime
is_prime = 1
while d<p{
if p%d = 0{
is_prime = 0
}
d = d+1
}
if is_prime != 0{
print ", ", p
}
p = p+1
}
}
Aufmerksamen Lesern fällt wahrscheinlich auf, dass ich is_prime nicht mit true initialisiere. Nun, dies liegt daran, dass dies kein numerischer Wert ist. Diese strikte Unterscheidung zwischen boolschem und numerischem Wert ermöglicht aber auch, ein einfaches Gleichzeichen sowohl für eine Zuweisung als auch für einen Vergleich zu nehmen. Es hat also sowohl Vor- wie auch Nachteile. In einer kompletten Sprache gäbe es natürlich auch einen boolschen Typ und dadurch wäre das Problem elegant gelöst.
Eine for-Schleife ist nicht signifikant anders und ihre Implementierung wird dem Leser überlassen.
11 Funktionen
Funktionen sind die Bausteine jedes komplexeren Programms. Darum will ich sie auch in diesem Artikel nicht auslassen. Mit Funktionen kann man sich, was die Komplexität angeht, beliebig nach oben steigern. Templatefunktionen sind ein gutes Beispiel dafür. Hier will ich mich aber auf recht einfache Funktionen beschränken. Für jede Funktion speichern wir den Namen und die Anzahl der Parameter. Da es nur int als Variablentyp gibt, brauchen wir keine Informationen über die Typen der Parameter zu speichern. Der Rückgabewert ist auch immer ein int. Funktionen ohne Rückgabetyp machen natürlich Sinn, aber das machen Templatefunktionen auch. Als Kompromiss zwischen Sinn und Einfachheit lassen wir es zu, dass ein Rückgabewert nicht initialisiert ist. Das heißt er enthält Schrott.
Funktionen müssen wie in C im Voraus deklariert werden und können mehrfach gleich deklariert werden. Sie können nicht überladen werden. Als Aufrufskonvention werden wir eine verwenden, die der des GCCs ähnlich ist. Dadurch können wir auch ohne weiteres gegen C-Code linken. Der Unterschied besteht darin, dass Argumente von links nach rechts auf den Stack gepackt werden. Beim GCC werden die Parameter von rechts nach links auf den Stack gepackt. Der Grund hierfür ist, dass es so für uns einfacher ist. Der Aufrufer ist dafür verantwortlich, die Argumente wieder vom Stack zu nehmen. Eine Funktion darf ihre Parameter verändern. Der Aufrufer darf also keine Argumente recyceln. Der Rückgabewert befindet sich in eax.
Als erstes brauchen wir eine Klasse, um die Informationen zu verwalten. Ich werde wieder einmal nur auf die Klassenschnittstelle eingehen und auf eine Implementierung verweisen.
exists überprüft, ob eine Funktion bekannt gemacht wurde.
get_label gibt das Label des Funktionskörper zurück.
Mit get_parameter_count findet man heraus, wie viele Parameter eine Funktion braucht.
Um auf Argumente zurückgreifen zu können und Werte zurückgeben zu können, müssen wir auch VarManager verändern. Konkret werden ein paar Methoden hinzugefügt und die Klasse sieht nun folgendermaßen aus:
add_arg gibt einem Parameter einen Namen. pos gibt an, um das wie vielte Argument von links es sich handelt. Das erste Argument hat den Index 0. Der Rückgabewert gibt an, ob es bereits einen solchen Parameter gab oder nicht. Argumente werden in ihrem eigenen Scope angelegt und darum können sie mit gleichnamigen lokalen Variablen koexistieren. Sie werden jedoch von denen verdeckt.
simulate_pop_all_scopes generiert Code, um alle Scopes zu löschen, ohne dies jedoch zu tun. Dies wird benötigt, um ein return wie in C zu realisieren. Für andere Formen von Sprüngen, wie zum Beispiel break, würde man einen ähnlichen Mechanismus benötigen.
In einem C-Programm unterscheidet man zwischen globalem Scope und Funktionskörper. In beiden Bereichen sind unterschiedliche Sprachkonstrukte erlaubt. So kann man zum Beispiel keine Funktion innerhalb einer anderen definieren. Schleifen können sich auch nicht im globalen Scope befinden. Diesen Unterschied wollen wir nun auch in unserer Sprache einführen.
Da wir keine globalen Variablen zulassen, kann man sich fragen, ob ein VarManager im globalen Scope Sinn macht. Meiner Meinung nach nicht. Deswegen gibt es auch für beide Teile des Programms unterschiedliche Env-Strukturen. Die eine nenne ich FuncEnv, da sie nur innerhalb einer Funktion Sinn macht und die andere einfach Env. Da wir uns bis jetzt nur um den Inhalt von Funktionen gekümmert haben, wird jedes alte Env in ein FuncEnv umbenannt.
Der FuncManager in FuncEnv ist konstant, da man keine Funktion innerhalb einer anderen erschaffen kann. In main wird eine Instanz von FuncManager angelegt wie wir es bereits für die meisten anderen Verwaltungsstrukturen gemacht haben.
Als nächstes brauchen wir eine Funktion zum Einlesen von Funktionsdefinitionen und Deklarationen.
for(unsigned j=0; j<arguments.size(); ++j)
if(!var.add_arg(arguments[j], arguments.size()-j-1))
cerr<<"2 arguments with same name "
<<arguments[j]<<" in function "
<<func_name<<endl;
for(unsigned j=0; j<arguments.size(); ++j)
if(!var.add_arg(arguments[j], arguments.size()-j-1))
cerr<<"2 arguments with same name "
<<arguments[j]<<" in function "
<<func_name<<endl;
for(unsigned j=0; j<arguments.size(); ++j)
if(!var.add_arg(arguments[j], arguments.size()-j-1))
cerr<<"2 arguments with same name "
<<arguments[j]<<" in function "
<<func_name<<endl;
Als nächstes sorgen wir dafür, dass eine Funktion auch aufgerufen werden kann. Dafür schreiben wir uns als erstes eine Funktion, die einen Aufruf einliest.
if(!env.func.exists(func_name))
cerr<<"Unknown function "<<func_name<<endl;
else if(env.func.get_parameter_count(func_name)
!= parameter_count)
cerr<<"Wrong number of parameters to function "
<<func_name<<endl;
else{
env.out<<"call "<<env.func.get_label(func_name)<<endl;
if(parameter_count != 0)
env.out<<"addl $"<<4*parameter_count
<<", %esp"<<endl;
guard.no_backtrack();
return true;
}
}
return false;
}
}
Eine Funktion kann innerhalb eines Ausdrucks und eines Befehls aufgerufen. Es müssen also parse_factor und parse_command verändert werden. In parse_factor muss man parse_function_call vor parse_literal aufrufen, da ansonsten der Funktionsname als Variable interpretiert wird.
Nun fehlt nur noch die Möglichkeit, einen Wert zurückzugeben. Die Funktion, die sich darum kümmert, sollte nicht zu kompliziert sein, wenn man es geschafft hat, bis hier zu folgen.
if(parse_program(env, pos, end)){
skip_spaces(pos, end);
if(pos != end)
cerr<<"Unexpected trailing characters at end of file"<<endl;
else
data.write_data_section(out);
}
}
Ein "Hello World"-Programm sieht nun wie folgend aus:
Code:
prog(){
print "Hello World!"
}
Code:
prog(){
print "Hello World!"
}
Code:
prog(){
print "Hello World!"
}
Selbst rekursive Funktionen sind kein Problem. Folgendes Programm berechnet die Fakultät einer Zahl rekursiv:
Code:
1 2 3 4 5 6 7 8 9 10
1 2 3 4 5 6 7 8 9 10
fact(n){
if n = 0
return 1
else
return n*fact(n-1)
}
prog(){
print fact(9)
}
Code:
1 2 3 4 5 6 7 8 9 10
fact(n){
if n = 0
return 1
else
return n*fact(n-1)
}
prog(){
print fact(9)
}
Code:
1 2 3 4 5 6 7 8 9 10
fact(n){
if n = 0
return 1
else
return n*fact(n-1)
}
prog(){
print fact(9)
}
Wir können aber nun auch gegen C-Funktionen linken. Zum Beispiel kann man so etwas von der Konsole einlesen.
C/C++ Code:
int read_int(){
int r = -1;
scanf("%d", &r);
return r;
}
C/C++ Code:
int read_int(){
int r = -1;
scanf("%d", &r);
return r;
}
C/C++ Code:
int read_int(){
int r = -1;
scanf("%d", &r);
return r;
}
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
read_int();
prog(){
var a
print "a = "
a = read_int()
var b
print "b = "
b = read_int()
print "a + b = ", a+b
print "a - b = ", a-b
print "a * b = ", a*b
print "a / b = ", a/b
print "a % b = ", a%b
}
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
read_int();
prog(){
var a
print "a = "
a = read_int()
var b
print "b = "
b = read_int()
print "a + b = ", a+b
print "a - b = ", a-b
print "a * b = ", a*b
print "a / b = ", a/b
print "a % b = ", a%b
}
Code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
read_int();
prog(){
var a
print "a = "
a = read_int()
var b
print "b = "
b = read_int()
print "a + b = ", a+b
print "a - b = ", a-b
print "a * b = ", a*b
print "a / b = ", a/b
print "a % b = ", a%b
}
Man muss nur beachten, dass die Parameter in C eine andere Reihenfolge haben werden. Die Funktion test(a,b,c) würde der C-Funktion test(c,b,a) entsprechen.
12 Parserbaum
Wer schon einmal vorher mit der Materie in Kontakt gekommen ist, wird sich wahrscheinlich wundern, warum ich ihn bis jetzt noch nicht erwähnt habe, ja vielleicht sogar wundern, wie wir überhaupt so weit ohne ihn gekommen sind. Die Rede ist vom Parserbaum.
Die Idee ist es, das Interpretieren der Eingangszeichenkette und das Generieren der Assemblercodes zu trennen. Diese Trennung bietet viele Vorteile, wobei die Möglichkeit, elegant mehrere Prozessoren zu unterstützen nur die offensichtlichste ist.
Der Parser liest den Code und generiert einen Baum, welcher alle wichtigen Informationen enthält. Der Generator nimmt diesen Baum dann und generiert den entsprechenden Code. Dies macht Arbeitsteilung im Entwicklerteam viel einfacher, da beide Teile nun unabhängig voneinander sind.
Ein weiterer großer Vorteil ist, dass es möglich wird, Funktionen zu inlinen und auch andere Optimierungen durchzuführen. Ich wüsste nicht, wie man dem Compiler, so wie er hier vorgestellt wurde, beibringen soll, Funktionsaufrufe zu ersetzen. Wenn man den Code allerdings als Baum vorliegen hat, so muss man nur einen Ast des Baums kopieren und noch ein paar Umformungen bezüglich der Variabelnamen durchführen. In einem Baum ist es auch leicht, Informationen über Unterausdrücke einzuholen. Zum Beispiel, ob ein Ausdruck noch etwas anderes tut außer den Rückgabewert zu berechnen. Dies ist wichtig um zum Beispiel 0*x durch 0 zu ersetzen.
In diesem Artikel wollen wir aber keinen Parserbaum mehr entwickeln. Es würde einfach den Rahmen sprengen.
13 Schlussbemerkung
Ziel dieses Artikels war es, den Leser in das Gebiet des Compilerbaus einzuführen und zwar ohne ihn mit Theorie zu bombardieren. Ich hoffe, Sie haben jetzt einen Überblick über die Materie und können sich vorstellen, wo man die Aussagen der Theorie praktisch einsetzen kann. Wer sich für das Thema interessiert, dem sei gesagt, dass es sich hier nur um die Spitze des Eisbergs handelt. Es gibt noch sehr viel, was in diesem Artikel noch nicht einmal mit einem Wort erwähnt wurde.
Ob es von meiner Seite einen weiteren Artikel zu diesem Thema geben wird, ist ungewiss. Es fällt mir schwer, einzuschätzen, ob ich in Zukunft noch genug Zeit und Motivation aufbringen kann. Natürlich hängt es auch davon ab, wie viele Leute sich dafür interessieren.
Zuletzt bearbeitet von estartu am 08:53:43 13.02.2008, insgesamt 6-mal bearbeitet
Wow
Kann mich meinem Badestrand da nur anschließen, äußerst beeindruckend!
Für mich persönlich einer der eindrucksvollsten und lehrreichsten Artikel.
Mich stört lediglich das man zur Ausgabe weiterhin C Funktionen benutzt (also diese rt.c).
Ist das so üblich, oder hast du das gemacht damit es plattformunabhängig bleibt?
[...]
Mich stört lediglich das man zur Ausgabe weiterhin C Funktionen benutzt (also diese rt.c).
Ist das so üblich, oder hast du das gemacht damit es plattformunabhängig bleibt?
Du kannst die Implementierung von print_int doch jederzeit ändern.
printf bietet sich hier aber aus zwei Gründen an:
1. Das Programm bleibt somit Plattformunabhängig (wobei diese durch die Assembler-Syntax und -Sprache bereits eingeschränkt ist)
2. Die C I/O bzw. RT ist deutlich schlanker als die C++ I/O-Streams und es sieht doch sehr unschön aus, wenn man so wenig Asm-Code generiert und dann ein mehrere hundert KB große Binärdatei erhält
Mich stört lediglich das man zur Ausgabe weiterhin C Funktionen benutzt (also diese rt.c).
Es gibt mehrere Gründe für diese Vorgehensweise. Die Platformunabhängigkeit hast du ja schon genannt. Der Code sollte so von Win über Linux bis Intelmac funktionieren. Ein weiterer Vorteil ist die Einfachheit der Benutzung. Du musst nicht erst umständlich Dlls oder so laden und es bedarf keiner Erklärung des Code da ja schließlich jeder hier wissen dürfte wie man mit diesen Funktionen umgeht. Im Artikel geht es ja schließlich nicht darum wie man eine Standardbibliothek implementiert (auch wenn zugegeben der Übergang flüssig ist...).
Der letzte Grund ist, dass zu mindest eine Win Implementierung in meinen Augen außerordentlich langweilig wäre. Es wäre praktisch nur Kopie&Paste aus der MSDN. (Stichworte: WriteFile, ReadFile, GetStdHandle, vielleicht WriteConsole)
Storm.Xapek.de schrieb:
Ist das so üblich, oder hast du das gemacht damit es plattformunabhängig bleibt?
Ich würd sagen, dass das recht üblich ist. Zum Beispiels sind die PHP Dateiströme ja praktisch eine 1 zu 1 Kopie der C Versionen.
hi,
ich muss echt sagen das is n toller Artikel. Ich werde vielleicht eine Arbeit genau über das Thema schreiben. Mal schauen.
Vielen Dank für deinen artikel
Wie lange
Unregistrierter
Wie lange Unregistrierter
17:09:56 03.09.2008 Titel:
Sehr guter Artikel, hab jedoch paar fragen^^
Versteht mich bitte nicht faslch^^
1.) Was ist Assembler?
2.) Ist es auch möglich einen CAompiler ohne Assembler zu schreiben?
Unter Assembler (ASM) versteht man entweder eine textuelle Darstellung eines kompilierten Programms oder das Programm welches diese Darstellung in eine für Prozessor und OS verständliche Datei überführt.
Ein Prozessor führt durch Bitfolgen kodierte Befehle aus. Diese nennt man Opcodes. In ASM sind diese Befehle nicht durch Bitfolgen sonder durch für Menschen lesbaren Text dargestellt.
Wenn deine Zielsprache kein ASM ist dann kommst du auch ohne aus. Du kannst zum Beispiel C-Code ausgeben und diesen dann durch einen C-Compiler schicken.
Es muss irgendein GCC installiert sein, um die Beispiele ausführen zu können. Ich werde nämlich dessen Assembler und Linker einsetzen.
Ist das mit dem Compiler gemachte Programm dan wie Java?
Bei Java benötigt man ja auch den GCC(Garbage Collectorr) um die anwendung zum laufen zu bringen, hat das irgentwas damit zu tun (In dem Beispiel) oder ist in diesem fall GCC was anderes? Falls ja wie kann man Einen Compiler ohne die GCC(Garbage collector) Machen?^^
Unter Assembler (ASM) versteht man entweder eine textuelle Darstellung eines kompilierten Programms oder das Programm welches diese Darstellung in eine für Prozessor und OS verständliche Datei überführt.
Ein Prozessor führt durch Bitfolgen kodierte Befehle aus. Diese nennt man Opcodes. In ASM sind diese Befehle nicht durch Bitfolgen sonder durch für Menschen lesbaren Text dargestellt.
Wenn deine Zielsprache kein ASM ist dann kommst du auch ohne aus. Du kannst zum Beispiel C-Code ausgeben und diesen dann durch einen C-Compiler schicken.
Ja und der C-Compiler macht dann asm draus wöde ich mal vermuten
Er könnte doch direkt die Bitfolge schreiben?
Ben04 schrieb:
Unter Assembler (ASM) versteht man entweder eine textuelle Darstellung eines kompilierten Programms oder das Programm welches diese Darstellung in eine für Prozessor und OS verständliche Datei überführt.
Ein Prozessor führt durch Bitfolgen kodierte Befehle aus. Diese nennt man Opcodes. In ASM sind diese Befehle nicht durch Bitfolgen sonder durch für Menschen lesbaren Text dargestellt.
Wenn deine Zielsprache kein ASM ist dann kommst du auch ohne aus. Du kannst zum Beispiel C-Code ausgeben und diesen dann durch einen C-Compiler schicken.
Ja und der C-Compiler macht dann asm draus wöde ich mal vermuten
Er könnte doch direkt die Bitfolge schreiben?
Joa stimmt^^
Was erzeugt eigentlich der Compiler von C++ in den Objekt-Daten.^^
Hmm also ich komm mal mit meiner übelegung zum Punkt^^
1.) Der Compiler macht daraus ASM Dateien (Muss es ASM sein?^^).
2.) Um daraus eine Exe machen zu können brauchen wir etwas wie einen Linker.
Jetzt bleibt die Frage:
Wie bastel ich einen Linker , obwohl das geht schon aus dem Thema raus^^
Ah ja und der Linker macht daraus ja die Exe das heißt wen ich einen Linker mache der etwas anderes haben muss wie ASM dann kann ich ja auch etwas anderes wie ASM nutzen^^
Nächstes Thema anzeigen Vorheriges Thema anzeigen
Sie können keine Beiträge in dieses Forum schreiben. Sie können auf Beiträge in diesem Forum antworten. Sie können Ihre Beiträge in diesem Forum nicht bearbeiten. Sie können Ihre Beiträge in diesem Forum nicht löschen. Sie können an Umfragen in diesem Forum nicht mitmachen.
c++.de ist Teilnehmer des Partnerprogramms von Amazon Europe S.à.r.l. und Partner des Werbeprogramms, das zur Bereitstellung eines Mediums
für Websites konzipiert wurde, mittels dessen durch die Platzierung von Werbeanzeigen und Links zu amazon.de
Werbekostenerstattung verdient werden kann.
Die Vervielfältigung der auf den Seiten www.c-plusplus.de, www.c-plusplus.info, www.c-sar.de, www.c-plusplus.net und www.baeckmann.de
enthaltenen Informationen ohne eine schriftliche Genehmigung des Seitenbetreibers ist untersagt
(vgl. §4 Urheberrechtsgesetz). Die Nutzung und Änderung der vorgestellten Strukturen und Verfahren in
privaten und kommerziellen Softwareanwendungen ist ausdrücklich erlaubt, soweit keine Rechte Dritter verletzt werden.
Der Seitenbetreiber übernimmt keine Gewähr für die Funktion einzelner Beiträge oder Programmfragmente, insbesondere
übernimmt er keine Haftung für eventuelle aus dem Gebrauch entstehenden Folgeschäden.