30.08.2014 Aufrufe

Paper (PDF) - STS - TUHH

Paper (PDF) - STS - TUHH

Paper (PDF) - STS - TUHH

MEHR ANZEIGEN
WENIGER ANZEIGEN

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

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!