Kategorie TECHNIK


Warum man fremdem Code nicht trauen darf

main():
  1. int a = 0;
  2. printf("Der Wert von Variable 'a' ist %d.\n", a);
  3. BoeseFunktion(4711);
  4. a = 1;
  5. printf("Der Wert von Variable 'a' ist jetzt %d.\n", a);
Was meint ihr, gelingt es mir als Programmierer von BoeseFunktion() dass die Ausgabe in der zweiten Zeile Der Wert von Variable 'a' ist jetzt 0. lautet? Selbst wenn 'a' nur lokal der Main-Funktion bekannt ist? Ich sage ja:
BoeseFunktion():
  1. void BoeseFunktion(int tmp) {
  2.   int* pointer;
  3.   pointer = &tmp - 1;
  4.   *pointer = *pointer + 8;
  5. }
Das funktioniert eigentlich ganz einfach. Mit pointer = &tmp - 1; hole mich mir einen Zeiger auf den ersten Funktionsparameter und reduziere die Adresse um 1. Auf x86-Systemen bin ich nun an der Rücksprungadresse von BoeseFunktion() auf dem Stack. So sieht das im Speicher aus: Stack Und mit *pointer = *pointer + 8; erhöhe ich anschließend die Rücksprungadresse um 8 Byte, was gerade soviel ist um das a = 1; in main() zu überspringen. Solches Gehacke mit Pointer-Arithmetik ist natürlich sowieso pfui. Aber was uns dieses Beispiel eigentlich lehren soll ist, dass man fremden Funktionen nicht trauen soll. Es könnte schließlich sein, dass ich sie geschrieben habe... 😉

Schadensabwehr

Aber wie bekommt man nun das Problem in den Griff, dass Fremdcode, den man in so gut wie jedem Projekt findet, unbeabsichtigt oder beabsichtigt Unfug anstellt und Daten oder die Programmausführung beeinträchtigt? Da gibt es mehrere Möglichkeiten. Unter anderem:
  • Logische Programmablaufkontrolle Man stelle sich vor, jede eigene Funktion meldet sich bei ihrem Aufruf an einer zentralen Instanz mit einem eindeutigen Key. Die zentrale Instanz überprüft dann anhand einer Tabelle ob es in Ordnung ist, dass Funktion3 nun auf Funktion2 folgt oder nicht. Bringt fremder Code - zum Beispiel eine defekte DLL - den Ablauf durcheinander so wird dies schnell aufgedeckt. Voraussetzung für ein solches System ist natürlich eine deterministische Reihenfolge der Funktionsaufrufe.
  • Datenredundanz Zuerst einmal sollten alle Daten, die irgendwie wichtig sind, redundant vorliegen; am besten redundant-invers. Das heißt, es gibt nicht eine Variable 'a', sondern z.B. 'unsigned char u8VentilSollwert' und 'unsigned char u8VentilSollwertInvers'. Das hat nicht nur einen eingängigeren Namen als 'a' sondern informiert uns auch gleich noch über das Präfix u8, dass es eine unsigned 8-Bit-Variable ist mit Wertebereich von 0..255. In der einen steht dann z.B. 1 (binär 0000 0001) und in der anderen das Gegenteil, also 254 (1111 1110). Vor Verwendung wird jedes Mal geprüft, ob die beiden Variablen noch Bit-invers sind und falls nicht mit Fehlermeldung abgebrochen.
  • Datensicherung Wenn die Daten umfangreicher sind, gibt es die Möglichkeit sie mit einer Checksumme zu sichern. Wird die Checksumme oder die Daten beschädigt, so fällt dies auf, sobald man die Checksumme nachrechnet und mit dem Prüfwert vergleicht.

PC-Lüfter: Tachosignal mit Kondensator

PC-Lüfter machen umso mehr Lärm, umso schneller sie laufen. Wenn man sie aber nur mit reduzierter Drehzahl laufen lässt, dann kühlen sie natürlich leider auch entsprechend weniger. Für meinen Desktop-PC habe ich mir darum einen 220mm-Lüfter besorgt, mit dem Gedanken, dass dieser auch bei halber Drehzahl noch soviel Luftstrom produziert wie zwei laute 80mm-Lüfter. Allerdings habe ich beim Kauf nicht darauf geachtet, dass er nur einen 4-pin-Molexstecker hat und mit diesem konstant auf 12V läuft. Darum habe ich einen alten 60mm-CPU-Lüfter um dessen Kabel erleichtert und an den neuen Fan gelötet:
camper mover
Hat sogar einen Signalausgang (an gelbem Kabel) und einen Hall-Sensor - leider fehlt ein Teil der Elektronik

So sollte er sich an die Lüfterregelung anschließen und regeln lassen. Hm, geht aber nicht, das Tachosignal fehlt. Und warum? Weil der Hersteller bei diesem Modell (AK-F2230SM-CB) 5 Cent gespart hat indem er einen Transistor und zwei Widerstände weggelassen hat. Toll. Den Hall-Sensor hingegen haben sie eingebaut. Na gut, was tut man dann als schnellen Workaround? → man lötet einen Kondensator zwischen Plus und Signal, dann ist auch die Lüfterregelung wieder glücklich:
camper mover
Mit dem Dremel etwas Platz gemacht, damit der Aufkleber danach wieder glatt aufliegt

Ich hatte einen 100nF-Kondensator zu Hand; ein etwas anderer Wert ist aber vermutlich auch nicht kritisch.

Qt: bloß keine Waisenkinder instantiieren

Wenn man in Qt eine Klasse implementiert, sollte man tunlichst darauf achten, dass alle instantiierten Attribute this als parent haben. Andernfalls kann es nämlich sein, dass irgendwann der Garbage-Kollektor auf das offenbar elternlose Kind-Objekt stößt und es im Sinne von "ist das Kunst oder kann das weg" in die Tonne wirft. Und dann hat man den Salat. Ist mir hiermit passiert:
  1. class Measurement : public Object
  2. {
  3. Q_OBJECT
  4. public:
  5.   Measurement(QObject* parent = 0);
  6.   ~Measurement();
  7. };
  8. class XYPlot : public QQuickPaintedItem
  9. {
  10. Q_OBJECT
  11. public:
  12.   XYPlot(QQuickPaintedItem* parent = 0);
  13. private:
  14.   Measurement m_Measurement;
  15. };
Das Elternelement hier ist XYPlot, welches ein Attribut der Klasse Measurement besitzt. Bei mir gab es unreproduzierbare SIGSEGV segmentation faults, was immer auf irgendein Problem mit ungültigem Pointer hinweist. Beim Debuggen habe ich dann festgestellt, dass kurz zuvor der Dekonstruktor von Measurement aufgerufen wurde - obwohl das zugehörige XYPlot-Objekt noch existierte. Woran lag es? ⇒ Ich hatte vergessen, dem Kind-Objekt zu sagen wer sein parent ist. Im Konstruktor der Elternklasse XYPlot also m_Measurement mit this instantiieren:
  1. XYPlot::XYPlot( QQuickPaintedItem* parent) : QQuickPaintedItem(parent), m_Measurement(this)
  2. {
  3. }
Also besser gut aufpassen bei so was, sonst kann das Debugging richtig ätzend werden.

Globale SVN-Revisionsnummer im Projekt - und zwar richtig!

Wenn man ein Projekt aufsetzt, dann hat man meist eine Versionsnummer im Format a.b.c.d, mit
  1. a = Hauptversionsnummer, ändert sich eigentlich nur bei massiven Änderungen
  2. b = zeigt an, dass neue Features hinzugekommen sind
  3. c = zeigt an, dass etwas gepatched bzw. geupdated wurde
  4. d = gibt den Revisionsstand der Software im Versionsverwaltungssystem an.
Bei SVN gibt es die Möglichkeit sich per Code-Kommentar eine Revisionsnummer direkt in den Quelltext schreiben zu lassen. Das sieht dann beispielsweise so aus:
  1. /*!
  2. *******************************************************************************
  3. * File identification: $Id:$
  4. * Revision of last commit: $Rev:$
  5. * Author of last commit: $Author:$
  6. * Date of last commit: $Date:$
  7. *******************************************************************************
  8. */
Und wird von SVN beim Commit ersetzt zu
  1. /*!
  2. *******************************************************************************
  3. * File identification: $Id: main.cpp 178 2016-02-08 14:42:36Z cypax $
  4. * Revision of last commit: $Rev: 178$
  5. * Author of last commit: $Author: cypax $
  6. * Date of last commit: $Date: 2016-02-08 15:42:36 +0100 (Mo, 08 Feb 2016) $
  7. *******************************************************************************
  8. */
Vorausgesetzt natürlich, dass die Datei die entsprechenden SVN-Keywords gesetzt hat:
SVN keywords

So weit, so gut. In dem Beispiel oben ist die Revisionsnummer 178. Wenn es jetzt aber noch eine zweite Datei gibt und Änderungen an dieser comitted werden, dann bleibt in main.cpp die 178 stehen. Warum? Weil die SVN-Revisionsnummer dateibezogen ist - nicht projektbezogen! Das ist ja jetzt nicht ganz das, was wir haben wollten. Zum Glück, gibt es aber ein Tool, welches genau für unsere Zwecke gedacht ist: svnversion! Um eine globale Revisionsnummer zu erhalten erstellt man eine leere Datei revision.h, checkt sie nicht in SVN ein und schreibt folgendes in die *.pro-Projektdatei eines Qt-Projekts:
  1. PRE_TARGETDEPS += $$PWD/code/revision.h
  2. # Obtain SVN revision
  3. SVN_REVISION = $$system(svnversion -n)
  4. # split into list by ':'
  5. REVISION_LIST = $$split(SVN_REVISION,:)
  6. # get last item of list
  7. HEAD_REVISION = $$last(REVISION_LIST)
  8. # remove M (modified working copy)
  9. HEAD_REVISION = $$replace(HEAD_REVISION,M,)
  10. # remove S (switched working copy)
  11. HEAD_REVISION = $$replace(HEAD_REVISION,S,)
  12. # remove P (partial working copy, from a sparse checkout)
  13. HEAD_REVISION = $$replace(HEAD_REVISION,P,)
  14. QMAKE_EXTRA_TARGETS += revtarget
  15. revtarget.target = $$PWD/code/revision.h
  16. unix {
  17. revtarget.commands = "$$system(echo \'/* generated file - do not edit */\' > $$revtarget.target)"
  18. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}ifndef REVISION_H\' >> $$revtarget.target)"
  19. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define REVISION_H\' >> $$revtarget.target)"
  20. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"\' >> $$revtarget.target)"
  21. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION\' >> $$revtarget.target)"
  22. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"\' >> $$revtarget.target)"
  23. revtarget.commands += "$$system(echo \'$${LITERAL_HASH}endif // REVISION_H\' >> $$revtarget.target)"
  24. }
  25. win32 {
  26. revtarget.commands = "$$system(echo '/* generated file - do not edit */' > $$revtarget.target)"
  27. revtarget.commands += "$$system(echo '$${LITERAL_HASH}ifndef REVISION_H' >> $$revtarget.target)"
  28. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define REVISION_H' >> $$revtarget.target)"
  29. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define SVN_REVISION \"$$SVN_REVISION\"' >> $$revtarget.target)"
  30. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION $$HEAD_REVISION' >> $$revtarget.target)"
  31. revtarget.commands += "$$system(echo '$${LITERAL_HASH}define HEAD_REVISION_STRING \"$$HEAD_REVISION\"' >> $$revtarget.target)"
  32. revtarget.commands += "$$system(echo '$${LITERAL_HASH}endif // REVISION_H' >> $$revtarget.target)"
  33. }
  34. revtarget.depends = FORCE
  35. QMAKE_DISTCLEAN += $$revtarget.target
Zur Erklärung: Als erstes wird qmake mitgeteilt, dass es eine zusätzliche Abhängigkeit gibt (PRE_TARGETDEPS, Zeile 1) - nämlich besagte revision.h-Datei. Dann werden ein paar Variablen mit den SVN-Revisionsinformationen gefüllt. Mit dem Kommando svnversion -n (Zeile 4) erhält man die SVN-Revisionsnummer. Der Parameter -n bewirkt, dass die Ausgabe keinen Zeilenumbruch enthält. Die Variable SVN_REVISION enthält somit die Ausgabe von svnversion, ausgehend vom Pfad, in der die Projektdatei liegt. Wenn die lokale Arbeitskopie allerdings modifiziert, unvollständig ausgecheckt oder zu einer anderen Revision geswitched wurde, ist die Revisionsnummer nicht einfach eine Zahl, sondern nach dem Schema [abc:]xyz[M|S|P] aufgebaut, wobei xyz die Headrevision ist (für Details einfach mal in der Konsole svnversion -help eingeben). Aus diesem Grund teilen wir die Ausgabe anhand des Trennzeichens ":" auf (Zeile 6), nehmen das letzte Element dieser Liste (Zeile 8) und entfernen alle M-, S- und P-Zeichen (Zeilen 10 - 14). In Zeile 16 wird qmake mitgeteilt, dass es ein zusätzliches Target revtarget gibt (QMAKE_EXTRA_TARGETS), welches die, im Unterordner /code befindliche, revision.h-Datei ist und welche stets neu zu erstellen ist (Zeile 39). Das Befüllen der revision.h erfolgt in den Zeilen 19 - 37. Zu beachten ist, dass die erste echo-Ausgabe mit einem einfachen > umgeleitet wird. Dadurch wird der der Inhalt der Datei überschrieben. Anhängen weiterer Zeilen erfolgt mit >>. Die echo-Anweisungen für Unix müssen übrigens deshalb extra escaped werden, weil ein echo /* generated file - do not edit */ erst alle Dateien unter / auflisten würde, dann "generated file - do not edit" ausgibt und dann alle Dateien im aktuellen Verzeichnis auflistet.