Private ist nicht privat – Wie Objektorientierung uns in falscher Sicherheit wiegt

Wir alle kennen die Situation: Wir sollen eine Bibliothek an eine andere Abteilung oder einen Kunden liefern, aber möchten gerne die Implementierungsdetails verbergen. Manche nennen dies hochtrabend das Information Hiding Principle. In der Realität möchte man einfach die vielen Krücken und Abkürzungen verbergen, die durch Kosten- und Zeitdruck erforderlich wurden. Aber einen Schritt nach dem anderen.

Wie können wir das Information Hiding Principle umsetzen?

Über Jahrzehnte war C eine der gefragtesten Programmiersprachen überhaupt. Anfang der 80er-Jahre bildete sich um Bjarne Stroustrup die Keimzelle einer Bewegung, die die sehr neue und unausgereifte Idee der Objektorientierung in die damals populärste Programmiersprache C trug. Bis dato waren alle Daten und Funktionen überall sichtbar, denn C sah keine Einschränkungen vor.

C++ nahm das von C bekannte struct und interpretierte es neu: Neben den Daten selbst sollte plötzlich auch zugehöriger Code daherkommen, womit der Alptraum der Vermischung von statischer und dynamischer Perspektive auf Systeme losgetreten war. Im Windschatten davon wurden auch weitere Schlüsselwörter in der Sprache eingeführt: public, protected und private. Einige Tool-Hersteller erweiterten dieses Konzept noch weiter, indem sie irreführende Schlüsselworte wie „sealed“ einführten, die eine vermeintliche unveränderliche Beständigkeit suggerieren, in der Realität jedoch vollkommen andere Bedeutungen haben. Man mag den Eindruck bekommen, dass Marketing-Abteilungen sich besser in die Gestaltung von unnützen billigen Werbe-Kulis einmischen sollten, als in die Gestaltung von Programmiersprachen.

public, protected, private

Als Ausgangspunkt nehmen wir nun also das Schlüsselwort private, um unsere Daten vermeintlich zu schützen:

class MyClassWithASecret {
public:
    MyClassWithASecret () {
        aLittleSecret = 42;
    }
private:
    int aLittleSecret;
};

Wir probieren nun aus, ob der Sprachmechanismus funktioniert und kompilieren folgendes kleines Beispielprogramm:

int main(void) {
    MySecretClass c;
    printf("%d\n",c.aLittleSecret);
}

Vollkommen korrekt weist uns der Compiler darauf hin, dass wir das mal besser nicht tun sollten:


login@system:~> gcc test_private.cpp
test_private.cpp: In function int main():
test_private.cpp:9:6: error: int MySecretClass::aLittleSecret is private
  int a;
      ^
test_private.cpp:19:18: error: within this context
  printf("%d\n",c.aLittleSecret);
                  ^

Wir lassen aber nicht locker, und versuchen vollkommen naiv, die Objekinstanz auf den von uns benötigten Typ zu casten:

int main(void) {
    MySecretClass c;
    int* i = (int*)&c;
    printf("%d\n", *i);
}

Und siehe da: Der Code kompiliert und liefert uns das vermeintlich private Ergebnis. Wir gewinnen einen ersten Eindruck, dass die Umsetzung des Information Hiding Principle in C++ vielleicht nur Augenwischerei ist. Wir brauchen folglich eine belastbare Lösung.

PIMPL – ein Entwurfsmuster, das heute niemand mehr kennt

Private Implementation – kurz PIMPL - ist ein Entwurfsmuster, mit dem man versucht, die privaten Attribute zu verbergen. Die Idee an sich ist prima: Statt die Vielzahl an privaten Attributen einer Klasse öffentlich zu exponieren, belässt man es bei einem Pointer auf eine private Datenstruktur, deren Details nicht durch den Header offengelegt werden müssen. Für den Compiler genügt es, die Existent einer Klasse vorwärtszudeklarieren, womit eine tolle Entkopplung hergestellt werden kann:


class pimpl;

class MySecretClass {
public:
    MySecretClass();
private:
    pimpl* _p;
};

Die Umsetzung im Code bedarf zwar einer gewissen Gewöhung, stört den Code aber nur wenig.


class pimpl {
public:
    int aLittleSecret;
};

MySecretClass::MySecretClass() {
    _p = new pimpl();
    _p-> aLittleSecret = 42;
}

void MySecretClass::someFunctionInvolvingPrivateAttributes () {
    int i = _p->aLittleSecret;
}

Wir sind uns bewusst, dass diese Indirektion des Speicherzugriffs CPU-Zeit kostet. Immerhin haben wir es aber geschafft, dass der Kunde nicht seine gesamte Software anpassen und neu kompilieren muss, wenn wir eine Änderung an unseren vermeintlich privaten Daten vornehmen.

Sorgt PIMPL für Datensicherheit?

Nein. Spätestens, wenn wir wissen, an welchem Adress-Offset innerhalb der Objektinstanz der Pointer auf das vermeintlich private Klassenobjekt liegt, können wir es dereferenzieren und haben damit Zugriff auf die privaten Daten. In unserem Beispiel ist dies sogar besonders einfach, weil unsere Klasse außer dem PIMPL-Objekt keine weiteren Attribute hat, und wir daher keinerlei Pointer-Arithmetik anwenden müssen, um an die vermeintlich privaten Daten zu kommen. Eine weitere Dereferenzierung ohne Offset reicht in unserem Beispiel vollkommen aus:

int main(void) {
    MySecretClass c;
    int** i = (int**)&c;
    printf("%d\n",**i);
}

Wir behaupten trotzdem nicht, dass PIMPL grundsätzlich falsch wäre. Durch das Verbergen der privaten Strukturen kann so mancher Entwickler immerhin Compile-Zeit sparen, selbst wenn später tausendfach die Nutzer der Software die performancefressenden Speicherindirektionen durch Wartezeit vorm Computer bezahlen müssen.

Die Programmiersprache und Assembler sind das Problem

Das eigentliche Problem liegt nicht in der Intention des Programmierers, sondern in der fehlenden Öffnung für alternative Lösungen. Solange Programme in Assembler-nahen Sprachen hängenbleiben, werden wir immer auf dem Acker der primitiven Daten hängenbleiben. Sowohl in Assembler als auch C und C++ können wir Daten beliebig erreichen und auch interpretieren. Und genau das passt nicht in unsere Zeit.

Hol‘ dir endlich moderne Tools

Mit dem Einzug von Java und C# als gemanagte Sprachen ist das Übel der Pointer-Arithmetik endlich Geschichte. Konsequent umgesetzt wurde das aber nicht überall – jedenfalls nicht für unseren Fall von privaten Daten.

Schon bei der Einführung der Sprachen wurden Techniken etabliert, die das Konzept von public, protected und private vorsätzlich aushebeln können: Reflection. Mit Reflection erhält man Meta-Informationen zu allen Typen, und mit ihnen auch direkten Zugriff auf vermeintlich private Daten.

MySecretClass c = new MySecretClass();
var prop = c.GetType().GetField("aLittleSecret ",
    System.Reflection.BindingFlags.NonPublic
    | System.Reflection.BindingFlags.Instance);
int secretValue = prop.GetValue(c);
prop.SetValue(c, secretValue + 23);

Wir brauchen Hilfe von der Hardware

Wir Software-Leute müssen uns eingestehen: Ohne fremde Hilfe schaffen wir es nicht, Daten wirklich unsichtbar zu machen. Beinahe überall wurden Sprachkonstrukte etabliert, die die gutgemeinten Ansätze von public, protected und private komplett untergraben, und damit gleichzeitig das ganze Information Hiding Principle. Warum sollen sich Entwickler dann mit postulierten Prinzipien wie dem Information Hiding Principle aufhalten, wenn die Prinzipien sowieso nicht funktionieren?

Mit ein wenig peinlicher Betroffenheit müssen wir eingestehen, dass wir die Hilfe der Hardware-Leute brauchen, um unseren Code in den Griff zu bekommen. Scheinbar waren die Hardware-Leute sich längst bewusst, dass sie helfen müssen, denn Memory Protection Units und Memory Management Units gibt es bereits seit Jahrzehnten. Erst mit Hilfe der Hardware durch Entkopplung der Speicherbereiche oder gar Entkopplung der Hardware wird der Schutz von Daten zuverlässig machbar. Dabei ist es egal, ob es sich um simples antiquiertes IPC, RPC, Message Queues oder Pipes handelt, oder ob man das ganze modern in Web-Services einpackt, die dank Internet-Kommunikationsprotokollen sogar auf verteilte Hardware deployed werden können.

Fazit

Vor dem historischen Kontext der 80er-Jahre waren Design-Tricks mit den C++-Schlüsselworten public, protected und private großartige Mittel, um Zuständigkeiten und Sichtbarkeiten als Konvention zu etablieren. Kaum ein Computer dieser Zeit kannte MMUs, Privilegien-Management oder Virtualisierung. C++ war ein Quantensprung.

Seitdem trennen uns jedoch knapp 40 Jahre von unserer heutigen IT-Realität. Keines der Sprachkonzepte konnte die Idee privater Daten wirklich umsetzen. Die Hardware-Leute sind uns zum Glück rechtzeitig zur Seite gestanden. Daten werden erst durch getrennte Speicherbereiche oder getrennte Hardware sicher privat. Software allein hat diesbezüglich versagt.