Jutta Degener, November 1994                                          Zurück zu Unix­1

Wie ein C­Programm aus Sourcecode entsteht.

Sourcecode, zum Beispiel die Datei hello.c,
    #include <stdio.h>
    main()
    {
    	printf("Hello, world!\n");
    	return 0;
    }
wird mit einem C­Compiler zu einem Programm compiliert, das man dann wie jedes andere Unix-Kommando ausführen kann.

    % gcc -o hello hello.c
    % ./hello
    Hello, world!
    %
Das sieht ganz einfach aus, ist aber in Wirklichkeit ein mehrstufiger Prozeß, bei dem mindestens drei Hilfsprogramme aus ungefähr vier verschiedenen Typen von Dateien das fertige Programm zusammenbauen.  Die Programme sind der Präprozessor, der Compiler und der Linker Die Dateisorten sind Header­Files, Source­Files, Object­Files und Libraries.

Der Präprozessor

Obwohl der ganze Sourcecode in der Datei hello.c zu stehen scheint, wird noch eine andere mitübersetzt: Mit der Präprozessor-Anweisung
    #include <stdio.h>
sagst Du ,,an dieser Stelle soll jetzt der Text eingefügt werden, der in der Datei stdio.h steht.``

Der Präprozessor war früher ein separates Programm; heute ist er meist nur eine Stufe des Compilers.  Er interpretiert nur die Statements in einem C­Programm, die mit # zu Beginn einer Zeile anfangen.

#include <filename> sorgt dafür, daß an dieser Stelle (anstelle der #include Anweisung selbst, die aus dem Text verschwindet) der Inhalt der Datei filename eingefügt wird.  Die Datei wird vom Präprozessor nicht im gleichen Verzeichnis wie der Quellcode gesucht, sondern in einem system­globalen Verzeichnis, normalerweise /usr/include.

Wenn man statt der spitzen Klammern Anführungszeichen benutzt, ändert sich das Suchverfahren; dann wird vor den globalen Verzeichnissen erst einmal das, in dem die Datei mit dem #include steht, durchsucht.

    #include "hello.h"
Das .h am Ende des Namens steht fuer header. In solchen header files stehen meist Deklarationen, die traditionell am Kopf (head) eines C-Programms geschrieben werden.  Das .h ist aber nur Konvention; man kann beliebige Dateien mit #include in den Quelltext einfügen.

Header-Files sind Teil von Schnittstellen zwischen Systemen.  Wenn das eine System einfach ,,die Sprache C`` oder ,,die Hardware`` ist, dann hat man es wahrscheinlich mit globalen Header-Files zu tun (in spitzen Klammern); wenn man mit anderen Menschen zusammenarbeitet, oder selbstgeschriebene Module benutzt, nimmt man lokale Header­Files in Anführungszeichen.

-I directory

Mit der -I-Compileroption kann man beim Übersetzen eines Programmes die Liste der Verzeichnisse erweitern, in denen nach einer Datei gesucht wird.
    gcc -Iinc hello.c
sucht nach stdio.h zuerst als inc/stdio.h, und erst dann als /usr/include/stdio.h.

-D symbol

Die zweite command line option, die man nach -I noch kennen sollte, ist -D. Das -D steht für ,,define;``
   % gcc -Dsymbol file.c
ist dasselbe, als hätte ich irgendwo in file.c ein
   #define symbol 1
geschrieben.  (In manchen Systemen ist es auch dasselbe wie
   #define symbol 0
oder
   #define symbol
man kann sich auf die 1 da nicht verlassen.)

Man benutzt -D häufig in Verbindung mit #ifdefs im Programmcode; wenn ich zum Beispiel meine Testausgaben mit #ifdef DEBUG einklammere

   #ifdef DEBUG
         fprintf(stderr, "Starting program run...\n");
   #endif
dann ist es bequem, dieses ,,DEBUG`` von der Aufrufzeile des Compilers aus ein­ und auszuschalten, ohne daß man jedesmal die Quelldatei selbst editieren muß.  (Übersetzen muß man sie trotzdem neu; ohne, daß der Präprozessor läuft, können Präprozessor-#defines nicht wirken.)

Die meisten Präprozessor bieten zusätzlich zum bloßen Definieren eines Symbols auch das Setzen auf einen Wert an; die Syntax ist dann -Dsymbol=value Zum Beispiel:

   % gcc -DDEBUG -DDEBUGLEVEL=5 files...

-E

Wenn man sich mal ansehen möchte, wie der eigene Sourcecode aussieht, nachdem er die Präprozessor­Phase des Compilers durchlaufen hat, kann man ihn sich mit -E ausgeben lassen:
   % gcc -E hello.c
   # 1 "hello.c"
   # 1 "/usr/gnu/lib/gcc-lib/sun4/2.5.8/include/stdio.h" 1 3
   
         ... 200 Zeilen später ...
  
   # 1 "hello.c" 2

   hello(char const * str)
   {
	printf("Hello, %s\n", str);
   }
Die Zeilen mit # am Anfang wurden vom Präprozessor eingefügt und sagen dem Compiler, aus welcher Zeile welcher Datei der kommende Sourcecode eigentlich wirklich stammt.

Der Compiler

Der ``eigentliche'' Compiler erzeugt aus dem Quelltext zusammen mit den durch #include-Anweisungen dazugekommenen Header­Files den Objektcode Dabei leistet er die Hauptarbeit des Übersetzens: Das Ergebnis dieser Stufe ist das Object­File, traditionell mit der File­Endung .o, also zum Beispiel hello.o

C­Compiler wie gcc oder cc hören nach dem Erzeugen von Object­Files mit dem Compilieren auf, wenn man ihnen die Option -c übergibt.  Mit

   % gcc -c hello.c
erzeuge ich also zu einer Datei hello.c das dazugehörige Object­File, hello.o.

-S

Mit der -S­Option kann man sich das Resultat des Übersetzungsvorgangs als Assembler­Code ansehen; zu einer Quelldatei file.c jeweils in einer Datei file.s (Filenamen für Assembler­Code enden traditionell in .s, für symbol, glaube ich.)
   % gcc -S hello.c
   % cat hello.s
   ___gnu_compiled_c:
   .text
           .align 8
   LC0:
           .ascii "Hello, %s!\12\0"
           .align 4
           .global _hello
           .proc   04
   _hello:
           !#PROLOGUE# 0
           save %sp,-112,%sp
           !#PROLOGUE# 1
           st %i0,[%fp+68]
           sethi %hi(LC0),&ouml;1
           or &ouml;1,%lo(LC0),&ouml;0
           ld [%fp+68],&ouml;1
           call _printf,0
           nop
   L1:
           ret
           restore
   %
Das nützt einem natürlich nur dann etwas, wenn man ein bißchen Assembler kann.  Dann ist es praktisch; zum Beispiel, wenn man ,,von Hand`` seinen Code optimiert, oder wenn man die genaue Natur eines Compilerfehlers erforschen möchte.

Der Linker

Ein Programm kann aus mehreren Quelldateien bestehen.
hello.c:
    #include <stdio.h>
    hello(char const * string)
    {
       	printf("Hello, %s!\n", string);
    }

goodbye.c:
    #include <stdio.h>
    goodbye(char const * string)
    {
       	printf("Good-bye, %s!\n", string);
    }

main.c:
    extern int hello(char const *);
    main()
    {
        hello("world");
        goodbye("moon");
	return 0;
    }
Die Quelldateien können getrennt compiliert werden,
   % ls
   goodbye.c       hello.c         main.c
   % gcc -c hello.c
   % ls
   goodbye.c       hello.c         hello.o         main.c
   % gcc -c main.c
   % gcc -c goodbye.c
   % ls 
   goodbye.c       hello.c         main.c
   goodbye.o       hello.o         main.o
   % 
müssen aber irgendwann miteinander verbunden, ge­linked werden. 
   % gcc -o hello main.o hello.o goodbye.o
   % ./hello
   Hello, world!
   Good-bye, moon!
   % 
Es ist günstig, Programme in mehrere Source­Files aufzuteilen, weil dann beim Entwicklen der Software nicht nach jeder kleinen Änderung alles neu übersetzt werden muß.  Linken geht schnell; das Übersetzen dauert lange.

Der Linker

Wenn alle Zahlen stimmen, und alle externen Referenzen stimmen, hat man ein ausführbares Programm.

Um nur die Linker­Stufe des Compilers gcc oder cc zu benutzen, braucht man keine besonderen Optionen anzugeben - wenn man nicht (zum Beispiel mit -c) das Gegenteil festlegt, macht ein C­Compiler immer bis zum ausführbaren Programm weiter.  Daß eine Datei Objektcode enthält, sieht der Compiler an der Endung: .o für Objektcode.

Die -o­Option, die einem in diesem Zusammenhang einfallen könnte, steht für output, nicht für object - das Wort nach ihr sagt, wie das ausführbare Programm heißen soll.  Wenn man -o nicht angibt, ist der default a.out; deshalb heißt das File-Format für ausführbare Programme unter Unix auch manchmal ,,a.out-Format.``

Bibliotheken

Außer main, goodbye und hello kommt in hello.c noch eine andere Funktion vor, printf() Wo steht deren Text eigentlich? Der Objektcode von printf() steht in einer Sammlung von vorcompilierten Object-Files, einer library, die zu allen C­Programmen unsichtbar mit dazugebunden wird.  Diese spezielle Library steht normalerweise im File /usr/lib/libc.a; sie enthält (fast) alle Funktionen, die zur Sprache C gehören.

Solche Libraries kann man als Programmierer auch selbst aus Objektdateien erzeugen, und zwar mit Hilfe des archivers ar Auch für Bibliotheken gibt es eine spezielle Endung, .a für archive.

   % ls 
   goodbye.c       hello.c         main.c
   goodbye.o       hello.o         main.o
   % ar cr libgreet.a hello.o goodbye.o
   % ls
   goodbye.c       hello.c         libgreet.a      main.o
   goodbye.o       hello.o         main.c
   % 
Die Aufrufsyntax von ar ist etwas eigenartig; als erstes gibt man einen ,,code`` an, der sagt, was denn ar überhaupt machen soll.  Der code cr bedeutet ,,create and replace`` - also, erzeuge die neue Bibliotheksdatei wenn nötig, und ersetze gegebenfalls schon vorhandene Dateien durch die folgenden neuen.  Danach steht der Name der Bibliotheksdatei.  Diese Namen hören immer mit .a auf und fangen immer mit lib an.  Als letztes steht eine Liste der Object­Files, die ich gerne in das Archiv aufnehmen möchte.  Sind sie neu, werden sie angehängt; gibt es sie schon im Archiv, ersetzt ar die schon vorhandene Datei durch die neue.

Libraries linked man ähnlich wie Object-Files, indem man sie einfach in der Kommandozeile des Compilers mit angibt.  Ebenfalls wie bei Object-Files sieht der Compiler an der Endung, welchen Typ Daten diese Datei enthält.

   % gcc -o hello main.o libgreet.a
   % ./hello
   Hello, world!
   Good-bye, moon!
   % 
In BSD­Unix muß man, nachdem das Archiv fertig ist, ein Inhaltsverzeichnis (englisch table of contents) anlegen; das macht das Programm ranlib:
   % ranlib libgreet.a
   %
Versucht man, eine Bibliothek zu verwenden, der dieses Inhaltsverzeichnis fehlt, gibt's manchmal vom Compiler eine Warnung:
   % gcc -o hello main.o libgreet.a
   ld: libgreet.a: warning: archive has no table of
   contents; add one using ranlib(1)
   %
(Dieses ld weist darauf hin, daß es eben die Linker­Stufe des Compilers ist, die sich mit der Library beschäftigt.  Das ld steht für link editor, nicht für loader.)

Obwohl man Bibliotheken wie Object-Files verwenden kann, behandelt der Linker sie anders:

Bei Bibliotheken ist die Position wichtig, an der sie in der Kommandozeile des Compiler­Aufrufs stehen.
   % gcc -o hello libgreet.a main.o
   ld: Undefined symbol 
      _goodbye
      _hello
   collect2: ld returned 2 exit status
   %
Hier kam die Bibliothek vor dem .o-File des Hauptprogramms, main.o In main.c werden die Funktionen goodbye und hello benutzt, also sind in main.o die Symbole _goodbye und _hello undefiniert.  (Unser Linker hängt da noch einen Unterstrich vor den Funktionsnamen, um Verwechslungen mit den Assembler­Symbolen aus der C­Library zu verhindern.)

Wenn er erst einmal vom Linker dazugebunden wurde, benimmt sich der Objekt­Code in den Bibliotheken nicht anders als der eines normalen Programmes.  Insbesondere kann er auch selbst wieder Symbole referenzieren, die noch nicht definiert sind.  Wenn man seine Software nicht strikt hierarchisch aufbaut, kann es passieren, daß dieselbe Bibliothek mehrfach auf der Kommandozeile angegeben muß - weil .o­Files, die beim ersten Mal dazugebunden wurden, Symbole in anderen Bibliotheken referenzierten, von deren .o­File aus wieder Symbole in der erstenBibliothek referenziert wurden.


Korrekturen bitte per Email an Jutta Degener, jutta@cs.tu-berlin.de.