original in en Lorne Bailey
en to de Rene S�ss
Lorne lebt in Chicago und arbeitet als Computer-Berater, spezialisiert auf den Umgang mit Oracle-Datenbanken. Seit seinem Umstieg auf ausschlie�liche *nix-Programmierung, hat Lorne die DLL-H�lle komplett vermieden. Derzeit arbeitet er an seinem Master-Titel in Informatik.
Kannst Du Dir vorstellen, Freie Software mit einem kommerziellen, "closed Source" Compiler
zu kompilieren?
Woher weist Du, was in deinem ausf�hrbarem Programm passiert?
Es k�nnte eine Art Hintert�r oder ein Trojaner eingebaut werden. Ken Thompson, schrieb f�r
einen
der gr��ten Hacks aller Zeiten, einen Compiler, der eine Hintert�r in das 'login' programm einbaute
und den Trojaner verewigte, als der Compiler merkte, dass er sich selbst kompilierte.
Lies seine Beschreibung dieses Klassikers
hier.
Gl�cklicherweise haben wir gcc.
Immer wenn Du ein configure; make; make install
ausf�hrst, macht gcc eine
Menge
Arbeit im Hintergrund. Wie lassen wir gcc f�r uns arbeiten? Wir werden damit anfangen, ein Kartenspiel
zu programmieren, aber wir werden nur soviel schreiben, um die Funktionalit�t des Compilers zu
demonstrieren. Da wir von Grund auf starten, haben wir gute Voraussetzungen dazu, den
Kompilierungsprozess zu verstehen und was in welcher Reihenfolge passiert, um ein ausf�hrbares
Programm zu erzeugen. Wir werden einen �berblick bekommen, wie ein C-Programm kompiliert wird,
und die Optionen, um dem gcc zu sagen, was wir wollen.
Die Schritte (und die Werkzeuge, die das machen ) sind Vor-kompilieren (gcc -E), Kompilieren (gcc), Assemblieren (as), und Linken (ld).
Zuerst sollten wir wissen wie man den Compiler aufruft. Es ist wirklich einfach. Wir werden mit dem klassischen ersten C-Programm anfangen. (Fortgeschrittene m�ssen mir vergeben).
#include <stdio.h> int main()
{ printf("Hello World!\n"); }
Speichere diese Datei als game.c
. Du kannst sie in der Kommandozeile �bersetzen mit:
gcc game.cDefaultm��ig generiert der C Compiler eine ausf�hrbare Datei namens
a.out
.
Ausf�hren kannst Du es mit:
a.out Hello WorldJedes Mal, wenn Du ein Programm kompilierst, �berschreibt das neue
a.out
das vorige Programm. Du wirst nicht wissen, von welchem Programm
das derzeitige a.out
ist.
Wir k�nnen dieses Problem l�sen, indem wir gcc mit der Option -o
mitteilen,
wie das erzeugte Programm hei�en soll. Wir werden das Programm game
nennen,
obwohl wir es irgendwie nennen k�nnten, da C keine Einschr�nkungen in der Wahl des Namens
hat wie etwa Java.
gcc -o game game.c
game Hello World
An diesem Punkt sind wir immer moch ein gutes St�ck davon entfernt, ein sinnvolles Programm zu haben. Wenn Du jetzt denkst, dass sei schlecht, solltest Du ber�cksichtigen, dass wir ein Programm haben, dass sich kompilieren l�sst und l�uft. Wenn wir jetzt St�ck f�r St�ck Funktionalit�t in das Programm bringen, dann nur um sicher zu gehen, dass es ausf�hrbar bleibt. Es scheint so zu sein, dass jeder angehende Programmierer zuerst 1000 Zeilen Sourcecode schreiben, und dann alles auf einmal korrigieren will. Niemand, und ich meine Niemand kann das. Du schreibst ein kleines Programm, das funktioniert, du �nderst es und l�sst es wieder laufen. Das schr�nkt die Anzahl der Fehler, die Du auf einmal korrigieren mu�t, ein. Und, Du wei�t genau, was Du gerade getan hast, damit Du es korrigieren kannst. Dies h�lt Dich davon ab, etwas zu erzeugen, von dem Du denkst, dass es arbeiten sollte, und auch kompiliert werden kann, aber niemals laufen wird. Erinnere Dich, nur weil es kompiliert wird, ist es nicht richtig.
Unser n�chster Schritt ist, ein Header-File f�r unser Spiel zu erzeugen. Ein Header-File sammelt Datentypen und Funktionsdeklarationen an einem Ort. Dies stellt sicher, dass die Definitionen der Datenstrukturen �bereinstimmen, so dass jeder Teil unseres Programms genau das gleiche sieht.
#ifndef DECK_H #define DECK_H #define DECKSIZE 52 typedef struct deck_t { int card[DECKSIZE]; /* number of cards used */ int dealt; }deck_t; #endif /* DECK_H */
Speichere diese Datei als deck.h
. Nur .c
Dateien werden
kompiliert, also m�ssen wir unsere Datei game.c �ndern. In Zeile 2 der Datei game.c, schreib
#include "deck.h"
. In Zeile 5 schreib deck_t
deck;
Um sicher zu gehen, dass wir nichts zerst�rt haben,
kompilieren wir es nochmal.
gcc -o game game.c
Keine Fehler, kein Problem. Wenn es nicht kompilierbar ist, arbeite daran, bis es funktioniert.
Wie wei� der Compiler, was ein
deck_t
Typ ist? W�hrend der Vor-kompilierung, wird die Datei "deck.h"
in die Datei "game.c" kopiert. Die Vor-kompilierer Anweisungen sind durch ein "#" gekennzeichnet.
Du kannst den Precompiler (Die Vor-kompilierer) �ber den gcc mit der Option -E
aufrufen.
gcc -E -o game_precompile.txt game.c wc -l game_precompile.txt 3199 game_precompile.txt3,200 Zeilen Ausgabe! Das meiste davon kommt von der
stdio.h
include-Datei, aber wenn Du es Dir n�her ansiehst,
sind unsere Deklarationen auch darin. Wenn Du keinen Namen f�r die erzeugte Datei
mittels -o
Option angibst, kommt die Ausgabe ins Textfenster. Der
Vorgang der Vorkompilierung gibt dem Code eine gr��ere Flexibilit�t durch
Erreichung folgender Ziele:
-E
Option direkt verwenden, jedoch wirst
Du den Output dem Compiler zukommen lassen.
Als Zwischenschritt �bersetzt gcc deinen Code in Assembler-Code. Um das zu tun, muss er ausrechnen, was Du tun wolltest, indem es deinen Code �bersetzt. Wenn Du einen Syntax-Error machst, wird er es Dir das mitteilen und der Kompilierungsvorgang wird abgebrochen. Manche Leute glauben, dass dies der einzige Schritt im Kompilierungsvorgang ist, aber es gibt noch mehr f�r gcc zu tun.
as
�bersetzt den Assembler Code in Object-Code.
Object-Code kann nicht am Prozessor verarbeitet werden, aber er ist sch�n geschlossen.
Die Compiler Option -c
verwandelt eine .c Datei in ein Object-File mit einer
.o Endung.
Wenn wir
gcc -c game.caufrufen, erzeugen wir automatisch eine Datei namens game.o. Hier sind wir an einem wichtigen Punkt angelangt. Wir k�nnen jede .c Datei nehmen und eine Object-Datei daraus erzeugen. Wie wir unten sehen, k�nnen wir diese Object-Files im Linker-Vorgang zu einem ausf�hrbahren Programm machen. Lass uns mit unserem Beispiel weitermachen. Da wir ein Kartenspiel programmieren und Kartenspiel definiert haben mit
deck_t
, werden wir eine Funktion schreiben, die unser Kartenspiel mischt.
Diese Funktion legt den Zeiger auf eine Kartenart und l�dt ihn mit Zufallswerten f�r die Kartenwerte.
Sie merkt sich auch, welche Karten bereits verwendet wurden mit dem 'drawn' array. Dieses array mit
DECKSIZE Mitgliedern, bewahrt uns davor, einen Kartenwert doppelt zu verwenden.
#include <stdlib.h> #include <stdio.h> #include <time.h> #include "deck.h" static time_t seed = 0; void shuffle(deck_t *pdeck) { /* Keeps track of what numbers have been used */ int drawn[DECKSIZE] = {0}; int i; /* One time initialization of rand */ if(0 == seed) { seed = time(NULL); srand(seed); } for(i = 0; i < DECKSIZE; i++) { int value = -1; do { value = rand() % DECKSIZE; } while(drawn[value] != 0); /* mark value as used */ drawn[value] = 1; /* debug statement */ printf("%i\n", value); pdeck->card[i] = value; } pdeck->dealt = 0; return; }
Speichere diese Datei als shuffle.c
.
Wir haben eine Debug-Anweisung in den Code eingef�gt, damit gibt es uns, wenn das Programm
l�uft, die Kartenwerte aus, die es generiert. Dies ver�ndert zwar nicht die Funktionalit�t
unseres Programms, aber es ist entscheidend, da wir wissen, was jetzt geschieht.
Da wir gerade erst mit dem Spiel anfangen, haben wir keine andere M�glichkeit, um zu testen, ob unser
Programm das tut, was wir wollen. Mit der printf Anweisung k�nnen wir exakt sehen, was gerade
passiert, also k�nnen wir jetzt mit der n�chsten Phase beginnen, wo wir wissen, dass unser Blatt gut
gemischt ist. Nachdem wir uns daran erfreut haben, dass es richtig arbeitet, k�nnen wir diese Zeile wieder
aus dem Code entfernen. Die Art, ein Programm zu debuggen, wirkt zwar etwa ordin�r, aber es es ist die
unkomplizierteste Art. Wir werden aufregendere debugger sp�ter kennenlernen.
shuffle.c
hat keine Funktion "main", und vorher kann kein ausf�hrbares
Programm erzeugt werden. Wir m�ssen es mit einem anderen Programm kombinieren, das
eine Funktion "main" hat, und die Funktion "shuffle" aufruft.
F�hre folgenden Befehl aus:
gcc -c shuffle.cund �berzeuge Dich, dass eine neue Datei namens
shuffle.o
erzeugt
wird. Editiere die game.c Datei, und in Zeile 7, nach der Deklaration der deck_t variable
deck
, f�ge folgende Zeile ein:
shuffle(&deck);Wenn wir jetzt versuchen, eine ausf�hrbare Datei zu erzeugen, bekommen wir eine Fehlermeldung.
gcc -o game game.c /tmp/ccmiHnJX.o: In function `main': /tmp/ccmiHnJX.o(.text+0xf): undefined reference to `shuffle' collect2: ld returned 1 exit statusDas Kompilieren funktionierte, da unsere Syntax richtig war. Das Linken schlug fehl, weil wir dem Compiler nicht gesagt haben wo die 'shuffle' Funktion ist. Was ist der link und wie sagen wir dem Compiler, wo er die Funktion finden kann?
Der Linker, ld
, nimmt den Object-Code, welcher zuvor von
as
erzeugt wurde, und wandelt es in ein ausf�hrbares Programm um mit
dem Befehl
gcc -o game game.o shuffle.oDies wird die zwei Objekte zusammenf�hren und die ausf�hrbare Datei
game
erzeugen.
Der Linker findet die shuffle
Funktion
vom shuffle.o Objekt und f�gt es in die ausf�hrbare Datei ein. Das wirklich tolle
an den Object-Files ist, dass, wenn wir die Funktion wieder verwenden wollen, wir nur
noch die "deck.h" Datei einf�gen und das shuffle.o
Object-File zur neuen auszuf�hrenden Datei dazulinken m�ssen.
Wiederverwendung von Code auf diese Art ist durchaus �blich. Wir haben nicht die
printf
Funktion geschrieben, die wir oben zum Debuggen verwendet haben, aber der Linker findet
die Definitionen in dem File, das wir mit #include <stdlib.h>
eingebunden haben,
und zeigt zu dem Object-Code in der C-Bibliothek (/lib/libc.so.6). Auf diese Art k�nnen wir Funktionen
von jemand anderem verwenden, ohne uns Sorgen zu machen, ob sie funktionieren, und uns um unsere
eigenen Probleme k�mmern.
Dies ist der Grund, weshalb Header-Dateien normalerweise die Daten- und Funktionsdeklarationen,
aber nicht die Funktionen selbst enthalten. Normalerweise machst Du Object-Files bzw.
Bibliotheken f�r den Linker, um es ins Programm zu schreiben.
Ein Problem k�nnte auftreten, da wir nicht alle Funktionsdefinitionen in unseren Header
geschrieben haben. Was k�nnen wir tun, um zu �berpr�fen, ob alles in Ordnung ist?
Die -Wall
Option schaltet alle Warnungen betreffend Syntax ein, damit wir sicher
sein k�nnen, dass unser Code in Ordnung und so weit wie m�glich portierbar ist.
Wenn wir diese Option verwenden und unseren Code kompilieren, sehen wir etwas �hnliches wie:
game.c:9: warning: implicit declaration of function `shuffle'Dies teilt uns mit, dass wir ein wenig mehr zu tun haben. Wir m�ssen eine Zeile in ein Header-File einf�gen, in der wir dem Compiler alles �ber unsere
shuffle
Funktion mitteilen, damit der Compiler alles �berpr�fen kann, was er
�berpr�fen soll. Es klingt l�stig, aber es trennt die Definiton von der Implementation und erlaubt
uns, unsere Funktion �berall anders zu verwenden, indem wir einfach einen neuen Header einbinden,
und zu unserem Object Code hinzulinken. Wir schreiben diese eine Zeile in unsere deck.h Datei.
void shuffle(deck_t *pdeck);Dies beseitigt die Warn-Ausgaben.
Eine weitere Compiler-Option ist die Optimierung.
-O#
(z.B.: -O2).
Dies teilt dem Compiler mit, welche Stufe der Optimierung Du m�chtest. Der Compiler hat
eine Menge Tricks, um Deinen Code ein bi�chen schneller zu machen. Bei kleinen Programmen
wie unserem hier wirst Du keine Unterschiede merken, gr��ere Programme jedoch werden
etwas kleiner. Du wirst es �berall sehen, deshalb solltest Du auch wissen, was es bedeutet.
Wie wir alle wissen, hei�t es nicht, wenn unser Code kompiliert wird, dass er auch so arbeitet, wie wir wollen. Du kannst �berpr�fen, ob alle Nummern verwendet werden, wenn du folgendes ausf�hrst
game | sort - n | lessund �berpr�fst, dass nichts fehlt. Was tun wir, wenn etwas fehlt? Was tun wir, wenn ein Problem auftritt? Wie k�nnen wir etwaige Fehler finden? Du kannst Dein Programm mit einem Debugger �berpr�fen. Die meisten Distributionen verwenden den klassischen Debugger gdb. Wenn Dir die Anzahl der Optionen auf der Kommandozeile zu viel sind, bietet KDE eine sehr sch�ne grafische Oberfl�che f�r den gdb an namens KDbg. Es gibt auch andere grafische Oberfl�chen, und sie sind sich alle sehr �hnlich.Um mit dem Debugging zu beginnen, w�hlst Du File->Executable und suchst dann Dein
game
Programm.
Wenn Du F5 dr�ckst, oder Execution->Run from the menu, solltest Du eine Ausgabe in einem anderen
Fenster sehen. Was ist passiert? Wir sahen nichts im Fenster.
Sorg Dich nicht, KDbg ist nicht kaputt. Das Problem kommt daher, dass wir keinerlei
Debugg-Information in unserem Programm haben, also kann uns KDbg nicht sagen,
was gerade passiert. Das Compiler Flag -g
gibt die notwendigen
Ausgaben in das Object-File. Du must das Object-File (.o Endung) mit diesem Flag
kompilieren, also lautet der Befehl
gcc -g -c shuffle.c game.c gcc -g -o game game.o shuffle.oDies markiert Sachen im ausf�hrbaren Programm so, dass gdb und KDbg in der Lage sind, anzuzeigen, was gerade passiert. Debuggen ist eine wichtige F�higkeit, es ist sicher wichtig zu lernen einen zu benutzen. Debugger helfen dem Programmierer mit der M�glichkeit "Breakpoints" im Source Code zu setzen. Versuche nun einen zu setzen, indem Du mit der rechten Maustaste auf die Zeile klickst, die die
shuffle
Funktion aufruft. Ein kleiner roter Kreis
sollte neben der Zeile erscheinen. Wenn Du jetzt F5 dr�ckst, bleibt das Programm an dieser
Stelle stehen. Dr�cke F8 um in die shuffle Funktion zu kommen. Hey, jetzt siehst Du den
Code von shuffle.c
!
Wir k�nnen die Abarbeitung des Programms Schritt f�r Schritt kontrollieren und schauen was wirklich
passiert. Wenn Du den Mauszeiger �ber eine lokale Variable stellst, siehst Du, was sie beinhaltet.
S��. Es ist viel besser als diese printf
Anweisungen, oder?
Dieser Artikel war eine rasante Tour duch Kompilieren und Debuggen von C-
Programmen. Wir diskutierten die Schritte, die der Compiler durchl�uft und
welche Optionen gcc verwendet, um diese Schritte zu machen. Wir streiften
das Linken mit shared libraries und endeten mit einer Einf�hrung �ber Debugger.
Es nimmt viel Zeit in Anspruch, zu lernen, was Du tust, doch ich hoffe, dies hat
dir geholfen, richtig zu beginnen. Mehr Informationen zu dem Thema findest Du
im man
und in den info
Seiten f�r gcc
,
as
und ld
.
Selbst zu programmieren, lehrt am meisten. Zur �bung kannst Du die einfachen Anregungen f�r das Kartenspielprogramm in diesem Artikel verwenden und ein Black Jack Spiel programmieren. Nimm Dir die Zeit, um zu lernen, wie man den Debugger verwendet. Es ist viel einfacher mit einem GUI wie KDbg anzufangen. Wenn Du ein bi�chen Funktionalit�t auf einmal einbringst, wirst Du fertig sein, bevor Du es merkst. Denk daran, halt es lauff�hig!
Hier einige Dinge, die Du brauchen wirst, um ein vollst�ndiges Spiel zu programmieren.