GCC - der Ursprung von allem

ArticleCategory:

Software Development

AuthorImage:[Here we need a little image from you]

[Lorne Bailey]

TranslationInfo:[Author + translation history. mailto: or http://homepage]

original in en Lorne Bailey 

en to de Rene S�ss

AboutTheAuthor:[A small biography about the author]

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.

Abstract:[Here you write a little summary]

Dieser Artikel geht davon aus, dass Du die Grundlagen der Programmiersprache C beherrscht, und wird Dir zeigen, wie Du den gcc als Compiler verwendest. Wir werden daf�r sorgen, dass Du den Compiler f�r einfachen C-Code direkt von der Kommandozeile aufrufen kannst. Danach werden wir einen Blick darauf werfen, was gerade passiert, und wie Du die Kompilierung Deiner Programme kontrollieren kannst. Au�erdem werden wir einen Blick auf die Verwendung eines Debuggers werfen.

ArticleIllustration:[One image that will end up at the top of the article]

[Illustration]

ArticleBody:[The main part of the article]

GCC ist klasse

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).

Zum Anfang...

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.c
Defaultm��ig generiert der C Compiler eine ausf�hrbare Datei namens a.out. Ausf�hren kannst Du es mit:
a.out
Hello World

Jedes 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.

Vor-Kompilieren

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.txt
3,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:
  1. Kopieren der "#include" Dateien in das zu kompilierende Source-File.
  2. Ersetzen der "#define" Texte mit den aktuellen Werten.
  3. Ersetzen der Macros in der Zeile wann immer Sie aufgerufen werden.
Dies erlaubt Dir, Konstanten zu verwenden (z.B.: DECKSIZE entspricht der Anzahl an Karten in einem Blatt), die im ganzen Code verstreut sind, an einer Stelle zu deklarieren, und automatisch zu �bernehmen, immer wenn Du den Wert �nderst. In der Praxis wirst Du nie die -E Option direkt verwenden, jedoch wirst Du den Output dem Compiler zukommen lassen.

Kompilieren

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.

Assemblieren

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.c
aufrufen, 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.

Notiere Dir zwei Dinge.
  1. Wir schreiben eine Variable mit ihrer Adresse, welche man mit dem '&' (Addresse von) Operator erh�lt. Diese �bergibt die Maschinenadresse der Variablen an die Funktion, also kann die Funktion selbst die Variable �ndern. Es ist zwar m�glich mit globalen Variablen zu arbeiten, aber diese sollten nur selten verwendet werden. Zeiger sind ein wichtiger Teil von C und sollten gut verstanden werden.
  2. Wir verwenden einen Funktionsaufruf von einer neuen .c Datei. Das Betriebssystem schaut immer nach einer Funktion namens "main" und startet dort. 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.c
und �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 status
Das 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?

Linken

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.o
Dies 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?

Zwei weitere wichtige Optionen

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.

Debuggen

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 | less
und �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.o
Dies 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?

Zusammenfassung

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.

Links