Übungspaket 17 Der gcc Compiler Übungsziele: Skript: 1. Sicherer Umgang mit gemischten Ausdrücken 2. Herleiten der unterschiedlichen Datentypen in gemischten Ausdrücken 3. Kenntnis über die implizite Durchführung von Typanpassungen Kapitel: 38, 39 und 40 Semester: Wintersemester 2017/18 Betreuer: Kevin, Theo, Thomas und Ralf Synopsis: Eines haben wir alle bis jetzt sicherlich gelernt, durch Eintippen von gcc quelltext.c wird ein C-Programm in Maschinencode übersetzt und damit lauffähig gemacht. Doch wenn etwas schiefgeht, fangen die Probleme an. Der Compiler selbst ist wie die meisten Programmierwerkzeuge ein recht komplexes Programm. Daher fällt es vielen Programmieranfängern sehr schwer, den Überblick über die einzelnen Teile des Compilers zu behalten und die aufgetretenen Fehlerursachen zu lokalisieren. Um hier Abhilfe zu schaffen, schauen wir uns in diesem Übungspaket den Compiler gcc und seine Komponenten ein wenig genauer an.
Teil I: Stoffwiederholung Aufgabe 1: Grobaufbau des gcc Compilers Durch den Aufruf gcc datei.c werden eigentlich vier größere Programme nacheinander aufgerufen. Benenne diese vier Programme und erläutere kurz, was ihre Aufgaben sind. 1. Programm: Präprozessor Funktion: Der Präprozessor erfüllt im Wesentlichen drei Funktionen: 1. Der Präprozessor ersetzt all #include-direktiven durch die angegebenen Dateien (diese Dateien werden vollständig in den Quelltext eingefügt). 2. Ersetzen aller #define-makros durch ihre entsprechenden Definitionen. 3. Übersetzen bzw. entfernen aller Anweisungen zwischen den #ifdef, #ifndef, #else und #endif Direktiven. 2. Programm: Eigentlicher Compiler (der C-Übersetzer) Funktion: 3. Programm: Assembler Funktion: 4. Programm: Linker Funktion: In dieser Phase wird das C-Programm in denjenigen Assembler-Code übersetzt, der zum gewählten Prozessor gehört. Das Ergebnis ist also eine Datei, die prozessorspezifisch ist. Ergebnis: eine Datei mit der Endung.s Der Assembler wandelt den (prozessorspezifischen) Assembler-Code, der noch lesbare Anweisungen enthält in Maschinencode um. Das Ergebnis ist eine Datei, die nur noch aus unverständlichen Nullen und Einsen besteht, die üblicherweise zu hexadezimalen Zahlen zusammengefasst werden. Ergebnis: eine Datei mit der Endung.o Der Linker fügt den Maschinencode und alle verwendeten Bibliotheken zu einem einzigen lauffähigen Programm zusammen. Erst dieses Programm kann vom Prozessor (in Zusammenarbeit mit dem Betriebssystem) auch wirklich ausgeführt werden. Ergebnis: eine ausführbare Datei mit dem angegeben Namen oder a.out Einführung in die Praktische Informatik, Wintersemester 2017/18 17-1
Aufgabe 2: Die Syntax der Präprozessor-Direktiven Erkläre kurz in eigenen Worten die Syntax der C-Präprozessor-Direktiven: Alle C-Präprozessor-Direktiven fangen mit einem Doppelkreuz # an, werden von einem der Schlüsselwörter include, define, ifdef, ifndef, else oder endif gefolgt und meistens mit einem Argument (Parameter) abgeschlossen. Zwischen dem Zeilenanfang, dem Doppelkreuz #, dem Schlüsselwort und den Argumenten dürfen beliebig viele Leerzeichen, Tabulatoren und Kommentare eingefügt werden. Ferner ist zu beachten, dass eine C-Präprozessor-Direktive in einer Zeile abgeschlossen wird, es sei denn, die Zeile wird mit einem Backslash \ abgeschlossen. Aufgabe 3: Die Präprozessor-Direktiven Erkläre jede Präprozessor-Direktive anhand je eines oder zweier Beispiele: 1. #include Mittels der Direktive #include kann man andere Dateien einbinden. Die #include wird dadurch vollständig durch den Inhalt der angegebenen Datei ersetzt. #include <stdio.h> #include "my.h" 2. #define // Einbinden der Standard Ein-/Ausgabe // Einbinden der eigenen Datei my.h Mittels #define kann man einfache Labels und ganze Makros definieren: #define NAME Cool // mein Name #define HiThere( x ) Hi Daddy x // meine Anrede HiThere( NAME ) Ergibt: Hi Daddy Cool 3. #ifdef... #else...#endif Die #ifdef-direktive erlaubt es, Dinge in Abhängigkeit anderer zu übersetzen. #ifdef my header #else #include "my-header.h" #endif In diesem Falle wird die Datei my header.h nur dann eingebunden, sofern das Label my header noch nicht definiert wurde. Zwischen #if, #else und #endif können beliebige Anweisungen stehen. In den.h-dateien werden oftmals Labels der obigen Form definiert, um das mehrmalige Ausführen eines #include zu vermeiden. 17-2 Wintersemester 2017/18, Einführung in die Praktische Informatik
Teil II: Quiz Aufgabe 1: Die Definition von Namen Gegeben sei folgendes Programmstück: 1 # define Tag Montag /* mein Arbeitstag */ 2 # define WOCHE 34 /* meine Urlaubswoche */ 3 4 # define Telefon 0381 498 72 51 5 # define FAX /* 0049 */ 0381 498 118 72 51 6 7 # define Ferien 8 9 # define MSG " heute geht es mir sehr gut " 10 11 # define NAME1 ein name 12 # define NAME2 NAME1 13 14 # define ZWEI_ZEILEN ZEILE -1 /* das ist die erste Zeile 15 # define ZWEITE_ZEILE hier ist Zeile 2 */ 16 17 # define ENDE " jetzt ist schluss " Finde heraus, welche Namen in obigem Programmstück definiert werden und welche Werte diese haben. Trage die definierten Namen nebst ihrer Werte in folgende Tabelle ein. Zeile Name Wert 1 Tag Montag 2 WOCHE 34 4 Telefon 0381 498 72 51 5 FAX 0381 498 118 72 51 7 Ferien 9 MSG "heute geht es mir sehr gut" 11 NAME1 ein name 12 NAME2 ein name 14 ZWEI ZEILEN ZEILE-1 15 ----------- Hier wird kein Name definiert, denn diese Zeile ist auskommentiert 17 ENDE "jetzt ist schluss" Einführung in die Praktische Informatik, Wintersemester 2017/18 17-3
Aufgabe 2: Definition von Makros Gegeben sei folgendes Programmstück: 1 // macro implemention of the formula : 2* x + 3* y 2 3 # define Formula_1 ( x, y ) 2 * x + 3 * y 4 # define Formula_2 ( x, y ) 2 * (x) + 3 * (y) 5 6 i = Formula_1 ( 1, 2 ); j = Formula_2 ( 1, 2 ); 7 i = Formula_1 ( 4, 2 ); j = Formula_2 ( 4, 2 ); 8 i = Formula_1 ( 1+2, 2+3 ); j = Formula_2 ( 1+2, 2+3 ); 9 i = Formula_1 ( 2+1, 3+2 ); j = Formula_2 ( 2+1, 3+2 ); Notiert in der folgenden Tabelle die Werte der Parameter x und y, das erwartete Ergebnis res = 2 * x + 3 * y, sowie die Resultate, die in den Variablen i und j abgelegt werden: Zeile x y res i j 6 1 2 8 8 8 7 4 2 14 14 14 Zeile x y res i j 8 3 5 21 13 21 9 3 5 21 16 21 Wie werden die Makros vom Präprozessor expandiert (ersetzt)? 6 i = 2 * 1 + 3 * 2; j = 2 * (1) + 3 * (2) ; 7 i = 2 * 4 + 3 * 2; j = 2 * (4) + 3 * (2) ; 8 i = 2 * 1+2 + 3 * 2+3; j = 2 * ( 1+2) + 3 * ( 2+3) ; 9 i = 2 * 2+1 + 3 * 3+2; j = 2 * ( 2+1) + 3 * ( 3+2) ; Überprüft eure Annahme durch Aufruf des Präprozessors. Nehmen wir an, ihr habt die paar Zeilen abgetippt und in der Datei quiz.c gespeichert. Dann einfach cpp quiz.c oder alternativ gcc -E quiz.c aufrufen; die Ergebnisse erscheinen dann direkt auf dem Bildschirm. Überprüft nochmals eure Ergebnisse, wie ihr sie in obiger Tabelle eingetragen habt. Erklärt, sofern sich Unterschiede auftun, was hierfür die Ursachen sind. Welche Schlussfolgerungen zieht ihr daraus für die Definition von Makros? Ursachen: In der Makro-Definition Formula 1 werden die beiden Parameter x und y nicht geklammert. Dadurch kann es sein, dass bei Übergabe von einfachen arithmetischen Ausdrücken die einzelnen Bestandteile aufgrund der Präzedenzregeln nicht so ausgewertet werden, wie man es erwartet. Problembehebung: Bei der Definition (der Implementierung) von Makros sollten die Parameter stets geklammert werden. So werden die Parameter immer zuerst ausgewertet und dann mit anderen Parametern verknüpft. 17-4 Wintersemester 2017/18, Einführung in die Praktische Informatik
Aufgabe 3: Einbinden von (Header-) Dateien Gegeben sei folgendes Programmstück: 1 # include <stdio.h> 2 # include <math.h> 3 4 # define C_TYPES <ctype.h> 5 # include C_TYPES 6 7 //# include < signal.h> Welche Dateien werden dadurch vom Präprozessor eingebunden? Folgende Systemdateien (aus /usr/include) werden eingebunden: stdio.h, math.h und ctype.h Die Datei signal.h wird nicht eingebunden, da diese Zeile auskommentiert ist. Aufgabe 4: Bedingtes Übersetzen Gegeben sei folgendes Programmstück: 1 # define A 4711 2 3 # ifdef A 4 # define N1 17 5 # ifdef B 6 # define N2 2 7 # else 8 # define N2 4 9 # endif 10 # else 11 # define N1 12 12 # define N2-3 13 # endif 14 15 int i = N1 * N2; Welche Labels werden mit welchen Werten definiert, welchen Wert erhält die Variable i? Zeile Label Wert 1 A 4711 4 N1 17 Zeile Label Wert 8 N2 4 15 i 68 Einführung in die Praktische Informatik, Wintersemester 2017/18 17-5
Teil III: Fehlersuche Aufgabe 1: Praktische Fehlersuche Das folgende Programm soll den Text Fehler beheben drei Mal ausgeben, jeweils eine Ausgabe pro Zeile. Diesmal war unser Starprogrammierer Dr. Bit-Byte zugange. Aber auch ihm sind einige Fehler unterlaufen. Finde und korrigiere diese. Zeige anschließend mittels einer Handsimulation, dass dein Programm korrekt arbeitet. 1 # define ANZAHL 3; 2 # define LOOP_CONDITION ( fehler >= 0) 3 # define MSG " Fehler " eheben " 4 5 int main ( int argc, char ** argv ); 6 { 7 int fehler = ANZAHL ; 8 do 9 printf ( MSG ); fehler = - 1; 10 while ( LOOP_CONDITION ); 11 return 0; 12 } Zeile Fehler Erläuterung Korrektur 0 #include fehlt Die Datei stdio.h wird nicht eingebunden. Wir brauchen sie aber, da wir etwas ausgeben wollen. #include <stdio.h> 1 Das Semikolon scheint zu viel zu sein. Aber in der Initialisierung in Zeile 7 ist es dennoch ok (aber unschön). 2 >= statt > Im Verbindung mit der do-while-schleife in den Zeilen 8 bis 10 ergeben sich vier statt drei Schleifendurchläufe. (fehler>0) 3 " statt b Hier scheint ein einfacher Tippfehler vorzuliegen. beheben 5 ; zu viel Bei der Funktionsdefinition darf am Ende kein ; stehen. Dieses ist nur bei Funktionsdeklarationen erlaubt, die dem Compiler den Namen und die Signatur der Funktion bekannt machen. argv ) 8/10 {} fehlen Da die do-while-schleife mehr als eine Anweisung ausführen soll, müssen sie durch {} geklammert werden. 3 {... } 9 fehler fehlt Da der Wert der Variablen fehler um eins reduziert werden soll, muss sie auch auf der rechten Seite auftauchen. fehler = fehler - 1 17-6 Wintersemester 2017/18, Einführung in die Praktische Informatik
Programm mit Korrekturen: 1 # include <stdio.h> 2 3 # define ANZAHL 3 4 # define LOOP_CONDITION ( fehler > 0) 5 # define MSG " Fehler beheben \ n" 6 7 int main ( int argc, char ** argv ) 8 { 9 int fehler = ANZAHL ; 10 do { 11 printf ( MSG ); 12 fehler = fehler - 1; 13 } while ( LOOP_CONDITION ); 14 return 0; 15 } Vor der Handsimulation ist es eine gute Übung, sich das Programm zu überlegen, das der Präprozessor aus der.c-datei generiert: Ergebnis nach Abarbeitung durch den Präprozessor: 7 int main ( int argc, char ** argv ) 8 { 9 int fehler = 3; 10 do { 11 printf ( " Fehler beheben \n" ); 12 fehler = fehler - 1; 13 } while ( ( fehler > 0) ); 14 return 0; 15 } Handsimulation: Zeile 9 11 12 13 11 12 13 11 12 13 14 fehler 3 2 2 1 1 0 0 printf() Fehler beheben Fehler beheben Fehler beheben Einführung in die Praktische Informatik, Wintersemester 2017/18 17-7
Teil IV: Anwendungen Aufgabe 1: Fehler Finden und Eliminieren Es ist ganz normal, dass man beim Entwickeln und Eintippen eines Programms viele Tippfehler macht. Da der Compiler ein recht pingeliger Zeitgenosse ist, findet er viele dieser Tippfehler und gibt entsprechende Fehlermeldungen und Hinweise aus. Die Kunst besteht nun darin, dieser Ausgaben richtig zu deuten und die wirklichen Fehlerursachen zu finden. Um dies ein wenig zu üben, greifen wir nochmals das fehlerhafte Programm aus dem vorherigen Abschnitt (vorherige Seite) auf, wobei wir davon ausgehen, dass ihr sowieso alle Fehler gefunden habt. Arbeite nun wie folgt: Arbeitsanleitung: 1. Tippe das fehlerhafte Programm ab und speichere es in einer Datei deiner Wahl. 2. Übersetze das fehlerhafte Programm mittels gcc. 3. Fahre mit Arbeitsschritt 6 fort, falls der Compiler keine Fehlermeldung oder Warnung ausgegeben hat. 4. Lese die erste(n) Fehlermeldung(en) aufmerksam durch und korrigiere einen Fehler. 5. Gehe zurück zu Arbeitsschritt 2. 6. Starte das Programm und überprüfe, ob es korrekt arbeitet. 7. Sollte das Programm korrekt arbeiten gehe zu Schritt 10 8. Korrigiere einen inhaltlichen Fehler (Semantikfehler). 9. Gehe zurück zu Arbeitsschritt 2. 10. Fertig! Hinweis: Diese Herangehensweise empfiehlt sich auch in Zukunft :-)! 17-8 Wintersemester 2017/18, Einführung in die Praktische Informatik
Aufgabe 2: Eigene Makro-Definitionen Definiere je ein Makro für die folgenden drei Formeln (Lösungen siehe unten): f(x) = 3x 2 + x/2 1 g(x, y) = x 2 3xy + y 2 h(x, y, z) = 2x 3 3y 2 + 2z Zu welchen Ergebnissen führen die folgenden drei Aufrufe (Einsetzen)? Aufruf Resultat Mathematik Resultat C-Programm f(1 + z) 3(1 + z) 2 + (1 + z)/2 1 3*(1 + z)*(1 + z) + (1 + z)/2-1 g(x, A + 1) x 2 3x(A + 1) + (A + 1) 2 x*x - 3*x*(A + 1) + (A + 1)*(A + 1) h(a, b, a + b) 2a 3 3b 2 + 2(a + b) 2*a*a*a - 3*b*b + 2*(a + b) Überprüft eure Makro-Definitionen durch Eintippen eines kleinen Programmstücks und Aufruf des Präprozessors (entweder mittels cpp <datei>.c oder gcc -E <datei>.c). Programmstückchen: 1 # define f(x) 3*( x)*(x) + (x)/2-1 2 # define g(x, y) (x)*(x) - 3*( x)*(y) + (y)*(y) 3 # define h(x, y, z) 2*( x)*(x)*(x) - 3*( y)*(y) + 2*( z) 4 5 f(1 + z) 6 g(x, A + 1) 7 h(a, b, a + b) Ausgabe des Präprozessors: 1 3*(1 + z) *(1 + z) + (1 + z)/2-1 2 (x)*(x) - 3*( x)*(a + 1) + (A + 1) *(A + 1) 3 2*( a)*(a)*(a) - 3*( b)*(b) + 2*( a + b) Einführung in die Praktische Informatik, Wintersemester 2017/18 17-9