Paper (PDF) - STS - TUHH
Paper (PDF) - STS - TUHH
Paper (PDF) - STS - TUHH
Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.
YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.
Bachelorarbeit<br />
Marcel Heing-Becker<br />
Bitfehlerinjektionen in Register<br />
auf der Basis von FITIn<br />
24. Juni 2013<br />
betreut durch:<br />
Prof. Dr. Sibylle Schupp<br />
Hamburg University of Technology (<strong>TUHH</strong>)<br />
Technische Universität Hamburg-Harburg<br />
Institute for Software Systems<br />
21073 Hamburg
Eidesstattliche Erklärung<br />
Ich, Marcel Heing genannt Becker, versichere an Eides statt, dass ich die vorliegende Bachelorarbeit<br />
mit dem Titel Bitfehlerinjektionen in Register auf der Basis von FITIn selbstständig<br />
verfasst und keine anderen als die angegebenen Quellen und Hilfsmittel verwendet<br />
habe. Diese Arbeit wurde in dieser oder ähnlicher Form bisher keiner anderen Prüfungskommission<br />
vorgelegt.<br />
Hamburg, den 24. Juni 2013<br />
(Unterschrift)<br />
Marcel Heing genannt Becker <br />
Matrikelnummer: 21046393<br />
Studiengang: Computational Informatics B. Sc.<br />
iii
Inhaltsangabe<br />
Mit FITIn wurde ein Valgrind-basiertes Werkzeug geschaen, das in einem Zielprogramm<br />
einen Bitfehler vornimmt. Zu diesem Zweck wurden bisher Ladeoperationen aus dem Prozessspeicher<br />
überwacht. Vor einer vom Anwender gewählten Ladeoperation führte FITIn<br />
an der Ladeadresse einen Bit-Flip durch. Dieses Prinzip verhinderte jedoch, dass der Benutzer<br />
vor Lesezugrien auf Registern etwa wenn ein Wert für mehrere Operationen in<br />
einem Register gehalten wird einen Fehler in diese injizieren konnte.<br />
Im Rahmen dieser Arbeit wird FITIn auf die Erweiterbarkeit für diesen Anwendungsfall<br />
untersucht. Dazu wird das Feld der Fehlerinjektion von einem höheren Standpunkt aus<br />
betrachtet, um FITIn in seinen theoretischen Fähigkeiten einordnen zu können. Es wird<br />
eine Implementierung vorgestellt, die einen Bitfehler nicht mehr vor der Ladeoperation<br />
sondern vor der Verwendung eines relevanten Werts vornimmt. Auf diese Weise kann der<br />
Benutzer von einer gröÿeren Transparenz bei der Verwendung von FITIn protieren.<br />
Die erweiterte FITIn-Version wird etlichen Analysen zur Anwendbarkeit und Performance<br />
unterzogen, die die Stärken und Schwächen demonstrieren. Insbesondere die Verwendung<br />
von Valgrind verdient zusätzliche Aufmerksamkeit, da das Framework etliche Vor-,<br />
aber auch Nachteile mit sich führt. Auÿerdem werden andere Konzepte zur Fehlerinjektion<br />
in Register vorgestellt und mit FITIn verglichen.<br />
Abstract<br />
FITIn is a Valgrind-based tool created for performing a bit error in a target application. So<br />
far, loading operations from the memory have been monitored to schedule error injections.<br />
Given a user-selected load operation, FITIn ipped a single bit on the memory at runtime.<br />
Doing it this way, the user faced a limitation if a value was not reloaded from the memory<br />
but remained inside a register. This potentially reduced the granularity for choosing a<br />
time when to perform a bit ip.<br />
This work focuses on extending FITIn to allow such a use case. By a more comprehensive<br />
look at the domain of fault injection, the position of FITIn will be highlighted.<br />
An implementation will be presented that moves the bit error injection from pre-load to<br />
pre-use time. This makes the user gain transparency when working with FITIn.<br />
The new version of FITIn is subject to multiple analyses regarding usability and performance.<br />
These analyses reveal strengths and weaknesses of FITIn. Valgrind deserves<br />
some attention in particular as this framework implies both benets and diculties for<br />
tools on top of it. Other approaches for injecting bit errors into registers will be presented<br />
and compared to FITIn.<br />
v
Inhaltsverzeichnis<br />
1 Einführung 1<br />
1.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1<br />
1.2 Problembeschreibung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1<br />
2 Fehlerinjektion 3<br />
2.1 Methoden . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
2.2 Anwendungsfälle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5<br />
2.3 Injektionsebenen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
2.3.1 Hardware . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
2.3.2 Betriebssystem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
2.3.3 Virtuelle Maschine . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
2.3.4 Kompilierungszeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
2.3.5 Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7<br />
3 Erweiterung von FITIn 9<br />
3.1 Dynamische Binärinstrumentierung . . . . . . . . . . . . . . . . . . . . . . . 9<br />
3.2 Valgrind . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
3.2.1 Allgemeines . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
3.2.2 Instrumentierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
3.2.3 VEX IR . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12<br />
3.3 FITIn . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15<br />
3.4 Registerbehandlung in Valgrind . . . . . . . . . . . . . . . . . . . . . . . . . 16<br />
3.5 Evaluierung von Ansätzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
3.6 Erweiterung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19<br />
3.6.1 Betrachtete Operationen . . . . . . . . . . . . . . . . . . . . . . . . . 20<br />
3.6.2 Instrumentierungszeit . . . . . . . . . . . . . . . . . . . . . . . . . . 21<br />
3.6.3 Ausführungszeit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24<br />
3.6.4 float, double und weitere Datentypen . . . . . . . . . . . . . . . . . 26<br />
3.6.5 Systemaufrufe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28<br />
4 Evaluierung von FITIn 31<br />
4.1 Analyse von Testfällen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31<br />
4.2 FlipSafe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38<br />
4.3 Performance-Strafe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40<br />
4.4 Speicherbedarf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 43<br />
4.5 Zwischenfazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44<br />
5 Konkurrierende Ansätze 47<br />
5.1 Programme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47<br />
5.2 DBI-Frameworks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 48<br />
5.3 Vergleichsfazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50<br />
vii
6 Abschluss 51<br />
6.1 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51<br />
6.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52<br />
Literaturverzeichnis 53<br />
Akronyme 55<br />
Abbildungsverzeichnis 57<br />
Anhang A Ergänzendes Material 59<br />
A.1 VEX IR-Spezikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59<br />
A.2 Instrumentierungsbeispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60<br />
Anhang B FITIn 65<br />
B.1 Benutzung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65<br />
B.2 Problembehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67<br />
viii
1 Einführung<br />
1.1 Motivation<br />
Es existieren Einüsse auf die Ausführung von Software, die nicht durch diese selbst, nicht<br />
durch eine ausführende virtuelle Maschine und nicht durch die Spezikation der darunterliegenden<br />
Hardware bedingt sind: Sowohl ein Defekt der Hardware als auch Ionenstrahlung<br />
oder elektromagnetische Interferenzen, die in Wechselwirkung mit einer Bitrepräsentation<br />
treten, gehören zu diesen Faktoren. Ein mögliches Resultat ist die Umkehrung eines Bits<br />
[1, S.3f]. In einem günstigen Fall wird ein sogenannter Bit-Flip unbemerkt bleiben, im<br />
unglücklichsten Fall einen Systemabsturz herbeiführen.<br />
Gegenmaÿnahmen, einen solchen Bit-Flip im Datenbereich zu erkennen und auch zu<br />
korrigieren, nden sich sowohl in Hardware, etwa bei Arbeitsspeicher mit ECC-Fähigkeit,<br />
als auch in Software, etwa durch redundante Speicherung von Daten. Softwareverfahren zur<br />
Behandlung dieser Fälle werden Softwareimplementierte Hardwarefehlertoleranz (SIHFT)-<br />
Techniken genannt [2, S.841].<br />
Seitens eines Entwicklers mag die Motivation bestehen, Analyse- und Testverfahren zu<br />
verwenden, die Bitfehler in der zu untersuchenden Software verursachen: zum systematischen<br />
Analysieren der Auswirkung auf die Ausführung, zum Testen von SIHFT-Techniken<br />
oder zur Beobachtung der Stabilität von numerischen Algorithmen.<br />
Ein derartiges Testverfahren setzt sich insofern von bekannten Techniken wie Blackund<br />
Whitebox-Tests oder Fuzzing ab, dass der Entwickler einen anderen Weg zur Beein-<br />
ussung der Ausführung einschlagen muss: Etwa durch manuelles Nachstellen eines solchen<br />
Falls im Quell- oder Binärcode, oder durch eine äuÿere Komponente, die auf das Programm<br />
Einuss nehmen kann. Weiterhin mag der Auftritt eines Bit-Flips deterministisch oder stochastisch<br />
gewünscht sein. Andere Anforderungen ergeben sich aus dem Ort, zum Beispiel<br />
ob im Arbeitsspeicher oder in einem CPU-Register, und der Zeit der Manipulation, um<br />
beispielsweise eine bestimmte Iteration oder Rekursionstiefe abzuwarten.<br />
FITIn ist in seiner Ursprungsform ein für das Ausführungs- und Instrumentierungs-<br />
Framework Valgrind entworfenes Plug-In, das dem Benutzer bei Vorlage des Quellcodes<br />
in C/C++ ermöglicht, einen Bit-Flip auf einem gewählten Bit zur Ausführungszeit vor<br />
einem gewählten Lesevorgang auf dem Speicher herbeizuführen. Im Vergleich zum manuellen<br />
Vorgehen macht es FITIn dem Benutzer einfacher, ein anderes Bit und einen anderen<br />
Zeitpunkt zu wählen.<br />
1.2 Problembeschreibung<br />
Die ursprüngliche Fassung von FITIn beschränkt sich darauf, einzig Lesezugrie auf den<br />
Prozessspeicher zu erkennen und zum Abwarten des Flip-Zeitpunkts zu verwenden. Das<br />
grundsätzliche Vorgehen bei der Benutzung sieht dabei wie folgt aus:<br />
Bei Einsicht des Quellcodes entscheidet sich der Benutzer für eine oder mehrere Variablen,<br />
die er für einen Bit-Flip in Betracht ziehen möchte. Dazu setzt er ein vorgegebenes<br />
C-Makro nach der Deklarierung der Variablen. Nach einer erneuten Kompilierung<br />
1
1 Einführung<br />
des Quellcodeabschnitts lässt sich das Programm sowohl wie gewohnt als auch für FITIn<br />
benutzen.<br />
Valgrind erkennt anhand des durch das Makro erweiterten Stacks, dass eine Client-<br />
Anfrage an FITIn gerichtet ist. FITIn erhält von einer Variablen die Startadresse sowie<br />
deren Gröÿe in Bytes und fügt diese Daten der eigenen Erkennungsliste hinzu. Weiterhin<br />
erhält FITIn von Valgrind eine Zwischenrepräsentation des auszuführenden Binärcodes,<br />
aus der sich Zugrie auf den Speicher erkennen lassen und die sich zur Erzeugung von<br />
Bit-Flips manipulieren lässt. Durch Maÿnahmen wird jeder relevante Lesezugri gezählt<br />
und zum gewählten Zeitpunkt das gewählte Bit im Prozessspeicher invertiert, bevor die<br />
Variable im Rahmen des ursprünglichen Programmusses aus dem Prozessspeicher gelesen<br />
wird.<br />
Auf vielen Prozessorarchitekturen werden Daten jedoch nicht vor jeder Benutzung<br />
erneut aus dem Speicher in ein CPU-Register geladen: In Abhängigkeit der Zielplattform<br />
führt der Compiler eine Analyse zur Registerallokation durch. Schlieÿlich verlangen gewisse<br />
CPU-Instruktionen, dass sich die Operanden in einem Register benden. Durch geschickte<br />
Anordnung der Instruktionen lassen sich Daten so lange wie möglich im Register halten,<br />
bevor diese in den Arbeitsspeicher ausgelagert oder zurückgeschrieben werden [3, S.101f].<br />
Dadurch werden nicht nur Lade- und Speicherinstruktionen eingespart, sondern auch der<br />
tatsächliche Transfervorgang in ein deutlich langsameres Speichermedium und der aus diesem<br />
hinaus.<br />
Stellt man FITIn dieser Tatsache entgegen, erkennt man die Lücke, die sich durch<br />
fehlendes Beobachten von Registerallokationen und -zugrien auftut. Im Rahmen dieses<br />
Dokuments wird untersucht, inwieweit das bestehende FITIn für diese Aufgabe erweitert<br />
werden kann. Dabei werden nicht nur verschiedene Ansätze verfolgt und eine Implementierung<br />
vorgestellt (Kapitel 3), sondern auch die Fehlerinjektion im weiteren Sinne betrachtet<br />
(Kapitel 2). Es wird darüber hinaus auf konkurrierende Ansätze eingegangen (Kapitel 5)<br />
um die Vorteile und Grenzen von FITIn und Valgrind auszuloten. In der Evaluierung wird<br />
FITIn verschiedenen Anwendungstests unterzogen und auf Fähigkeiten und Einschränkungen<br />
untersucht. Im Sinne der ursprünglichen Entwicklung wird FITIn erneut dazu benutzt,<br />
eine SIHFT-Bibliothek auf die Probe zu stellen. Auch über die Performance und<br />
über den Speicherverbrauch von FITIn werden Einschätzungen gegeben (Kapitel 4).<br />
2
2 Fehlerinjektion<br />
Das Injizieren von Fehlern in ein Programm ist auf verschiedene Art und Weise möglich und<br />
auf verschiedenen Ebenen der Hard- und Software realisierbar. In diesem Kapitel werden<br />
potentielle Möglichkeiten aufgezeigt und kurz erörtert.<br />
2.1 Methoden<br />
Den Spezialfall der Fehlerinjektion in Register vorerst auÿer Acht gelassen, erönen sich<br />
unterschiedliche Herangehensweisen, Einuss auf die Ausführung eines Programms zu erlangen.<br />
Grundsätzlich sind diese in hard- und softwarebasierte Methoden aufzuteilen. Die<br />
Abbildung 2.1 erlaubt einen Überblick über die Einordnung von Fehlerinjektionsmethoden<br />
[4, S.76].<br />
Hardwarebasiert<br />
Kontaktbasiert<br />
Tastkopf<br />
Sockel<br />
Fehlerinjektion<br />
Kontaktlos<br />
Vor Ausführung<br />
Softwarebasiert<br />
Zeitnehmer<br />
Zur Ausführungszeit<br />
Ausnahme<br />
Abbildung 2.1: Taxonomie der Fehlerinjektion<br />
Instrumentierung<br />
Hardwarebasierte Fehlerinjektion<br />
Die Manipulation von Hardware erfordert zusätzliche Hardware sowie die Fähigkeit, in<br />
ausreichend kurzer Zeit herbeigeführte Fehler und deren Folgen erkennen zu können. Die<br />
hardwarebasierte Fehlerinjektion teilt sich in zwei Untergruppen auf:<br />
• Kontaktbasierte Fehlerinjektion: Eine Möglichkeit besteht darin, einen Hardware-Pin<br />
mit einem aktiven Tastkopf zu berühren. Für die Dauer wird die Spannung an diesem<br />
verändert, sodass die Auösung des Signals auf den gewünschten Wert eingerastet<br />
wird. Komplexere Manipulationen lassen sich durch Sockelung der Hardware vornehmen:<br />
So können zusätzlich mittels integrierter Logikgatter oder Schaltwerke die<br />
Eingangssignale den Manipulationsanforderungen entsprechend angepasst werden.<br />
3
2 Fehlerinjektion<br />
• Kontaktlose Fehlerinjektion: Durch den Beschuss der Hardware mittels Ionen [5] oder<br />
durch elektromagnetische Interferenzen [6] können Bitrepräsentationen umgekehrt<br />
werden. Dieses Vorgehen ist mit Einbuÿen in der Präzision in Bezug auf Zeit und<br />
Ort sowie der Wiederholbarkeit verbunden, es erlaubt jedoch eine Beobachtung der<br />
Funktionsfähigkeit der Hardware in realen Strahlungsverhältnissen und elektrischen<br />
Kongurationen.<br />
Softwarebasierte Fehlerinjektion<br />
Im Vergleich zu hardwarebasierten Fehlerinjektionen stellen softwarebasierte Lösungen<br />
einen geringeren Kostenfaktor dar, und auch die Implementierung einer Fehlerinjektionssemantik<br />
ist exibler realisierbar. Natürliche Grenzen sind der Software im Rahmen dessen<br />
gesetzt, wie diese im Besitz von Hardwarezugrien ist: Eine Manipulation des Arbeitsspeichers<br />
vorbei an einem Paritätsbit, das Teil eines hardwarebasierten elektronischen Korrekturcodes<br />
(ECC) ist, ist daher zum Beispiel unmöglich. Man unterscheidet zwischen zwei<br />
Injektionsszenarien:<br />
• Vor der Ausführungszeit: An einem Zeitpunkt, bevor das Programm zur Ausführung<br />
in den Arbeitsspeicher geladen ist. In Frage kommen manuelle Anpassungen<br />
des Quellcodes, eine Compiler-Direktive oder die Manipulation des generierten Binärcodes.<br />
Letzteres ordnet sich in die statische Binärinstrumentierung ein [7, S.3].<br />
• Zur Ausführungszeit: Ist das Programm bereits als Prozess aktiv, bieten sich andere<br />
Methoden zur Fehlerinjektion an: Die des ablaufenden Zeitnehmers, die der Ausnahmebehandlung<br />
und die der Code-Instrumentierung. Betrachtet man den Ausführungs-<br />
Stack eines Programms, können diese Fehlerinjektoren ab der Betriebssystemebene<br />
sinnvoll implementiert werden.<br />
• Ablaufender Zeitnehmer: Ein hard- oder softwarebasierter Zeitnehmer erzeugt<br />
bei Ablauf einen Interrupt 1 . Eine auf diesen Interrupt gesetzte Callback-Methode<br />
ist daraufhin in der Lage, eine Fehlerinjektion im Programm vorzunehmen. Die<br />
Präzision dieser Methode ist begrenzt, da der Zeitablauf zum Beispiel durch<br />
variable E/A-Operationen, auf die die CPU wartet, unvorhersehbar eintreten<br />
kann.<br />
• Ausnahmebehandlung: Sowohl ein softwarebedingtes Ausnahmeereignis, das vom<br />
Programm vor der Ausführung des nachfolgenden Codeabschnitts ausgelöst<br />
wird, als auch ein Hardwareereignis können dazu verwendet werden, zu ausgewählten<br />
Zeitpunkten eine Fehlerinjektion vorzunehmen, sofern zuvor eine entsprechende<br />
Callback-Prozedur registriert wurde.<br />
• Code-Instrumentierung: Zur Laufzeit des Programms verfügt die fehlerinjizierende<br />
Softwarekomponente über das Wissen, welche Instruktion als nächstes<br />
ausgeführt wird und kann gegebenenfalls Instruktionen hinzufügen, die der Fehlerinjektion<br />
dienlich sind. Der wesentliche Vorteil liegt dabei darin, dass eine Injektionssemantik<br />
ungleich komplexer als in anderen Softwareansätzen gewählt<br />
werden kann. Die Schwierigkeit bei diesem Ansatz ist dagegen, dass das instrumentierende<br />
Programm mit dem ausgeführten Zielprogramm nicht weiter<br />
in Konikt treten darf, etwa durch Blockierung von Ressourcen oder durch<br />
Veränderung der denotationellen Semantik [8, S.266]. Allgemein, also nicht auf<br />
Fehlerinjektionen begrenzt, nennt man dieses Vorgehen dynamische Binärinstrumentierung<br />
(DBI) [7, S.3].<br />
1 Deutsch: Unterbrechung<br />
4
2.2 Anwendungsfälle<br />
2.2 Anwendungsfälle<br />
Binärfehler lassen sich in mehreren Formen und Auftrittsmustern konstruieren. Betrachtet<br />
man ein einzelnes Bit, so ergeben sich folgende Fehlfunktionsszenarien:<br />
• Bit-Flip: Ein Hardwaredefekt oder ein Umwelteinuss kehrt die Bitrepräsentation<br />
temporär um. Ist ein Bit-Flip strahlungsbedingt, so spricht man von einem Single<br />
Event Upset (SEU) [5, S.1].<br />
• Bit-Einrastung: Das Bit lässt sich nicht mehr aktualisieren, es verbleibt im Zustand<br />
0 oder 1. Dies kann einem Hardwaredefekt entsprechen, etwa degeneriertem Arbeitsspeicher.<br />
• Flüchtiges Bit: Ein Speichermedium verliert seine Integrität, sodass zeitlich versetzte<br />
Lesezugrie einen anderen Zustand zurückliefern können, aber nicht müssen [9,<br />
S.116]. Ein solches Verhalten lässt sich mittels einer Veränderungsfrequenz oder einer<br />
Zustandswahrscheinlichkeit bei Lesezugrien beschreiben.<br />
In diesem Dokument werden ausschlieÿlich Binärfehler betrachtet, die im Bezug zur<br />
Zustandsspeicherung stehen, dazu zählen CPU-Register und -Caches, der Arbeitsspeicher<br />
(RAM) und der Sekundärspeicher. Ausdrücklich ausgeschlossen seien Übertragungsfehler<br />
jeglicher Art.<br />
Möchte man die Auswirkung von Bitfehlern auf den Programmuss untersuchen, so<br />
sind die nachfolgenden Fälle von Interesse (basierend auf [10, S.37f]):<br />
• Operanden<br />
• Daten: Verfälschte Operanden, die der numerischen Berechnung dienen, werden<br />
das Berechnungsergebnis trüben.<br />
• Adressen: Abweichungen in Speicheradressen sind vergleichbar mit den Auswirkungen<br />
unbedachter Zeigerverwendung im Quellcode: Falsche Daten werden<br />
geladen, Daten werden unbeabsichtigt überschrieben und Operationen auf ungültigen<br />
Speicherbereichen führen zu Programmabstürzen.<br />
• Sprungkonditionen und -ziele: Einem modizierten Operanden, der als Bedingung<br />
für einen Sprung dient, kann das Einschlagen eines anderen Instruktionspfads<br />
folgen. Bei Sprüngen, die einen relativen oder absoluten Zieloperator besitzen,<br />
ist die möglicherweise drastischere Folge, dass die Ausführung in einem<br />
entfernten Abschnitt fortfährt.<br />
• Flags: Flags halten zusätzliche Informationen über den CPU-Zustand oder über<br />
die zuletzt ausgeführte Instruktion. Auch wenn diese, wie auf der x86-Architektur,<br />
nicht explizit les- und schreibbar sind, so ist der Einuss des manipulierten Flags<br />
entscheidend, wenn die nächste, davon abhängige Instruktion dadurch eine andere<br />
Operation ausführt. Ein Beispiel sei auf x86 die Instruktion JE (Jump If<br />
Equal). Eine zuvor erfolgte CMP-Instruktion (für Compare) setzt das Zero-Flag<br />
(ZF). JE interpretiert ein positives ZF als gleichwertige Operanden und wird in<br />
diesem Fall den Sprung ausführen und andernfalls zur nächstfolgenden Instruktion<br />
fortschreiten. Obwohl die Operanden unangetastet blieben, wird bei der<br />
Bit-Umkehr des Flags dennoch ein anderer Sprungeekt einsetzen.<br />
• Register: Sofern das Bitmuster einer Instruktion einen variablen Zugri auf Register<br />
ermöglicht, müssen auch diese als Operanden betrachtet werden. Verursacht<br />
der Zugri primär keine Zugrisverletzung, tritt sekundär einer der ersten<br />
drei erwähnten Fälle ein, da mit Sicherheit ein falscher Operand herangezogen<br />
wurde.<br />
• Befehle: Die Bitmanipulation einer Instruktion auÿerhalb der Operandenbits kann<br />
5
2 Fehlerinjektion<br />
weitreichende Konsequenzen haben. Ein beinahe wünschenswerter Fall ist der Tausch<br />
des Operatoren, wie es durch einen Programmierfehler plausibel wäre: Wird das Bit<br />
der x86-Instruktion JNE 0xF an der Bitposition zum Zählen sei der Operand gestrichen<br />
0 geippt, und geht dieser sinngemäÿ die Instruktion CMP 1, 1 voraus,<br />
so ergibt sich die Instruktion JE 0xF. C-äquivalent stellt dieser Fall ein irrtümliches<br />
if(1!=1) { } statt des korrekten if(1==1) { } dar. Je nach Position des Bit-Flips<br />
kann aber auch die Operation einer ganz anderen Klasse hervortreten. Auf Plattformen<br />
mit variabler Instruktionslänge kann es sogar dazu kommen, dass ein Bit-Flip<br />
eine Instruktion in mehrere kürzere Instruktionen zerfallen lässt oder die Bytes mehrerer<br />
kurzer Instruktionen nun als eine Instruktion gedeutet werden.<br />
2.3 Injektionsebenen<br />
In diesem Abschnitt wird untersucht, ob und wie ein Bit-Flip auf Registerinhalten grundsätzlich<br />
auf verschiedenen Ebenen der Erstellung und Ausführung eines Programms umsetzbar<br />
ist.<br />
2.3.1 Hardware<br />
Die in Abschnitt 2.1 vorgestellten Hardwareansätze lassen zwar eine theoretische Möglichkeit<br />
oen, Einuss auf Registerbits zu nehmen, in der Umsetzung stellen sich jedoch etliche<br />
Hürden in den Weg: Von der zu untersuchenden Hardware ist eine tiefgehende Kenntnis<br />
über den physikalischen Aufbau notwendig. Bei einer Technologieknotengröÿe im zweistelligen<br />
Nanometerbereich ist nicht nur eine ebenso präzise Positionierung der Werkzeuge<br />
nötig, sondern es ist auch sicherzustellen, dass der Wirkungsgrad auf ein einzelne Registerbits<br />
beschränkt werden kann. Für Benutzer ohne die entsprechende Hardware steht dieser<br />
Weg also auÿer Frage.<br />
Ein anderer Ansatz ermöglicht ein eektiveres Vorgehen bei deutlich weniger Aufwand:<br />
JTAG-normiertes On-Chip Debugging (OCD). Verfügt die Hardware über ein JTAG-<br />
Interface, lässt sich durch eine zusätzliche Steuerkomponente, die als JTAG-Controller fungiert,<br />
OCD durchführen. Der Controller setzt auf der Zielhardware einen Unterbrechungspunkt<br />
und kann bei Erreichen desselben eine Sequenz von JTAG-Instruktionen ausführen,<br />
die auch Lese- und Schreibvorgänge auf Benutzerregistern ermöglichen [11, S.103f]. Für<br />
unwillkürliche Bitmanipulationen sind Kenntnisse zum Adressraum und Binärcode des zu<br />
unterbrechenden Programms nötig.<br />
2.3.2 Betriebssystem<br />
Wann immer ein Wechsel vom Benutzermodus in einen ausreichend privilegierten Kernel-<br />
Modus stattndet seien es Systemaufrufe, Unterbrechungsbehandlungen oder ein Kontextwechsel<br />
, verfügt das Betriebssystem über volle Zugrisrechte auf den Prozesszustand<br />
einschlieÿlich der Registerinhalte. An dieser Stelle ist zum Beispiel in Abhängigkeit der<br />
Prozessidentikation und des Instruktionszeigers ein Bit-Flip ausführbar, sei es durch eine<br />
festgeschriebene Betriebssystemroutine oder ein Kernel-Modul.<br />
2.3.3 Virtuelle Maschine<br />
Der Kernvorteil einer Virtuellen Maschine (VM) liegt darin, dem auszuführenden Programm<br />
eine von der Hardware unabhängige Ausführungsmaschine anzubieten. Eine Rea-<br />
6
2.3 Injektionsebenen<br />
lisierung, die etwa die Java Virtual Machine verfolgt, ist die Verwendung von Operandenstacks,<br />
die für die Instruktionen der VM benutzt werden [12, S.17]. Ein anderer Ansatz ist<br />
die Benutzung von virtuellen Registern, sodass dem Programm eine beliebige Anzahl von<br />
Speicherstellen zur Verfügung steht und jede VM-Instruktion für die Operanden entsprechende<br />
Registerbezeichnungen verlangt.<br />
Aus diesem Grund ist es nicht möglich, innerhalb einer VM die Abbildung eines Programms<br />
auf Register der ausführenden CPU zu betrachten. Viel mehr ist es der virtuellen<br />
Maschine selbst überlassen, an welcher Stelle und unter welchen Bedingungen diese Bitmanipulationen<br />
vornehmen mag. Verfügt die VM über einen Just-In-Time (JIT)-Compiler,<br />
anstatt Bytecode bloÿ zu interpretieren, sollte es dieser jedoch ein Leichtes sein, Bitmanipulationen<br />
auf Registern in entsprechende Plattforminstruktionen zu übersetzen.<br />
2.3.4 Kompilierungszeit<br />
Zum Zeitpunkt des Schreibens des Programmquellcodes weiÿ der Entwickler nicht mit<br />
etwas Erfahrung wird sein Bauchgefühl ihm jedoch eine Vorstellung ermöglichen , zu<br />
welchem Zeitpunkt welche Variable in welches Register geladen und anschlieÿend gehalten<br />
wird. Diese Entscheidungen trit der Compiler in Abhängigkeit von Datentypen, dem<br />
Ergebnis der Registerallokationsoptimierung und anderen Optimierungsstufen. Aggressive<br />
Optimierungsstufen wie etwa -O3 des GNU C Compiler (GCC) können sogar dazu führen,<br />
dass aufgrund von statischer Zwischencodeanalyse ganze Codepassagen zur Kompilierungszeit<br />
auf einen Ausdruck reduziert werden und dadurch nicht die erwartete Übersetzung<br />
erfahren.<br />
Ein erster Ansatz ist der Blick in den vom Compiler erzeugten Assemblercode, wie er<br />
beim GCC mit dem Flag -S ausgegeben werden kann. Ausgestattet mit neuen Erkenntnissen<br />
lieÿe sich der Quellcode mit plattformspezischen asm-Direktiven ausstatten, um<br />
dadurch Bedingungen für die Manipulation von Registerbelegungen im Bereich von Interesse<br />
aufzusetzen. Diese Lösung ist aufgrund der Abhängigkeit von verschiedenen Compilern,<br />
Compiler-Versionen, Kongurationsparametern und Codeanpassungen jedoch zu<br />
fragil. Auÿerdem nimmt sie zusätzlich die Plattformunabhängigkeit, wenn diese ohne asm-<br />
Anweisungen gegeben ist.<br />
Eine denkbare Alternative lieÿe sich mittels eines Compilers umsetzen, der seitens<br />
des Quellcodes mit Makros dazu aufgefordert wird, Registermanipulationen vorzunehmen.<br />
Auf diese Weise lassen sich verschiedene Operationen einstreuen, beispielsweise Bit-Flip im<br />
Register X und Position i oder Bit-Flip beim nächsten Laden der Variable v in ein Register.<br />
Eine Verfeinerungen des Verfahrens erlaubt, dass zusätzlich auf die Rekursionstiefe oder<br />
einen Aufrufzähler Rücksicht genommen wird. Im Umkehrschluss bedeutet das, dass ein<br />
Compiler entweder auf die Optimierung bestimmter Bereiche verzichten muss, oder dass<br />
eine solche Code-Annotation nur bis zu einer gewissen Optimierungsstufe funktionieren<br />
wird.<br />
2.3.5 Programm<br />
Ist ein Programm kompiliert und zusammengelinkt, verbleibt vor der Ausführung die Möglichkeit<br />
zur statischen Binärinstrumentierung. Mit der Hilfe von Werkzeugen für Reverse<br />
Engineering, etwa einem Disassembler und Assembler, kann der Assemblercode manuell<br />
mit zusätzlichen Instruktionen ausgestattet werden. Per Hand ist das Vorgehen jedoch<br />
mühselig, da die Ausgabe hochgradig von der Compiler-Konguration und dem Quellcode<br />
abhängt. Erschwerend kommt hinzu, dass die hinzuzufügenden Instruktionen die bestehende<br />
Funktionalität, insbesondere relative Sprünge und CPU-Flags, berücksichtigen müssen.<br />
7
2 Fehlerinjektion<br />
Mit einem Framework zur Disassemblierung und statischen Binärinstrumentierung, wie in<br />
[13] und [14] vorgestellt, ist jedoch auch ein automatisiertes Vorgehen zu diesem Zweck<br />
denkbar: Bei einem registerorienterten Bit-Flip kann die Codeinjektion auf einfache Weise<br />
durch Zählung oder Zufall vorgenommen werden, wann immer dieses Register als Operand<br />
implizit oder explizit von einer Instruktion benutzt wird. Bei einem belegungsorientierten<br />
Bit-Flip ist zusätzlicher Aufwand nötig: Durch das Suchen von Ladeoperationen, die<br />
Daten von einer gewählten Speicheradresse in ein Register kopieren, und das Finden von<br />
nachfolgenden Instruktionen, die mit dieser Belegung operieren, lassen sich verschiedene<br />
Injektionsbedingungen aufstellen. Sind Daten erst zur Laufzeit auösbar, etwa Adressen<br />
mit einem registerbasierten Versatz, muss die Instrumentierung Code injizieren, der die<br />
Erkennung von Ladequellen und Bitmanipulationen zur Ausführungszeit vornimmt.<br />
Im Gegensatz zur Statischen Binäranalyse (SBA) wird bei der Dynamischen Binäranalyse<br />
(DBA) der Binärcode zur Programmlaufzeit und auch erst kurz vor der Ausführung<br />
durch die CPU analysiert und instrumentiert. Die Voraussetzungen dazu sowie die damit<br />
einhergehenden Vor- und Nachteile werden ausführlicher in Abschnitt 3.1 behandelt.<br />
Das im folgenden Kapitel vorgestellte Programm FITIn macht sich verschiedene Ansätze<br />
zu Nutze, die in diesem Abschnitt vorgestellt wurden: die Kompilierungszeit und<br />
die Anwendung eines DBI-Frameworks, das FITIn Instruktionen einer virtuellen Maschine<br />
bereitstellt.<br />
8
3 Erweiterung von FITIn<br />
Dieses Kapitel stellt die Ausführungsumgebung von FITIn, seine ursprüngliche Fassung,<br />
Erweiterungsansätze sowie die realisierte Implementierung vor.<br />
3.1 Dynamische Binärinstrumentierung<br />
Die dynamische Binäranalyse stellt einzig die Anforderung an das zu untersuchende Programm,<br />
dass dieses in einem unterstützen Binärformat vorliegt. Der wesentliche Unterschied<br />
zur SBA besteht darin, dass ein Programm erst als Prozess initialisiert wird, bevor<br />
die erste Binäranalyse vorgenommen wird, und nicht als Datei auf dem Sekundärspeicher<br />
untersucht und behandelt wird. Die SBA ist nur begrenzt in der Lage, Instruktionen zu<br />
erkennen, die dem Ergebnis von Selbstextraktion, Selbstmodikation oder dynamischem<br />
Erzeugen und Nachladen von Code entspringen, insbesondere dann nicht, wenn die genannten<br />
Vorgänge von externen Eingaben abhängen [7, S.3f].<br />
Gängige Anwendungsfälle sind Programme, die aus Gründen des Platzes oder der Ladeperformance<br />
zum Beispiel UPX-komprimiert ausgeliefert werden, Programme, die Schutzmechanismen<br />
zur Durchsetzung von digitalem Rechtemanagement besitzen, Plug-In-fähige<br />
Programme und Programme, die einen prozessinternen JIT-Compiler benutzen.<br />
Ein DBI-Framework initialisiert das zu untersuchende Programm analog zu einem Betriebssystem<br />
und beginnt mit der DBA an der ersten Instruktion des Programms. Abhängig<br />
vom Framework wird jede nachfolgende Instruktion oder auch ganze Blöcke von Instruktionen<br />
an die DBI-Komponente übergeben und anschlieÿend ausgeführt. Auf diese Weise<br />
wird im Unterschied zur SBA einzig tatsächlich auszuführender Code betrachtet, da sich die<br />
DBA ausschlieÿlich an erreichten Sprüngen und Abzweigungen des Codes entlangbewegt.<br />
Die Konsequenz ist, dass der Sprung in dynamisch erzeugten Binärcode für das DBI-<br />
Framework transparent bleibt. Die Kehrseite des Verfahrens ist verständlicherweise ein<br />
Einbruch in der Ausführungsgeschwindigkeit des Zielprogramms.<br />
Eine weitere Herausforderung, die ein DBI-Framework meistern muss, besteht darin,<br />
mit dem ausgeführten Programm nicht in Konikt zu treten oder solche Fälle angemessen<br />
zu behandeln, seien es Adressräume im virtuellen Speicher, die zwischen Framework und<br />
Prozess kollidieren könnten oder Ressourcen, auf die nur einer von beiden zur Zeit zugreifen<br />
kann. Das DBI-Framework darf weiterhin nicht die Ausführungsgewalt verlieren, wenn<br />
zum Beispiel durch einen Systemaufruf aus dem Kernel-Modus zurück in das Programm<br />
gesprungen wird. In den Kernel-Modus kann das DBI-Framework nicht eingreifen. Legt<br />
das Programm zusätzliche Threads an, sollte das DBI-Framework diese ebenbürtig zum<br />
Hauptthread behandeln können.<br />
Die Verwendung von DBI erfolgt allerdings nicht einzig aus der Motivation, Hindernisse<br />
in der Analyse zu überwinden. Viel mehr erlaubt DBI eine weitere Analyseklasse:<br />
Speicherbeschattung. Speicherbeschattung ist die Fähigkeit, jedes vom untersuchten Programm<br />
verwendete Byte durch zusätzlichen Speicher zu beschatten [15, S.89]. Die Schattenwerte<br />
genannten Daten werden dazu verwendet, Aussagen über das beschattete Byte<br />
zur Laufzeit zu treen. Beispielsweise lassen sich so Daten darüber erheben, wie oft ein<br />
Byte gelesen oder ob und wann es allokiert wurde. Diese Fähigkeit ermöglicht mächtige<br />
9
3 Erweiterung von FITIn<br />
Softwarewerkzeuge zur Laufzeitanalyse von Programmen.<br />
Ein DBI-Framework mit lückenloser Unterstützung zur Speicherbeschattung muss nach<br />
[15, S.90f] folgende Fähigkeiten bieten:<br />
1. Die Beschattung aller Register.<br />
2. Die Beschattung des gesamten Prozessspeichers.<br />
3. Die Instrumentierung von Lese- und Schreiboperationen auf den Prozessspeicher.<br />
4. Die Instrumentierung von Systemaufrufen, die auf den Prozessspeicher lesend oder<br />
schreibend zugreifen.<br />
5. Die Beschattung der initialen Speicherbelegung.<br />
6. Die Instrumentierung von speicherallokierenden und -freigebenden Systemaufrufen.<br />
7. Die Instrumentierung von Veränderungen am Stack-Zeiger.<br />
8. Die Instrumentierung von (De-)Allokationen auf dem Heap-Speicher.<br />
9. Ausgabemechanismen.<br />
Das DBI-Framework Valgrind erfüllt alle der genannten Punkte [15, S.96f].<br />
3.2 Valgrind<br />
3.2.1 Allgemeines<br />
Valgrind wurde im Jahr 2002 in seiner ersten oziellen Version veröentlicht, es steht<br />
unter der GNU Public License (GPL). Bekannt ist dieses DBI-Framework insbesondere<br />
durch das enthaltene Werkzeug Memcheck, das dazu benutzt wird, um Speicherzugrisfehler<br />
und Speicherlecks in einem Programm zu ermitteln. Zur Standarddistribution von<br />
Valgrind [16] gehören jedoch noch weitere DBA-Werkzeuge, zum Beispiel Cachegrind, das<br />
die Cachefreundlichkeit des Programms analysiert, oder Helgrind, das Race Conditions<br />
bei Multithreading erkennt. Zu den unterstützten Umgebungen gehören unterschiedliche<br />
Kombinationen aus den Architekturen x86, AMD64, PowerPC, ARM, S390 und MIPS und<br />
den Betriebssystemen Linux, Darwin und Android.<br />
Die Infrastruktur von Valgrind ist so aufgebaut, dass jedes der genannten Werkzeuge<br />
ein Plug-In darstellt, welches mit dem Parameter -tool= gestartet wird. Jedes Plug-In<br />
fügt sich in den Kompilierungsvorgang der Valgrind-Distribution ein, der Quellcode dabei<br />
ist wie auch für Valgrind in C zu schreiben.<br />
3.2.2 Instrumentierung<br />
Wichtig zu beleuchten, ist das interne Vorgehen von Valgrind, das Verhältnis des Framework-<br />
Kerns zum geladenen Plug-In und auch die Art und Weise, wie Valgrind nicht-triviale<br />
Probleme in Bezug auf die Ausführung und Speicherbeschattung behandelt.<br />
Mit dem Spezizieren des zu untersuchenden Programms als Kommandozeilenargument<br />
initialisiert Valgrind das Programm selbstständig, sodass aus der Sicht des Betriebssystems<br />
nur Valgrind als Prozess erkennbar ist. Beginnend am Einstiegspunkt des Programms<br />
stellt Valgrind sogenannte Superblocks (SB) zusammen, die für das weitere Vorgehen<br />
als isolierte Codeabschnitte behandelt werden. Ein SB ist zusammengestellt, sobald<br />
eine der folgenden Bedingungen erfüllt ist [15, S.93]:<br />
• Eine plattformspezische Anzahl von Instruktionen wurde erreicht.<br />
• Eine Instruktion für eine bedingte Codeabzweigung wurde erreicht.<br />
• Ein Sprung zu einem Bereich, für den noch kein SB existiert, wurde gefunden.<br />
10
3.2 Valgrind<br />
• Es wurden drei Sprünge zu Adressen gezählt, für die bereits SBs angelegt worden<br />
sind.<br />
Ist der aktuell gewählte Block noch nicht dem Plug-In übergeben worden, übersetzt<br />
Valgrind die nativen Instruktionen in eine eigene Repräsentation, die VEX Intermediate<br />
Representation (VEX IR). Im Plug-In wird ein Callback aufgerufen, dem die VEX IR<br />
übergeben wird. Es ist nun die Aufgabe des Plug-Ins, die VEX IR entsprechend seiner<br />
Aufgabe zu analysieren und zu instrumentieren und die instrumentierte VEX IR an Valgrind<br />
zurückzugeben, das die modizierte Fassung in native Instruktionen übersetzen wird.<br />
Der Vorgang, den Binärcode vollständig in eine Zwischenrepräsentation zu übersetzen und<br />
anschlieÿend in nativen Code zurückzuführen, wird Disassemble-and-Resynthesize (D&R)<br />
genannt.<br />
Dieser Vorgang wird je SB nur einmal ausgeführt. Ein Ausnahmefall ist selbstmodi-<br />
zierender Code: Valgrind erkennt Veränderungen am Code und setzt die SBs dafür neu<br />
zusammen.<br />
Beginn<br />
Wähle SB<br />
Weiterer<br />
SB?<br />
ja<br />
SB behandelt,<br />
aktuell?<br />
nein<br />
nein<br />
Disassemblierung<br />
Instrumentierung<br />
ja<br />
Kompilierung<br />
SB Ausführung<br />
Ende<br />
Abbildung 3.1: Programmausführung in Valgrind<br />
Mit der letzten Instruktion eines SB kehrt der Prozess zurück in einen Codebereich<br />
von Valgrind, in welchem der Code für den nächsten auszuführenden Block nachgeschlagen,<br />
zuvor gegebenenfalls instrumentiert, und dann ausgeführt wird. In Abbildung 3.1<br />
11
3 Erweiterung von FITIn<br />
ist die Ausführungsbehandlung von Valgrind zusammengefasst und als Kontrollussgraph<br />
dargestellt, einzig der Vorgang Instrumentierung involviert hier das Plug-In.<br />
In Valgrind wird die Ausführung verschiedener Threads serialisiert. Ein Blockierungsmechanismus<br />
stellt sicher, dass nur ein Thread zur Zeit SBs ausführen kann. So bendet<br />
sich zusätzlich zur Prozesszeiteinteilung des Betriebssystems innerhalb von Valgrind ein<br />
Zeitverwalter, der vorgibt, zu welchem Zeitpunkt ein Thread die Blockierung zugunsten<br />
eines anderen aufgeben muss. Die Entscheidung wird jeweils nach der Ausführung eines<br />
SB getroen [15, S.95f].<br />
Eine weitere erwähnenswerte Eigenschaft von Valgrind ist die Behandlung von Systemaufrufen.<br />
Ein Systemaufruf wird stets als eigenständiger SB behandelt, sodass der Valgrind<br />
den Benutzercode in einen SB davor und in einen danach zerteilt. Zur Instrumentierungszeit<br />
wird der Systemaufruf von Valgrind durch einen Sprung zu einem eigenen, spezischen<br />
Wrapper ersetzt. Für jede unterstützte Architektur und für jedes unterstütze Betriebssystem<br />
verfügt Valgrind Wissen über jeden einzelnen Systemaufruf, einschlieÿlich der Seiteneekte<br />
auf den Prozessspeicher und auf die Benutzerregister. Der spezische Wrapper ruft<br />
nun entsprechende Callbacks vor und nach der tatsächlichen Ausführung auf. Ein Plug-In<br />
kann für die Ereignisse, von denen es abhängt, Callbacks registrieren, beispielsweise auf<br />
die Ereignisse Ein lesender Registerzugri wird stattnden oder Ein Schreibvorgang auf<br />
den Prozessspeicher hat stattgefunden. Geeignete Übergabeparameter im Callback erlauben<br />
dem Plug-In eine lückenlose Verfolgung von Vorgängen, die im Zusammenhang mit<br />
Systemaufrufen stehen.<br />
Weitere Callbacks können bei Valgrind angemeldet werden, um dem Plug-In zum Beispiel<br />
die Veränderung des Stack-Zeigers oder das Verlassen des Benutzercodes zu signalisieren.<br />
3.2.3 VEX IR<br />
Die von Valgrind genutzte Zwischendarstellung ist unabhängig von der Architektur, auf<br />
welcher Valgrind benutzt wird. VEX IR liegt in Static Single Assignment-Form vor, sodass<br />
jede Codevariable nur ein einziges Mal eine Zuweisung erfahren kann.<br />
Das Plug-In erhält mit dem Instrumentierungs-Callback einen Zeiger auf eine IRSB-<br />
Struktur, die die VEX IR eines SB in Form einer Liste enthält. Die für die Beschreibung<br />
der Implementierung von FITIn essentiellen Datentypen der VEX IR sind:<br />
• IRTemp: Eine VEX IR-Variable. Angaben zu dieser bezüglich des Datentyps in Form<br />
von IRType benden sich in der IRSB-Kontextstruktur IRTypeEnv.<br />
• IRExpr: Ein VEX IR-Wrapper für alle Daten, die einen Ausdruck darstellen. Dazu<br />
gehören als Unterstrukturen innerhalb eines union:<br />
• Const: Ein konstanter, numerischer Wert.<br />
• Get: Der Registerinhalt an der angegebenen Position der Registerschattentabelle.<br />
Diese Tabelle dient dazu, dass Valgrind zur Ausführungszeit Speicherbereiche<br />
bereitstellen kann, die ersatzweise dem Datenaustausch über Register dienen.<br />
• Load: Der Inhalt an einer Speicheradresse.<br />
• RdTmp: Der Wert eines IRTemp.<br />
• {Un,Bin,Tri,Q}op: Ausdrücke, die bis zu vier Argumente erwarten, etwa logische<br />
oder mathematische Operatoren.<br />
• IRStmt: Der Typ einer VEX IR-Instruktion und Datentyp der Instruktionsliste von<br />
IRSB. Wichtige Instruktionen sind:<br />
• Dirty: Der Aufruf einer unreinen Hilfsmethode eine Methode, deren Operation<br />
12
3.2 Valgrind<br />
1 MOV EAX , [ EAX -8]<br />
2 ADD EAX , 1<br />
3 IMUL EAX , [ EAX -8]<br />
1 ------ IMark (0 x8048400 , 3, 0) ----<br />
2 t30 = Add32 (t23 ,0 xFFFFFFF8 : I32 )<br />
3 t32 = LDle : I32 ( t30 )<br />
4 ------ IMark (0 x8048403 , 3, 0) ----<br />
5 t8 = Add32 (t32 ,0 x1 : I32 )<br />
6 PUT (68) = 0 x8048406 : I32<br />
7 ------ IMark (0 x8048406 , 4, 0) ----<br />
8 t33 = Add32 (t23 ,0 xFFFFFFF8 : I32 )<br />
9 t11 = LDle : I32 ( t33 )<br />
10 t13 = Mul32 (t11 , t8 )<br />
Abbildung 3.2: Disassemblierung in Valgrind: x86-Programm (li.), VEX IR (re.)<br />
nicht einzig von den Parametern abhängt , der in einem IRDirty konguriert<br />
wurde.<br />
• Mark: Das Vorkommen eines solchen Markers trennt die Übersetzung zweier<br />
nativer Instruktionen voneinander.<br />
• Put: Das Schreiben eines IRExpr an eine angegebene Position in der Registerschattentabelle.<br />
• Store: Das Schreiben eines IRExpr an eine Speicheradresse.<br />
• WrTmp: Die Zuweisung eines IRExpr zu einem IRTemp.<br />
Eine vollständige Spezikation der Sprache bendet sich im Anhang A.1. Nachdem etwa<br />
das Plug-In die VEX IR eines SB instrumentiert hat, führt Valgrind die instrumentierte<br />
VEX IR auf einer virtuellen CPU aus, die Instrumentierungsfehler der VEX IR ermittelt.<br />
Im Fall eines Fehlers wird die Ausführung abgebrochen.<br />
Ein Beispiel für die textuelle Darstellung der VEX IR und für die Übersetzung von<br />
x86-Instruktionen nach VEX IR ist in Abbildung 3.2 gegeben. Die ersten beiden Parameter<br />
eines jeden IMark geben Auskunft über die ursprüngliche Adresse der Instruktion<br />
und über die Originalgröÿe in Bytes. Im Beispiel der VEX IR in Zeile 6 wird die Adresse<br />
der nachfolgenden Instruktion in die Registerschattentabelle an den Index 68 geschrieben:<br />
Auf der x86-Architektur bendet sich für Valgrind an dieser Stelle der Instruktionszeiger<br />
EIP. Die Instruktion der Zeile 6 ist überüssig, jedoch kennt der Optimierungsalgorithmus<br />
von Valgrind diesen Index nicht als Instruktionszeiger. Nicht einmal die Ursprungsplattform<br />
ist ihm bekannt. In der nicht-optimierten Fassung existiert für jede Instruktion eine<br />
vollständige Beschreibung aller für die VEX IR relevanten Prozessoreekte einschlieÿlich<br />
der Adresse der nächstfolgenden Instruktion, auf x86-Computern gesehen als Zuweisung<br />
der Adresse an EIP. Aus diesem Grund können Redundanzen in der VEX IR auftreten,<br />
für deren Beibehaltung aus der Sicht des Optimierers jedoch im Kontext des nicht vollständig<br />
aufgeführten IRSB eine Notwendigkeit besteht. Im Gegensatz dazu werden in<br />
diesem Beispiel die Flag-Eekte komplett eliminiert, etwa das Aktualisieren des ZF nach<br />
der Addition, da keine nachfolgende VEX IR-Operation auf diesen Flag-Zustand zugreift.<br />
Die Abbildung 3.3 erlaubt einen detaillierten Blick auf die D&R-Technik von Valgrind.<br />
Zu den VEX IR-Ausdrücken IRExpr sei angemerkt, dass einige Typen grundsätzlich eine<br />
beliebige Schachtelungstiefe ermöglichen. Aus Gründen der besseren Optimierbarkeit und<br />
einer einfachen Instrumentierung durch das Plug-In linearisiert Valgrind die VEX IR jedoch<br />
so, dass die Ausdruckstiefe nicht gröÿer als 1 ist. Für tiefer gelegene Ausdrücke wird<br />
für den Elternausdruck E i ein eigenes IRTemp t |env|+1 mittels einer WrTmp-Instruktion geschaen<br />
und E i im Ursprungsknoten durch ein RdTmp auf t |env|+1 ersetzt, solange bis die<br />
Ausdruckstiefe normalisiert ist. env sei dabei die Menge von bereits vergebenen IRTemp-<br />
13
3 Erweiterung von FITIn<br />
Indizes innerhalb des aktuellen IRSB.<br />
Die nach der Instrumentierung stattndenden Schritte entsprechen dabei dem klassischen<br />
Compiler-Konzept ab dem Punkt, ab dem der Code in einer Zwischenrepräsentation<br />
vorliegt: Optimierung der Repräsentation, die Wahl der Instruktionen, die Registerallokation<br />
und die Übersetzung in den letztendlichen Binärcode.<br />
Beginn<br />
Disassemblierung<br />
Linearisierung,<br />
Optimierung<br />
Instrumentierung<br />
Optimierung<br />
Baumbildung<br />
Instruktionswahl<br />
Registerwahl Binärcode Ende<br />
Abbildung 3.3: Disassemble-and-Resynthesize in Valgrind<br />
Abschlieÿend zu diesem Unterpunkt seien die wesentlichen Vorteile, die Valgrind aus<br />
der Sicht eines Plug-In-Entwicklers bietet, aufgeführt:<br />
• Abstraktion der Architektur: Der Entwickler steht nicht unter dem Druck, jede einzelne<br />
CPU-Instruktion detailliert kennen zu müssen, um überhaupt eine einzige Plattform<br />
vollständig unterstützen zu können. Diese Arbeit wird komplett von Valgrind<br />
übernommen und ermöglicht die Plattformunabhängigkeit des Plug-Ins.<br />
• Abstraktion des Betriebssystems: Analog muss dem Entwickler nicht jeder einzelne<br />
Systemaufruf aller unterstützten Betriebssysteme vollständig geläug sein. In Valgrind<br />
wird dieses Problem elegant gelöst, da ausschlieÿlich das Framework dieses<br />
Wissen besitzen muss, sodass der Entwickler sich einzig um bestimmte Ereignisklassen<br />
zu kümmern hat.<br />
• Serialisierung der Parallelität: Es ist nicht erforderlich, als Entwickler etwas über<br />
das Multithreading-Verhalten des Programms zu kennen. Ebenso kann sich der Entwickler<br />
der Tatsache sicher sein, dass zu jedem Zeitpunkt stets nur ein SB ausgeführt<br />
wird und Unterbrechungen eektiv nur zum Zeitpunkt von SB-Übergängen stattnden.<br />
Die Rechnung dafür hat der Anwender in der Form zu bezahlen, dass Valgrind<br />
die Interaktivität des Programms einschränkt und Performancegewinne, die durch<br />
echte Parallelausführung bedingt sind, einstreicht.<br />
Umgekehrt betrachtet, zwingt der erstgenannte Punkt den Entwickler dazu, den Benutzercode<br />
nicht weiter als die übergebene VEX IR betrachten zu dürfen. Die Prämisse,<br />
mit der Hilfe von Valgrind ein plattformspezisches oder -optimiertes Werkzeug entwickeln<br />
zu können, muss grundsätzlich verworfen werden. Dadurch ist ein Plug-In für Valgrind vergleichbar<br />
mit einer Instrumentierungskomponente in einer virtuellen Maschine.<br />
Weiterhin darf man nicht die Behauptung aufstellen, dass ein Plug-In von Valgrind<br />
an sich ein reines DBI-Werkzeug sei: Schlieÿlich wird die von Valgrind übergebene Zwischenrepräsentation<br />
lediglich einmal zu einem transparenten Zeitpunkt vor der erstmaligen<br />
Ausführung instrumentiert, und sie zwingt dadurch den Entwickler dazu, streng zwischen<br />
dem Zeitpunkt der Instrumentierung und dem Zeitpunkt der Ausführung zu unterscheiden.<br />
14
3.3 FITIn<br />
3.3 FITIn<br />
FITIn ist ein von Clemens Terasa entwickeltes Plug-In für Valgrind, das dem Zweck dient,<br />
einen Bit-Flip im Prozessspeicherraum zur Ausführungszeit des Programms vorzunehmen.<br />
Die Motivation hinter diesem Programm ist die Evaluierung von SIHFT-Maÿnahmen [17,<br />
S.21]. In diesem Abschnitt wird beschrieben, wie die ursprüngliche Fassung funktioniert.<br />
An erster Stelle steht die Notwendigkeit, dass dem Benutzer der Programmquellcode,<br />
geschrieben in C oder C++, vorliegt. Eine von FITIn erstellte C-Header-Datei<br />
fitin.h, welche sich im Verzeichnis include der Valgrind-Distribution bendet, muss<br />
mittels #include-Makro eingebunden werden. Dadurch stehen dem Entwickler die zur<br />
Instrumentierung benötigten Makros zur Verfügung: FITIN_MONITOR_VARIABLE(var) und<br />
FITIN_MONITOR_MEMORY(mem, size). Diese Makros stellen sogenannte Valgrind Client Requests<br />
dar: Der Compiler erweitert den Stack um einen Identiziererwert und fünf weitere<br />
Werte in Plattformadressbreite, die Ganzzahlen oder Adressen sein dürfen. Valgrind ist in<br />
der Lage, den Identizierer dem Plug-In zuzuordnen und übergibt diesem mittels Callback<br />
die bis zu fünf Argumente. Im Falle von FITIn werden zwei Argumente verwendet: Startadresse<br />
und Gröÿe des Bereichs, nach dem das Plug-In Ausschau halten soll. Intern wird<br />
jede Beobachtungsauorderung als Monitorable-Struktur in eine Liste aller zu beobachtenden<br />
Bereiche abgelegt.<br />
Im nächsten Schritt muss der Compiler eine Debug-Version des Programms erzeugen,<br />
da eventuell Variablen beobachtet werden, die nicht unmittelbar im Stack-Speicher von<br />
main verwendet oder referenziert werden. Selbst bei einem kleinen Programm durchläuft<br />
Valgrind eine drei- bis vierstellige Anzahl von SBs. Aus Gründen der Performance und der<br />
letztendlichen Benutzungsabsicht ist es nicht erforderlich, SBs, die nicht zum Benutzercode<br />
gehören, etwa die der C-Standardbibliothek, zu analysieren. Um dieses Vorgehen zu<br />
erlauben, muss es Valgrind jedoch möglich sein, die Prozedurzugehörigkeit des aktuellen<br />
SB ermitteln zu können. In einer Debug-Version sind Informationen wie der aktuelle Prozedurname<br />
in das Programm eingebettet. Der Benutzer ist in diesem Fall dazu aufgefordert,<br />
bei Prozeduren, die nicht main sind, mit dem Kommandozeilenargument --fnname= die zu<br />
analysierende Prozedur zu bestimmen oder durch --include= Valgrind mitzuteilen, welche<br />
SBs in Angabe des Quellcodeverzeichnisses zum Benutzercode gehören.<br />
Mit der Auorderung, dass Valgrind das Plug-In FITIn laden soll, kann das Programm<br />
im Sinne von FITIn benutzt werden. Zusätzlich sind die Kommandozeilenargumente<br />
--mod-bit= und --mod-load-time= anzugeben. --mod-load-time=n weist FITIn<br />
an, vor dem n-ten Lesezugri auf den Speicher des zutreenden Monitorable am Bit m aus<br />
--mod-bit=m einen Bit-Flip durchzuführen. Zutreend bezeichnet dabei das Monitorable,<br />
das im Rahmen des für alle Monitorable geltenden Zählers getroen wurde. Mit dem Argument<br />
--inst-limit= kann die Anzahl der insgesamt ausgeführten Instruktionen durch das<br />
Programm ohne die der Instrumentierung, der Ausführungshilfsmethoden von FITIn und<br />
des Valgrind-Kerns begrenzt werden, etwa um nach einem Bit-Flip eine Endlosschleife<br />
zu verhindern.<br />
Möchte der Benutzer lediglich einen Eindruck vom Lesezugrisverhalten des Programms<br />
auf Variablen inner- und auÿerhalb eines Monitorable erlangen, kann er einen<br />
Golden Run-Durchlauf ausführen (Kommandozeilenargument --golden-run=yes), der keinen<br />
Bit-Flip vornehmen wird.<br />
Der Instrumentierungsprozess lässt sich wie folgt beschreiben: Vor jedem IMark fügt<br />
FITIn einen IRDirty-Aufruf namens incrInst ein, dessen Aufgabe es ist, die Anzahl<br />
der originalen Instruktionen zu zählen und bei gesetztem Limit und der Überschreitung<br />
dessen die Ausführung abzubrechen. Stöÿt FITIn in einem relevanten SB auf eine Load-<br />
15
3 Erweiterung von FITIn<br />
1 MOV EBX , [ EBP -8]<br />
2 MOV EAX , EBX<br />
3 ADD EAX , 1<br />
4 IMUL EAX , EBX<br />
1 ------ IMark (0 x8048400 , 3, 0) ----<br />
2 t26 = Add32 (t19 ,0 xFFFFFFF8 : I32 )<br />
3 t28 = LDle : I32 ( t26 )<br />
4 PUT (20) = t28<br />
5 ------ IMark (0 x8048403 , 2, 0) ----<br />
6 ------ IMark (0 x8048405 , 3, 0) ----<br />
7 t8 = Add32 (t28 ,0 x1 : I32 )<br />
8 ------ IMark (0 x8048408 , 3, 0) ----<br />
9 PUT (40) = 0 x27 : I32<br />
10 PUT (44) = t28<br />
11 PUT (48) = t8<br />
12 PUT (52) = 0 x0 : I32<br />
13 t13 = Mul32 (t28 , t8 )<br />
14 PUT (68) = 0 x804840B : I32<br />
Abbildung 3.4: Registerübersetzung in Valgrid: x86-Programm (li.), VEX IR (re.)<br />
Instruktion, fügt es einen weiteren Aufruf vor diesem ein: preLoadHelper. Dieser Methode<br />
wird zur Laufzeit die Ladeadresse der nachfolgenden Instruktion übergeben: Sie iteriert<br />
über alle aktiven Monitorable und prüft dabei, ob die gespeicherten Angabe zur Startadresse<br />
der der Ladeinstruktion entspricht. In diesem Fall wird der globale Zugriszähler<br />
um 1 erhöht. Erreicht der Zähler den Wert von --mod-load-time=, führt die Methode auch<br />
den Bit-Flip auf der Speicheradresse aus.<br />
In jedem Programmdurchlauf kann maximal ein einzelner Bit-Flip vorgenommen werden.<br />
3.4 Registerbehandlung in Valgrind<br />
In Abschnitt 3.2.3 wurde bereits ein Einblick in das Übersetzungsverfahren von Instruktionen<br />
in Valgrind gegeben. In Hinblick auf die Erweiterung von FITIn, Bit-Flips auch auf<br />
Registern vornehmen zu können, muss die VEX IR jedoch noch etwas weiter betrachtet<br />
werden.<br />
Im Beispiel 3.4 nden die x86-Operationen nach dem Laden eines Werts vom Stack vollständig<br />
auf den Registern EAX und EBX statt. Die gezeigten IMark-Abschnitte entsprechen<br />
den Ursprungsinstruktionen, wie es sich durch den Vergleich verschiedener D&R-Schritte<br />
von Valgrind ermitteln lässt (Benutzung des Parameters --trace-flags= auf den entsprechenden<br />
SBs). Dieses Beispiel wird im Folgenden genauer erläutert:<br />
• IMark(0x8048400): Die EBP-versetzte (t19) Ladeadresse wird von t26 referenziert,<br />
der Ladewert anschlieÿend nach t28 geladen und an den Index 20 der Registerschattentabelle<br />
geschrieben, welcher korrekt das Register EBX bezeichnet.<br />
• IMark(0x8048403): Dieser Befehl wurde komplett eliminiert, es ist also nicht erkennbar,<br />
dass ein Register in ein anderes kopiert wurde (EBX nach EAX).<br />
• IMark(0x8048405): Die Addition des in t28 referenzierten Wertes um 1 ndet statt,<br />
jedoch wird das Ergebnis t8 nicht zum Aktualisieren eines Registers benutzt.<br />
• IMark(0x8048408): t28 wird mit t8 multipliziert. Es ndet jedoch erneut keine Registeraktualisierung<br />
(durch t13) statt. Die Indizes 40, 44, 48 und 52 werden mit<br />
Werten belegt, die Valgrind zur Behandlung der Flag-Eekte benutzt.<br />
Anhand des Beispiels 3.4 wird deutlich, dass eine ursprungsgetreue Abbildung der<br />
Registerbenutzung nicht gegeben ist. Es ist also nicht möglich, etwa vor Beginn oder am<br />
Ende eines IMark Instruktionen einzufügen, die, in Erwartung eines konsistenten Zustands<br />
16
3.5 Evaluierung von Ansätzen<br />
1 MOV EBX , [ EAX -8]<br />
2 MOV [ EBP +0 x14 ], EBX<br />
3 LEA ECX , [ EBX +0 x1 ]<br />
4 MOV [ EBP +0 x28 ], 0 x27<br />
5 MOV [ EBP +0 x2C ], EBX<br />
6 MOV [ EBP +0 x30 ], ECX<br />
7 MOV [ EBP +0 x34 ], 0 x0<br />
8 MOV [ EBP +0 x44 ], 0 x804840B<br />
9 MUL EBX , ECX<br />
Abbildung 3.5: Ergebnis der x86-Codesynthese<br />
der Registerschattentabelle, einen Bit-Flip auf dem entsprechenden Eintrag vornehmen<br />
können. Die Abbildung 3.5 verdeutlicht weiterhin, wie sehr die operationelle Semantik des<br />
Ursprungscodes in Abbildung 3.4 verändert wird.<br />
Der Optimierer von Valgrind ermittelt beim Disassemble-Vorgang nicht nur, welche<br />
Operationen im aktuellen IRSB eliminiert werden können, sondern es werden darüber hinaus<br />
alle Paare von PUT-GET-Instruktionen auf demselben Oset entfernt, wenn die Belegung<br />
über den IRSB hinaus nicht relevant ist.<br />
Weitere Konsequenzen auf die operationelle Semantik, die durch den Compiler von<br />
Valgrind bedingt sind:<br />
• Valgrind nimmt anhand des Datenussgraphen eine eigenständige Registerallokation<br />
vor.<br />
• Nicht alle CPU-Instruktionen werden für die Kompilierung unterstützt. So kann Valgrind<br />
auf x86-Hosts lediglich SSE- und SSE2-Instruktionen erzeugen.<br />
• PUT- und GET-Instruktionen werden nicht zwangsläug als Registeroperationen umgesetzt.<br />
Wie in Abb. 3.5 zu erkennen ist, wird der Befehl PUT(20) = t28 zu MOV<br />
[EBP+0x14], EBX übersetzt. Tatsächlich werden auf x86 PUT-Instruktionen bei einer<br />
Quellgröÿe von 4 Byte als MOV-Operationen mit einer Zielspeicheradresse realisiert.<br />
In Bezug auf die Registerbehandlung lässt sich zusammenfassend behaupten: Dem<br />
Plug-In wird die ursprüngliche Registerbelegung vorenthalten, es ist für das Plug-In nicht<br />
abzusehen, welche Register zur Ausführung benutzt werden und auch nicht, zu welchem<br />
Zeitpunkt welche Daten überhaupt in Registern gehalten werden.<br />
Im folgenden Abschnitt wird beleuchtet, wie diese und andere Gegebenheiten sich auf<br />
die Wahl einer Strategie für die Erweiterung von FITIn auswirken.<br />
3.5 Evaluierung von Ansätzen<br />
Unabhängig von technischen Rahmenbedingungen, die etwa ein Framework wie Valgrind<br />
vorgibt, kommt eine zentrale Frage dabei auf, wenn man ein Werkzeug zur Bitmanipulation<br />
von Registern konzipieren möchte: Wie soll es benutzbar sein? Insbesondere vor dem Hintergrund,<br />
dass ein Anwender lediglich eine Bedienungsanleitung durchlesen möchte, und<br />
nicht den Quellcode des Instrumentierers, um dessen Anwendung hinreichend zu verstehen.<br />
In diesem Abschnitt werden zwei mögliche Ansätze vorgestellt, die einem Benutzer<br />
bei der Verwendung von FITIn zusätzlich zur bestehenden Funktionalität dazu dienen<br />
könnten, gezielt Bit-Flips auf Registern vorzunehmen.<br />
17
3 Erweiterung von FITIn<br />
1 unsigned char result = 0 x42 ;<br />
2 asm (" movl $7 , % eax ");<br />
3 asm (" movl %% eax , %0 ":"=m"(<br />
result ));<br />
4 printf ("%d\n" , result );<br />
1 MOV [ ESP +31] , 66<br />
2 MOV EAX , 7<br />
3 MOV BYTE PTR [ ESP +31] , EAX<br />
4 MOVZX EAX , BYTE PTR [ ESP +31]<br />
5 MOVZX EAX , AL<br />
6 MOV DWORD PTR [ ESP +4] , EAX<br />
7 MOV DWORD PTR [ ESP ],<br />
OFFSET FLAT : .LC0<br />
8 CALL printf<br />
Abbildung 3.6: Beispiel für Ansatz I, C-Programm (li.), VEX IR (re.)<br />
Ansatz I: Registerorientierter Bit-Flip<br />
Dem Benutzer steht zusätzlich zu den vorhandenen FITIn-Makros das Makro namens<br />
FITIN_FLIP_REG(reg) zur Verfügung. Das Argument ist eine Nummer, die abhängig von<br />
der Ausführungsplattform speziziert ist und auf den Index der Registerschattentabelle<br />
übersetzt wird. Der Benutzer fügt dieses Makro an der gewünschten Stelle im Quellcode seines<br />
Programms ein und wählt die Nummer des Registers, auf welchem zu diesem Zeitpunkt<br />
ein Bit-Flip vorgenommen werden soll. Zusätzlich wird per Kommandozeilenargument das<br />
Bit gewählt, das zu invertieren ist.<br />
Es sei in Abbildung 3.6 ein Minimalprogramm vorgestellt, das mittels Inline-Assembler<br />
sicherstellt, dass die Konstante 7 zuerst in das Register EAX und anschlieÿend in die Variable<br />
result kopiert wird. Die erwartete Ausgabe ist 7.<br />
Es sei folgendes Szenario gegeben: Der Benutzer möchte vor dem Zurückschreiben im<br />
Register EAX ein Bit umkehren. Dazu fügt er nun das Makro FITIN_FLIP_REG(0), wobei 0<br />
für das Register EAX speziziert sei, zwischen die beiden asm-Instruktionen ein. Abbildung<br />
3.7 zeigt den modizierten C-Code und die resultierenden x86-Instruktionen.<br />
1 unsigned char result = 0 x42 ;<br />
2 asm (" movl $7 , % eax ");<br />
3 FITIN_FLIP_REG (0) ;<br />
4 asm (" movl %% eax , %0 ":"=m"(<br />
result ));<br />
5 printf ("%d\n" , result );<br />
1 MOV BYTE PTR [ ESP +19] , 66<br />
2 MOV EAX , 7<br />
3 MOV DWORD PTR [ ESP +24] ,<br />
1179189257<br />
4 MOV DWORD PTR [ ESP +28] , 0<br />
5 MOV DWORD PTR [ ESP +32] , 0<br />
6 MOV DWORD PTR [ ESP +36] , 0<br />
7 MOV DWORD PTR [ ESP +40] , 0<br />
8 MOV DWORD PTR [ ESP +44] , 0<br />
9 LEA EAX , [ ESP +24]<br />
10 [ ... ]<br />
11 MOV EAX , DWORD PTR [ ESP<br />
+20]<br />
12 MOV BYTE PTR [ esp +19] , EAX<br />
13 [ ... ]<br />
Abbildung 3.7: Ansatz I: Zu x86-Programm (re.) kompilierter C-Code (li.)<br />
Die Zeilen 1, 2 und 12 des Assemblercodes gehören zum bewusst erzeugten Code, die<br />
Zeilen 3 bis 8 zeigen die Stack-Erweiterung, die durch den Valgrind Client Request erzeugt<br />
wird. Das Debakel wird deutlich, wenn man die Zeile 9 betrachtet: Ohne eine Sicherung<br />
von EAX wird der Wert 7 überschrieben, folglich wird ein anderer Wert ausgegeben.<br />
18
3.6 Erweiterung<br />
Ein derartiges Vorgehen erfordert, dass der Anwender ständig zu überprüfen hat, ob<br />
das gewählte Register nicht durch ein Valgrind-Makro überschrieben wurde. Schlieÿlich<br />
ist dem Compiler nicht bekannt, dass das Argument 0 des Makros den Registerinhalt von<br />
EAX bewahren soll. Die ganze Anwendung würde damit schon von vornherein ad absurdum<br />
geführt werden. Dieser Ansatz entpuppt sich als untauglich.<br />
Ansatz II: Bit-Flip nach Schreibzugrien auf Register<br />
Ein anderer Ansatz erfordert keine Modikation des Quellcodes. Stattdessen speziziert<br />
der Benutzer mittels Kommandozeile Register, Bit und Zeitpunkt des Schreibens: Die Erweiterung<br />
zählt die Vorkommen der PUT-Instruktionen in den zugelassenen SBs auf das<br />
angegebene Register und führt bei Übereinstimmung des Zählers mit der Spezikation<br />
nach dem Schreiben auf das Register einen Bit-Flip durch. Es sei in Abbildung 3.8 ein Beispiel<br />
gegeben: Der Benutzer möchte ein beliebiges Bit an Register ECX invertieren, nachdem<br />
die Konstante 15 hineingeschrieben wurde.<br />
1 unsigned char result = 0 x42 ;<br />
2 asm (" movl $15 , % ecx ");<br />
3 asm (" movl %% ecx , %0 ":"=m"(<br />
result ));<br />
4 printf ("%u\n , result )<br />
1 ------ IMark (0 x804846C , 5, 0) ----<br />
2 PUT (12) = 0 xF : I32<br />
3 PUT (68) = 0 x8048471 : I32<br />
4 ------ IMark (0 x8048471 , 4, 0) ----<br />
5 t8 = GET : I32 (24)<br />
6 t7 = Add32 (t8 ,0 x1F : I32 )<br />
7 STle ( t7 ) = 0 xF : I32<br />
8 PUT (68) = 0 x8048475 : I32<br />
Abbildung 3.8: Beispiel für Ansatz II, C-Programm (li.), VEX IR (re.)<br />
Erneut fällt ein Problem ins Auge, dieses Mal durch die Optimierung seitens Valgrind:<br />
Selbst wenn nach der Zeile 2 (in VEX IR) die Registerschattentabelle an Index 12 um ein<br />
Bit korrumpiert wurde, ndet der nächste sinngemäÿe Zugri auf Register ECX in Zeile 7<br />
statt, an welcher der Registerwert zurück an eine Speicheradresse geschrieben wird. Anstatt<br />
des Zugris auf das Register durch einen GET-Ausdruck ist an dieser Stelle die propagierte<br />
Konstante 0xF zu nden. Eine Manipulation der Registerschattentabelle bliebe hier somit<br />
ohne Auswirkung und die Ausgabe würde keine Veränderung erkenntlich machen.<br />
So existiert auch für diesen Ansatz ein Fall, der nicht das gewünschte Ergebnis liefert.<br />
Berücksichtigt man die bisher gewonnenen Erkenntnisse und die Tatsache, dass in Valgrind<br />
einzig der Datenussgraph der IRTemp zuverlässig die Programmsemantik abbildet, so muss<br />
man einen anderen Weg einschlagen. Dieser wird im nächsten Abschnitt vorgestellt.<br />
3.6 Erweiterung<br />
Dieser Abschnitt stellt die Implementierung vor, die FITIn dahingehend verändert, dass<br />
Bit-Flips nicht weiter vor dem Lesen des Arbeitsspeichers vorgenommen werden.<br />
Die vorherigen Analysen haben gezeigt, dass registerfokussierte Ansätze in Valgrind<br />
kein konsistentes Vorgehen für einen Bit-Flip auf einem Register ermöglichen. Die x86-<br />
Plattform verfügt darüber hinaus über mathematische Instruktionen, die Operanden nicht<br />
einmal in einem Register erwarten, sondern mittels Adresse direkt auf dem Prozessspeicher<br />
arbeiten. Dadurch fällt zusätzlich die Annahme, dass jede Ladeoperation der VEX IR<br />
ursprünglich die Kopie von Daten in ein Register darstelle.<br />
Es sei in Abbildung 3.9 ein x86-Beispiel gegeben, das das ursprüngliche FITIn an seine<br />
19
3 Erweiterung von FITIn<br />
Grenzen führt: Es sind drei ganzzahlige Variablen auf dem Stack gegeben, seien sie a, b<br />
und c genannt (für die Adressen EBP-12, EBP-8 und EBP-4). Die abgebildeten Operationen<br />
entsprechen in C-Code b += a; und c *= a;.<br />
1 MOV EAX , [ EBP -12]<br />
2 MOV EDX , [ EBP -8]<br />
3 ADD EDX , EAX<br />
4 MOV [ EBP -8] , EDX<br />
5 MOV EDX , [ EBP -4]<br />
6 IMUL EDX , EAX<br />
7 MOV [ EBP -4] , EDX<br />
1 ------ IMark (0 x80483EC , 1, 0) ----<br />
2 t0 = GET : I32 (28)<br />
3 t24 = GET : I32 (24)<br />
4 t23 = Sub32 (t24 ,0 x4 : I32 )<br />
5 [...]<br />
6 ------ IMark (0 x8048407 , 3, 0) ----<br />
7 t32 = Add32 (t23 ,0 xFFFFFFF4 : I32 )<br />
8 t34 = LDle : I32 ( t32 )<br />
9 [...]<br />
10 t10 = Add32 (t37 , t34 )<br />
11 [...]<br />
12 t17 = Mul32 (t34 , t43 )<br />
Abbildung 3.9: Grenzbeispiel für FITIn, x86-Programm (li.), VEX IR (re.)<br />
Zu erkennen ist, dass das Ergebnis des Ladevorgangs von a in das Register EAX mit dem<br />
IRTemp t34 beschrieben ist. t34 wird in diesem IRSB zwei Mal verwendet: als Argument<br />
der Anweisungen zur Addition Add32(t37,t34) und der Multiplikation Mul32(t34,t43)<br />
(analog stehen t37,t43 für a,b).<br />
Sei die Variable a vom Benutzer im Quellcode mit einem Makro versehen und sei der<br />
Fehlerinjektionszeitpunkt zur Operation b += a; gewählt: preLoadHelper wird vor der<br />
WrTmp-Instruktion von t34 eingefügt und nimmt zur Ausführungszeit den Bit-Flip auf dem<br />
Prozessspeicher vor, sodass alle zukünftigen Ladeoperationen auf dieser Adresse ebenfalls<br />
einen modizierten Wert lesen werden.<br />
Diese Implementierung stellt den Benutzer allerdings vor ein Problem, wenn der Bit-<br />
Flip erst vor Ausführung der Operation c *= a; stattnden soll. Auch wenn das Szenario<br />
gegeben sein kann, dass der Compiler a vor der zweiten Operation tatsächlich neu laden<br />
lässt, ist es wie hier bisher unmöglich, den im Register gehaltenen Wert erst bei erneuter<br />
Verwendung zu manipulieren.<br />
Betrachtet man jedoch die Tatsache, dass ein Plug-In in dieser VEX IR feststellen<br />
kann, wo t34 verwendet wird, so erönet sich eine deutlich präzisere Betrachtung des Variablenzugris.<br />
Zusätzlich verringert es die Notwendigkeit, dass der Benutzer zuerst durch<br />
Betrachtung der Assemblerausgabe oder durch Variieren der Quellcode-Annotation und<br />
des Manipulationszeitpunkts selbst ermitteln muss, ob Daten zu einem Zeitpunkt überhaupt<br />
erneut aus dem Speicher ausgelesen werden. Hier wird die Erweiterung von FITIn<br />
ansetzen.<br />
3.6.1 Betrachtete Operationen<br />
In diesem Unterabschnitt werden die technischen Gegebenheiten mit den Erkenntnissen<br />
über die zukünftige Benutzung von FITIn zusammen betrachtet und es wird erläutert,<br />
welche Arbeit notwendig ist, um eine möglichst allumfassende Lösung zu entwickeln.<br />
Die im nachfolgenden genannten Fälle von VEX IR-Vorkommen und Valgrind-Ereignissen<br />
sind für die Erweiterung von FITIn von Relevanz:<br />
• WrTmp-Instruktionen, die ein neues IRTemp durch einen Load-Ausdruck einführen. Das<br />
Plug-In hat zu erkennen, welche Ladeadresse verwendet wird und zu entscheiden, ob<br />
diese zu einer überwachten Speicheradresse gehört.<br />
20
3.6 Erweiterung<br />
• IRTemp-Variablen, auf die in einem RdTmp-Ausdruck zugegrien wird. Es muss zur<br />
Instrumentierung ermittelbar sein, ob ein verwendetes IRTemp einem Ladeausdruck<br />
entstammt. Zur Ausführungszeit muss erkannt werden, ob dabei auf ein Monitorable<br />
zugegrien wird. Im positiven Fall hat bei Erreichen des Zugriszählers der Bit-Flip<br />
vor der Ausführung zu erfolgen.<br />
• PUT-Instruktionen: Belegungen der Registerschattentabelle können über den SB hinaus<br />
verwendet werden, etwa für Systemaufrufe, aber auch intern für Hilfsmethoden<br />
von Valgrind. FITIn muss feststellen, ob eine solche Instruktion Daten, die von Interesse<br />
sind, in der Tabelle ablegt.<br />
• IRDirty-Aufrufe: Valgrind fügt selbstständig Hilfsaufrufe dieser Art ein, um Instruktionen<br />
zu behandeln, die nicht allein durch VEX IR abbildbar sind. Zu ermitteln ist,<br />
ob diese lesend auf die Registerschattentabelle oder den Prozessspeicher zugreifen.<br />
In einem solchen Fall ist zu prüfen, ob es sich dabei um relevante Datenbereiche<br />
handelt, um zuvor gegebenenfalls einen Bit-Flip auszuführen. Dieser Fall blieb von<br />
FITIn bisher unberücksichtigt.<br />
• Systemaufrufe: Systemaufrufe wurden von FITIn bisher ebenso ignoriert. Das Plug-<br />
In muss bei Valgrind entsprechende Callbacks eintragen, die vor dem Lesen eines<br />
Registers oder des Prozessspeichers den Zugri analysieren, um eventuell eine Fehlerinjektion<br />
vorzunehmen.<br />
3.6.2 Instrumentierungszeit<br />
Die Datenstrukturen, die FITIn bereits enthält, können beibehalten werden: Monitorable<br />
als Repräsentation einer Speicheradresse, die überwacht werden soll aber auch deaktiviert<br />
werden kann und toolData, das die gesamte Konguration von FITIn sowie Datenstrukturen<br />
zur Ausführungszeit enthält, beispielsweise eine Liste aller Monitorable, die vom<br />
Typ XArray ist. Es existiert von dieser Struktur innerhalb von fi_main.c eine globale<br />
Variable mit dem Namen tData. Da andere C-Dateien auf diese allerdings keinen Zugri<br />
haben, wird tData vielen Funktionen der Erweiterung als Argument übergeben.<br />
XArray ist eine Datenstruktur, die von Valgrind bereitgestellt und auch für die Erweiterung<br />
von FITIn benutzt wird. Es handelt sich dabei um eine sortierbare Liste, an deren<br />
Ende Elemente eingefügt werden können. Die Operationen addToXA(XArray*, void*) und<br />
indexXA(XArray*, Word), zum Einfügen und indexbasierten Zugri, liegen in O(1). Eine<br />
schlüsselbasierte Nutzung setzt voraus, dass das XArray nach jeder Veränderung sortiert<br />
wird. Der Suchaufwand von lookupXA(XArray *array, void *key, Word *first, Word<br />
*last) beträgt O(log n).<br />
Die Instrumentierung ndet nach wie vor im Aufruf des Callbacks fi_instrument in<br />
fi_main.c statt. Viele Prozeduren, die für die Erweiterung der Instrumentierung erschaen<br />
wurden, benden sich in der Datei fi_reg.c, die Prototypen der öentlichen Methoden in<br />
fi_reg.h.<br />
Der erste Schritt zur Instrumentierung ist das Finden von WrTmp-Instruktionen, deren<br />
IRExpr vom Typ Load ist. Da die Adresse des Ladevorgangs jedoch nicht zwangsläug zur<br />
Instrumentierung ermittelbar ist und, viel wichtiger, die Menge der Monitorable erst zur<br />
Laufzeit bekannt ist, muss die Untersuchung zur Ausführungszeit erfolgen. Dieses Suchmuster<br />
ist von FITIn bereits gegeben und der eingefügte IRDirty-Aufruf preLoadHelper<br />
bleibt beibehalten. Vor der Erweiterung war die Methode preLoadHelper das Herzstück des<br />
Plug-Ins, da diese das Nachschlagen der Ladeadresse der folgenden Instruktion und auch<br />
den Bit-Flip übernahm. Der Erweiterungsansatz sieht jedoch nicht weiter vor, dass zum<br />
Zeitpunkt unmittelbar vor einem Speicherzugri ein Fehler injiziert wird. Die Fehlerinjek-<br />
21
3 Erweiterung von FITIn<br />
1 typedef struct {<br />
2 IRTemp dest_temp ;<br />
3 IRType ty ;<br />
4 IRExpr * addr ;<br />
5 IRTemp state_list_index ;<br />
6 } LoadData ;<br />
Abbildung 3.10: C-Struktur: LoadData<br />
tion wurde daher entfernt. Eine Änderung betrit jedoch den Rückgabewert: preLoadHelper<br />
liefert ein Ergebnis zurück, das einen Index zum Nachschlagen bestimmter Ladeinformationen<br />
zur Ausführungszeit darstellt. Da Informationen des Ladevorgangs später zum Zeitpunkt<br />
des potentiellen Bit-Flips benötigt werden, muss ein Zeiger auf diese erstellt und<br />
durchgereicht werden. Zur Instrumentierung wird der Rückgabewert als neues IRTemp repräsentiert.<br />
Für die Dauer der Instrumentierung eines IRSB verfügt FITIn nun über eine<br />
XArray-Liste namens loads, welche LoadData-Strukturen beherbergt. LoadData ist in<br />
Abb. 3.10 dargestellt. Die Methode instrument_load, die auch preLoadHelper einfügt,<br />
speichert bei Ladeoperationen nun die Daten 2 bis 4 je LoadData:<br />
1. dest_temp. Die Zielvariable, die gleichzeitig als Schlüssel für alle Einträge der loads-<br />
Liste fungiert. Anhand dieser ist FITIn in der Lage, für jedes RdTmp nachzuschlagen,<br />
ob für das assoziierte IRTemp Ladeinformationen existieren. So kann FITIn erkennen,<br />
ob es sich bei dem verwendeten IRTemp um einen geladenen Wert handelt.<br />
2. ty. Der Datentyp von dest_temp.<br />
3. addr. Der Ausdruck der Ladeadresse von dest_temp.<br />
4. state_list_index. Das IRTemp des Rückgabewerts von preLoadHelper, das für die<br />
Fehlerinjektionsmethoden gebraucht wird.<br />
dest_temp wird von der aufrufenden Methode fi_instrument gesetzt. Grundsätzlich<br />
unterstützt FITIn Ladezugrie auf alle Arten von Datentypen, Ausnahmen betreen jedoch<br />
einige plattformspezische Daten, die FITIn anhand des Typs des Ziel-IRTemp erkennen<br />
kann:<br />
• Ganzzahlen, die gröÿer sind als die Plattformadressbreite. Auf x86-Plattformen ist<br />
dies bei CPU-Erweiterungen der Fall, die eigene, breitere Register verwenden, beispielsweise<br />
SSE.<br />
• IEEE754-Datentypen, üblicherweise float und double, sofern diese als Operanden<br />
für FPU-Instruktionen geladen werden.<br />
• Native SIMD-Datentypen.<br />
Wird ein derartiger Datentyp festgestellt, wird LoadData verworfen. Im Unterabschnitt<br />
3.6.4 werden die Gründe für diesen Umstand näher betrachtet.<br />
Im Zuge der Iteration über alle IRStmt des SB wird nach RdTmp-Ausdrücken Ausschau<br />
gehalten und für jedes Vorkommen die Liste loads nach einem LoadData-Eintrag des referenzierten<br />
IRTemp abgesucht. Eine Ausnahme sind PUT-Instruktionen, da diese eine für<br />
den Benutzer transparente Datenbewegung darstellen und die Registerschattentabelle von<br />
FITIn an anderer Stelle behandelt wird. Valgrind stellt sicher, dass IRTemp nicht vor ihrer<br />
Denition verwendet werden können. Dadurch ist es nicht nötig, mit jedem WrTmp die<br />
Iteration über alle Instruktionen des IRSB von vorn zu beginnen. Ist ein Eintrag gefunden,<br />
fügt FITIn vor dem Zugri auf das IRTemp eine Hilfsmethode ein: fi_reg_flip_or_leave.<br />
fi_reg_flip_or_leave erhält als Argumente, neben einem Zeiger auf die FITIn-<br />
22
3.6 Erweiterung<br />
Konguration, die IRTemp des beobachteten Wertes und des Hilfsindex, der zuvor von<br />
preLoadHelper zurückgegeben wurde. Die Rückgabe dieser Hilfsmethode ist ein neues<br />
IRTemp, das möglicherweise um ein Bit manipuliert worden ist.<br />
Vor Store-Instruktionen wird eine abgewandelte Methode eingefügt: fi_reg_flip_<br />
or_leave_before_store, die zusätzlich die Zielspeicheradresse der Instruktion erhält und<br />
zur Ausführungszeit prüft, ob ein IRTemp kopiert wird.<br />
Der Instrumentierungs- und Ergänzungsalgorithmus von IRTemp lässt sich wie in Abbildung<br />
3.11 ohne Berücksichtigung von Sonderfällen darstellen.<br />
• Sei L = {t i | t i ein IRTemp} die Menge der für FITIn relevanten IRTemp, R =<br />
L × L eine Menge von Ersetzungstupeln (t alt , t neu ).<br />
• Für jedes IRStmt des IRSB:<br />
• IRStmt enthält Load-Ausdruck, Ladeadresse: t a , Ladeergebnis t j :<br />
• Füge zuvor t LD(a) ← preLoadHelper(t a ) ein.<br />
• L ← t j<br />
• IRStmt enthält RdTmp-Ausdruck, Zugri auf t i :<br />
• Beginnend bei t i , wende alle r ∈ R zur Ersetzung an. t k wird hervorgebracht.<br />
• Ersetze t i durch t k in diesem RdTmp-Ausdruck.<br />
• Sei t max das IRTemp mit dem an diesem Punkt gröÿten Index max.<br />
• Füge zuvor t max+1 ← fi_reg_flip_or_leave(t LD(k) , t k ) ein.<br />
• L ← t max+1<br />
• R ← (t k , t max+1 )<br />
• Ersetze t k durch t max+1 in diesem RdTmp-Ausdruck.<br />
Abbildung 3.11: Algorithmus zur Instrumentierung von IRTemp<br />
Ein weiterer Punkt, der zur Instrumentierungszeit vorzunehmen ist, ist die Ermittlung<br />
von PUT-Anweisungen. Dies dient dazu, zur Ausführungszeit Entscheidungen darüber<br />
treen zu können, ob sich in der Registerschattentabelle bei Lesezugri Daten benden,<br />
die unter Umständen zu manipulieren sind. toolData wurde um ein Array reg_temp_<br />
occupancies von IRTemp ergänzt, das konstant die Elementanzahl der Gröÿe der Registerschattentabelle<br />
in Bytes besitzt. Dieses Array dient einzig dazu, dass FITIn markieren<br />
kann, ob zur Instrumentierung interessante Daten an einem Index vorhanden sind<br />
(durch Setzen der Indizes mit den geladenen IRTemp) oder nicht (Wert IRTemp_INVALID).<br />
Trit FITIn auf ein PUT, wird ermittelt, ob es sich bei dem zugewiesenen Wert um ein<br />
IRTemp handelt und ob dieses in loads vorhanden ist. In dem Fall wird eine Hilfsmethode<br />
fi_reg_set_occupancy_origin vor der Instruktion eingefügt, die die nötigen Daten erhält,<br />
um zur Ausführungszeit eigene Schattentabellen bezüglich der Register zu pegen.<br />
Wird keine spezische Herkunft festgestellt und war die Registerschattentabelle zuletzt mit<br />
relevanten Daten belegt, wird ein Dirty-Aufruf namens fi_reg_set_occupancy_origin_<br />
irrelevant eingefügt, der entsprechende Abschnitte der eigenen Tabellen als unbenutzt<br />
markiert.<br />
Damit ist es nun möglich, IRDirty-Aufrufe zu behandeln, die seitens Valgrind gesetzt<br />
werden. Unter x86 ist ein Beispiel die Instruktion CPUID, die in Abhängigkeit des<br />
23
3 Erweiterung von FITIn<br />
1 typedef struct {<br />
2 Bool relevant ;<br />
3 Addr location ;<br />
4 SizeT size ;<br />
5 SizeT full_size ;<br />
6 } LoadState ;<br />
Abbildung 3.12: C-Struktur: LoadState<br />
Registerwerts von EAX unterschiedliche Informationen zur ausführenden CPU in die Benutzerregister<br />
schreibt. Verständlicherweise ist es nicht möglich, diesen höchst hardwareabhängigen<br />
Befehl durch reine VEX IR zu modellieren. Eine Hilfsmethode übernimmt<br />
stattdessen die Aufgabe, indem sie auf die Registerschattentabelle an der Stelle zugreift, auf<br />
die das Register EAX abgebildet ist. Die Aufrufdetails einer Dirty-Hilfsmethode, zu denen<br />
auch die Zugrismuster auf die Registerschattentabelle zählen, sind in IRDirty speziziert.<br />
FITIn kann die Zugrie in Abhängigkeit von der Belegung von reg_temp_occupancies auf<br />
IRTemp zuordnen. Für jedes IRTemp wird ein Aufruf auf die Methode fi_reg_flip_or_<br />
leave_registers_wrap vor der eigentlichen Hilfsmethode eingefügt. Als Argument wird<br />
der vorbereitenden Methode der Zugrisindex für jedes Register übergeben. Die Methode<br />
verfügt über eigene Registerschattentabellen, in welchen weitere Informationen zur Datenbelegung<br />
zu nden sind, um konsistent einen Bit-Flip durchzuführen. Gleichermaÿen wird<br />
für lesende Zugrie auf den Prozessspeicher vorher fi_reg_flip_or_leave_mem_wrap eingefügt.<br />
In Anhang A.2 bendet sich ein umfassenderes Beispiel für einen Vergleich zwischen<br />
der ursprünglichen VEX IR und der VEX IR nach der Instrumentierung durch FITIn.<br />
3.6.3 Ausführungszeit<br />
Eine Unterscheidung zwischen Instrumentierungszeit und Ausführungszeit ermöglicht es,<br />
einen Bit-Flip erst zu einem späteren Ausführungszeitpunkt oder zu einer bestimmten<br />
Rekursionstiefe eines SB vorzunehmen.<br />
Zum Austausch von Daten zwischen einigen Hilfsmethoden, die im Folgenden vorgestellt<br />
werden, ist toolData um eine XArray-Liste namens load_states erweitert worden,<br />
welche Daten vom Typ LoadState (siehe Abbildung 3.12) beheimatet.<br />
Die Methode preLoadHelper veranlasst nach wie vor eine Suche der übergebenen<br />
Adresse innerhalb der Liste von angelegten Monitorable. Bei jedem Aufruf, unabhängig<br />
vom Suchergebnis, wird bereits ein LoadState erstellt und load_states hinzugefügt, um<br />
mindestens zu vermerken, dass an dieser Stelle keine Daten geladen wurden, die einem<br />
aktiven Monitorable entstammen. Liegt ein positives Suchergebnis vor je Startadresse<br />
existiert nur noch maximal ein Monitorable, so führen zum Beispiel häuge Aufrufe einer<br />
Funktion nicht zur Häufung von Listeneinträgen wird die LoadState-Struktur um weitere<br />
Daten ergänzt. Zu den insgesamt erfassten Daten gehören:<br />
ˆ relevant: Dieses Feld zeigt an, ob der geladene Wert einer Adresse entstammt, die<br />
in einem aktiven Monitorable ermittelt wurde.<br />
ˆ location: Die Herkunftsadresse des Werts, sodass dieser später zurückgeschrieben<br />
werden kann.<br />
ˆ size: Die Gröÿe in Bytes, die tatsächlich von der Ladeoperation gelesen wird.<br />
ˆ full_size: Die ursprüngliche Gröÿe in Bytes, die im Monitorable abgelegt wurde.<br />
24
3.6 Erweiterung<br />
load_states ist keine sortierte Liste, Zugrie erfolgen einzig über den ganzzahligen<br />
Index. preLoadHelper besitzt einen Rückgabewert, welcher der Index des eingefügten<br />
LoadState in load_states ist. Durch die Instrumentierung ist bereits sichergestellt, dass<br />
der Rückgabewert als Argument für die Fehlerinjektionsmethoden verwendet wird.<br />
fi_reg_flip_or_leave ist eine dieser beiden Methoden: Neben dem tatsächlichen<br />
Wert, der möglicherweise zu manipulieren ist, wird der Index für load_states mitgegeben,<br />
an welchem sich die dazugehörigen Ladezustandsinformationen benden. Folgende<br />
Bedingungen müssen erfüllt sein, damit ein Bit-Flip durchgeführt werden kann: a) Es ist<br />
noch keine Injektion vorgenommen worden, b) der Datenursprung ist für den Benutzer<br />
relevant, c) es handelt sich nicht um einen Golden Run und d) der Zähler hat mit dem anstehenden<br />
Zugri den benutzerspezizierten Wert erreicht. In diesem Fall ist die Rückgabe<br />
der bitmanipulierte Wert, andernfalls bleibt dieser unverändert. Die Instrumentierung hat<br />
veranlasst, dass dieser Wert nun in jedem Fall von der nachfolgenden Instruktion verwendet<br />
wird.<br />
fi_reg_flip_or_leave_before_store prüft zusätzlich, ob die Adresse der anstehenden<br />
Schreiboperation in den Prozessspeicher unterschiedlich zur Ursprungsadresse des<br />
Werts ist, die in LoadState vermerkt wurde. So kann verhindert werden, dass Zuweisungen<br />
von a, die in C der Anweisung a = a; entsprechen, nicht als Lesezugri auf a betrachtet<br />
werden. Im Unterschied dazu steht das Kopieren des Wertes an eine andere Stelle, also<br />
etwa b = a;, wobei b kein Alias von a ist.<br />
Die Liste load_states wird im Callback auf das Ereignis track_stop_client_code<br />
geleert. Dieses Ereignis ndet immer dann statt, wenn Valgrind die Ausführung eines SB<br />
beendet hat. Auf diese Weise kann sichergestellt werden, dass load_states nicht stetig<br />
wächst, da die eingefügten Hilfsfunktionen nicht über einen einzelnen SB hinaus miteinander<br />
kommunizieren brauchen. Für Zugrie auf die Registerschattentabelle werden zwei<br />
zusätzliche Arrays konstanter Gröÿe in toolData verwendet, sodass nötige Informationen<br />
für einen Bit-Flip auch auÿerhalb eines Benutzer-SB vorgenommen werden können, etwa<br />
im Falle eines Systemaufrufs, der Register ausliest. Diese beiden Arrays heiÿen Addr<br />
*reg_origins und SizeT *reg_load_sizes. Die Speicherallokation für diese kann jedoch<br />
erst mit dem ersten Aufruf von fi_instrument stattnden, da dem Plug-In erst zu diesem<br />
Punkt erlaubt wird, die Gröÿe der Registerschattentabelle in Erfahrung zu bringen.<br />
reg_origins hält für jedes Byte der Registerschattentabelle, sofern im Rahmen der<br />
Instrumentierung erforderlich, die Herkunftsadresse aus dem Prozessspeicher, andernfalls<br />
zeigt 0 an, dass die Belegung zur Zeit nicht relevant ist. reg_load_sizes enthält doppelt<br />
so viele SizeT-Elemente wie die Registerschattentabelle Bytes hat. Dabei benden<br />
sich paarweise nebeneinander für jeden Index der Registerschattentabelle die Ladegröÿe<br />
und die Ursprungsgröÿe im Prozessspeicher. Das Setzen und Invalidieren beider Arrays an<br />
den geforderten Stelle wird von den Hilfsfunktionen fi_reg_set_occupancy_origin und<br />
fi_reg_set_occupancy_origin_irrelevant vorgenommen.<br />
fi_reg_flip_or_leave_registers_wrap (siehe Abschnitt 3.6.2) greift ebenfalls auf<br />
die Schattenwerttabellen von tData zu, da diese fi_reg_flip_or_leave_registers aufruft,<br />
welche nicht von load_states abhängig sein darf.<br />
Betrachtet man die vorgestellten Operationen, die in verschiedenen Anwendungsfällen<br />
zum Einsatz kommen, um einen Bit-Flip durchzuführen, fällt auf, dass diese bisher lediglich<br />
Zwischenwerte berühren. Diese Zwischenwerte werden sich entweder in der Registerschattentabelle,<br />
in einem Register oder im Auslagerungsspeicher wie dem Stack benden. Bisher<br />
scheint der Eekt des ursprünglichen FITIn verloren zu sein, dass Fehlerinjektionen in den<br />
Speicher auch bei bloÿen Lesezugrien persistiert werden. Tatsächlich würde ein Bit-Flip in<br />
einer Variable, die zur Manipulation aus dem Zwischenspeicher gelesen, in der Hilfsfunktion<br />
25
3 Erweiterung von FITIn<br />
behandelt und wieder zurückkopiert wird, nur solange dort verweilen, bis der temporäre<br />
Speicherort überschrieben wird. Bei einem erneutem Laden aus dem Prozessspeicher ist<br />
die Fehlerinjektion also nicht mehr erkennbar. Insbesondere bei Speicherbereichen, die für<br />
die Programmlaufzeit einzig lesend verwendet werden, würde der Benutzer enttäuscht feststellen,<br />
dass Bit-Flips ausschlieÿlich kurzlebig vorkommen können. Die Lösung ndet sich<br />
in Form des Kommandozeilenarguments --persist-flip=yes. Wie bereits erläutert, speichert<br />
FITIn nun stets die Herkunftsadressen geladener Werte, sowohl in load_states als<br />
auch in reg_origins. Ist dieses Kommandozeilenargument gesetzt und ndet nun tatsächlich<br />
ein Bit-Flip statt, kann FITIn ermitteln, woher der manipulierte Wert stammt und<br />
kann dadurch das unangetastete Byte an der richtigen Stelle im Prozessspeicher durch den<br />
manipulierten Temporärwert ersetzen. Ein SEU auf dem Arbeitsspeicher kann dadurch<br />
nun zusätzlich simuliert werden.<br />
Ein weiterer Punkt, der Aufmerksamkeit verdient, ist die Durchführung von Bit-Flips.<br />
Die Spezikation von --mod-bit=n lässt Werte von 0 bis 255 zu. Das Bit der Position 0<br />
ist das erste Bit der kleinsten Byte-Adresse des Werts, sodass der Anwender eine plattformspezische<br />
Byte-Reihenfolge bei der Bit-Wahl berücksichtigen muss. In FITIn wird<br />
zwischen zwei Formen von Bit-Flips unterschieden:<br />
• Primärer Bit-Flip: Ist die Bitposition n innerhalb der Bytes des tatsächlich geladenen<br />
Werts, welcher von der Ursprungsgröÿe im Monitorable abweichen darf, wird ein Bit-<br />
Flip sofort vorgenommen und auf Wunsch auch persistiert. Eine abweichende Gröÿe<br />
kann beispielsweise auftreten, wenn der Benutzer eine struct-Variable beobachtet,<br />
im Programm jedoch nur die erste Komponente gelesen wird, die eine kleinere Gröÿe<br />
als die Gesamtstruktur aufweist.<br />
• Sekundärer Bit-Flip: Wird im vorherigen Fall festgestellt, dass die Bitposition n auÿerhalb<br />
der Ladegröÿe liegt und ist zusätzlich das Zurückschreiben der Fehlerinjektion<br />
in den Speicher aktiviert, wird untersucht, ob der Bit-Flip auf der vollen Variablenbreite<br />
im Prozessspeicher durchgeführt werden kann. In diesem Fall ist von einem<br />
sekundären Bit-Flip die Rede, der erst bei einem ausreichend groÿen Ladevorgang in<br />
Erscheinung treten wird.<br />
Wird eine zu hohe Bitposition gewählt, wird der Injektionsvorgang verworfen. In der<br />
erweiterten Valgrind-Ausgabe (--verbose-Parameter in der Kommandozeile) wird dieser<br />
Fall kenntlich gemacht. So ndet im Unterschied zum ursprünglichen FITIn bei der Bitposition<br />
keine Modulo-Rotation mehr statt, wenn ein zu hoher Wert gewählt wurde.<br />
Es kann der Fall eintreten, dass durch mehrfaches Laden von einer Speicheradresse<br />
bedingt mehrere IRTemp innerhalb eines IRSB denselben Wert repräsentieren. Sollte einer<br />
von diesen IRTemp zur Ausführungszeit manipuliert werden, ndet keine Synchronisation<br />
mit den anderen Werten statt.<br />
Nach wie vor werden Monitorable automatisch deaktiviert, sollte festgestellt werden,<br />
dass die Bewegung des Stack-Zeigers Variablen an entsprechenden Stellen eliminiert hat.<br />
Soll dies zu einem früheren Zeitpunkt geschehen oder möchte der Benutzer nach einem<br />
free() oder aufgrund der Stack-Bewegung sichergehen, dass der Speicherbereich nicht<br />
mehr überwacht wird, hat er manuell das Makro FITIN_UNMONITOR_VARIABLE(var) oder<br />
FITIN_UNMONITOR_MEMORY(addr, size) im Quellcode entsprechend einzufügen.<br />
3.6.4 float, double und weitere Datentypen<br />
Das bereits vorgestellte Konzept der Instrumentierung ist in den unterstützten Datentypen<br />
eingeschränkt: Lediglich Ganzzahlen, die nicht die Plattformadressbreite überschreiten,<br />
können von FITIn behandelt werden. Nicht nur aus technischen, sondern auch aus<br />
26
3.6 Erweiterung<br />
konzeptionellen Gründen, die im Folgenden erläutert werden.<br />
Dirty-Aufrufe können unter Valgrind nur mit ganzzahligen Argumenten aufgerufen<br />
werden, die genau die Gröÿe der Plattformadressbreite haben. Rückgabewerte dürfen kleiner<br />
oder gleich dieser Gröÿe sein. Die Methoden fi_reg_flip_or_leave*, die direkt in<br />
die VEX IR eingebettet werden, können in ihrer implementierten Form also lediglich unter<br />
diesen Bedingungen arbeiten.<br />
Das erweiterte FITIn erkennt, ob es sich bei dem geladenen IRTemp um ein kleineres<br />
Ganzzahlformat handelt, und fügt unmittelbar vor dem Aufruf von fi_reg_flip_or_<br />
leave* eine native VEX IR-Operation ein, die das Ursprungsformat von 8, 16 und gegebenenfalls<br />
32 Bit Breite auf 32 oder 64 Bit vergröÿert. Der Rückgabewert von fi_reg_<br />
flip_or_leave* hat jedoch die Originalgröÿe, sodass keine weiteren Komplikationen mit<br />
dem Ursprungscode auftreten werden, etwa mit Operationen, die exakte Operandengröÿen<br />
erwarten. Beispielsweise würde die Operation t24 = Iop_I16Uto64(t12) t12 auf einer<br />
AMD64-Plattform von 16 auf 64 Bit erweitern, der breitere Wert t24 wird anschlieÿend<br />
als passendes Argument für die Flip-Funktion verwendet.<br />
Für gröÿere und andere Typen ist ein anderer Ansatz nötig. In einem experimentellen<br />
Zweig des Entwicklungscodes wurde ein solcher Ansatz untersucht: Anstatt des Übergebens<br />
des verwendeten Wertes als Argument der Bit-Flip-Methode und der Rückgabe eines<br />
neuen Werts, der in der folgenden Instruktion verwendet wird, wird lediglich der Index für<br />
LoadState dargereicht. LoadState wurde um Felder erweitert, sodass sich der zu manipulierende<br />
Wert im Falle von value != VAL_REGULAR im LoadState benden wird. Diese<br />
Erweiterung ist in Abbildung 3.13 zu sehen.<br />
1 typedef struct {<br />
2 Bool relevant ;<br />
3 Addr location ;<br />
4 SizeT size ;<br />
5 SizeT full_size ;<br />
6 enum {<br />
7 VAL_REGULAR ,<br />
8 VAL_LONG ,<br />
9 VAL_FLOAT ,<br />
10 VAL_DOUBLE<br />
11 } type ;<br />
12 union {<br />
13 ULong _long ;<br />
14 Float _float ;<br />
15 Double _double ;<br />
16 } value ;<br />
17 } LoadState ;<br />
Abbildung 3.13: Erweitertes LoadState<br />
Die Behandlung von Ganzzahlen gröÿer als die Plattformadressbreite, float und<br />
double wurde hinzugefügt: Vor der Verwendung eines solchen Wertes wird statt eines<br />
fi_reg_flip_or_leave* nun fi_reg_flip_or_leave_larger* eingefügt, das neben der<br />
Adresse von tData lediglich einen Zeiger auf LoadState erhält. Die neue Hilfsmethode<br />
extrahiert den Wert aus dem erweiterten LoadState und übergibt ihn den internen Bit-<br />
Flip-Hilfsmethoden. Da der manipulierte Wert allerdings nicht als Rückgabewert in die<br />
VEX IR zurückgeführt werden kann, muss dieser erneut aus dem Speicher geladen werden:<br />
FITIn muss also zusätzlich eine LD-Instruktion einfügen und den neuen IRTemp in die<br />
27
3 Erweiterung von FITIn<br />
1 [...]<br />
2 t24 = F64i {0 x7FF8000000000000 }<br />
3 t204 = DIRTY 1: I1 ::: preLoadHelper {0<br />
x38025b80 }(0 x382A9740 :I32 ,t18 ,0 x4 :I32 ,0<br />
x11007 : I32 )<br />
4 t26 = LDle : F32 ( t18 )<br />
5 DIRTY 1: I1 ::: fi_reg_flip_or_leave_larger [ rp<br />
=2]{0 x38027480 }(0 x382A9740 :I32 , t204 )<br />
6 t205 = LDle : F32 ( t18 )<br />
7 t25 = F32toF64 ( t205 )<br />
8 t27 = GETI (136:8 xI8 )[t21 , -1]<br />
9 t23 = Mux0X (t27 ,t25 , t24 )<br />
10 DIRTY 1: I1 ::: incrInst {0 x38026370 }()<br />
11 ------ IMark (0 x8048531 , 2, 0) ------<br />
12 t35 = F64i {0 x3FF0000000000000 }<br />
13 t36 = GETI (136:8 xI8 )[t21 , -2]<br />
14 t33 = Mux0X (t36 ,t35 , t24 )<br />
15 DIRTY 1: I1 ::: incrInst {0 x38026370 }()<br />
16 ------ IMark (0 x8048533 , 2, 0) ------<br />
17 t40 = AddF64 (0 x0 :I32 ,t23 , t33 )<br />
18 [...]<br />
Abbildung 3.14: Experimentelle Behandlung einer float-Addition<br />
nachfolgende Operation einsetzen.<br />
Dieser Ansatz wurde anhand einer float-Addition getestet. Das Instrumentierungsergebnis<br />
auf einer x86-CPU eines 32 Bit-Programms ist in Abbildung 3.14 gegeben. Zu<br />
erkennen ist die experimentelle Instrumentierung in den Zeilen 5 und 6. Die Instrumentierung<br />
erfolgt allerdings vor einer Erweiterung des Werts auf doppelte Präzision (Zeile 7), die<br />
tatsächliche Addition erst deutlich später (Zeile 17). Im Testszenario konnte der gewünschte<br />
Eekt des Umkehrens des Vorzeichenbits auf diese Art und Weise nicht beobachtet werden,<br />
die Instrumentierung ist also deplatziert. Es ist weiterer Aufwand nötig, einen zuverlässigen<br />
Ansatz zu erarbeiten und zu implementieren, der bei x86-FPU-Instruktionen ein zum bisherigen<br />
Konzept analoges Verhalten ermöglicht, sodass der Benutzer bei der Addition a +=<br />
1.0f; für eine float-Variable a ebenfalls bloÿ einen Lesezugri zu zählen hat. Dieses Dokument<br />
präsentiert lediglich eine Voruntersuchung dieses Falls, sodass die Erweiterung von<br />
FITIn vorerst keine anderen Datentypen als hinreichend kleine Ganzzahlen unterstützt.<br />
Erwähnt sei weiterhin, dass dasselbe Programm nicht einmal für die Nutzung von<br />
x86-FPU-Instruktionen kompiliert werden muss: Eine einfache Addition zweier float wird<br />
von einem GCC v4.7.3 mit dem Ziel -m64 -O0 zu Assemblercode aus SSE-Instruktionen<br />
und -Registern kompiliert. Die resultierende VEX IR unterscheidet sich hier erneut grundlegend<br />
von der in Abbildung 3.14. Dieser Fall ist bei einer Erweiterung von FITIn zu<br />
berücksichtigen.<br />
3.6.5 Systemaufrufe<br />
FITIn benötigt Informationen zu zwei Systemaufrufereignissen: pre_reg_read und pre_<br />
mem_read.<br />
pre_mem_read wird vor dem Aufruf eines Systemaufrufs ausgelöst, der lesend auf den<br />
Prozessspeicher zugreift. Ein Beispiel für so einen Systemaufruf unter Linux ist<br />
settimeofday(2), welcher einen Zeitstempel aus dem Benutzerprozess ausliest. Aus na-<br />
28
3.6 Erweiterung<br />
heliegenden Gründen wird ein Bit-Flip in so einem Fall immer und ausschlieÿlich im Prozessspeicher<br />
vorgenommen. Für jedes einzelne Byte der angegeben Lesebereichsgröÿe wird<br />
geprüft, ob ein Monitorable mit dieser Startadresse existiert, und gegebenenfalls eine Fehlerinjektion<br />
vorgenommen.<br />
Valgrind bietet zusätzlich das Ereignis pre_mem_read_asciiz an, das im Falle des<br />
Lesens eines NULL-terminierten Strings verwendet wird. FITIn ermittelt hier zusätzlich die<br />
String-Länge und übergibt ihn danach der Callback-Methode von pre_mem_read.<br />
Bei Aufrufen, die auf ein Benutzerregister zugreifen, wird das Ereignis pre_reg_read<br />
ausgerufen, so etwa unter Linux im Fall von sendfile(2). FITIn ermittelt alle Belegungen<br />
anhand der Registerschattentabellen reg_origins und reg_load_sizes und prüft für<br />
jede Belegung, ob eine Fehlerinjektion fällig ist. Bei Bedarf wird das geippte Byte auch<br />
im Prozessspeicher an der Herkunftsadresse abgespeichert. Dieses Ereignis ist praktisch<br />
gesehen jedoch mit Schwierigkeiten verbunden:<br />
• Der Benutzer muss über die Implementierung eines solchen Systemaufrufs im Detail<br />
Bescheid wissen, ob und welche Benutzerregister ausgelesen werden, um gegebenenfalls<br />
den Grund des Bit-Flips an dieser Stelle zu verstehen.<br />
• Jeder Systemaufruf wird zuvor von Valgrind in einem eigenen Wrapper behandelt, sodass<br />
zwischen einem Benutzercode-SB und dem Aufruf ein weiterer SB von Valgrind<br />
ausgeführt wird. Der Benutzer muss von diesem Umstand wissen, da die Belegung der<br />
Registerschattentabelle im Wrapper mit Parametern durchgeführt wird, die nun dupliziert<br />
im Stack-Fenster des Wrappers vorliegen. Es ist nötig, dass mittels --fnname=<br />
gezielt dieser Systemaufruf-Wrapper der immer gleichnamig zum tatsächlichen Aufruf<br />
ist angesprochen wird, sodass FITIn die Belegung der Registerschattentabelle<br />
verfolgen kann.<br />
• Weiterhin ist es unumgänglich, vor dem Wrapper-Aufruf die Adresse des Parameters<br />
zu kennen, die dieser im Stack-Fenster des Wrappers einnimmt. Da die einzige Möglichkeit<br />
darin besteht, einen Speicherbereich im Benutzercode mit Hilfe des Makros<br />
FITIN_MONITOR_MEMORY(addr, size) zu spezizieren, muss diese Adresse hier bereits<br />
relativ oder absolut ermittelt sein. Der nötige Aufwand dazu besteht darin, die<br />
Adressendierenz entweder auszuprobieren oder mittels groÿangelegter Analyse des<br />
spezischen SB des Wrappers und des kompilierten Programmcodes diese präzise zu<br />
ermitteln.<br />
Dieses Vorgehen ist mühselig und erfordert vom Benutzer tiefgehende Kenntnisse über<br />
Valgrind. Andererseits dürfte dieser Anwendungsfall für systemunspezische Programme<br />
nur eine schwindend kleine Bedeutung haben.<br />
29
4 Evaluierung von FITIn<br />
In diesem Kapitel wird anhand mehrerer Anwendungsbeispiele untersucht, inwieweit FITIn<br />
in der Lage ist, mit diesen umzugehen. Weiterhin werden Aussagen bezüglich der Performance-<br />
Einbuÿe und des zusätzlichen Speicherbedarfs durch das Plug-In getroen.<br />
4.1 Analyse von Testfällen<br />
Sofern nicht anders angegeben, sind alle Programme mit GCC v4.7.3 als 32 und 64 Bit-<br />
Programm kompiliert (Option -O0) und unter Linux Kernel 3.8 und der GNU C Library<br />
v2.17 getestet worden.<br />
Nichtlineare Ganzzahloperationen<br />
Im ersten Programm (Abb. 4.1) wird eine Variable sizeof(int) * 8 - 1 Mal mittels der<br />
Bitschiebeoperation <br />
2 # include " ../../ include / valgrind /<br />
fi_client .h"<br />
3<br />
4 int main () {<br />
5 unsigned int a = 1, i = 0,<br />
6 until = sizeof (a) * 8 - 1;<br />
7 FITIN_MONITOR_VARIABLE (a);<br />
8<br />
9 for (; i < until ; ++ i) {<br />
10 a = a
4 Evaluierung von FITIn<br />
Wird das Programm nun mit FITIn und den Parametern --mod-bit=0 und<br />
--mod-load-time=15 ausgeführt, lautet der Programmteil der Ausgabe tatsächlich:<br />
$ ../../ bin / valgrind -- tool = fitin --mod - bit =0 --mod - load - time =15 ./ bin / sample<br />
==2941== FITIn , A simple fault injection tool<br />
==2941== Copyright (C) 2013 , and GNU GPL 'd , by Clemens Terasa , Marcel Heing - Becker<br />
==2941== Using Valgrind -3.8.1 and LibVEX ; rerun with -h for copyright info<br />
==2941== Command : ./ bin / sample<br />
==2941==<br />
2147614720<br />
==2941==<br />
[ FITIn ] Totals ( of monitored code blocks ):<br />
[ FITIn ] Overall variable accesses : 126<br />
[ FITIn ] Monitored variable accesses : 15<br />
[ FITIn ] Instructions executed : 99451<br />
FITIn zählt nur so viele Zugrie auf die zu beobachtenden Variablen wie bis einschlieÿlich<br />
zur Ausführung des Bit-Flips stattnden. Daher ergibt sich hier die Anzahl von<br />
15 Zugrien auf a.<br />
Ein Blick in den Ausschnitt der VEX IR des Schleifen-Superblocks des Programms<br />
(Abb. 4.2) bestätigt die erwartete Instrukionswahl seitens des Compilers und die Zuverlässigkeit<br />
des Zählens der Zugrie in diesem Beispiel. Die Variable a wird in Zeile 6 vom Stack<br />
ausgelesen, in Zeile 9 wird die Bit-Flip-Hilfsmethode platziert und deren Rückgabewert für<br />
die Schiebeoperation Shl32 in Zeile 10 benutzt.<br />
1 [...]<br />
2 ------ IMark (0 x804853E , 4, 0) ------<br />
3 t15 = GET : I32 (24)<br />
4 t14 = Add32 (t15 ,0 x18 : I32 )<br />
5 t80 = DIRTY 1: I1 ::: preLoadHelper [ rp =3]{0<br />
x38025b80 }(0 x382A9740 :I32 ,t14 ,0 x4 : I32 )<br />
6 t16 = LDle : I32 ( t14 )<br />
7 DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
8 ------ IMark (0 x8048542 , 2, 0) ------<br />
9 t81 = DIRTY 1: I1 ::: fi_reg_flip_or_leave [ rp<br />
=3]{0 x38026ff0 }(0 x382A9740 :I32 ,t16 , t80 )<br />
10 t1 = Shl32 (t81 ,0 x1 : I8 )<br />
11 PUT (68) = 0 x8048544 : I32<br />
12 DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
13 [...]<br />
Abbildung 4.2: Beispielprogramm I, VEX IR<br />
Registergebundene Operationen I<br />
Zum Untersuchen der Funktionalität von FITIn mit registergebundenen Operationen ist<br />
das Schreiben plattformspezischen Codes nötig, sodass der generierte Binärcode im kritischen<br />
Teil nicht der Willkür des Compiler-Programms, dessen Version oder dessen Kon-<br />
gurationsparametern unterliegt.<br />
In dem Beispiel in Abbildung 4.3 wird eine x86-spezische Schleifeninstruktion verwendet:<br />
LOOP dest. Mit jedem Aufruf dieser Instruktion wird der Inhalt des Registers ECX<br />
um 1 reduziert. Sollte der Registerinhalt dadurch ungleich Null sein, springt der Instruktionszeiger<br />
zu dest, andernfalls wird mit der nachfolgenden Instruktion fortgefahren.<br />
32
4.1 Analyse von Testfällen<br />
Die Variable c wird mit dem Wert 21 initialisiert und anschlieÿend nach ECX geladen.<br />
In jedem Schleifendurchlauf wird der Variablenwert von result um Eins erhöht.<br />
1 # include < stdio .h ><br />
2 # include " ../../ include / valgrind /<br />
fi_client .h"<br />
3<br />
4 int main () {<br />
5 int c = 21 , result = 0;<br />
6 FITIN_MONITOR_VARIABLE (c);<br />
7<br />
8 asm (" movl %0 , %% ecx " :: "m"(c):" ecx ");<br />
9 asm (" start :");<br />
10 asm (" incl %0 ":"=m"( result ));<br />
11 asm (" loop start ");<br />
12<br />
13 printf ("%d\n" , result );<br />
14 return 0;<br />
15 }<br />
Abbildung 4.3: Beispielprogramm II<br />
Der Benutzer möchte beim 10. Lesezugri auf c, der zwangsläug auf einem Register<br />
stattnden wird, am Bit der Stelle 2 2 einen Bit-Flip durchführen. Bei diesem Zugri<br />
wird c den Wert 11 haben, mit einem Flip unmittelbar zuvor jedoch 15 und nach der<br />
Dekrementierung damit 14. Folglich kann in der Ausgabe von result nun 24 erwartet<br />
werden.<br />
Das Programm gibt entgegen der ersten Erwartung jedoch die Zahl 21 aus.<br />
$ ../../ bin / valgrind -- tool = fitin --mod - load - time =10 --mod - bit =2 ./ bin / sample<br />
==3011== FITIn , A simple fault injection tool<br />
==3011== Copyright (C) 2013 , and GNU GPL 'd , by Clemens Terasa , Marcel Heing - Becker<br />
==3011== Using Valgrind -3.8.1 and LibVEX ; rerun with -h for copyright info<br />
==3011== Command : ./ bin / sample<br />
==3011==<br />
21<br />
==3011==<br />
[ FITIn ] Totals ( of monitored code blocks ):<br />
[ FITIn ] Overall variable accesses : 23<br />
[ FITIn ] Monitored variable accesses : 1<br />
[ FITIn ] Instructions executed : 111327<br />
Eine erster Blick verrät: Ein einziger nach FITIn gemessener Zugri auf c bedeutet,<br />
dass ein 10. Lesezugri nie stattgefunden hat. Dieser Fall bedarf einer ausführlicheren<br />
Analyse.<br />
In Abbildung 4.4 sei die VEX IR des ersten relevanten main-SB nach der Instrumentierung<br />
durch FITIn gegeben. In den Zeilen 3-5 ist erkennbar, dass c vom Stack eingelesen<br />
wird. Der erste Zugri auf den Wert in t20 erfolgte in der Subtraktion in Zeile 9, FITIn hat<br />
jedoch zuvor fi_reg_flip_or_leave eingefügt und ersetzte damit t20 durch t45. Auällig<br />
ist jedoch, dass der dekrementierte Wert t28 in Zeile 10 in die Registerschattentabelle<br />
an Oset 12 geschrieben wird, auf welches unter x86 das Register ECX abgebildet ist. Da<br />
laut FITIn bloÿ ein einziger Zugri auf c erfolgte und Valgrind PUT(12) beibehielt, liegt es<br />
nahe, dass die weitere Programmlogik in einem anderen SB ausgeführt wird. Tatsächlich<br />
folgte auf der Testkonguration diesem SB mit der Nummer 1435 ein weiterer main-SB<br />
33
4 Evaluierung von FITIn<br />
1 [...]<br />
2 ------ IMark (0 x8048534 , 4, 0) ------<br />
3 t18 = Add32 (t13 ,0 x1C : I32 )<br />
4 t42 = DIRTY 1: I1 ::: preLoadHelper [ rp<br />
=3]{0 x38025b80 }(0 x382A9740 :I32 ,t18 ,0<br />
x4 : I32 )<br />
5 t20 = LDle : I32 ( t18 )<br />
6 [...]<br />
7 ------ IMark (0 x804853C , 2, 0) ------<br />
8 t45 = DIRTY 1: I1 :::<br />
fi_reg_flip_or_leave [ rp =3]{0 x38026ff0<br />
}(0 x382A9740 : I32 , t20 , t42 )<br />
9 t28 = Sub32 (t45 ,0 x1 : I32 )<br />
10 PUT (12) = t28<br />
11 [...]<br />
Abbildung 4.4: Beispielprogramm II, VEX IR (main-SB 1)<br />
mit Nummer 1436.<br />
In der VEX IR des nachfolgenden SB (Abbildung 4.5) ndet sich tatsächlich jede<br />
weitere Subtraktion des Registerinhalts um 1. Es liegt also ein Fall vor, in welchem Valgrind<br />
Werte zwischen verschiedenen SBs über die Registerschattentabelle weitergibt.<br />
1 [...]<br />
2 ------ IMark (0 x804853C , 2, 0) ------<br />
3 t17 = GET : I32 (12)<br />
4 t16 = Sub32 (t17 ,0 x1 : I32 )<br />
5 PUT (12) = t16<br />
6 [...]<br />
Abbildung 4.5: Beispielprogramm II, VEX IR (main-SB 2)<br />
Grundsätzlich erkennt FITIn zur Instrumentierungszeit, ob ein geladener Wert für ein<br />
PUT verwendet wird und kann im nächsten zu instrumentierenden SB anhand der Belegung<br />
von reg_temp_occupancies erkennen, ob ein GET-Ausdruck einen potentiell beobachteten<br />
Wert zurückliefern wird, um diesen gegebenenfalls der loads-Liste hinzuzufügen.<br />
Durch die erste Subtraktion in Abb. 4.4, Zeile 9 entsteht jedoch ein neuer, für FITIn<br />
anonymer Ausdruck, der beim Kopieren in die Registerschattentabelle nicht weiter beachtet<br />
wird. An diesem Verfahren ist bei FITIn auch festzuhalten, da Valgrind einige Instruktionen<br />
in mehrere elementare VEX IR-Instruktionen übersetzen wird (deutlich zu erkennen in<br />
Abb. 3.14) und dieses Zerlegen für den Benutzer transparent bleiben soll.<br />
In diesem Beispiel handelt es sich daher um einen von FITIn nicht behandelbaren Fall,<br />
welchen der Benutzer hier allerdings noch an der zu niedrigen Zugriszahl erkennen kann.<br />
Registergebundene Operationen II<br />
In einem weiteren Beispiel (Abb. 4.6) wird die Registerfähigkeit von FITIn nun durch<br />
explizite Registerverwendung der Instruktionen demonstriert. Innerhalb einer Schleife wird<br />
eine Operation ausgeführt, deren reines C-Äquivalent b = (b + a) * a; ist. Die erwartete<br />
34
4.1 Analyse von Testfällen<br />
1 # include < stdio .h ><br />
2 # include " ../../ include / valgrind /<br />
fi_client .h"<br />
3<br />
4 int main () {<br />
5 int a = 1, b = 0, i = 0;<br />
6 FITIN_MONITOR_VARIABLE (a);<br />
7<br />
8 for (; i < 10; ++ i) {<br />
9 asm (" movl %0 , %% eax " :: "m"(a):"<br />
eax ");<br />
10 asm (" movl %0 , %% ebx " :: "m"(b):"<br />
ebx ");<br />
11 asm (" addl %eax , % ebx ");<br />
12 asm (" imull %eax , % ebx ");<br />
13 asm (" movl %% ebx , %0 ":"=m"(b));<br />
14 }<br />
15 printf ("%d %d\n" , a , b);<br />
16 return 0;<br />
17 }<br />
Abbildung 4.6: Beispielprogramm III<br />
Ausgabe lautet: 1 10.<br />
Es werden zwei Durchläufe in FITIn vorgenommen: Zuerst ist ein Bit-Flip an der<br />
Stelle 2 1 vor dem 10. Zugri auf a auszuführen. Im zweiten Durchlauf wird der Bit-Flip in<br />
a zusätzlich an die Ursprungsadresse zurückgeschrieben.<br />
Zu erwarten ist, dass Zugrie mit geradem Zählerstand vor der Ausführung der Multiplikation<br />
stattnden, der 10. Zugri wird also im 5. Schleifendurchlauf gezählt. Der Wert<br />
von b beträgt zuvor 5, die Multiplikation wird nun jedoch nicht mit 1 erfolgen, sondern<br />
nach dem Bit-Flip mit 3. Da b in jedem Durchlauf erneut geladen und auch zurückgeschrieben<br />
wird, beträgt der Wert nun 15, der modizierte Wert von a geht aber in der<br />
folgenden Iteration durch das erneute Laden verloren. Das erwartete Endergebnis nach<br />
den verbleibenden fünf Iterationen ist also 1 20. Die Vorhersage bewahrheitet sich:<br />
$ ../../ bin / valgrind -- tool = fitin --mod - load - time =10 --mod - bit =1 ./ bin / sample<br />
==2048== FITIn , A simple fault injection tool<br />
==2048== Copyright (C) 2013 , and GNU GPL 'd , by Clemens Terasa , Marcel Heing - Becker<br />
==2048== Using Valgrind -3.8.1 and LibVEX ; rerun with -h for copyright info<br />
==2048== Command : ./ bin / sample<br />
==2048==<br />
1 20<br />
==2048==<br />
[ FITIn ] Totals ( of monitored code blocks ):<br />
[ FITIn ] Overall variable accesses : 53<br />
[ FITIn ] Monitored variable accesses : 10<br />
[ FITIn ] Instructions executed : 111711<br />
Im zweiten Versuch wird der Bit-Flip nun dank --persist-flip=yes an die Ursprungsadresse<br />
von a zurückgeschrieben, sodass auch in allen nachfolgenden Iterationen mit<br />
3 addiert und multipliziert wird. Es ergibt sich folgende Rechnung für den Endwert von b: b<br />
= ((((((((((15 + 3) * 3) + 3) * 3) + 3) * 3) + 3) * 3) + 3) * 3);. Die erwartete<br />
Ausgabe ist also 3 4734. Auch diese Vermutung bestätigt sich:<br />
35
4 Evaluierung von FITIn<br />
$ ../../ bin / valgrind -- tool = fitin --mod - load - time =10 --mod - bit =1 -- persist - flip = yes<br />
./ bin / sample<br />
==2044== FITIn , A simple fault injection tool<br />
==2044== Copyright (C) 2013 , and GNU GPL 'd , by Clemens Terasa , Marcel Heing - Becker<br />
==2044== Using Valgrind -3.8.1 and LibVEX ; rerun with -h for copyright info<br />
==2044== Command : ./ bin / sample<br />
==2044==<br />
3 4734<br />
==2044==<br />
[ FITIn ] Totals ( of monitored code blocks ):<br />
[ FITIn ] Overall variable accesses : 53<br />
[ FITIn ] Monitored variable accesses : 10<br />
[ FITIn ] Instructions executed : 111745<br />
Tatsächlich funktioniert die Instrumentierung (Abb. 4.7) wie erwünscht: Das geladene<br />
IRTemp t22 wird nacheinander durch t88 und t90 ersetzt. In dieser Abbildung nicht zu<br />
erkennen, jedoch beobachtet, ist die Duplikation der VEX IR eines ganzen SB innerhalb<br />
seiner selbst, in welcher sich lediglich die vergebenen IRTemp voneinander unterscheiden.<br />
Der Optimierer von Valgrind betreibt auf diese Weise ein partielles Aufrollen der Schleife.<br />
1 [...]<br />
2 ------ IMark (0 x8048547 , 2, 0) ------<br />
3 t88 = DIRTY 1: I1 :::<br />
fi_reg_flip_or_leave [ rp =3]{0 x38026ff0<br />
}(0 x382A9740 : I32 , t22 , t87 )<br />
4 t89 = DIRTY 1: I1 :::<br />
fi_reg_flip_or_leave [ rp =3]{0 x38026ff0<br />
}(0 x382A9740 : I32 , t19 , t86 )<br />
5 t2 = Add32 ( t88 , t89 )<br />
6 DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
7 ------ IMark (0 x8048549 , 3, 0) ------<br />
8 t90 = DIRTY 1: I1 :::<br />
fi_reg_flip_or_leave [ rp =3]{0 x38026ff0<br />
}(0 x382A9740 : I32 , t89 , t86 )<br />
9 t7 = Mul32 ( t90 , t2 )<br />
10 [...]<br />
Abbildung 4.7: Beispielprogramm III, VEX IR<br />
Subroutinen<br />
Dieses Beispiel (Abb. 4.8) untersucht die Fähigkeit von FITIn, mit Subroutinen umgehen<br />
zu können. Eine Methode just_do_it erhöht eine Variable mit konstanter Initialisierung<br />
um Eins und gibt diese zurück. Hier ist erneut der Grund hervorzuheben, der<br />
eine Debug-Version des Programms nötig macht: FITIn muss darüber informiert werden,<br />
dass abweichend von main nun die SBs der Methode just_do_it von Relevanz sind. Dazu<br />
wird die Option --fnname=just_do_it gesetzt. So ist Valgrind dazu in der Lage, die<br />
Debug-Informationen auszulesen und auf diese Weise dem SB seinen Ursprungsbezeichner<br />
zuzuordnen, welchen FITIn zur Filterung benutzt. Ohne die Debug-Informationen könnte<br />
dieses Beispiel nicht getestet werden.<br />
In dem vorgestellten Fall möchte der Benutzer den 12. Lesezugri auf a in just_do_it<br />
manipuliert sehen, um diesen nach der Rückgabe auch betrachten zu können.<br />
Ohne Eingri ist die Ausgabe ganz oensichtlich 2. Eine Manipulation bei besagter<br />
36
4.1 Analyse von Testfällen<br />
1 # include < stdio .h ><br />
2 # include " ../../ include / valgrind /<br />
fi_client .h"<br />
3<br />
4 int just_do_it () {<br />
5 int a = 1;<br />
6 FITIN_MONITOR_VARIABLE (a);<br />
7 a ++;<br />
8 return a;<br />
9 }<br />
10 int main () {<br />
11 int i = 0, result = 0;<br />
12<br />
13 for (; i < 12; ++ i)<br />
14 result = just_do_it () ;<br />
15 printf ("%d\n" , result );<br />
16 return 0;<br />
17 }<br />
Abbildung 4.8: Beispielprogramm IV<br />
Zugriszeit an Bit 2 2 hätte die Ausgabe 5 zur Folge. Auf einen Auszug der Ausgabe sei<br />
in diesem Beispiel verzichtet. Wichtiger ist hier die Beobachtung, dass dieser Testfall auf<br />
zwei Plattformen zwei verschiedene Ergebnisse hervorbrachte.<br />
Auf der am Anfang des Abschnitts erwähnten Linux-Konguration erfolgte tatsächlich<br />
die erwartete Ausgabe. Auf einem Mac-Computer mit Mac OS X v10.8.3 und dem GCC,<br />
der Xcode v4.6.2 beiliegt, funktionierte dieses Beispiel jedoch nicht! Im Golden Run-Modus<br />
wurden zudem deutlich mehr Lesezugrie gezählt als erwartet.<br />
Zur Analyse des Scheiterns auf dem Mac sei in Abbildung 4.9 der Ausschnitt des<br />
Assemblercodes gezeigt, der für die Methode just_do_it generiert wurde. In der Erwartung,<br />
dass hinreichend kleine Rückgabewerte auf x86-Architekturen für gewöhnlich im<br />
EAX-Register zurückgegeben werden, entsprechen die Zeilen 2 bis 5 den Instruktionen, die<br />
man aus Sicht des Quellcodes annehmen kann. Die darauf folgenden Zeilen führen zu ober-<br />
ächlich unergründlichem Kopieren des Rückgabewertes auf den verbleibenden Platz auf<br />
dem Stack. Maÿgeblich für die Abweichung in der Zählung der Zugrie ist jedoch die Zeile<br />
6: Das bloÿe Laden der Variablen a in Zeile 5 wird nicht als Zugri gezählt, es ist angesichts<br />
des return a; auch zu erwarten. Das Kopieren eines geladenen Wertes an eine<br />
andere Adresse jedoch wie in Zeile 6 zählt als vollzogener Lesezugri. So generiert der<br />
Compiler Assemblercode, der dazu führt, dass nicht bloÿ ein, sondern zwei Lesezugrie je<br />
just_do_it-Aufruf erfolgen. In diesem Wissen könnte der Benutzer die Zählung anpassen,<br />
was für gewöhnlich jedoch voraussetzt, dass dieser den generierten Assemblercode vor der<br />
Verwendung in Augenschein genommen hat.<br />
Weitere Testfälle<br />
Die Erweiterung von FITIn zieht weitere Testfälle nach sich: Aufrufe von register- oder<br />
speicherlesenden Systemaufrufen, die Behandlung von Hilfsmethoden von Valgrind oder<br />
den Umgang mit kleineren Datentypen.<br />
An dieser Stelle wird jedoch nicht weiter auf diese eingegangen. FITIn wurde mit einer<br />
Sammlung von Minimaltests vergleichbar mit den in diesem Abschnitt vorgestellten <br />
37
4 Evaluierung von FITIn<br />
1 [ ... ]<br />
2 MOV EAX , [ EBP -16]<br />
3 ADD EAX , 1<br />
4 MOV [ EBP -16] , EAX<br />
5 MOV EAX , [ EBP -16]<br />
6 MOV [ EBP -8] , EAX<br />
7 MOV EAX , [ EBP -8]<br />
8 MOV [ EBP -4] , EAX<br />
9 MOV EAX , [ EBP -4]<br />
10 [ ... ]<br />
11 RET<br />
Abbildung 4.9: Beispielprogramm IV, x86-Assemblercode<br />
erweitert, um die Sicherheit im Umgang mit solchen Fällen prinzipiell zu zeigen. Weiterhin<br />
dienen diese auch als Regressionstests.<br />
Diese Testsammlung ist der Distribution des Quellcodes von FITIn beigelegt [18]. Auf<br />
einem x86-Computer mit Linux als Betriebssystem und einem modernen C-Compiler wird<br />
das Bestehen aller Testfälle erwartet.<br />
4.2 FlipSafe<br />
FITIn wurde primär dazu konzipiert, SIHFT-Techniken zu evaluieren. In der Erstveröentlichung<br />
von FITIn wurde dazu die SIHFT-Bibliothek FlipSafe [19] auf die Probe gestellt<br />
[17, S.33]. Dieses Experiment wird nun mit der erweiterten Version von FITIn wiederholt.<br />
FlipSafe ist eine in C++ geschriebene Template-Bibliothek, die verschiedene fehlertolerante<br />
Implementierungen des Datentyps bool bereitstellt: BCBOOL, CSBOOL, EPBOOL, RMBOOL<br />
und SHBOOL.<br />
Betrachtet wird in diesem Experiment eine C-Portierung des Dhrystone-Tests [20].<br />
Dhrystone zeichnet sich dadurch aus, dass auf Gleitkomma-Datentypen und, bis auf die<br />
Standardausgabe, auf Ein- und Ausgabefunktionen verzichtet wird. Die von C. Terasa<br />
verwendete Fassung von Dhrystone wurde jedoch einigen Modikationen unterworfen [18]:<br />
• Es wird eine kongurierbare Anzahl (Standard: 50) von Ausführungsiterationen statt<br />
einer zur Laufzeit dynamischen gewählt.<br />
• Makro-Schalter wurden eingefügt, die die Verwendung der verschiedenen FlipSafe-<br />
Datentypen ermöglichen.<br />
• Die prozessinterne Zeitmessung der Ausführung wurde entfernt.<br />
• Die Dhrystone-Prozedur Proc_4 wurde modiziert, um eine Fehlererkennung zu ermöglichen.<br />
Mit dem ursprünglichen FITIn wurden zwei Experimente durchgeführt: Zum einen ein<br />
Test, der zu zufällig gewählten Ladezeitpunkten zufällig gewählte Bits manipuliert. Zum<br />
anderen ein Test, der für jeden einzelnen Ladezeitpunkt jedes einzelne Bit umkehrt.<br />
Beide Experimente werden nun wiederholt, die Durchführungsprogramme wurden der<br />
veränderten Ausgabe des erweiterten FITIn jedoch angepasst. 1 Zwischen beiden Versio-<br />
1 Die Experimente mussten jedoch mit einer 32 Bit-Version des Benchmarks durchgeführt werden. Der<br />
Grund dafür liegt in einem ungeklärten Fehler, der Valgrind dazu verleitet, während der Ausführung des<br />
erweiterten FITIn für einen SB inmitten des Dhrystone-Programms fehlerhafte Plattform-Instruktionen<br />
38
4.2 FlipSafe<br />
nen nden unverändert 152 Lesezugrie beziehungsweise Ladeoperationen im Dhrystone-<br />
Programm auf zu beobachtenden Speicherbereichen im Golden Run statt. Obwohl der<br />
Fehlerinjektionszeitpunkt mit der neuen FITIn-Version abweichen wird, lässt sich erkennen,<br />
dass der Compiler auf x86 Binärcode generiert hat, der betreende Speicherbereiche<br />
vor jeder Verwendung erneut von dort ausliest.<br />
Für beide Experimente sind zuerst verschiedene Versionen von Dhrystone zu kompilieren:<br />
Eine Fassung für die ursprünglich verwendeten, anfälligen Datentypen, und jeweils<br />
eine Fassung für jeden FlipSafe-Datentyp, die ausgewählte Variablen durch die eigene,<br />
fehlertolerante Implementierung ersetzt.<br />
Im zufallsgesteuerten Experiment wird zuerst ein Golden Run von FITIn ausgeführt,<br />
der die Obergrenze der Zugrie auf Variablen ermittelt, die in einem Monitorable abgelegt<br />
sind. Anschlieÿend werden 100 Durchläufe ausgeführt: In jedem einzelnen Durchlauf werden<br />
Ladezeitpunkt innerhalb der Obergrenze und Bit zufällig gewählt, wobei nach wie vor<br />
derselbe Seed-Wert verwendet wird. Als nächstes wird für jeden FlipSafe-Typen ein Golden<br />
Run ausgeführt, der die Anzahl der Zugrie festhält. In jedem Durchlauf folgt auf die<br />
Ausführung der ungeschützten Programmversion die Ausführung aller anderen, gesicherten<br />
Programmfassungen. Hierbei werden die Zugriszeitpunkte versetzt, wenn die Implementierung<br />
im Golden Run im Vergleich zum Original mehr Zugrie benötigt. Nachdem das<br />
Experiment durchgeführt wurde, erlaubt ein weiteres Programm die Auswertung aller gespeicherten<br />
Ausführungsausgaben: Dhrystone verfügt über eine Prüfung aller Endwerte der<br />
Berechnung und eine Ausgabe, die belegt, ob ein Wert in Ordnung ist oder abweicht. Das<br />
Auswertungsprogramm zählt die Anzahl der Abweichungen, gruppiert nach verwendeter<br />
Version, zusammen.<br />
Das zweite Experiment nimmt deutlich mehr Ausführungen vor, da FITIn für jeden<br />
Lesezugri und jedes Bit gestartet wird. Auch hier werden am Ende die Ergebnisse aller<br />
Programmversionen mit dem Golden Run der jeweiligen Version verglichen und die Anzahl<br />
der aufgespürten Fehler zusammengezählt.<br />
Der Vergleich beider FITIn-Versionen in Bezug auf das erste Experiment ist in Abbildung<br />
4.10 zu sehen: Es gibt keinerlei Unterschiede zu verzeichnen. Trotz geänderter<br />
Zeitpunkte der Fehlerinjektion ist EBPOOL nach wie vor nicht in der Lage, alle Fälle von<br />
Bit-Flips abzufangen. Die Option --persist-flip=yes wirkt sich nicht auf die Zahl der<br />
erkannten Fehler aus.<br />
Für die vollständige Abdeckung aller möglichen Bitfehler ergibt sich ein etwas abweichendes<br />
Bild (Abb. 4.11). Nach wie vor werden in 182 Fällen des Programms ohne SIHFT-<br />
Techniken Fehler in der Ausgabe erkannt. Einzig die EPBOOL-Implementierung weist in<br />
etwa doppelt so vielen Fällen Fehler auf. Es ist also anzunehmen, dass die neue FITIn-<br />
Version diesem SIHFT-Datentyp noch deutlich mehr Schwierigkeiten bereitet. Erneut hat<br />
die Option, manipulierte Temporärwerte an ihre Ursprungsadresse zu übernehmen, keine<br />
Auswirkung auf die Anzahl von Fehlern. Eine qualitative Untersuchung der Fehler würde<br />
möglicherweise jedoch einen Unterschied deutlich machen, wie in Abschnitt 4.1 gezeigt<br />
werden konnte.<br />
zu generieren, die zu einem Segmentation Fault führen. Dieser Fehler trat nur bei diesem 64 Bit-<br />
Programm und der Verwendung des Parameters --include= auf, nicht<br />
jedoch isoliert unter der Verwendung von --fnname=.<br />
39
4 Evaluierung von FITIn<br />
13 13<br />
Entdeckte Fehler<br />
10<br />
5<br />
2<br />
2<br />
0<br />
0 0 0 0<br />
0 0 0 0<br />
Ungeschützt<br />
BCBOOL<br />
CSBOOL<br />
EPBOOL<br />
RMBOOL<br />
SHBOOL<br />
FITIn (ursprg.)<br />
FITIn (mit/ohne --persist-flip)<br />
Abbildung 4.10: Experiment I: FITIn-Vergleich<br />
4.3 Performance-Strafe<br />
Eine präzise Aussage über den Ausbremsungsfaktor von FITIn zu treen, gestaltet sich<br />
schwierig, da verschiedene Faktoren bei der Ausführungsgeschwindigkeit zu berücksichtigen<br />
sind:<br />
• Der Vorgang der Disassemblierung.<br />
• Die Zeit, die das Plug-In damit verbringt, einen IRSB zu analysieren und zu instrumentieren.<br />
• Das Zurückkompilieren in Binärcode. Der Compiler von Valgrind hat unter Umständen<br />
eine geringere Mächtigkeit bezüglich der Zielinstruktionswahl (siehe 3.4) und<br />
wird Instruktionen, auch in Hinsicht auf die Instrumentierung, anders wählen und<br />
anordnen. Eine Aussage über die Ezienzänderung des auszuführenden Binärcodes<br />
ist aus der Sicht des Plug-Ins unmöglich, und ist trotzdem besonders wichtig, wenn<br />
ein SB oft ausgeführt wird.<br />
• Die Zeit, die aufgrund des zusätzlich instrumentierten Codes nötig ist.<br />
• Der Verlust der Parallelität bei mehreren Threads auf Mehrkernprozessoren.<br />
Aus den genannten Gründen wird in diesem Abschnitt anhand einiger Beispiele lediglich<br />
eine Einschätzung darüber gegeben, wie sehr Valgrind mit FITIn die Ausführung eines<br />
Programms ausbremst.<br />
Für diese Untersuchung werden zwei Benchmarks herangezogen: Das bereits erwähnte<br />
Dhrystone, dieses Mal jedoch in seiner originalen C-Portierung, und Linpack, ebenfalls in<br />
einer C-Portierung [21]. Linpack ist ein Programm zur Lösung von linearen Gleichungssystemen,<br />
das im Unterschied zu Dhrystone auch Gleitkomma-Datentypen verwendet. Beide<br />
Benchmarks verfügen über ein eigenes Bewertungsschema: Dhrystone stellt das Ergebnis<br />
in Form einer VAX MIPS-Einstufung aus, das Ergebnis von Linpack wird in MFLOPS<br />
(Mega Floating Point Operations Per Second) ausgedrückt. In beiden Fällen werden dazu<br />
prozessinterne Zeitmessungen vorgenommen und diese unter Umständen mit weiteren<br />
Faktoren verrechnet. Ebenfalls besitzen beide Benchmarks nur eine geringe Abhängigkeit<br />
40
4.3 Performance-Strafe<br />
600<br />
604<br />
Entdeckte Fehler<br />
400<br />
200 182 182<br />
300<br />
0<br />
0 0 0 0<br />
0 0 0 0<br />
Ungeschützt<br />
BCBOOL<br />
CSBOOL<br />
EPBOOL<br />
RMBOOL<br />
SHBOOL<br />
FITIn (ursprg.) FITIn (mit/ohne --persist-flip)<br />
Abbildung 4.11: Experiment II: FITIn-Vergleich<br />
von der Geschwindigkeit des Sekundärspeichers, da lediglich am Ende das Ergebnis in eine<br />
Datei geschrieben wird.<br />
Es ist also möglich, zwei verschiedene Messungen mit den beiden Programmen vorzunehmen:<br />
Die Messung der tatsächlichen Ausführungszeit, wie sie von der Kommandozeile<br />
aus gesehen wird, und die Untersuchung der jeweiligen Bewertungsschemata. Es gelten<br />
dieselben Ausführungsbedingungen, die bereits in Abschnitt 4.1 vorgegeben wurden, die<br />
Programme wurden jedoch zur Messung ausschlieÿlich als 32 Bit-Programm betrachtet.<br />
Es werden für beide Untersuchungen drei Ausführungsprole verwendet: Die Ausführung<br />
des jeweiligen Benchmarks ohne Valgrind, die Ausführung des Benchmarks mit Valgrind<br />
und dem Plug-In Nulgrind oder none, das keine Instrumentierung vornimmt, und die<br />
Ausführung mit Valgrind und FITIn unter voller Betrachtung aller Benutzercode-SBs, jedoch<br />
ohne Durchführung einer Fehlerinjektion. Die Einbeziehung des none-Plug-Ins erlaubt<br />
eine Einschätzung über die Einbuÿe, die allein durch Valgrind zustande kommt.<br />
Die Messung der Ausführungszeit wird von einem Ruby-Programm vorgenommen, das<br />
die verschiedenen Ausführungsprole durchführt. In jedem Ausführungsprol wird dieselbe<br />
Anzahl von Benchmark-Durchläufen ausgeführt und die Gesamtausführungszeit durch<br />
die Anzahl der Durchläufe geteilt. In jedem Fall ist ein kleiner zeitlicher Anteil des Ruby-<br />
Prozesses enthalten, der angesichts der Gesamtausführungszeit die auf der Testkonguration<br />
nie unterhalb etlicher Sekunden lag jedoch zu vernachlässigen ist.<br />
Die Ergebnisse der Untersuchung sind in Abbildung 4.12 zu nden. Die in Sekunden<br />
gemessenen Zeiten wurden auf das Ausführungsprol des nativen Durchlaufs normiert, da<br />
die Betrachtung der Vervielfachung der Ausführungszeit und der Vergleich untereinander<br />
hier im Vordergrund stehen.<br />
Auällig in den Ergebnissen der Ausführungszeitmessung ist, dass Dhrystone durch<br />
beide Plug-Ins etwa doppelt so stark ausgebremst wird wie Linpack. Doppelter Verwaltungsaufwand<br />
durch Valgrind kann ausgeschlossen werden, da beide Programme in Valgrind<br />
vergleichbar viele SBs hervorbringen. Eine mögliche Theorie ist, dass Dhrystone<br />
deutlich stärker als Linpack unter der Aushebelung von Optimierungen seitens des Quell-<br />
41
4 Evaluierung von FITIn<br />
Bremsfaktor der Ausführungszeit<br />
6<br />
4<br />
2<br />
3,35<br />
1,56<br />
1 1<br />
2,8<br />
6,9<br />
Linpack Dhrystone<br />
ohne Valgrind none fitin<br />
Abbildung 4.12: Vergleich der Ausführungszeiten<br />
codes leidet. Für fundierte Aussagen ist jedoch eine tiefere Analyse beider Programme in<br />
Valgrind nötig. In diesem Beispiel hat der Benutzer mit FITIn bis zu sieben Mal länger<br />
auf die Ausführung zu warten als ohne.<br />
In der nächsten Untersuchung werden erneut alle Ausführungsprole benutzt, anstatt<br />
einer Zeitmessung werden jedoch nach einem Durchlauf die numerischen Benchmarkwerte<br />
aus der Ergebnisdatei extrahiert und diese am Ende des Prols gemittelt. Die verschiedenen<br />
Skalen werden auf die Werte normiert, die sich aus den Durchläufen ohne Valgrind ergeben,<br />
sodass der Zeitverlust durch Valgrind besser zu erkennen ist. Die Beobachtungsergebnisse<br />
sind in Abbildung 4.13 visualisiert.<br />
normierte Benchmarkwerte<br />
1<br />
0,8<br />
0,6<br />
0,4<br />
0,2<br />
0<br />
1 1<br />
0,32<br />
0,26<br />
6 · 10 −3 7 · 10 −3<br />
Linpack Dhrystone<br />
ohne Valgrind none fitin<br />
Abbildung 4.13: Vergleich der Benchmarkergebnisse<br />
In der Untersuchung der prozessinternen Messungen ist nur ein geringfügiger Unterschied<br />
zwischen Dhrystone und Linpack auszumachen. Viel erstaunlicher ist der Abstand<br />
zwischen den Plug-Ins none und FITIn: Ein Durchlauf mit FITIn resultiert nur in einem<br />
42
4.4 Speicherbedarf<br />
Vierzig- bzw. Fünfzigstel des Benchmarkwerts im Vergleich zu none. Im Vergleich zum<br />
Valgrind-freien Durchlauf betragen die Benchmarkwerte sogar nur rund 1<br />
150<br />
des Ursprungs.<br />
Bei der ersten Betrachtung mögen die Untersuchungsergebnisse der beiden Experimente<br />
widersprüchlich wirken. Die Zeitmessungen werden von den Benchmarks allerdings engmaschig<br />
um tatsächliche Berechnungsabschnitte herumgelegt, sodass etliche Initialisierungs-,<br />
Verwaltungs- und Abschlussvorgänge seitens des Programms sowie Valgrinds unberücksichtigt<br />
bleiben.<br />
Die Instrumentierung eines SB in FITIn involviert mehrere Fälle, in denen Daten den<br />
beiden Instrumentierungslisten loads und replacements hinzugefügt und anschlieÿend<br />
sortiert werden müssen. Für n IRTemp, die erfasst werden müssen, ist je SB ein Aufwand<br />
von O(n log n) für das Hinzufügen notwendig. Für alle IRTemp eines SB, die insgesamt m<br />
Mal auftreten, ist für die Auösung der Ersetzung und das Nachschlagen der Daten der<br />
IRTemp im schlimmsten Fall mit einem Aufwand von O(m · n log n) zu rechnen.<br />
Zur Ausführungszeit fallen die Hilfsmethoden von FITIn ins Gewicht. Nicht einzig<br />
durch die Ausführungszeit, sondern auch weil zusätzliche Kopierinstruktionen nötig sind,<br />
um diesen etwa Parameter gemäÿ der Aufrufkonvention zu übergeben. Jedoch steht einzig<br />
die Methode preLoadHelper in nicht-konstanter Abhängigkeit von Daten, da diese für jede<br />
Ladeadresse die Liste monitorables zu durchsuchen hat. Auÿerhalb des instrumentierten<br />
Binärcodes wiegt zusätzlich das sortierende Einfügen von Monitorable, das beim Erkennen<br />
eines FITIn-Makros vorgenommen wird. Für das Beispiel der beiden Benchmarks sind diese<br />
Fälle jedoch nur im Rahmen des zusätzlichen Aufrufcodes zu betrachten, da unmodizierte<br />
Programme verwendet wurden, die folglich keine Monitorable verzeichnen können.<br />
Unbestritten ist die Tatsache, dass FITIn einen signikanten bis gravierenden Einbruch<br />
in der Ausführungsperformance mit sich bringt. Welchen Bremsfaktor den der Gesamtausführungszeit<br />
oder den eines bestimmten Rechenabschnitts man betrachten möchte,<br />
hängt letztendlich vom Benutzerinteresse ab. Ein Benutzer, der wissen möchte, wie viel<br />
langsamer ein Programm mit FITIn ausgeführt wird, ist sicherlich an der Gesamtausführungszeit<br />
interessiert. Schlieÿlich besteht bereits durch Valgrind ein signikanter Geschwindigkeitsverlust,<br />
vom Aufgeben der Parallelität ganz abgesehen. Möchte man hingegen eine<br />
möglichst präzise Aussage über FITIn treen, ist ein einzelner SB in Instrumentierung<br />
sowie Ausführung zu betrachten.<br />
Allerdings sind die durchgeführten Untersuchungen bloÿ ein kleiner Ausschnitt dessen,<br />
was für eine gestützte Aussage wirklich nötig ist: Die Wahl einer gröÿeren Programmstichprobe,<br />
das Durchführen der Messungen auf verschiedenen Plattformen und verschiedenen<br />
Compiler-Optimierungsstufen und die Berücksichtigung von Multithreading.<br />
Optimierungspotential bietet FITIn etwa bei der Anordnung von case-Blöcken in<br />
zwei switch-Anweisungen, die die statistische Häugkeit von VEX IR-Instruktionen und<br />
-Ausdrücken berücksichtigen sollten. Weiterhin ist die Verwendung von XArray in Hinblick<br />
auf das Einfügen von Elementen teurer als etwa ein AVL-Baum. Auch das Erschöpfen der<br />
Ersetzung eines IRTemp mit Tupeln aus replacements lieÿe sich wahrscheinlich optimieren,<br />
indem statt des Anlegens von Ketten von Tupeln bloÿ bestehende Tupel mit dem letzten<br />
anzuwendenden IRTemp aktualisiert würden und das Ersetzen damit von O(n log n) auf<br />
O(log n) reduziert werden könnte.<br />
4.4 Speicherbedarf<br />
FITIn hält für die gesamte Ausführungszeit drei Arrays konstanter Gröÿe bereit, die über<br />
dieselbe Anzahl von Indizes für den jeweiligen Datentyp verfügen wie die Registerschatten-<br />
43
4 Evaluierung von FITIn<br />
tabelle von Valgrind Bytes einnimmt. Für die Tabellen der Typen IRTemp, Addr (doppelte<br />
Anzahl von Indizes) und SizeT ergibt sich auf einem x86-Computer ein konstanter Gebrauch<br />
von etwa 5,3kB. Auf einem AMD64-Computer beträgt die Gesamtgröÿe der eigenen<br />
Schattentabellen etwa 23kB.<br />
Für die Instrumentierung kann jeder SB für sich betrachtet werden: Ist n die Anzahl<br />
von Ladeoperationen, die FITIn mit preLoadHelper instrumentiert, so wächst der<br />
Speicherbedarf für den Datentyp LoadData linear zu n. Ist m die Anzahl der Verwendungen<br />
eines geladenen IRTemp, so ist zu erwarten, dass die Ersetzungsliste vom Typ ReplaceData<br />
auf O(n·m) anwächst. Weiterhin wird eine Kopie des originalen IRSB erzeugt, deren Gröÿe<br />
durch die Instrumentierung linear zur ursprünglichen Anzahl von Instruktionen wächst.<br />
Zur Ausführungszeit eines jeden einzelnen SB wird die XArray-Liste load_states mit<br />
jedem Aufruf von preLoadHelper gefüllt. Sind also n dieser Aufrufe platziert worden,<br />
wächst diese Liste vom Datentyp LoadState linear zu n. Über alle SB hinweg wird die Liste<br />
von Monitorable geführt. Da sichergestellt ist, dass je Startadresse nur ein Monitorable<br />
existieren kann allenfalls der Wert der Ladegröÿe kann erhöht werden wächst die Liste<br />
monitorables nur linear in Abhängigkeit aller paarweise unterschiedlichen Startadressen,<br />
die im Benutzercode speziziert werden.<br />
Bei der Benutzung von FITIn ist demnach keine Warnung vor ausartendem Speicherverbrauch<br />
auszusprechen. Anzumerken ist jedoch, dass diese Untersuchung interne Gegebenheiten<br />
von Valgrind auÿer Acht lässt.<br />
4.5 Zwischenfazit<br />
Die Betrachtung der fehlgeschlagenen Tests aus Abschnitt 4.1 oenbart, mit welchen Schwierigkeiten<br />
die Benutzung von FITIn verbunden ist: dem Weg vom Quellcode zum Binärcode<br />
und in Valgrind von der Segmentierung und Disassemblierung des Binärcodes zum Plug-In.<br />
Der erste kritische Punkt ist die Übersetzung des Quellcodes und die damit verbundene<br />
Wahl von Instruktionen. Selbst bei deaktivierten Optimierungsstufen kann sich der Benutzer<br />
nicht in der Sicherheit wiegen, dass der Compiler Binärcode generiert, der sich auf die<br />
Operationen des Quellcodes beschränkt. Zu den transparenten Operationen gehören beispielsweise<br />
zusätzliche Kopiervorgänge von Werten oder auch Gröÿenkonvertierungen. Um<br />
einer Diskrepanz zwischen den im Quellcode gezählten und den tatsächlich vorgenommenen<br />
Variablenzugrien vorzubeugen, darf sich der Benutzer nur auf die generierten Instruktionen<br />
verlassen. Bei gröÿerem Quellcode und anspruchsvolleren Mastering-Prozessen ist das<br />
Generieren der Assemblerausgabe des Compilers jedoch mühselig und fordert den Benutzer<br />
zusätzlich dazu auf, das Zwischenergebnis so zu begutachten, dass er die kritischen<br />
Abschnitte vollständig erkennen und auch die Zugrie zuverlässig zählen kann.<br />
Bei der Verwendung von beobachteten Variablen als Abbruchkondition einer Schleife<br />
ist ebenso Vorsicht geboten, allerdings ist der Benutzer hier weitestgehend machtlos: Valgrind<br />
kann die Instruktionen so auf verschiedene SBs aufteilen, dass etwa die erste oder<br />
die letzte Evaluierung der Sprungkondition abseits des Schleifenkörpers stattndet. Wird<br />
die Kondition für die Benutzung zwischen verschiedenen SBs in der Registerschattentabelle<br />
abgelegt und wird diese zwischenzeitlich modiziert, ist es für FITIn unmöglich, diese<br />
Zugrie zu erfassen.<br />
Weiterhin ist die Übersetzung von Binärcode nach VEX IR ein Minenfeld von Fehleranfälligkeiten:<br />
So konnte auf AMD64 beobachtet werden, dass nach Lade-Instruktionen, auf<br />
die ein PUT folgt, 32 Bit-Datentypen wie int zuvor auf 64 Bit erweitert werden. Ein solcher<br />
Fall ist beispielsweise eine registerbasierte Prozedurrückgabe. Die Konvertierungsoperati-<br />
44
4.5 Zwischenfazit<br />
on als Unop-Instruktion der VEX IR stellt jedoch im allgemeinen Fall einen Lesezugri<br />
dar, der im Unterschied zu einer 32 Bit-Plattform die Anzahl der Verwendungen zusätzlich<br />
erhöht. FITIn verfolgt daher auf 64 Bit-Plattformen die IRTemp über diese Konvertierungsoperationen<br />
hinaus. Dieser Fall stellt die einzige Spezialbehandlung von FITIn dar, da das<br />
Plug-In auf AMD64 für Standard-Datentypen sonst faktisch unbenutzbar ist. Es ist jedoch<br />
unklar, ob auf weiteren Plattformen oder bei anderen Instruktionen ähnlich gelagerte Fälle<br />
ans Tageslicht gelangen werden. Insbesondere bei bisher nicht untersuchten, semantisch<br />
komplexen Instruktionen auf CISC-CPUs ist davon auszugehen.<br />
Auf die Behandlung von Spezialfällen, die die Zählung abweichen lassen, etwa auf<br />
heuristische Art und Weise, sollte in einem Werkzeug wie FITIn jedoch generell verzichtet<br />
werden. Schlieÿlich könnte eine Konstellation von VEX IR-Instruktionen nicht einzig<br />
der Modellierung einer komplexeren Instruktionen herrühren, sondern tatsächlich durch<br />
den Benutzer beabsichtigt sein, etwa eine Hin- und Herkonvertierung von Daten. Mit dem<br />
im vorherigen Paragraphen genannten Beispiel einer Spezialbehandlung wird dieser Benutzercode<br />
nun möglicherweise interferieren. Ebenso ist die Ausführungsplattform für ein<br />
Valgrind-Plug-In transparent, sodass Sonderbehandlungen nicht zuverlässig an- und abgeschaltet<br />
werden können. Auch der Quellcode von FITIn würde dabei stark an Wartbarkeit<br />
und Erweiterbarkeit einbüÿen.<br />
Die Performance-Strafe durch FITIn sollte mit Blick auf die Gesamtausführungszeit<br />
in einem akzeptablen Rahmen liegen. Für die Reduzierung der theoretischen Kosten der<br />
Instrumentierung eines einzelnen Superblocks dürfte jedoch noch ein signikantes Potential<br />
existieren.<br />
45
5 Konkurrierende Ansätze<br />
In diesem Kapitel werden einige ähnliche Projekte vorgestellt, die eine Bitfehlerinjektion<br />
in Register von Benutzerprogrammen ermöglichen. Zusätzlich werden zwei weitere DBI-<br />
Frameworks vorgestellt und in Hinblick auf FITIn mit Valgrind verglichen.<br />
5.1 Programme<br />
FERRARI<br />
Mit FERRARI wurde bereits 1992 ein wissenschaftliches Projekt zur Fehlerinjektion implementiert<br />
[22]. FERRARI macht sich unter Unix die Fähigkeiten von Systemaufrufen<br />
zu Nutze, um in einem Zielprozess während der Ausführung Eingrie vorzunehmen. Dazu<br />
wird ein Manager-Prozess gestartet, der einen Kindprozess mittels fork(2) erstellt und<br />
durch den ptrace(2)-Systemaufruf die Manipulierbarkeit durch einen externen Prozess<br />
erlaubt. Schlieÿlich ersetzt der Kindprozess sich mit dem Aufruf von execv(3) durch den<br />
Zielprozess, der nun unter einer gewissen Kontrolle des Managers steht. Das Programm<br />
bietet drei Möglichkeiten zur Fehlerinjektion und zwei verschiedene Fehlerklassen an. Angebotene<br />
Fehlerinjektionsmethoden sind:<br />
• Eine Fehlerinjektion in den Prozessspeicher des Zielprogramms vor der Ausführung<br />
der ersten Instruktion.<br />
• Das Zählen von Vorkommen von gewählten Speicheradressen, wobei beim n-ten Vorkommen<br />
eine Softwareunterbrechnung stattndet und der Fehler injiziert werden<br />
kann.<br />
• Ein Zeitnehmer, der nach Ablauf das Zielprogramm unterbricht und eine Hardwareunterbrechung<br />
signalisiert. Diese Methode ist jedoch, wie bereits in Abschnitt 2.1<br />
erwähnt, für reproduzierbare Ergebnisse ungeeignet.<br />
FERRARI erlaubt vorübergehende Fehler, die auf eine einzelne Instruktion begrenzt<br />
sind, und permanente Fehler, die unbeschränkt lange leben können. Eine Bitfehlerinjektion<br />
in ein Register wird als vorübergehende Fehlerinjektion angeboten.<br />
Der modulare Aufbau soll Portierungen auf verschiedene Architekturen und Betriebssysteme<br />
ermöglichen können, als unterstütze Plattformen werden SPARC/SunOS, RS/<br />
6000/AIX und VAX/VMS aufgeführt. Über den aktuellen Stand von FERRARI ist jedoch<br />
nichts bekannt.<br />
FITgrind<br />
Bei FITgrind handelt es sich wie bei FITIn ebenfalls um ein Werkzeug, das auf Valgrind<br />
aufsetzt. FITgrind wird vorgestellt als Werkzeug, das es dem Benutzer ermöglicht, zur Fehlerinjektion<br />
fault probabilities, fault types, and target applications 1 [10, S.38] wählen zu<br />
können. Obwohl keine Implementierungsdetails erwähnt werden, ist anzunehmen, dass die<br />
Kernidee bei FITgrind die Verwendung einer Fehlerwahrscheinlichkeit ist. Die Verwendung<br />
eines Seed-Wertes erlaubt die Reproduzierbarkeit von Fehlerinjektionsszenarien.<br />
1 Deutsch: Fehlerwahrscheinlichkeiten, Fehlerklassen und Zielanwendungen<br />
47
5 Konkurrierende Ansätze<br />
Zwar wird im Gegensatz zu FITIn keine Bearbeitung des Quellcodes vorausgesetzt,<br />
ein Werkzeug wie FITgrind erlaubt im Gegenzug jedoch keine gezielte Analyse des Fehlerverhaltens<br />
in bestimmten Programmabschnitten oder eine bewusste Evaluierung von<br />
SIHFT-Techniken.<br />
Auch bei FITgrind liegen keine weiteren Informationen zur Entwicklungsaktivität vor.<br />
In der von 2006 vorgestellten Form kann dieses Plug-In unter Valgrind aktuell nicht mehr<br />
verwendet werden, da Valgrind die Zwischenrepräsentation UCode mittlerweile durch VEX<br />
IR ersetzt hat.<br />
Xception<br />
Xception ist als Fehlerinjektionswerkzeug für PowerPC-Prozessoren konzipiert worden [23].<br />
Es bedient sich dabei etlicher Analyse- und Debug-Fähigkeiten dieser Prozessorarchitektur,<br />
um auf das Zielprogramm mittels Hardwareunterbrechungen Einuss nehmen zu können.<br />
Der Quellcode des Zielprogramms ist nicht erforderlich, kann jedoch zur Vermeidung eines<br />
zusätzlichen Steuerungsprozesses herangezogen werden.<br />
Unter Xception können Fehler in etliche Komponenten injiziert werden:<br />
• In Benutzerregister.<br />
• In den Adress- oder Datenbus.<br />
• In verschiedene Funktionseinheiten der CPU, z.B. die MMU oder FPU.<br />
• In den Arbeitsspeicher.<br />
Ähnlich wie bei FERRARI können Fehler bei der Verwendung gewählter Speicheradressen<br />
oder nach Ablauf eines Zeitnehmers in eines der aufgeführten Ziele injiziert werden.<br />
Für Register und Arbeitsspeicherzellen unterstützt Xception nicht nur Bit-Flips sondern<br />
auch eingerastete Bits.<br />
Dem Benutzer stehen auf einem Host-Computer von Xception Programme zum Verwalten<br />
und Auswerten der Fehlerinjektion zur Verfügung. Die Ausführung und die Auswertung<br />
des Zielprogramms können so auf separaten Computern vorgenommen werden.<br />
Das Ursprungsprogramm wurde mutmaÿlich kommerzialisiert: Die Firma Critical Software<br />
mit Sitz in Coimbra, Portugal am selben Ort, an dem die Forschungseinrichtung<br />
1995 die Erstversion von Xception vorstellte vertreibt mittlerweile eine industrielle Version<br />
unter demselben Namen. Nach eigenen Angaben verfügt die Software nun auch über<br />
Unterstützung von x86-CPUs [24, S.1].<br />
5.2 DBI-Frameworks<br />
Intel Pin<br />
Pin ist kein Fehlerinjektionswerkzeug, sondern ein weiteres DBI-Framework [25]. Es steht<br />
für x86 und AMD64-Prozessoren zur Verfügung und unterstützt im Unterschied zu Valgrind<br />
auch Windows.<br />
Pin kann das zu untersuchende Programm von sich aus starten oder auch an einen bestehenden<br />
Prozess angefügt werden. Ebenso ist immer ein Plug-In, ein Pintool, anzugeben,<br />
welches die tatsächliche Analyse und Instrumentierung vornehmen wird.<br />
Anders als Valgrind, das dem Plug-In ausschlieÿlich ganze Superblocks bereitstellt,<br />
erlaubt Pin auf Verlangen des Pintools weitere Behandlungsmöglichkeiten:<br />
• Trace-Instrumentierung: Ein Trace besteht aus mehreren Basic Blocks (BB) von<br />
Instruktionen. BBs können im Unterschied zu SBs, wie in Valgrind, nur an einer<br />
48
5.2 DBI-Frameworks<br />
Stelle, nämlich am Ende, verlassen werden. Pin stellt Traces und BBs anhand des<br />
Abzweigungsverhaltens einzelner Instruktionen zusammen.<br />
• Instruktionsweise: Dem Pintool wird jede Instruktion einzeln übergeben.<br />
• Abbildungs-Instrumentierung: Ein ganzes Programmabbild kann mittels Iteration<br />
über Sektionen, Prozeduren oder Instruktionen analysiert und instrumentiert werden.<br />
• Prozedur-Instrumentierung: Eine gewählte Prozedur wird vollständig zur Instrumentierung<br />
übergeben.<br />
Die Zwischenrepräsentation von Pin wird in Form eines Abbilds auf ein eigenes API<br />
betrachtet, das dem Pintool-Entwickler Eigenschaften in Bezug auf Register- und Speicherverhalten<br />
der Instruktionen abstrahiert. Dieses API erlaubt jedoch stärker als VEX IR<br />
Rückschlüsse auf die tatsächlich verwendeten Speicherorte und -operationen. Dadurch ist<br />
es Pin möglich, die abgebildeten Instruktionen beinahe identisch in Binärcode zurückzukompilieren.<br />
Anders als bei Valgrind sind jedoch nicht alle Aufrufe von Pintool-Callbacks threadsicher,<br />
der Pintool-Entwickler hat also eventuell Blockierungsmaÿnahmen für sein Plug-In<br />
zu benutzen. Mit Rücksicht auf Blockierungen seitens des Programms und seitens Pin ist<br />
auch in Bezug auf Dead-Locks Aufmerksamkeit gefordert.<br />
Ein Pintool kann ebenfalls Callbacks für Zeitpunkte vor und nach einem Systemaufruf<br />
registrieren. Im Gegensatz zu Valgrind signalisiert Pin jedoch nicht, ob dabei eine Registeroder<br />
Speicheroperation ansteht oder durchgeführt wurde. Diese Fälle hat ein Pintool selbst<br />
zu erkennen und zu behandeln.<br />
Eine besondere Fähigkeit in Hinsicht auf FITIn fehlt Pin jedoch: Es gibt keine vorhandene<br />
Möglichkeit, dass das auszuführende Programm mit Pin oder mit dem Pintool<br />
kommunizieren kann. Ein Mechanismus einschlieÿlich einer C-Header-Datei für das Zielprogramm<br />
wie bei Valgrind, der zu diesem Zweck zusätzliche Daten auf dem Stack-<br />
Speicher ablegt, müsste manuell für ein Pintool implementiert werden.<br />
DynamoRIO<br />
Das quelloene DBI-Framework DynamoRIO ist unter Windows und Linux auf x86 und<br />
AMD64-Prozessoren lauähig [26]. Werkzeuge, die auf der Basis von DynamoRIO entwickelt<br />
werden, heiÿen Clients. DynamoRIO stellt den Benutzercode in zwei Einheiten zur<br />
Verfügung: Basic Blocks und Traces. BBs, die mehrfach in einer bestimmtem Reihenfolge<br />
hintereinander ausgeführt werden, werden von DynamoRIO zu Traces zusammengestellt,<br />
sodass zwischen den einzelnen BBs kein Rücksprung mehr in DynamoRIO-Code erfolgen<br />
muss. Dieser Mechanismus wird benutzt, um besonders häug verwendete Codeabschnitte<br />
zu beschleunigen.<br />
Eine Zwischenrepräsentation als solche existiert nicht. Soll der Client über die Behandlung<br />
allgemeiner Ereignisse hinaus arbeiten oder instruktionsgenaue Analysen oder Modi-<br />
kationen vornehmen, muss dieser den Instruktionsblock explizit in eine API-Darstellung<br />
decodieren lassen. Das API von DynamoRIO erlaubt dabei die Repräsentation und Verwendung<br />
jeder einzelnen x86- und AMD64-Instruktion. Allerdings stehen dem Client-<br />
Entwickler ebenso Methoden zur Verfügung, die Abfragen über verwendete Speicherstellen<br />
und -operationen einer Instruktion zulassen.<br />
DynamoRIO stellt dem Client eine ganze Reihe von Events bereit, zu welchen dieser<br />
Callbacks hinterlegen kann: etwa bei dynamischem Nachladen von Bibliotheken, vor und<br />
nach Systemaufrufen und zu Beginn und Ende von weiteren Threads. Ähnlich Pin erfordert<br />
DynamoRIO vom Client-Entwickler gröÿere Selbstverantwortung in Bezug auf Multithreading<br />
oder dem Erkennen bestimmter Operationen, die Valgrind in eigenen Ereignissen<br />
49
5 Konkurrierende Ansätze<br />
zusammenfasst.<br />
Client-Entwickler werden eindringlich dazu aufgefordert, dass ihr Werkzeug bei der<br />
Ausführung eines Zielprogramms transparent für dieses zu funktionieren hat. Vor diesem<br />
Hintergrund existiert auch in DynamoRIO kein vorgesehener Weg zur Kommunikation<br />
zwischen Zielprogramm und Client.<br />
5.3 Vergleichsfazit<br />
Es scheint, dass der Ansatz von FITIn, Möglichkeiten zur Fehlerinjektion im Quellcode<br />
abzustecken und diese unter Zuhilfenahme eines DBI-Frameworks zu nutzen, für ein Fehlerinjektionswerkzeug<br />
eher ungewöhnlich ist. Ungeachtet der Schwierigkeiten in Verbindung<br />
mit Valgrind, die in Abschnitt 4.1 erläutert wurden, erlaubt das Vorgehen von FITIn dem<br />
Benutzer jedoch eine gröÿere Kontrolle über den Zeitpunkt der Fehlerinjektion. Weiterhin<br />
ist es aufgrund der Bindung an Speicheradressen nicht erforderlich, dass der Benutzer<br />
sich mit dem Speicherort auseinandersetzen muss, an dem ein Bit-Flip stattnden soll. Es<br />
ist überwiegend nicht einmal notwendig, zur Kompilierungs- oder Ausführungszeit einen<br />
relativen oder absoluten Adresswert zu kennen. So ist im Vergleich zu den vorgestellten<br />
Lösungen nicht prinzipiell eine Vor- oder Nachanalyse im Assembler- oder Binärcode des<br />
Programms durchzuführen, um die Bedeutung einer gewählten Speicheradresse zu ermitteln.<br />
Auch ein Vergleich von Alternativen zu Valgrind erlaubt einige interessante Gedanken:<br />
Valgrind bietet dem Plug-In-Entwickler durch die Abschirmung von Prozessorarchitektur,<br />
Betriebssystem, Multithreading und besonderer Vorkommnisse einen besonders hohen<br />
Grad an Komfort, da sich dieser damit grundsätzlich nicht auseinandersetzen braucht.<br />
Die Vorteile liegen eindeutig in der Übersichtlichkeit der VEX IR-Spezikation und dem<br />
Registrieren von Callbacks für weitere Ereignisse, die es erlauben, ein Plug-In für eine<br />
DBA-Anwendung zügig zu entwickeln. Als Nachteile sind der aufwendige Prozess von Disassemblierung<br />
und Rekompilierung und der Verlust der Parallelität zu erwähnen. Die Übersetzung<br />
nach VEX IR zieht weitere Konsequenzen für ein Werkzeug wie FITIn nach sich,<br />
das über die reine Binäranalyse hinausgeht und dabei eine transparente Benutzung ermöglichen<br />
möchte.<br />
Ein Framework wie Pin oder DynamoRIO könnte möglicherweise einige Probleme,<br />
die der Übersetzung des Binärcodes nach VEX IR geschuldet sind (siehe Abschnitt 4.1),<br />
hinfällig machen: Eine ursprungsgetreue Abbildung der Binärinstruktionen verhindert ein<br />
weiteres Verzerren der gewünschten Anwendungsmethode. So wäre es für den Benutzer ein<br />
deutlich transparenterer Prozess, Abweichungen des Zugriszählers alleine im von ihm verwendeten<br />
Kompilierungsvorgang zu suchen. Umgekehrt beschränkt die Verwendung dieser<br />
alternativen Frameworks die Plattformen auf x86 und AMD64 mit geringer Aussicht auf<br />
zukünftige Portabilität. Ebenfalls nicht zu vernachlässigen ist der wohl deutlich gröÿere<br />
Entwicklungsaufwand, ein Programm wie FITIn in Bezug auf alle denkbaren Spezialfälle<br />
von Systemaufrufen, Multithreading und Kommunikation mit dem Zielprogramm auf einer<br />
dieser Plattformen zu realisieren.<br />
50
6 Abschluss<br />
6.1 Zusammenfassung<br />
In dieser Arbeit wurde das Programm FITIn maÿgeblich erweitert. FITIn als Valgrindbasiertes<br />
Fehlerinjektionswerkzeug besaÿ zu Beginn der Entwicklung nur die Fähigkeit,<br />
auf das Auslesen von Speicherinhalten an Adressen zu gewählten Variablen zu achten.<br />
Vor einem vom Benutzer gewählten Ladezeitpunkt führte FITIn einen Bit-Flip auf dem<br />
Prozessspeicher an einem gewählten Bit durch. Wie in Abschnitt 3.6 gezeigt werden konnte,<br />
ist dieses Verfahren unpraktikabel, wenn der Benutzer zu einem späteren Zeitpunkt einen<br />
Fehler injiziert wünscht, der Wert aber durch das Halten in einem Register nicht erneut<br />
aus dem Prozessspeicher geladen wird. FITIn dahingehend zu erweitern, diesen Fall auf<br />
eine möglichst transparente Art und Weise für den Benutzer zugänglich zu machen, stand<br />
im Mittelpunkt dieser Arbeit.<br />
Zur Einordnung von FITIn in Hinsicht auf Typen von Bitfehlern, Injektionsebenen<br />
und zu manipulierenden Daten wurde die Fehlerinjektion in Kapitel 2 von einem globalen<br />
Stand aus betrachtet. Grundsätzlich erönen sich einem Benutzer die Möglichkeiten zur<br />
hardware- oder softwarebasierten Fehlerinjektion. Bitfehler durch Hardwareprobleme oder<br />
ein SEU in Software zu simulieren, stellt sich in gewissen Anwendungsrahmen als deutlich<br />
ökonomischer und kontrollierbarer heraus. Für die softwarebasierte Fehlerinjektion bieten<br />
sich etliche Ebenen an, die auf unterschiedliche Weise Bitfehler in eine Zielanwendung<br />
einschleusen können.<br />
Im Fall von FITIn wird das DBI-Framework Valgrind dazu verwendet, Kontrolle über<br />
das Zielprogramm für eine Fehlerinjektion auszuüben. Der Benutzer fügt C-Makros im<br />
Quellcode der Zielanwendung ein, die das Beobachten bestimmter Ladeadressen zulassen.<br />
Der Benutzer steuert mit Hilfe der Kommandozeile den Fehlerinjektionszeitpunkt und das<br />
zu manipulierende Bit. In Kapitel 3 wurde zunächst das Konzept der dynamischen Binärinstrumentierung<br />
vorgestellt und auf die Besonderheiten von Valgrind eingegangen. Mit<br />
Blick auf das Ziel dieser Arbeit wurden verschiedene Ansätze der Erweiterung untersucht.<br />
Die gewählte Implementierung wurde in Abschnitt 3.6 ausführlich vorgestellt. In diesem<br />
Kapitel wurden ebenso erste Beschränkungen beleuchtet: Insbesondere die fehlende Unterstützung<br />
von Gleitkomma-Datentypen el dabei ins Auge, die nur schwer zu verschmerzen<br />
sein dürfte.<br />
Anschlieÿend wurde FITIn in Kapitel 4 etlichen Untersuchungen für die Evaluierung<br />
verschiedener Aspekte unterzogen. Mehrere Testfälle demonstrierten den Erfolg oder auch<br />
Misserfolg von Fehlerinjektionsszenarien unter der Verwendung von FITIn. Die Auswertung<br />
der Fehlschläge erlaubte eine Aussage über grundsätzliche Schwierigkeiten, die durch<br />
den Compiler oder Valgrind bedingt sind. Ebenso wurde die SIHFT-Bibliothek FlipSafe<br />
erneut auf die Probe gestellt: Mit Ausnahme eines einzelnen Datentyps kommt die Bibliothek<br />
weiterhin mit den vorgenommenen Bitfehlern zurecht. Für eine Einschätzung über<br />
den Performance-Verlust eines Programms unter der Verwendung von FITIn wurden die<br />
Benchmarks Dhrystone und Linpack betrachtet. Obgleich ein sehr starkes Ausbremsen<br />
durch FITIn beobachtet wurde, wurde auch auf Verbesserungspotential eingegangen.<br />
In der erlangten Erkenntnis über Stärken und Schwächen von Valgrind und FITIn wur-<br />
51
6 Abschluss<br />
den abschlieÿend (Kap. 5) Ansätze anderer Programme betrachtet, die ebenfalls Bitfehler<br />
auf Registern vornehmen können. Auÿerdem wurde ein kurzer Vergleich zwischen Valgrind<br />
und weiteren DBI-Frameworks gezogen.<br />
6.2 Ausblick<br />
Der Stand von FITIn, der mit dem Abschluss dieser Arbeit erreicht wurde, markiert keinesfalls<br />
die letzte Station. An mehreren Stellen konnte gezeigt werden, dass FITIn nach wie vor<br />
nicht nur von technischen, sondern auch von konzeptionellen Gegebenheiten eingeschränkt<br />
wird.<br />
Zur Erweiterung der Anwendungsmöglichkeiten von FITIn sollte das Hinzufügen der<br />
Unterstützung der C-Datentypen float und double und möglicherweise auch der verbleibenden<br />
SIMD-Datentypen eine hohe Priorität genieÿen. Allerdings oenbart dieser<br />
Punkt auch, dass das Konzept von FITIn möglicherweise noch nicht hinreichend ist. Oder<br />
andernfalls, dass es eventuell unumgänglich ist, FITIn um die Erkennung von VEX IR-<br />
Mustern zu erweitern, da ein Belang wie dieser stark plattformabhängig ist. Zum Umgang<br />
von FITIn mit bisher fehlgeschlagenen Testfällen in Abschnitt 4.1 ist ebenfalls eine weitere<br />
Untersuchung sinnvoll.<br />
In einem weniger oenen Rahmen sollte FITIn hinsichtlich des Instrumentierungsaufwands<br />
etwas Optimierung genieÿen. Insbesondere die verwendete Listenstruktur und das<br />
Verfahren zur Ersetzung von IRTemp sollten dabei ins Auge gefasst werden.<br />
Da FITIn ausschlieÿlich auf den verwandten CISC-Prozessorarchitekturen x86 und<br />
AMD64 entwickelt und getestet wurde, sind Testläufe von FITIn auf anderen Plattformen<br />
ebenfalls interessant. Vor allem in Hinsicht auf RISC-Prozessoren, deren Instruktionen oft<br />
semantisch ärmer sind als die von CISC-CPUs, könnten Testläufe zeigen, ob FITIn auf<br />
diesen möglicherweise zuverlässiger zu benutzen ist.<br />
Bisher ist FITIn darauf beschränkt, einen einzigen Bit-Flip je Programmdurchlauf<br />
vornehmen zu können. Die Fehlersimulation lieÿe sich erweitern, indem der Benutzer bei<br />
FITIn eine beliebige Anzahl von Fehlern in beliebig vielen Bits spezizieren kann.<br />
52
Literaturverzeichnis<br />
[1] Ziegler, J.F. ; Curtis, H. W. u. a.: IBM experiments in soft fails in computer<br />
electronics (19781994). In: IBM Journal of Research and Development 40 (1996), Nr.<br />
1, S. 318. http://dx.doi.org/10.1147/rd.401.0003. DOI 10.1147/rd.401.0003.<br />
[2] Piotrowski, A. ; Makowski, D. u. a.: The automatic implementation of Software<br />
Implemented Hardware Fault Tolerance algorithms as a radiation-induced soft errors<br />
mitigation technique. In: Proceedings of IEEE Nuclear Science Symposium Conference<br />
Record, 2008. NSS '08., 2008, S. 841846.<br />
[3] Manteuffel, Henning: High-Level FPGA-Programmierung mit automatisch generierten<br />
Netzwerken von Automaten, TU Hamburg-Harburg, Dissertation, 2012.<br />
http://doku.b.tu-harburg.de/volltexte/2012/1174/.<br />
[4] Hsueh, Mei-Chen ; Tsai, Timothy K. ; Iyer, Ravishankar K.: Fault Injection<br />
Techniques and Tools. In: Computer 30 (1997), April, Nr. 4, S. 7582. http:<br />
//dx.doi.org/10.1109/2.585157. DOI 10.1109/2.585157.<br />
[5] Alme, J. ; Fehlker, D. u. a.: Radiation tolerance studies using fault injection on the<br />
Readout Control FPGA design of the ALICE TPC detector. In: Journal of Instrumentation<br />
8 (2013), Nr. 01, S. C01053. http://dx.doi.org/10.1088/1748-0221/8/<br />
01/C01053. DOI 10.1088/17480221/8/01/C01053.<br />
[6] Hayashi, Yu-ichi ; Homma, Naofumi u. a.: Non-invasive Trigger-free Fault Injection<br />
Method Based on Intentional Electromagnetic Interference. In: The Non-Invasive<br />
Attack Testing Workshop (NIAT 2011), 2011, S. 1519.<br />
[7] Nethercote, Nicholas: Dynamic Binary Analysis and Instrumentation, University of<br />
Cambridge, A Dissertation, November 2004. http://valgrind.org/docs/phd2004.<br />
pdf.<br />
[8] Schmidt, David A.: Programming language semantics. In: ACM Comput. Surv.<br />
28 (1996), März, Nr. 1, S. 265267. http://dx.doi.org/10.1145/234313.234419. <br />
DOI 10.1145/234313.234419.<br />
[9] Seong, Nak H. ; Woo, Dong H. u. a.: SAFER: Stuck-At-Fault Error Recovery for<br />
Memories. In: Proceedings of the 2010 43rd Annual IEEE/ACM International Symposium<br />
on Microarchitecture (MICRO), 2010, S. 115124.<br />
[10] Wappler, Ute ; Fetzer, Christof: Hardware Fault Injection Using Dynamic Binary<br />
Instrumentation: FITgrind. In: Proceedings of the Sixth European Dependable<br />
Computing Conference (EDCC 2006), 2006, S. 3738.<br />
[11] Portela-Garcia, M. ; Lopez-Ongil, C. u. a.: A Rapid Fault Injection Approach<br />
for Measuring SEU Sensitivity in Complex Processors. In: Proceedings of 13th IEEE<br />
International On-Line Testing Symposium, 2007 (IOLTS 07), 2007, S. 101106.<br />
[12] Lindholm, Tim ; Yellin, Frank u. a.: The Java® Virtual Machine Specication.<br />
Java SE 7. Oracle America, Inc., 2013.<br />
53
LITERATURVERZEICHNIS<br />
[13] Nanda, Susanta ; Li, Wei u. a.: BIRD: Binary Interpretation using Runtime Disassembly.<br />
In: Proceedings of the International Symposium on Code Generation and<br />
Optimization, IEEE Computer Society, 2006 (CGO '06), S. 358370.<br />
[14] Laurenzano, M.A. ; Tikir, M.M. u. a.: PEBIL: Ecient static binary instrumentation<br />
for Linux. In: Proceedings of the IEEE International Symposium on Performance<br />
Analysis of Systems Software (ISPASS), 2010, S. 175183.<br />
[15] Nethercote, Nicholas ; Seward, Julian: Valgrind: a framework for heavyweight<br />
dynamic binary instrumentation. In: SIGPLAN Not. 42 (2007), Juni, Nr. 6, S. 89100.<br />
http://dx.doi.org/10.1145/1273442.1250746. DOI 10.1145/1273442.1250746.<br />
[16] Die Valgrind-Entwickler: Valgrind v3.8.1. http://valgrind.org/, 2012. [Online;<br />
letzter Abruf 27. Mai 2013]<br />
[17] Terasa, C.: A Valgrind-based Soft Error Injection Tool for SIHFT Evaluations, TU<br />
Hamburg-Harburg, Master Thesis, März 2013<br />
[18] Terasa, C. ; Heing-Becker, M.: FITIn. https://github.com/MarcelHB/<br />
valgrind-fitin/fitin/, 2013. [Online; letzter Abruf 11. Juni 2013]<br />
[19] Munkby, G.: FlipSafe. http://www.sts.tu-harburg.de/research/flipsafe.<br />
html, 2011. [Online; letzter Abruf 09. Juni 2013]<br />
[20] Longbottom, R.: Dhrystone Benchmark v2.1. http://www.roylongbottom.org.<br />
uk/linux%20benchmarks.htm, 1996. [Online; letzter Abruf 02. Juni 2013]<br />
[21] Longbottom, R.: Linpack Benchmark. http://www.roylongbottom.org.uk/<br />
linux%20benchmarks.htm, 2010. [Online; letzter Abruf 02. Juni 2013]<br />
[22] Kanawati, Ghani A. ; Kanawati, Nasser A. u. a.: FERRARI: A Flexible Software-<br />
Based Fault and Error Injection System. In: IEEE Trans. Comput. 44 (1995),<br />
Februar, Nr. 2, S. 248260. http://dx.doi.org/10.1109/12.364536. DOI<br />
10.1109/12.364536.<br />
[23] Carreira, Joao ; Madeira, Henrique u. a.: Xception: Software Fault Injection and<br />
Monitoring in Processor Functional Units. In: Processor Functional Units, DCCA-5,<br />
Conference on Dependable Computing for Critical Applications, 1995, S. 135149.<br />
[24] Critical Software Ltd.: Xception. http://asd.criticalsoftware.com/upload_<br />
case/csXCEPTION-flyer.pdf, 2005. [<strong>PDF</strong>; Online; letzter Abruf 07. Juni 2013]<br />
[25] Intel Corporation: Pin 2.12 User Guide. http://software.intel.com/sites/<br />
landingpage/pintool/docs/58423/Pin/html/, 2013. [Online; letzter Abruf 09.<br />
Juni 2013]<br />
[26] Die DynamoRIO-Entwickler: DynamoRIO API. http://dynamorio.org/docs/,<br />
2013. [Online; letzter Abruf 08. Juni 2013]<br />
54
Akronyme<br />
BB Basic Block. 48, 49<br />
D&R Disassemble-and-Resynthesize. 11, 13, 16<br />
DBA Dynamische Binäranalyse. 810, 50<br />
DBI Dynamische Binärinstrumentierung. 4, 810, 14, 4752<br />
GCC GNU C Compiler. 7, 28, 31, 67, 68<br />
JIT Just-In-Time. 7, 9<br />
SB Superblock. 1016, 19, 21, 22, 24, 25, 29, 33, 34, 36, 3841, 43, 44, 48, 6568<br />
SBA Statische Binäranalyse. 8, 9<br />
SEU Single Event Upset. 5, 26, 51<br />
SIHFT Softwareimplementierte Hardwarefehlertoleranz. 1, 15, 38, 39, 48, 51<br />
VEX IR VEX Intermediate Representation. 1114, 16, 1821, 24, 27, 28, 3234, 36, 4345,<br />
4850, 52, 57, 59, 60, 68<br />
VM Virtuelle Maschine. 6, 7<br />
55
Abbildungsverzeichnis<br />
2.1 Taxonomie der Fehlerinjektion . . . . . . . . . . . . . . . . . . . . . . . . . . 3<br />
3.1 Programmausführung in Valgrind . . . . . . . . . . . . . . . . . . . . . . . . 11<br />
3.2 Disassemblierung in Valgrind: x86-Programm (li.), VEX IR (re.) . . . . . . 13<br />
3.3 Disassemble-and-Resynthesize in Valgrind . . . . . . . . . . . . . . . . . . . 14<br />
3.4 Registerübersetzung in Valgrid: x86-Programm (li.), VEX IR (re.) . . . . . 16<br />
3.5 Ergebnis der x86-Codesynthese . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
3.6 Beispiel für Ansatz I, C-Programm (li.), VEX IR (re.) . . . . . . . . . . . . 18<br />
3.7 Ansatz I: Zu x86-Programm (re.) kompilierter C-Code (li.) . . . . . . . . . . 18<br />
3.8 Beispiel für Ansatz II, C-Programm (li.), VEX IR (re.) . . . . . . . . . . . 19<br />
3.9 Grenzbeispiel für FITIn, x86-Programm (li.), VEX IR (re.) . . . . . . . . . 20<br />
3.10 C-Struktur: LoadData . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22<br />
3.11 Algorithmus zur Instrumentierung von IRTemp . . . . . . . . . . . . . . . . . 23<br />
3.12 C-Struktur: LoadState . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24<br />
3.13 Erweitertes LoadState . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27<br />
3.14 Experimentelle Behandlung einer float-Addition . . . . . . . . . . . . . . . 28<br />
4.1 Beispielprogramm I . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31<br />
4.2 Beispielprogramm I, VEX IR . . . . . . . . . . . . . . . . . . . . . . . . . . 32<br />
4.3 Beispielprogramm II . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33<br />
4.4 Beispielprogramm II, VEX IR (main-SB 1) . . . . . . . . . . . . . . . . . . . 34<br />
4.5 Beispielprogramm II, VEX IR (main-SB 2) . . . . . . . . . . . . . . . . . . . 34<br />
4.6 Beispielprogramm III . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35<br />
4.7 Beispielprogramm III, VEX IR . . . . . . . . . . . . . . . . . . . . . . . . . 36<br />
4.8 Beispielprogramm IV . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37<br />
4.9 Beispielprogramm IV, x86-Assemblercode . . . . . . . . . . . . . . . . . . . 38<br />
4.10 Experiment I: FITIn-Vergleich . . . . . . . . . . . . . . . . . . . . . . . . . . 40<br />
4.11 Experiment II: FITIn-Vergleich . . . . . . . . . . . . . . . . . . . . . . . . . 41<br />
4.12 Vergleich der Ausführungszeiten . . . . . . . . . . . . . . . . . . . . . . . . . 42<br />
4.13 Vergleich der Benchmarkergebnisse . . . . . . . . . . . . . . . . . . . . . . . 42<br />
57
A Ergänzendes Material<br />
A.1 VEX IR-Spezikation<br />
Im Nachfolgenden ist der Aufbau von VEX IR dargestellt. Diese Spezikation wurde gemäÿ<br />
der Datei VEX/pub/libvex_ir.h aus der Standarddistribution von Valgrind v3.8.1<br />
aufgestellt [16].<br />
Die Spezikation orientiert sich an der Backus-Naur-Form. Präxbehaftete Symbole<br />
stellen Terminale dar. Nichtterminale, die in dieser Spezikation nicht abgeleitet werden<br />
können, werden von Valgrind mit Rücksicht auf die ausführende Plattform an anderer Stelle<br />
deniert. IROp wurde aufgrund des Umfangs weitestgehend ausgelassen. Nichtterminale, die<br />
als Datenstruktur ein- oder zweifach dereferenziert werden müssen, werden nicht gesondert<br />
hervorgehoben. Die Alternativen von Nichtterminalen in Iex und Ist führen zusätzlich die<br />
Bezeichner, die im Hauptteil des Dokuments verwendet werden.<br />
IRType ::= Ity_INVALID | Ity_I1 | Ity_I8 | Ity_I16 | Ity_I32 | Ity_I64 | Ity_I128 |<br />
Ity_F32 | Ity_F64 | Ity_D32 | Ity_D64 | Ity_D128 | Ity_F128 | Ity_V128 |<br />
Ity_V256<br />
IREndness ::= Iend_LE | Iend_BE<br />
IRConstTag ::= Ico_U1 | Ico_U8 | Ico_U16 | Ico_U32 | Ico_U64 | Ico_F32 | Ico_F32i |<br />
Ico_F64 | Ico_F64i | Ico_F64i | Ico_V128 | Ico_V256<br />
Ico ::= Bool | UChar | UShort | UInt | ULong | Float | Double<br />
IRConst ::= IRConstTag Ico<br />
IRCallee ::= Int HChar void UInt<br />
IRRegArray ::= Int IRType Int<br />
IRTemp ::= UInt<br />
IROp ::= Iop_INVALID | ... | Iop_Min64Fx4<br />
IRExprTag ::= Iex_Binder | Iex_Get | Iex_GetI | Iex_RdTmp | Iex_Qop | Iex_Triop |<br />
Iex_Binop | Iex_Unop | Iex_Load | Iex_Const | Iex_Mux0X | Iex_CCall<br />
IRTriop ::= IROp IRExpr IRExpr IRExpr<br />
IRQop ::= IROp IRExpr IRExpr IRExpr IRExpr<br />
Iex ::= Binder : Int<br />
| Get : Int IRType<br />
| GetI : IRRegArray IRExpr Int<br />
| RdTmp : IRTemp<br />
| Qop : IRQop<br />
| Triop : IRTriop<br />
| Binop : IROp IRExpr IRExpr<br />
| Unop : IROp IRExpr<br />
| Load : IREndness IRType IRExpr<br />
| Const : IRConst<br />
| CCall : IRCallee IRType IRExpr<br />
| Mux0X : IRExpr IRExpr IRExpr<br />
59
A Ergänzendes Material<br />
IRExpr ::= IRExprTag Iex<br />
IRJumpKind ::= Ijk_INVALID | Ijk_Boring | Ijk_Call | Ijk_Ret | Ijk_ClientReq |<br />
Ijk_Yield | Ijk_EmWarn | Ijk_EmFail | Ijk_NoDecode | Ijk_MapFail | Ijk_TInval |<br />
Ijk_NoRedir | Ijk_SigTRAP | Ijk_SigSEGV | Ijk_SigBUS | Ijk_Sys_syscall |<br />
Ijk_Sys_int32 | Ijk_Sys_int128 | Ijk_Sys_int129 | Ijk_Sys_int130 |<br />
Ijk_Sys_sysenter<br />
IREffect ::= Ifx_None | Ifx_Read | Ifx_Write | Ifx_modify<br />
IRDirtyFxState ::= IREffect UShort UShort UChar UChar<br />
IRDirtyFxStates (n) ::= IRDirtyFxState IRDirtyFxStates (n -1)<br />
IRDirtyFxStates (0) ::=<br />
IRDirty ::= IRCallee IRExpr IRExpr IRTemp IREffect IRExpr Int Bool Int<br />
IRDirtyFxStates (N)<br />
IRCAS ::= IRTemp IRTemp IREndness IRExpr IRExpr IRExpr IRExpr IRExpr<br />
IRMBusEvent ::= Imbe_Fence | Imbe_CancelReservation<br />
IRPutI ::= IRRegArray IRExpr Int IRExpr<br />
IRStmtTag ::= Ist_NoOp | Ist_IMark | Ist_AbiHint | Ist_Put | Ist_PutI | Ist_WrTmp |<br />
Ist_Store | Ist_CAS | Ist_LLSC | Ist_Dirty | Ist_MBE | Ist_Exit<br />
Ist ::= NoOp :<br />
| IMark : Addr64 Int UChar<br />
| AbiHint : IRExpr Int IRExpr<br />
| Put : Int IRExpr<br />
| PutI : IRPutI<br />
| WrTmp : IRTemp IRExpr<br />
| Store : IREndness IRExpr IRExpr<br />
| CAS : IRCAS<br />
| LLSC : IREndness IRTemp IRExpr IRExpr<br />
| Dirty : IRDirty<br />
| MBE : IRMBusEvent<br />
| Exit : IRExpr IRConst IRJumpKind Int<br />
IRStmt ::= IRStmtTag Ist<br />
IRTypeEnv ::= IRType Int Int<br />
IRSB ::= IRTypeEnv IRStmt Int Int IRExpr IRJumpKind Int<br />
A.2 Instrumentierungsbeispiel<br />
Dieser Abschnitt enthält ein umfangreicheres Beispiel für die Instrumentierung eines Programms.<br />
Gewählt wurde tests/sample4.c aus dem FITIn-Codeverzeichnis [18]. In der<br />
linken Spalte ist die VEX IR des IRSB von main vor der Instrumentierung zu sehen, in der<br />
rechten Spalte nach der Instrumentierung durch FITIn (sichtbar durch --trace-flags=<br />
01000000 bzw. --trace-flags=00100000).<br />
Zu sehen ist, wie FITIn die Load-Operationen im Original erkennt und unmittelbar<br />
zuvor preLoadHelper einfügt. In diesem Beispiel werden Daten geladen, die mit 8 Bit kleiner<br />
sind als die Plattformadressbreite von 32 Bit. Um diese den fi_reg_flip_or_leave-<br />
Aufrufen übergeben zu können, werden davor 8Uto32-Instruktionen hinzugefügt. Die Rückgabe<br />
der Flip-Methode wird zurück in die VEX IR geführt und ersetzt das ursprüngliche<br />
Argument der nachfolgenden Instruktion.<br />
60
A.2 Instrumentierungsbeispiel<br />
------ IMark (0 x804852B , 4, 0) ------ DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
t16 = GET : I32 (24) ------ IMark (0 x804852B , 4, 0) ------<br />
t15 = Add32 ( t16 ,0 x14 : I32 ) t16 = GET : I32 (24)<br />
t17 = GET : I32 (16) t15 = Add32 (t16 ,0 x14 : I32 )<br />
STle ( t15 ) = t17 t17 = GET : I32 (16)<br />
PUT (68) = 0 x804852F : I32 STle ( t15 ) = t17<br />
------ IMark (0 x804852F , 4, 0) ------ PUT (68) = 0 x804852F : I32<br />
IR - NoOp DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
IR - NoOp ------ IMark (0 x804852F , 4, 0) ------<br />
PUT (68) = 0 x8048533 : I32 PUT (68) = 0 x8048533 : I32<br />
------ IMark (0 x8048533 , 5, 0) ------ DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
t21 = Add32 ( t16 ,0 x11 : I32 ) ------ IMark (0 x8048533 , 5, 0) ------<br />
t24 = LDle : I8 ( t21 ) t21 = Add32 ( t16 ,0 x11 : I32 )<br />
t23 = 8 Uto32 ( t24 ) t58 = DIRTY 1: I1 ::: preLoadHelper [ rp =3]{0<br />
------ IMark (0 x8048538 , 1, 0) ------ x38025b80 }(0 x382A9740 : I32 , t21 ,0 x1 : I32 )<br />
t25 = Add32 ( t23 ,0 x1 : I32 ) t24 = LDle : I8 ( t21 )<br />
IR - NoOp t59 = 8 Uto32 ( t24 )<br />
IR - NoOp t60 = DIRTY 1: I1 ::: fi_reg_flip_or_leave [ rp<br />
IR - NoOp =3]{0 x38026ff0 }(0 x382A9740 : I32 , t59 , t58 )<br />
IR - NoOp t23 = 8 Uto32 ( t60 )<br />
IR - NoOp DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
PUT (68) = 0 x8048539 : I32 ------ IMark (0 x8048538 , 1, 0) ------<br />
------ IMark (0 x8048539 , 4, 0) ------ t25 = Add32 ( t23 ,0 x1 : I32 )<br />
t32 = Add32 ( t16 ,0 x12 : I32 ) PUT (68) = 0 x8048539 : I32<br />
STle ( t32 ) = t25 DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
PUT (68) = 0 x804853D : I32 ------ IMark (0 x8048539 , 4, 0) ------<br />
------ IMark (0 x804853D , 5, 0) ------ t32 = Add32 ( t16 ,0 x12 : I32 )<br />
t35 = Add32 ( t16 ,0 x13 : I32 ) STle ( t32 ) = t25<br />
t38 = LDle : I8 ( t35 ) PUT (68) = 0 x804853D : I32<br />
t37 = 8 Uto32 ( t38 ) DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
PUT (16) = t37 ------ IMark (0 x804853D , 5, 0) ------<br />
PUT (68) = 0 x8048542 : I32 t35 = Add32 (t16 ,0 x13 : I32 )<br />
------ IMark (0 x8048542 , 5, 0) ------ t61 = DIRTY 1: I1 ::: preLoadHelper [ rp =3]{0<br />
t39 = Add32 ( t16 ,0 x12 : I32 ) x38025b80 }(0 x382A9740 : I32 , t35 ,0 x1 : I32 )<br />
t42 = LDle : I8 ( t39 ) t38 = LDle : I8 ( t35 )<br />
t41 = 8 Uto32 ( t42 ) t62 = 8 Uto32 ( t38 )<br />
------ IMark (0 x8048547 , 2, 0) ------ t63 = DIRTY 1: I1 ::: fi_reg_flip_or_leave [ rp<br />
t7 = Add32 ( t41 , t37 ) =3]{0 x38026ff0 }(0 x382A9740 : I32 , t62 , t61 )<br />
PUT (40) = 0 x3 : I32 t37 = 8 Uto32 ( t63 )<br />
PUT (44) = t41 PUT (16) = t37<br />
PUT (48) = t37 PUT (68) = 0 x8048542 : I32<br />
PUT (52) = 0 x0 : I32 DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
PUT (8) = t7 ------ IMark (0 x8048542 , 5, 0) ------<br />
PUT (68) = 0 x8048549 : I32 t39 = Add32 (t16 ,0 x12 : I32 )<br />
------ IMark (0 x8048549 , 4, 0) ------ t64 = DIRTY 1: I1 ::: preLoadHelper [ rp =3]{0<br />
t43 = Add32 ( t16 ,0 x13 : I32 ) x38025b80 }(0 x382A9740 : I32 , t39 ,0 x1 : I32 )<br />
t45 = GET : I8 (8) t42 = LDle : I8 ( t39 )<br />
61
A Ergänzendes Material<br />
STle ( t43 ) = t45 t65 = 8 Uto32 ( t42 )<br />
PUT (68) = 0 x804854D : I32 t66 = DIRTY 1: I1 ::: fi_reg_flip_or_leave [ rp<br />
------ IMark (0 x804854D , 5, 0) ------ =3]{0 x38026ff0 }(0 x382A9740 :I32 , t65 , t64 )<br />
t46 = Add32 ( t16 ,0 x12 : I32 ) t41 = 8 Uto32 ( t66 )<br />
t49 = LDle : I8 ( t46 ) DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
t48 = 8 Uto32 ( t49 ) ------ IMark (0 x8048547 , 2, 0) ------<br />
PUT (8) = t48 t7 = Add32 ( t41 , t37 )<br />
------ IMark (0 x8048552 , 3, 0) ------ PUT (40) = 0 x3 : I32<br />
t51 = GET : I8 (8) PUT (44) = t41<br />
t50 = 8 Sto32 ( t51 ) PUT (48) = t37<br />
PUT (8) = t50 PUT (52) = 0 x0 : I32<br />
PUT (68) = 0 x8048555 : I32 PUT (8) = t7<br />
------ IMark (0 x8048555 , 4, 0) ------ PUT (68) = 0 x8048549 : I32<br />
t52 = Add32 ( t16 ,0 x4 : I32 ) DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
STle ( t52 ) = t50 ------ IMark (0 x8048549 , 4, 0) ------<br />
PUT (68) = 0 x8048559 : I32 t43 = Add32 (t16 ,0 x13 : I32 )<br />
------ IMark (0 x8048559 , 7, 0) ------ t45 = GET : I8 (8)<br />
STle ( t16 ) = 0 x8048600 : I32 STle ( t43 ) = t45<br />
PUT (68) = 0 x8048560 : I32 PUT (68) = 0 x804854D : I32<br />
------ IMark (0 x8048560 , 5, 0) ------ DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
t55 = Sub32 ( t16 ,0 x4 : I32 ) ------ IMark (0 x804854D , 5, 0) ------<br />
PUT (24) = t55 t46 = Add32 ( t16 ,0 x12 : I32 )<br />
STle ( t55 ) = 0 x8048565 : I32 t67 = DIRTY 1: I1 ::: preLoadHelper [ rp =3]{0<br />
PUT (68) = 0 x80482F0 : I32 ; exit - Call x38025b80 }(0 x382A9740 : I32 , t46 ,0 x1 : I32 )<br />
t49 = LDle : I8 ( t46 )<br />
t68 = 8 Uto32 ( t49 )<br />
t69 = DIRTY 1: I1 ::: fi_reg_flip_or_leave [ rp<br />
=3]{0 x38026ff0 }(0 x382A9740 : I32 , t68 , t67 )<br />
t48 = 8 Uto32 ( t69 )<br />
PUT (8) = t48<br />
DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
------ IMark (0 x8048552 , 3, 0) ------<br />
t51 = GET : I8 (8)<br />
t50 = 8 Sto32 ( t51 )<br />
PUT (8) = t50<br />
PUT (68) = 0 x8048555 : I32<br />
DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
------ IMark (0 x8048555 , 4, 0) ------<br />
t52 = Add32 ( t16 ,0 x4 : I32 )<br />
STle ( t52 ) = t50<br />
PUT (68) = 0 x8048559 : I32<br />
DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
------ IMark (0 x8048559 , 7, 0) ------<br />
STle ( t16 ) = 0 x8048600 : I32<br />
PUT (68) = 0 x8048560 : I32<br />
DIRTY 1: I1 ::: incrInst {0 x38026300 }()<br />
------ IMark (0 x8048560 , 5, 0) ------<br />
t55 = Sub32 ( t16 ,0 x4 : I32 )<br />
t70 = GET : I32 (24)<br />
62
A.2 Instrumentierungsbeispiel<br />
DIRTY 1: I1 ::: VG_ ( unknown_SP_update )[ rp<br />
=3]{0 x3803dc20 }( t70 , t55 ,0 xE18 : I32 )<br />
PUT (24) = t55<br />
STle ( t55 ) = 0 x8048565 : I32<br />
PUT (68) = 0 x80482F0 : I32 ; exit - Call<br />
63
B FITIn<br />
In diesem Anhangskapitel wird auf die Verwendung von FITIn aus Benutzersicht eingegangen.<br />
B.1 Benutzung<br />
Der erste Schritt zur Verwendung von FITIn besteht im Heranziehen des Programmquellcodes<br />
des zu untersuchenden Programms. Der Quellcode muss in C oder C++ vorliegen.<br />
Ist Valgrind mit FITIn vollständig installiert, so ist an geeigneter Stelle die Header-Datei<br />
valgrind/fi_client.h aus dem Installationsverzeichnis des include-Ordners von Valgrind<br />
einzubinden. Mit dieser Header-Datei stehen nun folgende, unterstützte C-Makros<br />
bereit:<br />
• FITIN_MONITOR_VARIABLE(var): FITIn wird dazu aufgefordert, die Verwendungen<br />
der Variablen var zu zählen und auf dieser gegebenenfalls einen Bit-Flip durchzuführen.<br />
Schweigend ignoriert werden Variablen vom Typ float und double. Nicht<br />
akzeptiert werden konstante Werte.<br />
• FITIN_MONITOR_MEMORY(mem, size): Anstatt einer Variablen kann FITIn die Startadresse<br />
mem und die Bereichsgröÿe size auch direkt mitgeteilt werden. Dies kann<br />
beispielsweise bei wachsenden Arrays sinnvoll sein, die zum Zeitpunkt des Makros<br />
noch nicht vollständig benannt werden können, jedoch nur sofern die Startadresse<br />
des Heap-Speicherblocks nicht verändert wird.<br />
• FITIN_UNMONITOR_VARIABLE(var): Dieses Makro fordert FITIn dazu auf, die bezeichnete<br />
Variable var nicht länger zu betrachten.<br />
• FITIN_UNMONITOR_MEMORY(mem, size): Analog hat FITIn mit diesem Makro aufzuhören,<br />
für die Startadresse mem zu zählen, die zuvor mit der Gröÿe size angegeben<br />
war.<br />
Weitere wichtige Verwendungshinweise zu den Makros:<br />
• Für die Erkennung aller Zugrie einer Variablen sollte das Makro immer unmittelbar<br />
auf die Deklaration derselben folgen.<br />
• Mehrere Makro-Verwendungen auf dieselbe Variable oder dieselbe Kombination von<br />
Startadresse und Gröÿe verhalten sich idempotent. Nur wenn festgestellt wird, dass<br />
die angegebene Ursprungsgröÿe gröÿer als die zuvor notierte ist, wird diese Information<br />
intern aktualisiert.<br />
• Soll FITIn weitere als die SBs einer einzigen Unterprozedur beobachten, sollten mit<br />
Rücksicht auf das Stack-Verhalten auf MONITOR-Makros innerhalb einer Prozedur<br />
stets UNMONITOR-Anweisungen erfolgen.<br />
• Der Benutzer hat sicherzustellen, dass das Programm an der Adresse der zu zählenden<br />
Variablen über Schreibrechte verfügt.<br />
Im Anschluss an die Ergänzung der Makros kann der Code kompiliert werden. Für eine<br />
möglichst einwandfreie Benutzung unter FITIn wird empfohlen, auf Optimierungsstufen zu<br />
verzichten, Code-Inlining zu deaktivieren und Debug-Symbole hinzuzufügen.<br />
Das Programm kann nun von Valgrind für FITIn gestartet werden:<br />
65
B FITIn<br />
$ valgrind -- tool = fitin < Optionen > < Programmpfad ><br />
Zur Steuerung von FITIn ist aus folgenden Kommandozeilenoptionen zu wählen:<br />
Option Werte Standard Beschreibung<br />
--fnname= Funktionsname main Der Name der Prozedur, die von<br />
FITIn beobachtet werden soll,<br />
falls Valgrind die SBs dieser korrekt<br />
zuordnen kann.<br />
--include= Pfadname - Der Ordner des Quellcodes, welcher<br />
(in Teilen) zur Erzeugung<br />
von Debug-Symbolen im Zielprogramm<br />
benutzt wurde. Alle<br />
aundbaren SBs dieses Ordners<br />
werden von FITIn beobachtet.<br />
--mod-load-time= unsigned long 1 Der Zeitpunkt des Zugris auf<br />
eine beobachtete Variable, zu<br />
dem ein Bit-Flip durchgeführt<br />
wird.<br />
--mod-bit= unsigned char 0 Das Bit, beginnend bei 0 als niederwertigstes<br />
Bit, das invertiert<br />
wird.<br />
--inst-limit= unsigned long 0 Die Anzahl von Programminstruktionen,<br />
die höchstens auszuführen<br />
ist, bevor das Programm<br />
abgebrochen wird. 0 bedeutet<br />
unbegrenzt.<br />
--golden-run= yes, no no Ist ein Golden Run gewählt,<br />
wird keine Fehlerinjektion vorgenommen.<br />
--persist-flip= yes, no no Wird diese Option aktiviert,<br />
wird ein Bit-Flip nicht nur auf<br />
dem potentiell üchtigen Wert<br />
vorgenommen, sondern auch an<br />
seiner Herkunftsadresse.<br />
--verbose - (nicht gesetzt) Diese Option erhöht die Ausgabe<br />
von hilfreichen Informationen<br />
zur Ausführung durch Valgrind<br />
und FITIn. Eine mehrfache<br />
Verwendung erhöht das<br />
Ausgabelevel.<br />
Weitere wichtige Verwendungshinweise zu den Kommandozeilenoptionen:<br />
• --fnname= und --include= können nur exklusiv verwendet werden.<br />
• Wird bei --mod-bit= ein Bit gewählt, das auÿerhalb des geladenen Speicherbereichs<br />
liegt, wird die Injektion verworfen. Ist --persist-flip=yes gesetzt, wird ermittelt,<br />
ob ein Bit-Flip im Prozessspeicher vorgenommen werden kann. Die Benutzung von<br />
--verbose erlaubt einen Hinweis auf den Erfolg dieses Verhaltens.<br />
66
B.2 Problembehandlung<br />
• Valgrind stellt weitere Optionen zur Verfügung. Diese können über --help eingesehen<br />
werden.<br />
Für die Wahl des Fehlerzeitpunkts kann der Benutzer ausgehend vom Programmquellcode<br />
eine eingeschränkt präzise Zählweise anwenden. Zu beachten ist, dass der Zählung<br />
für alle mit einem Makro behafteten Variablen für die gesamte Ausführungszeit gilt. Die<br />
Faustregel lautet, unter Berücksichtigung des Kontrollusses im Quellcode die Anzahl von<br />
lesenden Verwendungen der Variablen zu zählen. Dazu einige Hinweise:<br />
• Bedeutet in einer Prozedur die Rückgabe einer beobachteten Variablen mittels return,<br />
dass diese in ein Register abgelegt wird (z.B. EAX auf x86), zählt dies nicht als Zugri.<br />
• Das Kopieren an eine andere Adresse zählt als einfacher Zugri. Die Verwendung der<br />
Kopie im folgenden Programmablauf wird von FITIn jedoch nicht berücksichtigt.<br />
• Das Zurückschreiben eines Wertes an seine Herkunftsadresse zählt nicht als Zugri.<br />
Dieser Fall sollte nur durch Unachtsamkeit in handgeschriebenem Inline-Assemblercode<br />
vorkommen.<br />
• In anonymen Ausdrücken zählt nur die tatsächliche Anzahl von Lesezugrien auf<br />
eine Variable.<br />
• Wird auf eine Variable abweichend von der Startadresse zugegrien, etwa über eine<br />
frühere oder spätere Adresse (z.B. int a, b; ... ; ((char*)&a) + b;), kann<br />
FITIn diesen Zugri nicht erkennen.<br />
Beispiele für Quellcode, Aufrufparameter und die Zählweise können dem Abschnitt 4.1<br />
entnommen werden.<br />
B.2 Problembehandlung<br />
Als Problem wird in diesem Abschnitt ein ausbleibendes oder ein anderes als das erwartete<br />
Verhalten bezüglich der Fehlerinjektion verstanden. So sei im Folgenden eine Liste von<br />
Punkten gegeben, die der Benutzer durchgehen sollte, wenn FITIn nicht das gewünschte<br />
Ergebnis liefert:<br />
1. Sind alle Hinweise aus Abschnitt B.1 berücksichtigt worden?<br />
2. Gibt die Verwendung von --verbose weitere Hinweise auf Besonderheiten?<br />
3. Ist das Programm ohne aggressive Optimierungen kompiliert worden? Unter GCC<br />
können Optimierungen mit der Option -O0 deaktiviert werden.<br />
4. Es sollte geprüft werden, ob der Compiler möglicherweise zusätzliche Instruktionen<br />
generiert hat oder ob eine komplexere Instruktion als erwartet verwendet worden ist.<br />
Die GCC-Option zur Ausgabe des Assemblercodes einer C-Datei lautet -S. Daraufhin<br />
wird eine Datei mit der Endung .s erstellt, die den Assemblercode enthält. So<br />
können neue Erkenntnisse über die tatsächliche Anzahl von Zugrien gewonnen und<br />
die Zählweise korrigiert werden.<br />
5. Valgrind kann mit der Kommandozeilenoption --trace-flags=00100000 gestartet<br />
werden, um zusätzliche Einblicke in die durchlaufenen SBs eines Programms zu ermöglichen.<br />
6. Erscheinen die Prozeduren, die mittels --fnname= oder --include= von FITIn zu<br />
beobachten sind, in der SB-Liste von --trace-flags=00100000 in der zweiten Spalte<br />
von rechts? Ist dies nicht der Fall und werden viele UNKNOWN_FUNCTION für das<br />
Benutzerprogramm angezeigt, so sollte sichergestellt werden, dass das Programm<br />
Debug-Symbole enthält. Für den GCC sollten dabei die Optionen -g oder sogar -g3<br />
gewählt sein.<br />
67
B FITIn<br />
7. Erscheinen die Prozeduren, die mittels --fnname= oder --include= von FITIn zu<br />
beobachten sind, in der SB-Liste von --trace-flags=00100000 in der zweiten Spalte<br />
von rechts? Ist dies nicht vollständig der Fall oder werden überwiegend main für das<br />
Benutzerprogramm angezeigt, so sollte sichergestellt werden, dass der Compiler kein<br />
Code-Inlining durchführt. Die GCC-Option dazu lautet -fno-inline.<br />
8. Erscheinen die Prozedureinträge, die mittels --fnname= von FITIn zu beobachten<br />
sind, in der SB-Liste von --trace-flags=00100000 in der zweiten Spalte von rechts?<br />
Ist dies der Fall, sollte bei der Verwendung geprüft werden, ob sich die SB-Bezeichung<br />
und die Angabe von --fnname= decken: Möglicherweise ist ebenfalls die Aufrufsignatur<br />
anzugeben. Bei C++-Quellcode können Namen von Instanz- und Klassenmethoden<br />
zusätzlich dekoriert sein, was bei der Angabe ebenfalls zu berücksichtigen ist.<br />
Zu ignorieren ist jedoch das nummerische Sux, das dem Prozedurnamen eines SB<br />
angehängt werden kann.<br />
9. Wie verändert sich das Verhalten von FITIn, wenn das Programm für eine andere<br />
Plattform kompiliert wird? Auf AMD64 können in der Regel auch x86-Programme<br />
ausgeführt werden. Für den GCC kann das Ziel durch die Angabe von -m32 zu einem<br />
32 Bit-Programm kompiliert werden.<br />
10. Wie weicht die statistische Ausgabe von FITIn am Ende der Ausführung von der<br />
Erwartung ab?<br />
11. Kann durch eine Reduzierung der beobachteten Variablen und Prozeduren ein besseres<br />
Ergebnis erzielt werden?<br />
Für einen Benutzer mit Kenntnissen über VEX IR erlaubt Valgrind weitere Wege zur<br />
Analyse: Durch die Kombination der Angaben --trace-notbelow=, --trace-notabove=<br />
(jeweils mit Angaben der Nummern zu den zu untersuchenden SBs) und den --trace<br />
-flags=-Bitmustern 10000000 (Binärcode zu VEX IR), 01000000 (VEX IR, die FITIn<br />
erhält) und 00100000 (VEX IR nach der Instrumentierung) kann der Anwender die Übersetzung<br />
der ursprünglichen Instruktionen bis zur Instrumentierung der VEX IR durch<br />
FITIn verfolgen. Dieser kann sich nun mit den nachfolgenden Fragen auseinandersetzen:<br />
12. Lädt die Ladeoperation, die auf einen preLoadHelper einer beobachteten Variablen<br />
folgt, einen Datentypen, der von FITIn noch nicht unterstützt wird oder gröÿer als<br />
die Plattformadressbreite ist?<br />
13. Folgen nach dem Ladevorgang von beobachteten Variablen bei Zugrien auf diese<br />
Platzierungen von fi_reg_flip_or_leave und fi_reg_flip_or_leave_before_<br />
store semantisch eventuell zu früh, zu oft oder gar nicht?<br />
14. Folgt dem Ladevorgang einer beobachteten Variablen eine Typenkonvertierung vor<br />
der anschlieÿenden Verwendung?<br />
15. Wird ein beobachteter Wert in die Registerschattentabelle abgelegt und ist dieser Vorgang<br />
mit fi_reg_set_occupancy_origin instrumentiert worden? Wird das Auslesen<br />
des Registers an nachfolgender Stelle mit demselben Datentyp durchgeführt, der zum<br />
Beschreiben benutzt wurde?<br />
16. Verwendet Valgrind eigene Hilfsmethoden, die möglicherweise versetzt von der Startadresse<br />
auf eine Variable zugreifen oder undurchsichtige Zugrie auf die Registerschattentabelle<br />
tätigen?<br />
Ist nach wie vor nicht zu erkennen, warum ein konkretes Problem besteht, sollte sich<br />
der Benutzer mit allen gewonnenen Erkenntnissen an den zuständigen Entwickler wenden<br />
oder den Quellcode von FITIn studieren.<br />
68