Arbeitsblatt #4

Werbung
Arbeitsblatt #4
Einführung in die Systemprogrammierung SS 2014
14. Mai 2014
In dieser und den beiden folgenden Übungen betrachten wir die Grundkonzepte der Programmiersprache C. Dazu erstellen wir C-Programme, die wir danach übersetzen und ausführen.
Die Tutoren können Ihnen bei der Auswahl eines geeigneten Editors helfen.
Ein vom Benutzer angegebenes C-Programm wird also zunächst in ein Assemblerprogramm kompiliert, und von dort aus in Maschinensprache übersetzt. Tatsächlich bleiben diese Details jedoch meist
verborgen. So können Sie ein C-Programm programm.c auf der Kommandozeile sehr direkt in ein
ausführbares Programm programm übersetzten:
gcc --std=c11 programm.c -o programm
Sofern beim Übersetzen keine Fehler auftreten, wird der C-Übersetzer gcc das ausführbare Programm
programm erzeugen. Eventuelle Fehler werden mit Zeilennummer und Erklärung angegeben.
Die Angabe von --std=c11 weist den Übersetzer darauf hin, den C-Standard C11 zu verwenden,
statt des älteren C89, das meist noch die Standardeinstellung ist. Dies vereinfacht einige Konstrukte.
Um diesen Parameter nicht jedes Mal angeben zu müssen, können Sie der Shell sagen, daß sie ein
neues Kommando gcc11 anbieten soll:
alias gcc11=gcc --std=c11
Nun können Sie gcc11 statt gcc --std=c11 verwenden:
gcc11 programm.c -o programm
Beachten Sie dabei, daß diese Einstellung nur für ihre aktuelle Kommando-Shell gilt. Falls sie das
Alias immer automatisch einrichten möchten, können Sie es an die Datei /.bashrc anhängen, so daß
es automatisch beim Start Ihrer Shell ausgeführt wird1 .
Wir verwenden zwei Operationen, die durch die Präprozessoranweisung
# include <stdio .h>
importiert werden können:
• printf(), das sich wie in Übung #01 verhält und das wir hier zum Ausgeben von Zeichenketten
und Zahlen verwenden. printf nimmt einen oder mehrere Parameter, wobei der erste Parameter immer eine Zeichenkette sein muß. Diese erste Zeichenkette kann Formatierungszeichen
beinhalten, wie z.B. %d oder %s. Mit Ausnahme des Formatierungszeichens %%, das immer ein
Prozentsymbol ausgibt, bezieht sich das nte Formatierungssymbol der Zeichenkette immer auf
den (n + 1)ten Parameter und fügt diesen an der betreffenden Stelle in der Zeichenkette ein.
Dabei gilt:
– %d erwartet als zugehörigen Parameter eine int-Zahl und fügt diese als Dezimalzahl ein.
– %s erwartet als zugehörigen Parameter eine Zeichenkette (ein char-array) und fügt diese
direkt ein. printf ignoriert Formatierungszeichen in dieser eingefügten Zeichenkette.
1
Diese Angaben, wie die meisten Angaben bezüglich der Shell, gehen davon aus, daß Sie die bash verwenden, die
verbreitetste Shell. Für andere Shells wie die tccsh gelten analoge aber andere Regeln.
1
Beispiel: printf("%d%% von %d sind %d\n", 20, 45, 9); gibt 20% von 45 sind 9“ aus,
”
gefolgt von einem Zeilenumbruch.
• scanf(), das das Gegenstück zu printf zum Einlesen von Zahlen ist. Wir benötigen es hier nur
in der Form scanf("%d", &i), in der es eine einzelne Zahl einliest und in die int-Variable ‘i’
schreibt (die genaue Bedeutung des &-Präfixoperators behandeln wir in der nächsten Vorlesung).
1
Hallo, Welt!
Testen Sie den C-Übersetzer, indem Sie ein ‘Hallo-Welt’-Programm schreiben. Das Programm sieht
aus wie folgt:
# include <stdio .h>
int
main(int argc , char ** argv)
{
printf ("Hallo , Welt !\n");
return 0;
}
Beachten Sie, daß es sich geringfügig von der in der Vorlesung vorgestellten Fassung unterscheidet:
• #include <stdio.h> macht die Funktion ‘printf’ verfügbar2
• return 0 springt vom Hauptprogramm mit dem Ergebnis 0 zurück. Dieser Wert wird von der
Kommando-Shell aufgefangen und kann dort verwendet werden, um zu prüfen, ob das Programm
erfolgreich terminiert hat oder nicht: 0 bedeutet ‘erfolgreich’, 1 bedeutet ‘mit Fehler’3 .
2
Schleifen und Eingaben [9.1–9.3]
Schreiben Sie ein Programm, das in einer Schleife zwei Zahlen einliest und deren Summe ausgibt.
Die Schleife soll terminieren, sobald das Ergebnis der Berechnung 0 ist. Wählen Sie eine angemessene
Schleifenform.
a. Schreiben und testen Sie Ihr Programm.
b. Was passiert, wenn Sie die Zahl 3000000000 (drei Milliarden) mit Ihrem Programm mit sich
selbst addieren? Versuchen Sie, das Ergebnis zu erklären.
3
Arrays [9.1]
Übersetzen Sie den folgenden Programmcode:
# include <stdio .h>
int main ()
{
char * daten [2] = {" Hallo ", "Welt!"};
printf ("%s\n", daten [0]);
return 0;
}
a. Führen Sie das Programm wie angegeben aus.
2
Strenggenommen wird dadurch nur der Typ von printf und einigen anderen Funktionen/Strukturen definiert; wir
werden dieses Detail später genauer untersuchen.
3
Der C-Standard definiert einige Konstanten für diesen Zweck, die in der Praxis statt 0 oder 1 verwendet werden
sollten.
2
f e d c b a 9 8 7 6 5 4 3 2 1
Rezessiv
Dominant
langstielig
saftig
hell
dunkel
oval
fleischig
klein
gross
nachtleuchtend
braun
wuerzig
sauer
suess
fest
weich
gerippt
Bit
0
Abbildung 1: Genom der simulierten Tomatenpflanze und Bit-Kodierung
b. Ändern Sie daten[0] auf daten[-1]. Was passiert nun, wenn Sie das Programm ausführen?
c. Ändern Sie auf daten[2]. Was passiert nun, wenn Sie das Programm ausführen?
d. Was passiert, wenn Sie in C die Grenzen eines Datenfeldes überschreiten?
4
Funktionen und Bitoperationen [9.1–9.3, 9.8]
In der Systemprogrammierung ist es oft wichtig, Speicherplatz zu sparen: Wenn Sie auf einem eingebetteten System arbeiten, haben Sie inhärent wenig Platz zur Verfügung, und wenn Sie an einem
Betriebssystemkern, einer Systembibliothek, oder einem Laufzeitsystem arbeiten, kann ein verschwendetes Wort im Speicher sich schnell vervielfachen und ggf. durch zusätzliche Auslastung der Caches
zu Systemverlangsamungen führen.
Auf dem Rest dieses Arbeitsblattes werden wir daher zur Übung ein Beispielprogramm entwickeln,
das die Verwendung von Bitoperationen demonstriert. Diese Operationen können verwendet werden,
um Daten kompakt im Speicher zu repräsentieren.
Wir entwickeln dazu eine Tomatenpflanzensimulation, in der wir eine (stark vereinfachte und biologisch nicht realistische) genetische Struktur von Tomatenpflanzen mit verschiedenen Eigenschaften
simulieren.
Wir nehmen an, daß Tomaten nur eine Art von Chromosom verwenden, wobei jedes Chromosom
exakt 16 Gene beinhaltet. Wir gehen davon aus, daß jedes Gen entweder ‘ja’ oder ‘nein’ ausdrückt
und daher mit einem einzigen Bit simuliert werden kann. Wenn wir davon ausgehen, daß Tomaten
zwei Elternteile haben können, die jeweils ein Chromosom beisteuern, benötigen wir also 32 Bit, um
das komplette Genom einer Tomatenpflanze zu simulieren: Je 16 Bit für jeden Elternteil.
Da int-Werte auf modernen Rechnern mindestens 32 Bit kodieren, können wir also das gesamte
Erbgut einer Tomatenpflanze in einem int kodieren. Abbildung 1 beschreibt die 16 Eigenschaften und
ihre Bit-Kodierung.
a. Ausgabe: Schreiben Sie eine Funktion void ausgabe(int gene), die die genetischen Informationen der Tomatenpflanze ausgibt. Dabei sollen in einer Zeile alle Eigenschaften, getrennt durch
je ein Leerzeichen, ausgegeben werden, gefolgt von einem Zeilenumbruch. Verwenden Sie dazu
eine Schleife.
Beispiel: 0x0103 soll zu folgender Ausgabe führen:
langstielig saftig wuerzig
Die Datei http://web.sepl.cs.uni-frankfurt.de/2014-ss/b-sysp/4\_4.c beinhaltet die
nötigen Header-Deklarationen, sowie ein Datenfeld gene, das Zeichenketten für die 16 Eigenschaften beinhaltet. Sie können mit gene[0] bis gene[15] auf diese Eigenschaften zugreifen.
3
b. Ausgabe des zweiten Elternteils: Wir wollen nun die genetischen Informationen beider Elternteile
in einem 32-Bit-Wort speichern. So ist z.B. 0x80012002 eine Pflanze, die von einem Elternteil
die Eigenschaften langstielig“ und sauer“, und vom anderen Elternteil die Eigenschaften fest“
”
”
”
und saftig“ geerbt hat. Wie müssen Sie Ihre Funktion aufrufen, wenn Sie beide Elternteile
”
kodiert haben, z.B.
int g = 0 x80012002 ;
und nun beide ausgeben möchten?
c. Phänotyp berechnen: Der Phänotyp eines Lebewesens beschreibt dessen Aussehen, im Gegensatz
zum Genotyp, den wir bisher betrachtet haben. Um den Phänotyp zu berechnen, müssen wir
folgende Schritte durchführen:
(i) Kombinieren der Erbgutinformationen beider Eltern. Hierbei müssen wir zwischen dominanten und rezessiven Eigenschaften unterscheiden:
• Dominante Eigenschaften sind im Phänotyp ersichtlich genau dann wenn sie in mindestens einem der beiden Sätze an Erbinformationen auftauchen.
• Rezessive Eigenschaften sind im Phänotyp ersichtlich genau dann wenn sie in beiden
der Sätze an Erbinformationen auftauchen.
Die Abbildung führt auf, welche Eigenschaften rezessiv und welche dominant sind. Versuchen Sie, diese effizient mit Bitoperationen zu berechnen.
(ii) Entfernen von gegensätzlichen Eigenschaften: Die Eigenschaftspaare süß“/ sauer“, sowie
”
”
groß“/ klein“, fest“/ weich“, und hell“/ dunkel“ schließen sich gegenseitig aus. Wenn
”
”
”
”
”
”
beide Eigenschaften im Phänotyp enthalten sind, entfernen wir sie beide; eine Tomate, die
z.B. sowohl groß“ als auch klein“ ist, unterscheidet sich nicht sichtbar von einer Tomate
”
”
ohne beide Eigenschaften.
Schreiben Sie eine Funktion int phaenotyp(int genotyp), die den Phänotyp berechnet. Berechnen Sie mit dieser Funktion die Phänotypen der folgenden Pflanzen und geben Sie sie aus:
• 0xff00e301 (langstielig, wuerzig, braun, fest)
• 0x010c7608 (fleischig, oval)
d. Kreuzen: Wir wollen nun zwei Tomatenpflanzen kreuzen. Dazu führen wir Meiose durch. Wir
verwenden in unserer Simulation einen vereinfachten Prozeß:
• Für jedes Gen (aus den 32) wählen wir zufällig ein Gen von einem oder von dem anderen
Elternteil.
• Anschließend drehen wir genau eines der 32 Bits um, um einen Transkriptionsfehler zu
simulieren.
Verwenden Sie die in 4 4.c enthaltene Funktion int zufall int(), um eine Folge von 32 pseudozufälligen Bits (in einem int kodiert) zu erhalten. Um eine Zufallszahl zwischen 0 und 31
zu erhalten, können Sie die Funktion int rand() aus der Standardbibliothek verwenden. Diese
liefert eine nichtnegative Pseudozufallszahl, die allerdings sehr hoch sein kann; Sie können die &bzw. %-Operatoren verwenden, um daraus eine Zahl zwischen 0 und 31 zu extrahieren.
Implementieren Sie die Funktion int meiose(int elternteil1, int elternteil2) und stellen Sie sicher, daß die erzeugten Ergebnisse plausibel sind.
Anmerkung zu Pseudozufallszahlen: Der Pseudozufallszahlengenerator rand() wird bei
jedem Programmstart gleich initialisiert, so daß er deterministisch identische Ergebnisse liefert4 .
4
Wenn Sie nichtdeterministische Pseudozufallszahlen wünschen, können Sie die Header-datei time.h mit einbinden.
Danach können Sie den Generator auf einer Zahl initialisieren, die sich jede Sekunde ändert, indem Sie zu Beginn Ihres
Programmes den Aufruf srand(time(NULL)) durchführen.
4
Herunterladen