Wacker Art Software

Die Programmiersprache C

Wappen der Familie Wacker




Nächste Seite
Bild: "Barcode Kryptologie" 56,4cm x 42cm


Seiten Anfang Die C-Katastrophe


Die genaue Bezeichnung für diese Programmiersprache ist eigentlich b++, wenn man ganz genau sein will sogar (a++)++. Die Bezeichnung d-- wird seltener verwendet. (C ist der Nachfolger der Programmiersprache B)

Ich persönlich habe in der folgenden Reihenfolge die Programmiersprachen Fortran, Pascal, Ada und C gelernt. Dabei habe ich bei den Übergängen von Fortran über Pascal nach Ada immer einen technologischen Fortschritt feststellen können. Insbesondere ein Ada-Programmierkurs im Jahre 1987 hat bei mir die Grundlagen des modernen Software Engineerings gelegt.

Das erlernen von C war für mich ein technologischer Rückschritt. Alle Flüchtigkeitsfehler, die ein Ada-Compiler automatisch findet, mußte man durch mühseliges testen oder debuggen, wie in Fortran Zeiten, wieder selber suchen. Ein Ada-Compiler generiert aus den vorgegebenen Files und Compilation-Units automatisch ein ausführbares Programm. Bei C muß man wieder mühselig makefiles schreiben. Alle mächtigen Sprachkonstrukte, die einem beim Schreiben eines Programmes in Ada das Leben erleichtern sind bei C nicht vorhanden alles muß wieder "zu Fuß" programmiert werden.

Seit nun ca. 12 Jahren beobachte ich große Software Projekte, die in C erstellt werden und sehe Terminverzug, Kostenüberschreitungen und fehlerhafte Programme (C-Programme stürzen ab). Die Entropie im Projekt steigt an und keiner blickt mehr durch. Insbesondere wenn der betreffende Entwickler die Firma verlassen hat.
Abteilungen werden geschlossen und die Firma zieht sich aus technologisch interessanten Arbeitsfeldern zurück.


Seiten Anfang Die C-Katastrophe

Schwächen

  • Fehlendes Modulkonzept (wird üblicherweise durch Paare von .c- und .h-Dateien notdürftig nachgebaut)

  • Nur eingeschränkt typsicher

  • Die Sprachdefinition besitzt Lücken (Verhalten undefiniert)

  • Verglichen mit anderen prozeduralen Sprachen wie (das ältere!) Pascal oder Modula-2 zu wenig klare Syntax, was zu sehr komplexen und fehleranfälligen Compilern führt

  • "Wilde" Zeiger
    • Man kann Zeiger auf beliebige Stellen des Speichers richten. Insbesondere zeigen nicht explizit initialisierte Stack-Variablen oft auf beliebige Stellen des Speichers. Die Folge sind schwer zu diagnostizierende Fehler.

  • Felder
    • C kennt zwar den Datentyp "Feld" und erlaubt sogar die Definition von Feldern, die mit Konstanten vorbelegt sind. Intern werden Felder jedoch immer als Zeiger verwaltet. Dies bedeutet, dass eine eventuell nötige dynamische Speicherverwaltung von Feldern vom Programmierer implementiert werden muss. Auch die Feldgröße wird beim Zugriff nicht überprüft. Durch Programmierfehler können Speicherbereiche durch illegale Feldzugriffe während der Laufzeit unabsichtlich oder gezielt (Pufferüberlauf) verändert werden.
    • Mehrdimensionale Felder werden in der numerischen Mathematik für Matrizen benötigt. Dafür ist die Struktur der C-Felder jedoch völlig ungeeignet.

  • Zeichenketten
    • Zeichenketten (Strings) werden in C als so genannte nullterminierte Strings in Variablen und Konstanten ("Hallo") gespeichert und durch ihre Adressen (Zeiger!) angesprochen. Die große Schwäche dieses Konzepts liegt in der großen Programmiererverantwortung, denen reale Programmierer nicht immer gewachsen sind (siehe wilde Zeiger, zu kleine Felder, vergessene Abschlussnull...)
    • C hat keine integrierten Zeichenketten. Statt dessen wird ein Zeichenfeld verwendet, das mit einem Nullzeichen abgeschlossen wird. Die Speicherverwaltung von Zeichenketten muss vom Programmierer vorgenommen werden.

  • Speicherverwaltung
    • Der Programmierer muss den dynamischen Speicher selbst verwalten. Hierzu stehen Bibliotheksfunktionen zur Verfügung.

  • niedriger Abstraktionsgrad

  • Portabilitätsprobleme
    • C schreibt die Speichergröße verschiedener Typen in der Sprachdefinition nicht vor. Dies ermöglicht die Portierung bestehender Programme auf andere, auch neue Prozessoren. Es ist nur zugesichert, dass ein short int nicht länger sein darf als ein long int. In den 1980er und 1990er Jahren wurden vorwiegend 32-Bit-Systeme wie VAX, 68000, i386 eingesetzt. Bei diesen waren Zeiger, int und long alle 32 Bits lang, so dass sich dies als Quasistandard etabliert hat. Dies bereitet Probleme bei der Portierung auf modernere 64-Bit-Architekturen, falls der Programmierer von bestimmten Längen ausgegangen ist.
    • Einige weitere Eigenschaften der Sprachdefinition (Ergebnistyp bei Zeigersubtraktion, Ausrichtung (Alignment) von Datentypen) bereiten ebenfalls Probleme, wenn statt der empfohlenen abstrakten Typen (wie ptrdiff_t für Zeigersubtraktionen, size_t für Größen von Speicherbereichen) direkt fundamentale Typen wie int verwendet werden.
    • In der Sprachversion C99 sind Datentypen mit expliziten Bit-Längen definiert (int8_t, int16_t, etc.).

Zusammenfassung

Man kann sagen, dass die größte Stärke von C - die uneingeschränkte Freiheit des Programmierers im Umgang mit Zeigern und Speicherstrukturen - gleichzeitig ihre größte Schwäche ist: Was für die Programmierung von Treibern und Betriebssystemen häufig benötigt wird, das ist für die Programmierung gewöhnlicher Desktop-Anwendungen eher ein lästiges Hindernis, häufig sogar ein Sicherheitsrisiko.
Viele bekannte Sicherheitsprobleme sind durch Speicherüberläufe entstanden.

Da der freizügige Umgang der Programmiersprache mit dem Speicher in kritischen Umgebungen (Kreditinstituten, Börsen, Versicherungen, Raumfahrt usw.) leicht hohe Schäden nach sich ziehen kann, wird hier mittlerweile ernsthaft erwogen, diese Programmiersprache bei neuen Projekten zu verbieten.


Die Programmiersprache C Pointer in C


Die hier aufgeführten Beispiele kann man natürlich nur vollständig verstehen, wenn man etwas C programmieren kann. Für nicht C-Experten oder Anfänger sollen die hier aufgeführten Programm-Beispiele nur der Abschreckung dienen. Für diese Leser habe ich die textuelle Beschreibung verfasst.

Erstes Beispiel

/* Programm: "Eine C-Katastrophe" */

#include <stdio.h>
#include <limits.h>

int main ( void ) {

  int c; /* Der C-Katastrophenindex */
  int a[10];

  for ( c=0; c<INT_MAX; c++ ) {
     printf ( "%d\n", c );
     a [c] = c;
     printf ( "%d\n", a[c] );
  }

  printf ( "INT_MAX reached\n" );
  return 0;
}

Das Programm zum Erzeugen einer C-Katastrophe ist folgendermaßen aufgebaut:

  • Es wird eine Integer-Variable "c" definiert.
  • Es wird ein Feld "a[10]" mit zehn Integer-Variablen definiert.
  • Danach wird eine Schleife definiert, dessen Schleifenindex die Variable "c" ist.
  • Die Schleife läuft von "c = 0" bis zum größten auf der aktuellen Maschine definierten Integerwert INT_MAX.
  • Die Operation c++ bedeutet, daß der aktuelle Wert von "c" um eins erhöht wird. Dies geschieht immer am Ende eines Schleifendurchlaufs.
  • Innerhalb der Schleife wird zuerst der aktuelle Wert von "c" über das printf Kommando ausgegeben.
  • Dann wird dem c'ten Element des Feldes "a", "a[c]" der aktuelle Wert von "c" zugewiesen (a[c]=c).
  • Danach wird der aktuelle Wert des Feldes "a" beim Element "c" ausgegeben. Dieser sollte gleich dem Wert "c" sein.
  • Das Seltsame an diesem Programm ist, daß das Feld "a" laut Deklaration nur zehn Elemente hat.
  • Bei meinem Versuch konnte ich aber auf das Element 323 lesend und schreibend zugreifen.
  • Erst bei dem Versuch auf das 324igste Element zuzugreifen beendete sich das Beispiel im default Mode eines C-Programmes, dem "Core Dump".
  • Die Ausgabe am Ende des Programmes, welche andeutet, daß der Index den Wert INT_MAX erreicht hat, wurde bisher leider noch nicht ausgeführt. Wir harren der Dinge die kommen.
15. März 2005
Franz Fritsche
lcc-win32 (aktuelle Version) unter Win2k Pro
4612 Programmabbruch (keine Fehlermeldung)

11. März 2005
gcc (Minimal GNU for Windows Version 3.1.0) unter Win2k Pro
Array a bis zum Wert 4141 und den Index c bis 4142.
Kein Fehler, aber Programmabbruch.

7. März 2005
Mike Reiche
2300 "Speicherzugriffsfehler"
gcc version 3.3.3 20040412 (Red Head Linux 3.3.3-7)

10. Januar 2005
Peter Schwindt
1340 "Segmentation Fault"
gcc version 3.3.5 (Debian 1:3.3.5-5)

17. Dezember 2004
Neuer Rekord im Wettbewerb wir bauen eine C-Katastrophe.
Stefan Kaysersberg schafft mit dem gcc3.3 einen beachtlichen Rekord von c = 420 und löst einen "Segmentation Fault" aus.
(gcc 3.3.5-2 unter 2.6.9 i686 GNU/Linux)

Erster Rekord ist c = 323;  (Unix?)
Hermann Wacker



Beliebte Zustände bzw. Modi eines C-Programmes sind:
Segmentation Fault
Core Dump
Häufig beendet sich ein C-Programm auch über diese Modi.



Zweites Beispiel

#include <stdio.h>
#include <limits.h>

int main ( void ) {

   int Kontostand             = 100000;
   int Siemens_Aktien         = 1000;
   int BMW_Aktien             = 1000;
   int DaimlerChrysler_Aktien = 1000;

   int a[10];
   int c;

   for ( c=0; c<INT_MAX; c++ ) {
      printf ( "%d\n", c );
      a [c] = 0;
      printf ( "%d\n", a[c] );

      printf ( "Kontostand: %d",                Kontostand );
      printf ( " Siemens Aktien: %d",           Siemens_Aktien );
      printf ( " BMW Aktien: %d",               BMW_Aktien );
      printf ( " DaimlerChrysler Aktien: %d\n", DaimlerChrysler_Aktien );
   }

   printf ( "INT_MAX reached\n" );
   return 0;
}
Das zweite Beispiel zeigt was für Katastrophen beim Einsatz von c in der Software einer Bank passieren können. Der Grundaufbau ist ähnlich wie beim ersten Beispiel. Nur sind jetzt vier neue Variablen eingeführt worden, welche den Kontostand, und das Aktiendepot eines Kunden repräsentieren. Nach 14 Iterationen sind diese Variablen alle auf null gesetzt, ohne daß man explizit auf diese Variablen zugegriffen hat.
Die Software hat die gesamten Einlagen des Kunden auf Null gesetzt, ohne daß eine Transaktion statt gefunden hat.
Ist auch Ihre Kontoverwaltungssoftware in C geschrieben?


Lesbarkeit
Zur Erhöhung der Unlesbarkeit von Computerprogrammen bietet c leistungsstarke Konstrukte, welche diese Eigenschaft schon lokal erzwingen:

Beispiele:
char (*(*x()) [])()
x: Funktion mit Resultat Zeiger auf Vektor [] mit Zeiger auf Funktion mit Resultat char.

char (*(*x[3])())[5]
x: Vektor [3] mit Zeiger auf Funktion mit Resultat Zeiger auf Vektor[5] mit char.

Was will uns dieser nette C-Programmierer sagen?
(* ( void(*)() ) 0) ();
Aufruf einer Funktion die bei der Adresse 0 gespeichert ist.
´
Einige C-Compiler sollen sogar den folgenden Code übersetzen:
a+++++b;


Eindeutig zweideutig
Was wird zu erst durchgeführt? Die Inkrementierung von n oder die "power" Operation mit n als Parameter:
printf("%d %\n", ++n, power(2, n) );

Die Frage ist, ob der alte Wert von i oder der neue Wert als Index benutzt wird:
a[i] = i++;

In der Anweisung:
x = f() + g()
kann f vor oder nach g bewertet werden. Wenn also f oder g eine Variable ändern, von der jeweils die andere Funktion abhängt, so kann x von der Reihenfolge der Bewertung abhängen.

Bei dem folgenden Programmstück ist nicht festgelegt, ob "funktion" mit (200. 700) oder (1200, 600) aufgerufen wird.

a = 100;
funktion (a*=2, a+=500);

Datenkapselung:
Durch die Verwendung von include Anweisungen kann man sehr schnell alle möglichen Variablen und Objekte globalisieren.
#include "allesMoegliche"


Makros:
Makros sind in C sehr wichtig.

#define SQUARE(a) a*a
int x = 5;
int y = SQUARE (x+2); /* berechnet wird: y = x + (2*x) + 2 */

Bjarne Stroustroup äussert sich in seinem berühmten Buch "Die C++ Programmiersprache" wie folgt:
"Fast jedes Makro demonstriert eine Schwäche in der Programmiersprache, im Programm oder beim Programmierer. Da Makros den Programmtext ändern, bevor der Compiler ihn richtig liest, sind Makros außerdem ein großes Problem für viele Programmierwerkzeuge. Wenn man also Makros benutzt, muß man schlechtere Dienste von Werkzeugen wie Debuggern, Crossreferenz-Werkzeugen und Profilern erwarten."

Auch in C++ sind Makros erlaubt:
#define private public


Das Verhalten von C ist implementierungsabhängig
Mit dem Attribute const kann bei der Vereinbarung einer "Variablen" angegeben werden, daß sich ihr Wert nicht ändert.

const double pi = 3.141592

Versucht man, eine const-Variable zu ändern, ist das Resultat abhängig von der Implementierung des C-Compilers. Man kann sich nicht darauf verlassen, daß es unmöglich ist eine "Konstante" zu verändern. Insbesondere, wenn man mit Pointern arbeitet. Es wird deshalb in der Regel eine define Anweisung statt const verwendet und man überläßt das Einfügen von Konstanten dem Präprozessor.


Sicherheitskritische Software in C++:

#include <string.h>
#include <iostream.h>

class Ganz_unsicher {
   public:  Ganz_unsicher() { strcpy(passwort, "geheim"); }
   private: char passwort[100];
};

void main() {
   Ganz_unsicher gleich_passierts;
   char *boesewicht = (char*)&gleich_passierts;
   cout « "Passwort: " « boesewicht « endl;
}


Mann kann die Programmiersprache C für alles einsetzen.
Sogar in Sicherheitskritischen Anwendungen.
Ob das unter dem Strich eine preiswerte Lösung ist, steht natürlich auf einem anderen Blatt.



C-Programme im Einsatz beim Anwender
Hängt es mit der Verwendung einer bestimmten Programmiersprache zusammen, wenn teure Luxusautos mit viel Elektronik und Bordcomputern wegen eines Softwarefehlers nicht fahren können, oder wenn Bordsysteme von Autos ein merkwürdiges Verhalten zeigen?


Die C-Katastrophe Vorteile der Programmiersprache C

Zeiger und Pointer in C

Mit Pointern (Zeigern auf Speicheraddressen) bietet die Programiersprache-C ein mächtiges Werkzeug um kryptischen Sourcecode zu erzeugen.
In der nachfolgenden Tabelle sind einige mögliche und unmögliche Konstrukte aufgeführt.
Vielleicht ist diese Tabelle auch hilfreich, wenn man vor der Herausforderung steht, vorhandenen C-Code zu entschlüsseln.


Deklaration Interpretation Legale Deklaration

int i; Integer Variable. Ja
int *p; Pointer auf eine Interger Variable. Ja
int a[]; Ein Array von Integer Variablen. Ja
int f(); Eine Funktion mit dem Rückgabewert Integer. Ja

int **pp; Pointer auf einen Pointer auf eine Integer Variable. Ja
int (*pa)[]; Pointer auf ein Array von Integer Variablen. Ja
int (*pf)(); Pointer auf eine Funktion mit dem Rückgabewert Integer. Ja
int *ap[]; Ein Array von Pointern auf Integerwerte. Ja
int aa[][]; Ein Array von Arrays mit Integerwerten. Ja
int af[](); Ein Array von Funktionen die Integer als Rückgabewert liefern. Nein
int *fp(); Eine Funktion, die einen Pointer auf einen Integerwert als Rückgabewert liefert. Ja
int *fa()[]; Eine Funktion die ein Array mit Integerwerten liefert. Nein
int ff()(); Eine Funktion mit einer Funktion, als Rückgabewert, mit Rückgabewert Integer. Nein

int ***ppp; Pointer auf einen Pointer auf einen Pointer auf einen Integerwert. Ja
int (**ppa)[]; Pointer auf einen Pointer auf ein Array von Integerwerten. Ja
int (**ppf)(); Pointer auf einen Pointer auf eine Funktion mit Rückgabewert Integer. Ja
int *(*pap)[]; Pointer auf ein Array von Pointern auf Integerwerte. Ja
int (*paa)[][]; Pointer auf Array von Array von Integerwerten. Ja
int (*paf)[](); Pointer auf array von Funktionen mit dem Rückgabewert Integer. Nein
int *(*pfp)(); Pointer auf Funktionen mit Rückgabewert Pointer auf Integer. Ja
int (*pfa)()[]; Pointer auf Funktionen mit dem Rückgabewert eines Array von Integer Werten. Nein
int (*pff)()(); Pointer auf Funktionen mit Funktionen als Rückgabewert, welche den Rückgabewert Integer haben. Nein

int **app[]; Array von Pointern auf Pointern auf Integerwerte. Ja
int (*apa[])[]; Array von Pointern auf Array von Integerwerten. Ja
int (*apf[])(); Ein Array von Pointern auf Funktionen mit dem Rückgabewert Integer. Ja
int *aap[][]; Ein Array von Arrays von Pointern auf Integerwerte. Ja
int aaa[][][]; Array von Arrays von Arrays von Integerwerten. Ja
int aaf[][](); Array von Arrays von Funktionen mit Rückgabewert Integer. Nein
int *afp[](); Array von Funktionen, mit Pointern auf Integerwerten als Rückgabewert. Nein
int afa[]()[]; Array von Funktionen, mit Arrays von Integerwerten als Rückgabewert. Nein
int aff[]()(); Array von Funktionen mit Funktionen als Rückgabewert, welche Integerwerte als Rückgabewert haben. Nein

int ***fpp(); Eine Funktion, mit Pointer auf Pointer auf Pointer auf einen Integerwert, als Rückgabewert. Ja
int (*fpa())[]; Eine Funktion, mit Pointer auf ein Array von Integerwerten, als Rückgabewert. Ja
int (*fpf())(); Eine Funktion, mit dem Pointer auf eine Funktion als Rückgabewert, welche einen Integerwert als Rückgabewert hat. Ja
int *fap()[]; Eine Funktion, mit einem Array von Pointern auf Integerwerte, als Rückgabewert. Nein
int faa()[][]; Eine Funktion, mit einem Array von Arrays von Integerwerten, als Rückgabewert. Nein
int faf()[](); Eine Funktion, mit einem Array von Funktionen, welche Integerwerte als Rückgabewerte haben, als Rückgabewert. Nein
int *ffp()(); Eine Funktion welche eine Funktion, mit einem Pointer auf einen Integerwert als Rückgabewert, als Rückgabewert hat . Nein
int *ffa()()[]; Eine Funktion welche eine Funktion, mit einem Array von Integerwerten als Rückgabewert, als Rückgabewert hat. Nein
int fff()()(); Eine Funktion, mit einer Funktion als Rückgabewert, ,mit einer Funktion als Rückgabewert, welche einen Integerwert als Rückgabewert hat. Nein


Pointer in C Zitate


Innovation durch die Programmiersprache C
Einer der großen Vorteile der Programmiersprache C ist es, daß man mit dem malloc() Kommando dynamisch Speicherplatz allokieren kann. Dieses Kommando wird von C-Programmieren gern und häufig verwendet. Leider ist aber vielen C-Programmierern nicht bekannt, das es ein free() Kommando gibt, das den allokierten Speicherplatz wieder frei gibt. Ist das free() Kommando bekannt, so wird der Aufruf gerne vergessen.
Natürlich gibt es auch C-Programmierer die ein free() Statement verwenden. Es kann dann aber durch den virtuosen Einsatz von komplexen Kontrollstrukturen leicht ein Programmfluß erzeugt werden, bei dem das free() Statement nicht aufgerufen wird. Besonders hilfreich ist hierbei der Einsatz von #IFDEF oder anderer mächtiger Preprozessor Kommandos.

Der Einsatz von malloc() führt deshalb in der Regel dazu, das die Größe eines C-Programms während der Laufzeit kontinuierlich wächst.
Hersteller von C-Programmen empfehlen deshalb ihren Kunden immer die Verwendung der neusten Rechnergeneration mit maximalem Speicherausbau. Dadurch wird die Programiersprache C zu einem entscheidenden Motor der Inovation in der Computer-Industrie. Die Chiphersteller bauen immer leistungsfähigere Speicherbausteine um dem wachsenden Speicherbedarf gerecht zu werden. Die CPU's müssen immer schneller werden um noch ein vernünftiges Speichermanagement zu sichern.
Schnelle Festplatten mit ausreichend Speicherplatz werden gebraucht um ein effektives "Pageing" zu gewährleisten.


Vorteile der Programmiersprache C Linksammlung


Ein falsches Programm bleibt ein falsches Programm sei es auch noch so schnell.
Ich glaube dieses Zitat stammt von Grady Booch.

Jean Ichbiah - inventor of Ada
"C was designed to be written; Ada was designed to be read"

Herbert Schildt - ANSI C++ Standards Committee
- "C gives the programmer what the programmer wants; few restrictions, few complaints . . . C++ maintains the original spirit of C, that the programmer not the language is in charge."

P.G. Plauger - ANSI C Committee
- "Beyond 100,000 lines of code, you should probably be coding in Ada."

Sextant - French aerospace contractor
- "Lack of experience in Ada causes poor code performance; lack of experience in C produces code errors."

CelsiusTech - Swedish defense contractor
- "We no longer need worry about getting more efficient at producing software because that's not where our cost is."

Nicolaus Wirth - Entwickler von Pascal und Modula
Der Vorteil einer echten höheren Programmiersprache, und dazu zähle ich C nicht, ist der, daß die Sprachregeln vom Compiler überprüft werden können...
C lässt strukturierte Programmierung zu, genau wie Assemblercode, aber es unterstützt sie nicht.
Mehr Disziplin als selbst gute Programmierer aufbringen ist nötig, um Fehler zu vermeiden.

M.Schulz:
C++ ist ein objektorientierter Makroassembler.

Unbekannt:
C ergänzt die Nachteile von Assembler (Unsicherheit) mit inkonsistenter und fehlerträchtiger Syntax.

Die Datenstrukturen von C sind so mächtig, daß man schon bei kleinen Softwareprojekten gezwungen ist eine Datenbank einzusetzen.

Die Programmiersprache C ist eine infantile Hackersprache.

Die letzten Worte eines C-Programmieres:
To make, or not to make.


Zitate Seitenende


Von Hackern für Hacker







Inhaltsverzeichnis Wacker Art Homepage

Kontakt
E-Mail

14. Januar 2006 Version 3.0


Copyright: Hermann Wacker Uhlandstraße 10 D-85386 Eching bei Freising Germany Haftungsausschluß