Zum Inhalt springen

C - Der Systemnahe Standard

C ist seit Jahrzehnten der etablierte Standard für systemnahe Programmierung – nicht ohne Grund. Die Sprache bietet eine einfache Syntax und ein bewährtes Ökosystem.

Doch trotz dieser Stärken gibt es fundamentale Probleme:

  • Speicherverwaltungsfehler sind häufig und können zu Sicherheitslücken führen
  • Mangelnde Typsicherheit durch schwache Typisierung sorgen für schwer zu findende Fehler
  • Null-Referenzen haben laut Tony Hoare Schäden in Milliardenhöhe verursacht
  • Parallelität ist extrem schwierig korrekt zu implementieren

C ist eine standardisierte Sprache, für die es zahlreiche Compiler-Implementierungen gibt. Der C-Standard legt die Syntax und das Verhalten der Sprache fest, lässt jedoch bewusst einige Aspekte offen. Diese Aspekte bezeichnet man als undefiniertes Verhalten.

int* ptr = malloc(sizeof(int));
*ptr = 42;
free(ptr);
*ptr = 10; // Use-after-free → UB!
int x = -1;
unsigned int y = x; // Implizite Konvertierung → große positive Zahl
char* str = NULL;
printf("%s", str); // Crash oder UB
Terminal-Fenster
$ tcc -run main.c # tcc ist ein C-Kompiler
Segmentation fault (core dumped) tcc -run main.c

Durch das Verstehen der C-Probleme wird klar, warum moderne Sprachen wie Rust entwickelt wurden:

Memory Safety ohne Garbage Collection

  • Starke Typisierung

  • Absence of Null (Option-Types)

  • Sichere Parallelität

  • Bewusste Sprachenwahl für neue Projekte

  • Bessere Code-Qualität durch Problembewusstsein

  • Sicherheitskritische Systeme richtig entwickeln

  • Technische Schulden in Legacy-Code identifizieren

Die Industrie von fehleranfälligen zu sicheren systemnahen Programmiersprachen zu bewegen, ohne Performance-Einbußen oder die Kontrolle zu verlieren, die C bietet.

int main() {
int i = 2;
int result = --i + i++;
return 0;
}

Ein Buffer Overflow tritt auf, wenn mehr Daten in einen fest dimensionierten Speicherbereich (Buffer) geschrieben werden, als dieser fassen kann. Dies führt dazu, dass benachbarte Speicherbereiche überschrieben werden, was zu unvorhersehbarem Verhalten oder Sicherheitslücken führen kann.

#include <stdio.h>
#include <string.h>
int main() {
char name[5];
printf("Enter your name: ");
gets(name);
printf("Hello, %s!\n", name);
return 0;
}
  • Allokieren von Speicher
  • Schreiben in den Speicher
  • Freigeben des Speichers
  • Lesen aus dem Speicher -> Fehlerhaft
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int)); // TODO check allocation success
*ptr = 42;
printf("ptr: %d\n", *ptr);
free(ptr);
// Pointer is dangling
printf("ptr after free: %d\n", *ptr);
return 0;
}
  • Speicher wird allokiert
  • Pointer wird an free uebergeben
  • Speicher is freigegeben, Pointer existiert weiter
  • Pointer wird erneut an free uebergeben -> Welcher Speicher wird bereinigt?
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
free(ptr);
return 0;
}
  • Index ist groesser als array
  • Value enthaelt falsche Daten
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int value = arr[6];
printf("Value: %d\n", value);
return 0;
}
  • Undefinded
  • Manche Programme brechen ab, manche arbeiten mit falschem Ergebnis weiter
#include <stdio.h>
int main() {
int numerator = 10;
int denominator = 0;
int result = numerator / denominator;
printf("Result: %d\n", result);
return 0;
}
  • Value in einem lokalen Scope
  • Dereferenzieren des Values im lokalen Scope
  • Referenz (Pointer) wird returned
  • Value existiert nicht mehr, Pointer schon
  • Pointer zeig auf fehlerhaften Speicher
int *getDanglingPointer() {
int value = 42;
return &value;
}
int main() {
int *ptr = getDanglingPointer();
*ptr = 10; // Unsicher welcher Speicher beschrieben wird
printf("%d", *ptr);
return 0;
}

Aus den vorhergegangenen Beispielen wird ersichtlich, dass gültige C Programme zu einigen Probelmen führen können.

Rust

  • Memory Safety