hello.c
,
#include <stdio.h> main() { printf("Hello, world!\n"); return 0; }wird mit einem CCompiler 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 HeaderFiles, SourceFiles, ObjectFiles und Libraries.
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 CProgramm, 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 systemglobalen
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 HeaderFiles in Anführungszeichen.
-I
-Compileroption kann man beim Übersetzen
eines Programmes die Liste der Verzeichnisse erweitern, in denen nach
einer Datei gesucht wird.
gcc -Iinc hello.csucht nach
stdio.h
zuerst als inc/stdio.h
,
und erst dann als /usr/include/stdio.h
.
-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 1geschrieben. (In manchen Systemen ist es auch dasselbe wie
#define symbol 0oder
#define symbolman kann sich auf die 1 da nicht verlassen.)
Man benutzt -D
häufig in Verbindung mit
#ifdef
s im Programmcode; wenn ich zum Beispiel
meine Testausgaben mit #ifdef DEBUG
einklammere
#ifdef DEBUG fprintf(stderr, "Starting program run...\n"); #endifdann 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-
#define
s 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 -D
symbol=
value.
Zum Beispiel:
% gcc -DDEBUG -DDEBUGLEVEL=5 files...
-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.
#include
-Anweisungen dazugekommenen
HeaderFiles den Objektcode. Dabei leistet er
die Hauptarbeit des Übersetzens:
.o
, also zum
Beispiel hello.o
.
CCompiler wie gcc
oder cc
hören
nach dem Erzeugen von ObjectFiles mit dem Compilieren auf, wenn man
ihnen die Option -c
übergibt. Mit
% gcc -c hello.cerzeuge ich also zu einer Datei
hello.c
das dazugehörige
ObjectFile, hello.o
.
-S
Option kann man sich das Resultat des
Übersetzungsvorgangs als AssemblerCode ansehen; zu einer
Quelldatei file.c
jeweils in einer Datei
file.s
. (Filenamen für
AssemblerCode 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),ö1 or ö1,%lo(LC0),ö0 ld [%fp+68],ö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.
hello.c:Die Quelldateien können getrennt compiliert werden,#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; }
% 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, gelinked werden.
% gcc -o hello main.o hello.o goodbye.o % ./hello Hello, world! Good-bye, moon! %Es ist günstig, Programme in mehrere SourceFiles 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
Um nur die LinkerStufe 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 CCompiler 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.``
main
, goodbye
und hello
kommt in hello.c
noch eine andere Funktion vor,
printf()
. Wo steht deren Text eigentlich?
hello.c
wird sie nur aufgerufen, aber nicht definiert.
/usr/include/stdio.h
finden wir vielleicht
ihre Deklaration
int printf(const char * fmt, ...);aber keine Definition.
printf()
steht in einer
Sammlung von vorcompilierten Object-Files, einer
library, die zu allen CProgrammen
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 ObjectFiles, 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 BSDUnix 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
LinkerStufe 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:
% 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 AssemblerSymbolen aus der CLibrary zu
verhindern.)
Wenn er erst einmal vom Linker dazugebunden wurde, benimmt
sich der ObjektCode 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.