6. Ein erstes C-Programm

#include <stdio.h>

int main(void)
{
  printf("Hello World\n");
  return 0;
}

In Python:

print "Hello world"

C wird (im Allgemeinen) kompiliert:

examples> ls
hello.c
examples> make hello
cc     hello.c   -o hello
examples> hello
Hello World

Ein paar Worte zur Kompilation: C-Compiler sind Programme wie alle anderen. Unter Unix heißen sie in der Regel cc oder gcc, unter Windows gibts häufig IDEs, also Programme, die den Compiler hinter einem Editor und Menüs verbergen. Mit cygwin oder ähnlichem (vgl. auch die einschlägige Wiki-Seite) lässt sich aber auch ein Windows-System so herrichten, dass die hier beschriebenen Beschwörungen funktionieren – und das ist ziemlich empfehlenswert.

Gesteuert werden die Compiler durch mehr oder minder viele Optionen. Da niemand all die Optionen im Kopf behalten kann und sie sich auch von Programm zu Programm ändern, wird man in der Regel ein Programm namens make benutzen, das ausrechnen kann, wie ein Compiler aufgerufen werden soll. Wir werden später mehr von make hören. Ohne weiteres weiß make jedenfalls, dass es, wenn wir make hello schreiben und es eine Datei hello.c im aktuellen Verzeichnis gibt, das Kommando cc hello.c -o hello ausführen muss. (probiert aus, was passiert, wenn ihr make hello.o tippt – die Erklärung folgt). Dieses Kommando sagt so viel wie: “Kompiliere die Datei hello.c und schreibe die Ausgabe (den Output, daher -o) in die Datei hello.”

In der Tat ist das nicht ganz das, was wir wollen. C-Compiler können Warnungen ausgeben, die meistens darauf hinweisen, dass man irgendwas nicht so gemacht hat, wie man es hätte machen sollen. Der gcc, der unter Unix üblicherweise verwendet wird, muss dazu mit dem Flag -Wall aufgerufen werden. Die einfachste Art, dafür zu sorgen und trotzdem die Kompilation immer noch per make machen zu lassen, ist, eine Datei namens Makefile mit dem Inhalt CFLAGS += -Wall in das aktuelle Verzeichnis zu schreiben. Erklärungen folgen auch hier.

Kompilation ist die Übersetzung eines Programms in eine maschinennähere Sprache. Im Fall von C ist das im Allgemeinen die Maschinensprache selbst.

Die Maschinensprache sind dabei die Bits, die der Prozessor des Rechners wirklich verarbeiten kann. Python wird übrigens auch kompiliert, allerdings nicht in die Maschinensprache des Zielprozessors, sondern in eine Zwischensprache, die wiederum zur Laufzeit interpretiert wird. Dieser Prozess ist aber transparent, d.h. als ProgrammiererIn merkt man nicht viel davon. Die .pyc- oder .pyo-Dateien, die nach einem import in python erzeugt werden, enhalten diesen Bytecode.

Das folgende ist ein Ausschnitt aus der Ausgabe von objdump -S hello und stellt ganz links eine (recht bedeutungslose) Adresse, dann in Sedezimalnotation die Instruktionen an den Prozessor. Ganz rechts steht eine marginal menschenlesbare Repräsentation dieser Zahlen. Die Wörtchen wie push, mov, sub, and usf. entsprechen (fast) eins zu eins Teilen der Zahlen, die die CPU wirklich ausführt (die restlichen Teile dieser Zahlen werden durch die Operanden ausgemacht, wie das geht, steht auf der nächsten Folie). Weil diese Wörtchen letztlich nur Merkhilfen für die Zahlen sind, heißen sie auch Mnemonics.

Dies ist im Wesentlichen die Ausgabe des Compilers, in diesem Fall des gcc 3.3.1 auf einem Athlon XP.

08048654 <main>:
#include <stdio.h>

int main(void)
{
 8048654:       55                      push   %ebp
 8048655:       89 e5                   mov    %esp,%ebp
 8048657:       83 ec 08                sub    $0x8,%esp
 804865a:       83 e4 f0                and    $0xfffffff0,%esp
 804865d:       b8 00 00 00 00          mov    $0x0,%eax
 8048662:       29 c4                   sub    %eax,%esp
        printf("Hello World.\n");
 8048664:       c7 04 24 44 87 04 08    movl   $0x8048744,(%esp,1)
 804866b:       e8 08 ff ff ff          call   8048578 <_init+0x38>
        return 0;
 8048670:       b8 00 00 00 00          mov    $0x0,%eax
}
 8048675:       c9                      leave
 8048676:       c3                      ret

Wenn ihr dieses Experiment selbst macht, werdet ihr feststellen, dass der Compiler noch weit mehr Kram in das kleine Hello-Programm reingeschrieben hat. Das ist im Groben Code, der verwendet wird, damit die main-Funktion auch in einer Umgebung leben kann, in der sie sich wohl fühlt.

In Python kann man sich so etwas ähnliches durch das Modul dis (für “Disassembler”) ausgeben lassen. Das kann z.B. so aussehen:

>>> def main():
...     print "Hello World"
...
>>> dis.dis(main)
  2           0 LOAD_CONST         1 ('Hello World')
              3 PRINT_ITEM
              4 PRINT_NEWLINE
              5 LOAD_CONST         0 (None)
              8 RETURN_VALUE

Man ahnt schon, dass die einzelnen Instruktionen der durch den Bytecode definierten Python-Maschine mächtiger sind als die der realen Maschine, auch wenn das in diesem Trivialbeispiel noch nicht wirklich gut herauskommt.

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Macht euch mit der Bedienung eures Compilers vertraut. Compiliert das Beispielprogramm. Andert den String im Quelltext und überzeugt euch, dass, wenn ihr euer Compilat laufen lasst, die Änderung natürlich nicht dort angekommen ist. Re-kompiliert den Quelltext und überzeugt euch, dass danach das Programm in der Tat die neue Ausgabe hat.

Tut euch den Gefallen und verwendet make.


Markus Demleitner

Copyright Notice