SWTPP Code Quality
SWTPP Code Quality
[Link]
[Link] ist eine Reihe, die Theorie und
Praxis aus allen Bereichen der Informatik für
die Hochschulausbildung vermittelt.
Dirk W. Hoffmann
Software-Qualität
2., aktualisierte und korrigierte Auflage
Dirk W. Hoffmann
Hochschule Karlsruhe
Karlsruhe, Deutschland
ISSN 1614-5216
ISBN 978-3-642-35699-5 ISBN 978-3-642-35700-8 (eBook)
DOI 10.1007/978-3-642-35700-8
Springer Vieweg
© Springer-Verlag Berlin Heidelberg 2008, 2013
Dieses Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt. Jede Verwertung, die nicht
ausdrücklich vom Urheberrechtsgesetz zugelassen ist, bedarf der vorherigen Zustimmung des Verlags.
Das gilt insbesondere für Vervielfältigungen, Bearbeitungen, Übersetzungen, Mikroverfilmungen und
die Einspeicherung und Verarbeitung in elektronischen Systemen.
Die Wiedergabe von Gebrauchsnamen, Handelsnamen, Warenbezeichnungen usw. in diesem Werk be-
rechtigt auch ohne besondere Kennzeichnung nicht zu der Annahme, dass solche Namen im Sinne der
Warenzeichen- und Markenschutz-Gesetzgebung als frei zu betrachten wären und daher von jedermann
benutzt werden dürften.
Springer Vieweg ist eine Marke von Springer DE. Springer DE ist Teil der Fachverlagsgruppe Springer
Science+Business Media
[Link]
Vorwort
v
vi Vorwort
Bevor wir in die Tiefen der verschiedenen Methoden und Verfahren der Software-
Qualitätssicherung eintauchen, möchte ich an dieser Stelle all denjenigen meinen
Dank aussprechen, die mich bei der Durchführung dieses Projekts unterstützt und
damit maßgeblich zum Gelingen dieses Buches beigetragen haben. Besonders er-
wähnen möchte ich Herrn Rüdiger Brünner. Seine zahlreichen Hinweise zur ersten
Auflage haben es mir ermöglicht, die vor Ihnen liegende Neuauflage an vielen Stel-
len zu verbessern.
1 Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1 Aufbruch in das ubiquitäre Computerzeitalter . . . . . . . . . . . . . . . . . . . 1
1.2 Was ist Software-Qualität? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.3 Warum ist Software eigentlich so schlecht? . . . . . . . . . . . . . . . . . . . . . 12
1.4 Gibt es Licht am Ende des Tunnels? . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.1 Produktqualität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.4.2 Prozessqualität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
2 Software-Fehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.1 Lexikalische und syntaktische Fehlerquellen . . . . . . . . . . . . . . . . . . . . 27
2.2 Semantische Fehlerquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.3 Parallelität als Fehlerquelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41
2.4 Numerische Fehlerquellen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 44
2.5 Portabilitätsfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
2.6 Optimierungsfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.7 Von tickenden Zeitbomben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.8 Spezifikationsfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.9 Nicht immer ist die Software schuld . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.10 Fehlerbewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61
3 Konstruktive Qualitätssicherung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.1 Software-Richtlinien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65
3.1.1 Notationskonventionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.1.2 Sprachkonventionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.2 Typisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
3.2.1 Typsysteme . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
3.2.2 Grenzen der Typisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81
3.3 Vertragsbasierte Programmierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.3.1 Vor- und Nachbedingungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
3.3.2 Invarianten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
3.3.3 Zusicherungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
vii
viii Inhaltsverzeichnis
4 Software-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
4.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
4.2 Testklassifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 158
4.2.1 Prüfebenen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 159
4.2.2 Prüfkriterien . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170
4.2.3 Prüftechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 173
4.3 Black-Box-Testtechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
4.3.1 Äquivalenzklassentest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 175
4.3.2 Grenzwertbetrachtung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
4.3.3 Zustandsbasierter Software-Test . . . . . . . . . . . . . . . . . . . . . . . . 183
4.3.4 Use-Case-Test . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
4.3.5 Entscheidungstabellenbasierter Test . . . . . . . . . . . . . . . . . . . . . 190
4.3.6 Paarweises Testen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 192
4.3.7 Diversifizierende Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
4.4 White-Box-Testtechniken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 200
4.4.1 Kontrollflussmodellierung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 202
4.4.2 Anweisungsüberdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206
4.4.3 Zweigüberdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 209
4.4.4 Pfadüberdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 210
4.4.5 Bedingungsüberdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 214
4.4.6 McCabe-Überdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 216
4.4.7 Defs-Uses-Überdeckung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
4.4.8 Required-k-Tupel-Überdeckung . . . . . . . . . . . . . . . . . . . . . . . . 227
4.5 Testmetriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
4.5.1 Überdeckungsmetriken . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
4.5.2 Mutationstest . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
4.6 Grenzen des Software-Tests . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 243
6 Software-Verifikation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
6.1 Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 333
6.2 Deduktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
6.2.1 Vor- und Nachbedingungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 338
6.2.2 Das Hoare-Kalkül . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 342
6.3 Modellprüfung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
6.3.1 Temporallogik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 352
6.3.2 Verifikation temporaler Eigenschaften . . . . . . . . . . . . . . . . . . . 356
6.4 Abstrakte Interpretation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 361
6.4.1 Fixpunktiteration nach Floyd, Park und Clarke . . . . . . . . . . . . 362
6.4.2 Datenabstraktion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
7 Software-Lebenszyklus . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
7.1 Wenn Software altert . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 371
7.2 Gründe der Software-Alterung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
7.2.1 Bewegliche Ziele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
7.2.2 Auch Software setzt an . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 380
7.2.3 Kaschieren statt Reparieren . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
7.2.4 Rückwärtskompatibilität . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
7.2.5 Wissen ist flüchtig . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 391
7.3 Ist die Software-Alterung unumgänglich? . . . . . . . . . . . . . . . . . . . . . . 395
7.3.1 Refactoring . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 396
7.3.2 Redesign . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 406
8 Software-Infrastruktur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 415
8.1 Versionsverwaltung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 417
8.1.1 Anforderungen und Konzeption . . . . . . . . . . . . . . . . . . . . . . . . 419
8.1.2 Revisionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 423
8.1.3 Entwicklung in großen Teams . . . . . . . . . . . . . . . . . . . . . . . . . . 428
x Inhaltsverzeichnis
9 Managementprozesse . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 491
9.1 Vorgehensmodelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 493
9.1.1 Wasserfallmodell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 493
9.1.2 V-Modell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 496
9.1.3 Rational Unified Process . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 502
9.1.4 Extreme Programming . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 506
9.2 Reifegradmodelle . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 514
9.2.1 Historische Entwicklung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 515
9.2.2 CMM . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 519
9.2.3 CMMI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 530
9.2.4 ISO 15504 (SPICE) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 535
9.2.5 Bewertung und Kritik . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 540
Literaturverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 547
Sachverzeichnis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 557
Abkürzungsverzeichnis
xi
xii Abkürzungsverzeichnis
meinen Trend nicht verwehren. Ob wir es wollen oder nicht, die Technisierung
schreitet in großen Schritten weiter voran.
Gateway
Elektronik, die uns in die Werkstatt zwingt. Setzt sich die Entwicklung in der ein-
geschlagenen Richtung fort, so wird bereits im Jahre 2010 jede zweite Autopanne
ihre Ursache im Versagen der Elektronik haben. Beschränken wir die Betrachtung
an dieser Stelle auf das technikverwöhnte Segment der Oberklassewagen, so ist die
Elektronik bereits heute die Fehlerquelle Nummer eins. Mit anderen Worten: Jede
zweite Panne eines modernen Oberklassewagens geht inzwischen auf das Versagen
der Bordelektronik zurück – ein Problem, das nicht nur die Fahrzeugführer, sondern
auch die Automobilhersteller in zunehmendem Maße in Bedrängnis bringt.
Obwohl die Automobilindustrie mit Hochdruck an Lösungen arbeitet, machen
zahlreiche Rückrufaktionen der letzten Zeit immer wieder deutlich, dass es mit
1.1 Aufbruch in das ubiquitäre Computerzeitalter 5
der Zuverlässigkeit moderner Kfz-Elektronik noch immer nicht zum Besten steht.
Sollte es an dieser Stelle nicht gelingen, die Zuverlässigkeit zukünftiger Systeme
deutlich zu erhöhen, ist es mehr als fraglich, ob sich das elektronische Wettrüsten
im Bereich der Kraftfahrzeugelektronik in der bestehenden Form fortsetzen lässt.
Zukunftsweisende Konzepte wie die Steer-by-Wire-Technologie, die automatische
Vollbremsung oder die autonome Querregelung bedingen eine Zuverlässigkeit, die
viele der heutigen IT-Systeme nicht zu leisten im Stande sind. Der Qualitätssiche-
rung wird daher in Zukunft noch eine deutlich größere Rolle zukommen, als sie
bereits heute innehat.
Doch welche Maßnahmen der Qualitätssicherung sind notwendig und überhaupt
sinnvoll? Zur Beantwortung dieser Frage kommen wir nicht umhin, den eigentli-
chen Ursachen der Misere auf den Grund zu gehen. Die historische Entwicklung
der Automobilelektronik liefert uns an dieser Stelle erste Gründe, warum es heu-
te um viele computergestützte Systeme nicht zum Besten steht. Betrachten wir die
Komplexitätsentwicklung typischer Kfz-Steuergeräte über die Zeit, so nehmen zwei
langfristige Trends klare Konturen an:
I Die Systemkomplexität steigt dramatisch an. Schon lange sind die Zeiten vor-
bei, in denen ein Projekt von einem einzigen Programmierer alleine und völlig
selbstständig zum Erfolg gebracht werden kann. Die hohe Systemkomplexität
zwingt uns heute dazu, in großen Arbeitsgruppen zu entwickeln. Projekte, in
denen einen Vielzahl von Hardware- und Software-Ingenieuren abteilungs- und
sogar firmenübergreifend zusammenarbeiten, sind heute allgegenwärtig. In ent-
sprechender Weise ist auch das Know-how nicht mehr länger auf ein paar wenige
Entwickler konzentriert, sondern fast vollständig dezentralisiert. Die Folgen sind
an dieser Stelle nicht nur positiver Natur – insbesondere gibt es in größeren Pro-
jekten heute niemanden mehr, der sämtliche Aspekte eines Systems vollständig
versteht oder gar jemals verstehen könnte.
Die entstehenden Probleme sind kein singuläres Problem der Software-
Industrie und treten ähnlich gelagert in allen Bereichen auf, in denen große
Projektgruppen zu koordinieren sind. Nichtsdestotrotz kommen im Bereich der
Software-Entwicklung mehrere Faktoren erschwerend hinzu. Anders als z. B. in
den klassischen Ingenieur-Disziplinen ist das Problem der adäquaten Zeit- und
Kostenschätzung im Bereich der Software-Entwicklung immer noch ungelöst.
zu denken war. Schnell boten die entstandenen Systeme die Leistung der Groß-
computer von Einst und in dem gleichen Umfang wuchs auch die Größe der
verarbeiteten Programme. Bereits seit einigen Jahren übersteigt der Aufwand für
die Entwicklung der Software-Komponenten die der Hardware-Komponenten er-
heblich. Tragischerweise fällt der Software in diesem Zusammenhang eine zwie-
spältige Rolle zu. Genauso wie der Großteil der Funktionalität aktueller Systeme
ohne Software nicht zu realisieren wäre, ist es heute die Software, die für die
Mehrzahl der Fehler verantwortlich zeichnet. Mit anderen Worten: Software ist
zur Fehlerquelle Nummer eins geworden.
Aber warum ist die Qualität vieler Software-Systeme eigentlich so schlecht? Und
viel wichtiger noch: Stehen wir der Misere mittellos gegenüber? Welche Methoden
und Techniken stehen uns heute zur Verfügung, die Qualität komplexer Software-
Systeme sicherzustellen oder zu erhöhen?
Die Beantwortung dieser Fragen wirft zwangsläufig eine weitere auf: Was genau
ist eigentlich Software-Qualität? Den Begriff der Zuverlässigkeit haben wir bereits
weiter oben ins Spiel gebracht und eine enge Verknüpfung mit dem Begriff der
Software-Qualität liegt auf der Hand. Qualität mit Zuverlässigkeit gleichzusetzen
wäre jedoch bei weitem zu kurz gedacht. Bevor wir uns also den verschiedenen
Spielarten der modernen Software-Qualitätssicherung zuwenden, werden wir im
nächsten Abschnitt den Begriff der Software-Qualität in all seinen verschiedenen
Facetten näher beleuchten.
Funktionalität Übertragbarkeit
Zuverlässigkeit Änderbarkeit
Ef$zienz Testbarkeit
Benutzbarkeit Transparenz
Bausteine der
kundenorientiert Software-Qualität herstellerorientiert
I Laufzeit (Performance)
Jedes Software-System unterliegt gewissen Laufzeitanforderungen, auch wenn
diese nicht immer explizit in der Spezifikation vermerkt sind. Für etliche Appli-
kationstypen stellt die Einhaltung dieser Anforderungen keine ernstzunehmende
Herausforderung dar und beschränkt sich mitunter auf die Etablierung interakti-
ver Mensch-Maschine-Dialoge. In einem ganz anderen Licht erscheint die Situa-
tion im Bereich der Echtzeitsysteme. Hier unterliegen alle Operationen Zeitanfor-
derungen, die entweder im statistischen Mittel (weiche Echtzeit) oder in jedem
Einzelfall (harte Echtzeit) zwingend erfüllt werden müssen. Die Kriterien Lauf-
zeit und Funktionalität sind in diesen Fällen eng miteinander verwoben und von
gleichrangiger Bedeutung.
8 1 Einführung
I Zuverlässigkeit (Reliability)
Software ist heute in vielen sicherheitskritischen Anwendungen im Einsatz. Die
Bordelektronik unseres Pkws, die Steer-by-Wire-Technik in der Avionik oder
die robotergestützte Chirurgie sind nur wenige Beispiele. Jedes Systemversagen
kann sich in diesen Bereichen unmittelbar auf die Unversehrtheit der beteiligten
Personen auswirken, so dass die Zuverlässigkeit in diesen Bereichen die zen-
trale Rolle spielt. Wie wir später herausarbeiten werden, ist das Kriterium der
Zuverlässigkeit mit den meisten anderen Kriterien stark gekoppelt. Mit anderen
Worten: Eine hohe Systemzuverlässigkeit kann nicht singulär erreicht werden,
sondern bedingt die Optimierung einer ganzen Reihe anderer Kriterien.
I Benutzbarkeit (Usability)
Der Begriff der Benutzbarkeit subsumiert alle Eigenschaften eines Systems, die
sich mit der Mensch-Maschine-Schnittstelle und damit in direkter Weise mit der
Benutzerinteraktion befassen. Waren die Benutzungsschnittstellen früherer Com-
putersysteme sowohl in ihrer Erscheinungsform als auch in ihrer Bedienbarkeit
eher spartanischer Natur, so besitzen moderne computergestützte Systeme viel-
fältige, individuell auf die jeweilige Anwendung abgestimmte Bedienungskon-
zepte. Nicht in allen Bereichen sind optimierte Benutzungsschnittstellen glei-
chermaßen von Bedeutung. Hat z. B. die Automobilindustrie höchste Anforde-
rungen in diesem Bereich zu erfüllen, besitzt das Bedienungskonzept einer nur
von wenigen Experten verwendeten Nischen-Software ein deutlich geringeres
Gewicht.
I Wartbarkeit (Maintainability)
Die Möglichkeit, Software auch nach dessen Inbetriebnahme korrigieren, mo-
difizieren und erweitern zu können, ist eine wesentliche Eigenschaft langlebi-
ger Software-Produkte. Leider entpuppt sich insbesondere die Wartung des Pro-
grammcodes als ein in der Praxis häufig unterschätztes Problem. In vielen Fällen
hört die Projektplanung mit der Inbetriebnahme eines Software-Systems auf und
ignoriert damit die Notwendigkeit, Software-Fehler nachträglich zu beheben. In
anderen Fällen werden Subunternehmer für die Entwicklung einzelner Software-
Komponenten beauftragt, ohne sich um die Zeit nach der Auslieferung ausrei-
chend Gedanken zu machen. Dabei ist gerade die Wartbarkeitseigenschaft eines
Software-Produkts eine der wichtigsten Voraussetzungen, um sich länger als die
Konkurrenz am Markt zu behaupten.
I Transparenz (Transparency)
Dieses Kriterium bewertet, auf welche Art und Weise die nach außen sichtbare
Programmfunktionalität intern umgesetzt wurde. Verbirgt sich unter der sichtba-
ren Benutzungsoberfläche ein geradliniges Programm, das strukturiert und mit
wenigen aufeinander aufbauenden Elementen das Problem löst? Oder steckt hin-
ter der Fassade ein programmiertes Chaos, das zwar funktional korrekt ist, einem
erfahrenen Programmierer bei näherer Betrachtung jedoch die Tränen in die Au-
gen treibt? Statistisch gesehen nimmt die Transparenz eines Software-Systems
im Zuge seiner Weiterentwicklung kontinuierlich ab. Ganz ähnlich dem zweiten
1.2 Was ist Software-Qualität? 9
I Übertragbarkeit
Hinter der Übertragbarkeit bzw. der Portierbarkeit verbirgt sich die Frage, wie
einfach eine bestehende Software in eine andere Umgebung übertragen werden
kann. Der abstrakte Begriff der Umgebung wurde an dieser Stelle bewusst ge-
wählt und steht stellvertretend für eine ganze Reihe, in der Praxis häufig auf-
tretender Übertragbarkeitsszenarien. Die Anpassung älterer 32-Bit-Programme
an moderne 64-Bit-Architekturen, die Unterstützung alternativer Betriebssys-
teme oder die Migration klassischer Desktop-Anwendung auf mobile Endgeräte
sind typische Portierungsaufgaben. Am Markt befindliche Software-Systeme un-
terscheiden sich bezüglich ihrer Übertragbarkeitseigenschaften erheblich. Lässt
sich eine sauber programmierte 32-Bit-Applikation in der Theorie ohne Ände-
rung auch auf einem 64-Bit-System übersetzen und ausführen, müssen in vielen
aktuellen Projekten Millionenbeträge für die 64-Bit-Migration nachträglich in-
vestiert werden – Kosten, die unter der Berücksichtigung elementarer Regeln der
Software-Technik niemals entstanden wären.
I Testbarkeit (Testability)
Ist die Wartbarkeit eines Software-Systems ein entscheidendes Kriterium,
Software-Fehler zeitnah zu beheben, so ist die Testbarkeit die Voraussetzung,
Fehler rechtzeitig zu erkennen. Die Testbarkeit eines Programms wird durch zwei
wesentliche Faktoren erschwert: Zum einen ist die Komplexität selbst kleiner
Programme bereits so groß, dass es schlicht unmöglich ist, die Ausgabe für alle
möglichen Eingabekombinationen zu überprüfen. Zum anderen lassen sich viele
Algorithmen überhaupt nur dann sinnvoll testen, wenn auf die internen Zustände
und Datenstrukturen des Programms von außen zugegriffen werden kann. Durch
den Black-Box-Charakter vieler Software-Systeme besteht auf diese Informatio-
nen zunächst kein Zugriff. Die zu testende Software muss aus diesem Grund um
zusätzlichen Code ergänzt werden, der Variablen und Datenstrukturen nach au-
ßen sichtbar macht (design for test). Im Bereich eingebetteter Systeme kommt
ein anderes Problem erschwerend hinzu. Wird ein Produkt für den Massenmarkt
hergestellt, werden die Hardware-Komponenten spätestens in der Endphase der
Entwicklung mit nur noch wenigen oder gar keinen Debugging-Möglichkeiten
mehr ausgestattet. Aus Kostengründen macht dieses Vorgehen durchaus Sinn, al-
lerdings wird die Behebung sehr spät erkannter Fehler hierdurch ungleich kom-
plizierter.
Die Kriterien Funktionalität, Laufzeit, Zuverlässigkeit und Benutzbarkeit sind un-
mittelbar nach außen sichtbar und bilden zusammen die Qualitätssicht des Anwen-
ders ab. Folgerichtig sind es genau diese Kriterien, die sich unmittelbar auf die
10 1 Einführung
ei t
Tr zba it
ei
e
Ü par t
i
rk
e
nu igk
z
en
ba t
rk
t
ei
W agb
Be läss
Te ark
rk
Zu eit
rtr
tb
t
r
s
z
ve
an
uf
be
st
ar
La
Funktionale Korrektheit - + + + + +
Laufzeit - - - - -
Zuverlässigkeit + +
Benutzbarkeit
Transparenz + + +
Übertragbarkeit
Wartbarkeit
Qualität
Kosten Zeit
Kriterien sind negativ korreliert, d. h., die Verbesserung eines Kriteriums kann nur
zu Lasten der anderen erfolgen. Im magischen Dreieck wird die Korrelation durch
die Forderung veranschaulicht, den Flächeninhalt des aufgespannten Dreiecks stets
konstant zu halten. Wird einer der Eckpunkte nach außen bewegt, so muss sich min-
destens einer der anderen Eckpunkte nach innen verschieben, um das Kriterium der
konstanten Fläche zu erfüllen. In manchen Darstellungen findet anstelle des magi-
schen Dreiecks ein magisches Viereck Verwendung – die Grundidee bleibt jedoch
stets die gleiche.
1994 auf 0.2 – 0.05 zurück (siehe hierzu [9]). Betrachten wir anstelle der relativen
Defekthäufigkeit jedoch die absolute Defektanzahl, so erscheinen die Ergebnisse
in einem völlig anderen Licht. Durch die dramatisch gestiegenen Programmgrößen
wird die fallende Defektdichte heute mehr als aufgezehrt.
Neben der Bekämpfung klassischer Programmierfehler sieht sich die Software-
Industrie noch mit einem ganz anderen Phänomen konfrontiert, das in anderen Inge-
nieurdisziplinen seinesgleichen sucht. Die Rede ist von dem vollständigen Scheitern
eines Projekts. Im ersten Anlauf furios missglückte Vorhaben wie die automatische
Gepäckverteilung am Flughafen Denver oder die Einführung des elektronischen
Mautsystem Toll Collect veranschaulichen das Dilemma, in dem sich weite Teile
der Software-Industrie seit längerer Zeit befinden. Die Anzahl der gescheiterten IT-
Projekte exakt zu quantifizieren, ist ein schwieriges, wenn nicht sogar unmögliches
Unterfangen. Neben öffentlichen Großprojekten, die im Falle eines Scheiterns in
der hiesigen Presse regelmäßig ein großes Echo hervorrufen, werden Jahr für Jahr
Milliarden in firmeninterne Projekte investiert, die niemals die Entwicklungsabtei-
lungen verlassen. Oft erinnern nach ein paar Jahren nur noch die Arbeitstitel an
deren Existenz.
Im Folgenden wollen wir einige der Gründe genauer untersuchen, die für die
mangelhafte Qualität heutiger Software-Systeme verantwortlich zeichnen. Der Kern
der Misere lässt sich in vielerlei Hinsicht auf einen zentralen Aspekt zurückführen –
den Aspekt der Komplexität. Die vermeintlich unbegrenzte Flexibilität, die Softwa-
re gegenüber Hardware aufweist, gestattet es dem Entwickler, jedes noch so kom-
plizierte Gedankenkonstrukt in kurzer Zeit in ein mehr oder weniger lauffähiges
Programm umzusetzen.
Auf der negativen Seite fordert die schier grenzenlose Freiheit ihren Tribut in
Form einer dramatisch ansteigenden Komplexität. Wenige Zeilen Code reichen aus,
um die Anzahl der entstehenden Ausführungspfade in eine Größenordnung zu he-
ben, die einen vollständigen Software-Test unmöglich macht. Mit anderen Worten:
Bereits für winzige Programme ist es nicht mehr möglich, das korrekte Verhalten
für alle möglichen Eingaben explizit zu überprüfen.
Erschwerend kommt hinzu, dass die durchschnittliche Software-Größe seit Jah-
ren rapide ansteigt und bereits heute gigantische Ausmaße erreicht hat. Abb. 1.5 ver-
deutlicht die Situation am Beispiel der Größenentwicklung des Linux-Kernels. Um-
fasst das komprimierte Programmarchiv von Linus Torvalds’ Ur-Kernel nur knapp
70 KiB, so erreicht der Linux-Kernel in der Version 2.6 bereits die stattliche Größe
von ca. 40 MiB – mehr als das fünfhundertfache der ersten Kernel-Version. In der
Tat geht rund die Hälfte des Größenzuwachses auf die immer zahlreicher werdenden
Gerätetreiber zurück und ist dem Kernel damit nur im weiteren Sinne zuzurechnen.
Verfolgen wir die Zunahme der Code-Größe über die Zeit, so manifestiert sich
eine fast exponentielle Wachstumskurve. Hinter diesem Zuwachs verbirgt sich mit-
nichten eine Eigenheit des Linux-Kernels, sondern ein Phänomen, das sich groß-
flächig in nahezu allen Bereichen der Software-Entwicklung beobachten lässt: Die
Größe von Software wächst seit Jahren rapide an – und mit ihr die Komplexität.
Die schiere Größe heutiger Software hat weitreichende Konsequenzen. Konn-
te ein Programm in den frühen Tagen der Computertechnik von einem einzi-
14 1 Einführung
99
Linux Kernel 0.01 96
92 94 MB
MB
MB MB
10.239 Codezeilen (LOC)
Archiv-Größe (.[Link])
0.01 0.02 2.06 0.12 0.95 1.0 1.1 1.2 2.0 2.2 2.4 2.6 3.0 3.2 3.4 3.6
Kernel-Version
gen Mitarbeiter vollständig verstanden und kontrolliert werden, lassen sich gegen-
wärtige Software-Projekte nur noch in großen Teams bewältigen. Die Software-
Entwicklung wird heute durch punktuelles Wissen diktiert und zwingt den Program-
mierer zu einem Rückzug in immer spezieller werdende Teilbereiche. Durch den in-
tensiven Einsatz moderner Konfigurationswerkzeuge ist es im Zeitalter der globalen
Vernetzung ein Leichtes, die Projektdurchführung immer weiter zu dezentralisieren.
In der Konsequenz verteilt sich das Wissen über das erstellte Produkt nicht nur auf
verschiedene Schultern, sondern auch geographisch immer weiter.
Im direkten Vergleich mit dem kurzlebigen Projektgeschäft ist die Entwick-
lung langlebiger Software-Produkte mit zusätzlichen Problemen verbunden. So sind
die Erschaffer eines solchen Produkts nur selten so „langlebig“ wie das Produkt
selbst. In den Boom-Zeiten der IT-Industrie stand ein Software-Entwickler im US-
amerikanischen Silicon Valley durchschnittlich drei Jahre auf der Gehaltsliste ein
und desselben Unternehmens. Anders formuliert bedeutet dieses Ergebnis, dass an
einem zehn Jahre alten Software-Produkt bereits die vierte Entwicklergeneration ar-
beitet. Viele Programmierer der ersten Stunde haben die Firma zu diesem Zeitpunkt
längst verlassen – und mit ihnen auch ihr Wissen. Nicht selten wird ein Software-
Modul durch den Verlust eines Mitarbeiters von einem Tag auf den anderen wertlos,
und nach mehreren Jahren der Entwicklung stehen viele Unternehmen der undank-
baren Aufgabe gegenüber, die Interna der firmeneigenen Software-Produkte mit Hil-
fe von Backengineering-Techniken selbst zu ergründen.
Volkswirtschaftlich gesehen trifft der häufige Mitarbeiterwechsel (brain drain)
die US-amerikanische IT-Industrie ungleich härter als die europäische. Auch hier
ist die durchschnittliche Verweilzeit von IT-Fachkräften seit Jahren ebenfalls rück-
läufig, aber noch weit von dem US-amerikanischen Niveau entfernt.
1.3 Warum ist Software eigentlich so schlecht? 15
Jährliche Wachstums-
rate des BIP: 3% I Jährliche Entwicklung des BIPs
Land Wachstumsrate
Jährliche Wachstums- Indien 0, 65 %
rate des BIP: 0.65% Indonesien 1 %
Japan 3%
Abb. 1.6 Als Mensch neigen wir zum linearen Denken. Exponentielle Wachstumsraten sind für
uns nur schwer abzuschätzen
Indonesien betrug das Wachstum zur gleichen Zeit durchschnittlich ca. 1 % und in
Japan ca. 3 % pro Jahr. Im Falle von Indien ergibt sich, dass sich das Bruttoinland-
sprodukt in diesen 100 Jahren gerade einmal verdoppelt hat. In Indonesien ist das
BIP um das 2, 7-fache gewachsen. Die gleiche Rechnung für Japan ergibt aber, dass
das Bruttoinlandsprodukt im gleichen Zeitraum um fast das Zwanzigfache gestiegen
ist.
Das Ergebnis mag so manchen Leser verblüffen – insbesondere der direkte Ver-
gleich zwischen den Ländern Japan und Indonesien bringt ein eher unerwartetes Er-
gebnis zum Vorschein. In der Tat neigen die meisten Menschen dazu, das japanische
Gesamtwachstum als etwas mehr als das dreifache des Indonesischen abzuschätzen.
Schuld ist an dieser Stelle erneut das lineare Denken, das zwar in vielen Alltagssi-
tuationen unser Überleben sichert, im Falle exponentieller Faktoren jedoch kläglich
versagt.
Die Tendenz zu linearisieren ist weit verbreitet und wird zudem in den frühen
Schuljahren intensiv gefördert. Vielleicht entsinnen auch Sie sich an eine der klassi-
schen Textaufgaben aus dem Mathematikunterricht zurück, die uns die Verwendung
des Dreisatzes näher bringen sollen: „Wenn ein Handwerker zwei Tage zum Errich-
ten einer Mauer benötigt, wie viele Tage benötigen dann zwei Handwerker?“
Die Antwort „ein Tag“ scheint auf der Hand zu liegen. Dass sich das mathema-
tische Modell nicht eins zu eins auf die Praxis übertragen lässt, zeigt die gleiche
Modellrechnung mit 2880 Handwerkern. Dem Prinzip der Linearität folgend, wer-
den 2880 Handwerker die gleiche Mauer in einer Minute fertig stellen. Wer immer
noch keine Bedenken hegt, möge die gleiche Rechnung mit 172.800 Handwerkern
wiederholen (vgl. Abb. 1.7).
Im übertragenen Sinne bedeutet diese plakative Rechnung nichts anderes, als
dass die oft gescholtene, aber gebetsmühlenartig angewendete Zeitschätzungsfor-
mel
Zeit in Mannjahren
Entwicklungsdauer = (1.1)
Anzahl Mitarbeiter
in der Praxis schlicht zum Scheitern verurteilt ist. Im Bereich der Software-
Entwicklung ist es kein Geheimnis, dass die späte Hinzunahme von Mitarbeitern
die Entwicklungsdauer in zeitkritischen Projekten nicht verkürzt, sondern in den
meisten Fällen sogar verlängert [90, 269]. Leider klaffen die akademische Erkennt-
nis und die industrielle Praxis in diesem Punkt so weit auseinander wie in kaum
einem anderen Bereich. In zahlreichen Unternehmen dient Gleichung (1.1) immer
noch als die Grundlage der Projektplanung. Selbst in mehrfach zertifizierten Orga-
nisationen – hier ist die Etablierung adäquaterer Zeitschätzungsmodelle in der Regel
eine Zertifikatsgrundlage – stellt sich die Situation oft nicht besser dar. Komplexe
Zeitschätzungsmodelle werden in den seltensten Fällen gelebt. Zwangsläufig drängt
sich an dieser Stelle die Frage auf, warum Gleichung (1.1), so falsch sie auch sein
mag, immer wieder angewendet wird. Wahrscheinlich ist es auch hier wiederum
unsere Intuition, unser lineares Denken, das uns stets auf’s Neue von der durchweg
linearen Gleichung (1.1) verführen lässt.
Alle erfahrenen IT-Projektleiter sind an die folgenden zwei Sätze ihrer Entwick-
ler längst gewöhnt: „Das Programm ist fast fertig – nur noch diese Änderung“ und
1.3 Warum ist Software eigentlich so schlecht? 17
...
Abb. 1.7 Die klassischen Zeitschätzungsformeln des Projektmanagements sind in der Praxis zum
Scheitern verurteilt
seiner Natur immateriell und damit in weiten Teilen unsichtbar. Diese Eigenschaft
hat weitreichende Konsequenzen, die sich nicht auf die Fortschrittsbewertung be-
schränken. So bleiben die meisten der in Abschnitt 1.2 diskutierten Qualitätsmerk-
male nach außen hin verborgen. Insgesamt spielt damit die Fähigkeit, verschiedene
Aspekte eines Programms zu externalisieren, eine Schlüsselrolle für die erfolgrei-
che Durchführung eines Software-Projekts.
Software-Metriken gehen einen Schritt in diese Richtung. Angefangen von der
einfachen Lines-of-Code-Metrik (LOC) bis hin zur Messung der zyklomatischen
Komplexität nach McCabe wurden in der Vergangenheit zahlreiche Metriken vor-
geschlagen, die jede für sich einen bestimmten Teilaspekt eines Programms quan-
titativ erfassen. Im industriellen Umfeld wird deren Leistungspotenzial heute leider
nur selten konsequent ausgeschöpft. In vielen Entwicklungsabteilungen ist der Be-
griff der Software-Metrik schlicht unbekannt. In anderen herrscht die Meinung vor,
dass die Erhebung von Metriken nur mit unverhältnismäßig großem Arbeitsaufwand
gestemmt werden kann. Wie weit die Fehleinschätzungen an dieser Stelle reichen,
beweisen zahlreiche Werkzeuge, die den Software-Entwickler bei der Datenerhe-
bung unterstützen und in zahlreichen Fällen sogar eine vollständig automatisierte
Messung ermöglichen.
Jeder Programmierer kennt wahrscheinlich das folgende Phänomen: Zum Zeit-
punkt der Erstellung wirkt der eigene Programmcode bis in jedes Detail hinein
wohlstrukturiert, verständlich und konsistent. Nach ein paar Wochen Abstinenz be-
ginnt das eigene Werk befremdlich zu wirken. Noch ein paar Wochen, und das
eigene Programm wird in Teilen selbst nicht mehr verstanden. Dieses Phänomen
beschreibt einen zentralen Punkt, dem in der Software-Industrie heute noch nicht
ausreichend Rechnung getragen wird: Software besteht aus mehr als nur dem Code.
Die Langlebigkeit eines Software-Systems kann nur dann erreicht werden, wenn
neben dem Primärwissen in Form der Quelltexte auch das Sekundärwissen über die
gesamte Lebensdauer des Programms hinweg konserviert wird. Genau wie das Pri-
märwissen beschreibt auch das Sekundärwissen die interne und externe Funktions-
weise eines Software-Systems. Während jedoch die Quelltexte die Information in
einer computerverständlichen Form ablegen, subsumiert der Begriff des Sekundär-
wissens alle menschenverständlichen Formen der gleichen Information. An dieser
Stelle drängt sich die Frage nach der Existenzberechtigung jeglichen Sekundärwis-
sens regelrecht auf, schließlich liegt mit dem Quelltext eine genauso exakte wie
eindeutige Beschreibung für das tatsächliche Verhalten der Software vor. Die Ant-
wort liegt abermals in der Natur des Menschen begründet. Zwar sind wir stark darin,
komplexe Modelle in computerverständliche Beschreibungen (Programme) abzubil-
den, jedoch ungleich schwächer, aus einem gegebenen Programm das ursprüngliche
Gedankengebilde zu rekonstruieren.
In der Praxis wird ein Großteil des Sekundärwissens in Form der Software-
Dokumentation konserviert. Umso schwerer wiegt, dass in vielen Firmen entweder
gar keine Dokumentation angefertigt oder die Verantwortlichkeit vollständig auf die
einzelnen Entwickler delegiert wird. Entsprechend häufig schallen Sätze wie “Our
code is our documentation” selbst durch die Flure ausgewachsener IT-Unternehmen.
Andere Firmen meinen es zu gut mit der Technik des Dokumentierens. Vor allem
1.4 Gibt es Licht am Ende des Tunnels? 19
1.4.1 Produktqualität
Dieser Bereich fasst alle Methoden und Techniken zusammen, die sich primär mit
der Verbesserung der in Abschnitt 1.2 eingeführten Qualitätsmerkmale beschäfti-
20 1 Einführung
Software-Qualität
Produktqualität
Konstruktive Qualitätssicherung
Software-Richtlinien
Typisierung
Vertragsbasierte Programmierung
Fehlertolerante Programmierung
Portabilität
Dokumentation
Analytische Qualitätssicherung
Software-Test
Black-Box-Test
White-Box-Test
Test-Metriken
Statische Analyse
Software-Metriken
Konformitätsprüfung
Exploit-Analyse
Anomalienanalyse
Manuelle Software-Prüfung
[Link]
Prozessqualität
Software-Infrastruktur
[Link]
Build-Automatisierung
Test-Automatisierung
Defektmanagement
Managementprozesse
Vorgehensmodelle
Reifegradmodelle
gen. Der Bereich der Produktqualität untergliedert sich in die Gebiete der konstruk-
tiven Qualitätssicherung und der analytischen Qualitätssicherung.
1.4 Gibt es Licht am Ende des Tunnels? 21
1.4.2 Prozessqualität
Anders als bei den Methoden und Techniken zur direkten Verbesserung des
Software-Produkts, beschäftigt sich die Prozessqualitätssicherung ausschließlich
mit der Entstehung desselben. Damit umfasst dieser Bereich sämtliche Maßnah-
men, die eine geregelte Entwicklung eines Software-Produkts in Form eines defi-
nierten Prozesses gewährleisten. Bei genauerer Betrachtung vereint die Prozessqua-
lität zwei Seiten der gleichen Medaille. Während aus der Entwicklerperspektive der
Bereich der Software-Infrastruktur von erstrangiger Bedeutung ist, rückt auf der
Führungsebene der Bereich der Managementprozesse in den Vordergrund.
Jetzt ist es an der Zeit, Licht in das Dunkel der Methoden und Techniken der
Software-Qualitätssicherung zu bringen. Die folgenden Kapitel verfolgen allesamt
das Ziel, die einzelnen Themen nicht rein theoretisch aufzuarbeiten, sondern in einer
für die Praxis verwertbaren Form zu präsentieren. Dieser Idee folgend, beginnen wir
unsere Reise in Kapitel 2 zunächst mit der Analyse typischer, immer wiederkehren-
der Software-Fehler. Dieses Kapitel soll uns ein Gefühl für die Materie vermitteln,
mit der wir in der Welt der Software-Fehler permanent konfrontiert werden. Dass
es hier nicht um graue Theorie geht, wird durch den Rückgriff auf reale Software-
Projekte untermauert: Projekte, die zu trauriger Berühmtheit gelangten und neben
erheblichen Sach- und Image-Schäden auch zum Verlust von Menschenleben führ-
ten.
Kapitel 2
Software-Fehler
Wie wird diese Anweisung durch den Compiler interpretiert? Selbst langjährige C-
Programmierer parieren diese Frage nicht immer mit einer schnellen Antwort und
greifen voreilig zur Tastatur. Da die Programmiersprache C sowohl den Subtrakti-
onsoperator („-“) als auch den Dekrementierungsoperator („--“) kennt, stehen die
folgenden beiden Alternativen zur Auswahl:
c = (a--)-b;
c = a-(--b);
Glücklicherweise gibt es in C eine einzige klare Regel, mit deren Hilfe sich die
Frage eindeutig beantworten lässt. Andrew Koenig spricht in [154] bezeichnend von
der Maximal munch strategy, hinter der sich nichts anderes als die in der Informatik
häufig verwendete Greedy-Methode verbirgt [58]. Koenig beschreibt die Regel in
treffender Weise wie folgt:
Mit anderen Worten: Die einzelnen Tokens werden während der Extraktion aus
dem Zeichenstrom stets so groß wie irgend möglich gewählt. Abb. 2.1 zeigt den
resultierenden Token-Stream, den die Greedy-Strategie für das klassische Hello-
World-Programm produziert. Jetzt wird auch auf einen Schlag klar, wie der Aus-
druck c = a---b; ausgewertet wird. Durch Anwendung der Greedy-Regel wird
aus dem Zeichenstrom --- zunächst das Token -- und anschließend das Token
- extrahiert. Der Ausdruck c = a---b; ist damit äquivalent zu c = (a--)-b;.
Da c = a-(--b); ebenfalls ein gültiger C-Ausdruck ist, schleichen sich durch die
Missachtung oder die falsche Auslegung der lexikalischen Regeln Fehler ein, die
durch den C-Compiler nicht erkannt werden können.
Die nächsten drei C-Anweisungen sind weitere Beispiele von Ausdrücken, die
einen handfesten lexikalischen Fehler oder zumindest eine potenzielle Fehlerquelle
enthalten (vgl. [154]):
I Beispiel 1
x=-1; /* Initialisiere x mit dem Wert -1 */
hello_world.c
int main(int argc, char **argv) 1
{ 2
printf("Hello, world\n"); 3
return 1; 4
} 5
Greedy-Strategie:
Der Zeichenstrom wird sequenziell in einen
Symbolstrom umgewandelt. Ein neues
Symbol wird erst begonnen, falls das alte
nicht mehr vergrößert werden kann.
wie sie z. B. die Separierung der einzelnen Elemente durch Leerzeichen leistet,
nicht nur die Lesbarkeit, sondern auch die Robustheit des Programmtextes.
Die Verwendung von Leerzeichen ist eine sichere Methode, um in den meisten
Programmiersprachen die verschiedenen Tokens voneinander zu trennen. Dass
dies nicht in allen Sprachen der Fall ist, wird uns der weiter unten diskutierte und
wahrscheinlich prominenteste Software-Fehler der Programmiersprache FORT-
RAN demonstrieren.
I Beispiel 2
struct { int vorwahl; char *city; }
phone_book[] = { 07071, "Tübingen",
0721, "Karlsruhe",
089, "München" };
Erkennen Sie bereits den Fehler? In C wird jede numerische Zeichenfolge, die
mit der Ziffer 0 beginnt, als Oktalzahl (Basis 8) gedeutet. Die Zeichenfolge 0721
entspricht damit nicht der Dezimalzahl 721. Stattdessen wird der Ausdruck wie
folgt interpretiert:
Selbst der Struktureintrag 089 führt nicht in jedem Fall zu einer Fehlermeldung,
obwohl die Ziffern 8 und 9 außerhalb des oktalen Zahlenbereichs liegen. Einige
C-Compiler sehen über die Bereichsüberschreitung großzügig hinweg und inter-
pretieren die Zahl folgendermaßen:
089 = 8 × 81 + 9 × 80 = 64 + 9 = 73 (2.2)
I Beispiel 3
y = x/*p /* p ist Pointer auf Divisor */
Abb. 2.3 Von der NASA im Rahmen des Mercury-Projekts eingesetzter FORTRAN-Code
mit dem Schlüsselwort END DO abgeschlossen werden. var bezeichnet die Schlei-
fenvariable. Diese wird zu Beginn mit dem Wert first initialisiert und nach jeder
Schleifeniteration um den Wert inc erhöht. Fehlt die Angabe der Schrittweite, so
wird auf var nach jeder Iteration eine 1 addiert. Die Schleife terminiert, wenn die
Zählvariable einen Wert größer als last erreicht.
Ein genauer Blick auf das Schleifenkonstrukt in Zeile 20 zeigt, dass die Inter-
vallgrenzen nicht mit einem Komma, sondern mit einem Punkt separiert wurden.
Dieser mit bloßem Auge kaum zu entdeckende Fehler hat dramatische Auswirkun-
gen. Durch das Fehlen des Kommas erkennt FORTRAN den Ausdruck nicht mehr
länger als Zählschleife, sondern interpretiert den Ausdruck schlicht als einfache Va-
riablenzuweisung:
DO5K = 1.3
Dass der Fehler durch den FORTRAN-Compiler nicht als solcher erkannt wird,
liegt an zwei Besonderheiten der Sprache, die sie von den meisten anderen Pro-
grammiersprachen unterscheidet. Zum einen erlaubten frühe FORTRAN-Versionen,
Leerzeichen an beliebiger Stelle einzufügen – insbesondere auch innerhalb von
Bezeichnern und Variablennamen. Diese Eigenschaft erscheint aus heutiger Sicht
mehr als fahrlässig. Zur damaligen Zeit bot sie jedoch durchaus Vorteile, da die ers-
ten FORTRAN-Programme noch auf Lochkarten gespeichert wurden. Werden alle
Leerzeichen ignoriert, kann ein Programm selbst dann noch erfolgreich eingelesen
werden, wenn zwischen zwei gestanzten Zeilen versehentlich eine ungestanzte üb-
rig bleibt.
Zum anderen ist es in FORTRAN gar nicht nötig, Variablen vor ihrer ersten Nut-
zung zu deklarieren. Hier wird besonders deutlich, wie wertvoll die Bekanntma-
chung von Funktionen und Variablen in der Praxis wirklich ist. Dass eine Variable
DO5K bereits an anderer Stelle deklariert wurde, ist so unwahrscheinlich, dass jeder
Compiler die Übersetzung der mutmaßlichen Zuweisung verweigert hätte. Kurzum:
Die Verwendung einer deklarationsbasierten Programmiersprache, wie z. B. C, C++
oder Java, hätte den Software-Fehler des Mercury-Projekts vermieden – der Fehler
wäre bereits zur Übersetzungszeit durch den Compiler entdeckt worden.
Es bleibt die Frage zu klären, wie der hier vorgestellte FORTRAN-Bug eine der-
art große Berühmtheit erlangen konnte, um heute zu den meistzitierten Software-
Fehlern der IT-Geschichte zu zählen? Die Antwort darauf ist simpel. Der vorge-
stellte Fehler demonstriert nicht nur wie kaum ein anderer die Limitierungen der
Sprache FORTRAN, sondern zeichnet zugleich für eine der größten Legenden der
Computergeschichte verantwortlich. Auf zahllosen Internet-Seiten, wie auch in der
gedruckten Literatur, wird der FORTRAN-Bug beharrlich als Ursache für den Ab-
sturz der Raumsonde Mariner I gehandelt, die am 22. Juli 1962 von der NASA an
Bord einer Atlas-Trägerrakete auf den Weg zur Venus gebracht werden sollte. Kurz
nach dem Start führte die Trägerrakete unerwartet abrupte Kursmanöver durch und
wich deutlich von der vorbestimmten Flugbahn ab. Alle Versuche, korrigierend ein-
zugreifen, schlugen fehl. Nach 290 Sekunden fällte die Flugkontrolle schließlich die
Entscheidung, die Trägerrakete aus Sicherheitsgründen zu sprengen.
34 2 Software-Fehler
Beispiel 1
float nrm(float x) 1
{ 2
while (abs(x)>0,1) 3 Programm
x = x / 10; 4 terminiert
return x; 5 nicht
} 6
7
Beispiel 2
int abs(int x) 1
{ 2
if (x < 0) 3 Diesen Fehler
x = -x, 4 erkennt der
return x; 5 Compiler
} 6
7
Beispiel 3
void fill() 1
{ 2
int i=0, a[10]; 3 Programm-
4 verhalten ist
for (; i<=10; i++) 5 unterschiedlich
a[i] = 0; 6
} 7
Die Ursache für den Absturz der Mariner-Trägerrakete ist weit unspektakulärer
als gemeinhin angenommen und geht schlicht auf die falsche Umsetzung der Spezi-
fikation zurück. Obwohl die Anforderungsbeschreibung der Flugsteuerung korrekt
vorgab, die Verlaufskurve eines Messwerts geglättet zu verwenden, wurde diese in
der Implementierung ungeglättet weiterverarbeitet. Trotzdem: Der FORTRAN-Bug
der Mercury-Mission wird heute immer noch so beständig mit dem Mariner-Absturz
in Verbindung gebracht, dass er wahrscheinlich auch in Zukunft als spektakuläre Er-
klärung für das Scheitern dieser Mission herhalten muss.
An dieser Stelle mag sich so mancher Leser zu dem Gedanken hinreißen lassen,
dass der Fehler schlicht auf die Eigenarten einer fast schon prähistorischen Pro-
grammiersprache zurückgeht – insbesondere haben wir weiter oben herausgearbei-
tet, dass der Fehler in Sprachen wie C oder C++ nicht unentdeckt geblieben wäre.
Doch stellen C bzw. C++ wirklich den erhofften Fortschritt in dieser Richtung dar?
Dass auch in diesen Sprachen der Teufel im Detail steckt, demonstrieren die drei in
Abb. 2.4 aufgeführten Beispielprogramme.
2.1 Lexikalische und syntaktische Fehlerquellen 35
Das erste Beispielprogramm definiert die Funktion nrm. Als einzigen Über-
gabeparameter nimmt die Funktion eine float-Variable entgegen und dividiert
den erhaltenen Wert so lange durch 10, bis das Ergebnis innerhalb des Intervalls
[−0, 1; 0, 1] liegt. Unabhängig von der übergebenen Zahl verweigert das Programm
in der abgedruckten Form beharrlich seinen Dienst und verfängt sich stets auf’s
Neue in einer Endlosschleife.
Genau wie im Fall des oben geschilderten FORTRAN-Bugs reduziert sich die
Ursache auf die Vertauschung von Punkt und Komma – anstelle eines Punkts wur-
de in der Intervallgrenze zur Trennung der Ziffern ein Komma eingefügt. Da die
Programmiersprache C das Komma als eigenständigen Operator kennt, führt die
Vertauschung zu keinem Fehler.
Mit Hilfe des Komma-Operators werden in C mehrere Anweisungen verkettet, so
dass der Wert von x zunächst mit 0 verglichen und anschließend der Ausdruck 1 aus-
gewertet wird. Da die Konstante 1 in C dem Wahrheitswert True entspricht und der
Wert des letzten ausgewerteten Teilausdrucks gleichzeitig dem Wert des Gesamtaus-
drucks, ist die Schleifenbedingung permanent erfüllt. Kurzum: Eine Endlosschleife
entsteht.
Das zweite Beispielprogramm definiert die Funktion abs, die den Absolutwert
des vorzeichenbehafteten Übergabeparameters berechnet. Hierzu wird der Wert des
int-Arguments x mit 0 verglichen und gegebenenfalls mit Hilfe des Negationsope-
rators in eine positive Zahl gewandelt. Anschließend wird der Inhalt von x an die
aufrufende Funktion zurückgegeben. Ein genauerer Blick auf die Zuweisung inner-
halb des If-Körpers zeigt, dass an dieser Stelle ein Komma anstelle des nötigen Se-
mikolons eingetippt wurde. Hierdurch müsste die Return-Instruktion ungewollt dem
Körper der If-Anweisung zugeordnet werden und für x ≥ 0 ein undefinierter Rück-
gabewert entstehen. Anders als im ersten Beispiel wird dieser Fehler aber durch
den Compiler erkannt. Der Grund hierfür ist einfach: Um Probleme dieser Art zu
vermeiden, verbietet die C-Grammatik, die Return-Instruktion in Kombination mit
dem Komma-Operator zu verwenden.
Das dritte Beispielprogramm definiert die parameterlose Prozedur fill, die das
zehnelementige Array a und die Laufvariable i lokal deklariert. Innerhalb der Funk-
tion werden alle Elemente des Arrays mit Hilfe einer For-Schleife mit 0 initialisiert.
Das Programm enthält einen gravierenden Fehler, der sich in Abhängigkeit des ver-
wendeten Compilers unterschiedlich auswirkt und eine Endlosschleife verursachen
kann. Die Ursache für das Fehlverhalten geht auf die Schleifenendbedingung i<=10
zurück. Da das erste Array-Element in C immer den Index 0 besitzt, wird die Schlei-
fe nicht zehn, sondern elfmal ausgeführt. Die elfte Iteration führt dazu, dass die
zugewiesene 0 in den Speicher außerhalb des Arrays geschrieben wird.
Soweit so gut. Aber warum kann das Programm zu einer Endlosschleife führen?
Der Grund hierfür liegt in der Speicheranordnung lokaler Variablen. Die meisten
älteren C-Compiler ordnen die Variablen direkt hintereinander in absteigender Rei-
henfolge an und erzeugen die in Abb. 2.5 dargestellte Anordnung.
Das Speicherabbild deckt auf, warum in diesem Fall eine Endlosschleife entsteht.
Die letzte Zuweisung (a[10]=0) schreibt eine Null an diejenige Speicherstelle, in
der normalerweise die Variable i gespeichert wird. Konsequenterweise wird i vor
36 2 Software-Fehler
... a[0] a[1] a[2] a[3] a[4] a[5] a[6] a[7] a[8] a[9] i ...
... 0 0 0 0 0 0 0 0 0 0 0 ...
Abb. 2.5 Speicherabbild auf dem Stack. Die meisten Compiler ordnen lokale Variablen in ab-
steigender Speicherrichtung an, so dass die Variablen a[10] und i in diesem Beispiel dieselbe
Speicheradresse referenzieren
dem Erreichen der Abbruchbedingung stets auf 0 zurückgesetzt und die komplette
Schleife hierdurch neu gestartet.
Tatsächlich ist die Chance, mit dem abgebildeten Programm unter realen Bedin-
gungen eine Endlosschleife zu erzeugen, mittlerweile gering. Dies liegt daran, dass
fast alle modernen Compiler die Stack-Elemente nicht mehr lückenlos anordnen,
sondern zusätzliche Platzhalter einfügen, um die Vorhersage des Speicherabbilds zu
verhindern. Auf diese Weise wird es schwerer, eine Software durch gezielt herbei-
geführte Pufferüberläufe anzugreifen; gleichsam führt der Mechanismus dazu, dass
Fehler wie der geschilderte, heute in vielen Fällen unbemerkt bleiben.
at_and_t.c
... 1
switch (line) { 2
... 3
case THING1: 4
doit1(); 5
break; 6
7
case THING2: 8
if (x == FOO) { 9
do_first_stuff(); 10
11
if (y == BAR) 12
/* Skip "do_later_stuff" function call 13
by dropping out of the If-Statement */ 14
break; 15
16
do_later_stuff(); 17
} 18
initialize(); 19
break; 20
21
default: 22
processing(); 23
} 24
... 25
ja
x == FOO do_rst_stuff()
nein
nein
y == BAR do_later_stuff()
ja
initialize()
Die gravierenden Symptome des Systemausfalls ließen zunächst auf einen ge-
zielten Hacker-Angriff schließen, der sich im Laufe der Untersuchungen jedoch
nicht bestätigte. Stattdessen wurde ein fehlerhafter Code-Abschnitt in der Software
isoliert, die in allen 114 Vermittlungsknoten für die Weiterleitung der Anrufe zum
Einsatz kam.
Der fehlerhafte Programmcode stammt aus einer Initialisierungsroutine und ist in
abstrahierter Form in Abb. 2.6 dargestellt (vgl. [164]). Innerhalb eines umschließen-
den Switch-Case-Konstrukts führt das C-Programm zunächst eine Fallunterschei-
dung bezüglich der Variablen line durch. Für unsere Betrachtung ist der zweite
Case-Zweig von Bedeutung, in dem verschiedene Initialisierungsroutinen in Ab-
hängigkeit der Variableninhalte von x und y aufgerufen werden.
Der korrekte Kontrollfluss ist durch das Ablaufdiagramm in Abb. 2.7 vorge-
geben. Um eine einwandfreie Initialisierung zu gewährleisten, wird die Variable
x zunächst mit einem gewissen Wert verglichen und gegebenenfalls die Funktion
do_first_stuff aufgerufen. Ist zusätzlich die Variable y auf einen bestimmten
Wert gesetzt, soll mit Hilfe des C-Schlüsselworts break aus der aktuellen Klam-
merungsebene herausgesprungen und der Aufruf von do_later_stuff verhindert
werden. Unabhängig von den Werten von x und y wird am Ende die Funktion
initialize aufgerufen.
Sehen Sie bereits den Fehler? Die Ursache der Vermittlungsausfälle geht auf
die falsche Verwendung des Schlüsselworts break zurück, dessen genaue Seman-
tik erfahrungsgemäß von vielen Programmierern nicht hinreichend genau verstan-
den wird. Anders als der Urheber der Software unterstellt, unterbricht break nicht
die aktuelle Klammerungsebene, sondern lediglich die innerste Do-, While- oder
Switch-Umgebung. Folgerichtig beendet der so platzierte break-Befehl nicht nur
den aktuellen If-Befehl, sondern gleich das gesamte Switch-Konstrukt. Dies wie-
derum hat zur Folge, dass zwar, wie gewünscht, der Aufruf von do_later_stuff
unterbunden wird, der Aufruf von initialize aber ebenfalls unterbleibt. Exakt
dieser ausgelassene Funktionsaufruf führte zu dem Fehler, der in Form einer un-
glücklichen Kettenreaktion für den bedeutendsten Ausfall des Telefonnetzes in der
Geschichte von AT&T führte.
2.2 Semantische Fehlerquellen 39
Der AT&T-Bug bringt zwei Aspekte typischer Software-Fehler klar zum Vor-
schein. Zum einen demonstriert der Vorfall, dass die Eigenschaft der nahezu belie-
bigen Replizierbarkeit in Hinsicht auf die resultierenden Fehlerszenarien zum Bu-
merang wird. Anders als physikalisch bedingte Hardware-Fehler, die in der Regel
zum singulären Ausfall einer einzigen Baugruppe führen, betreffen Software-Fehler
alle homogen installierten Komponenten in gleichem Maße. Hätte der Fehler in der
AT&T-Vermittlungssoftware nicht alle Vermittlungsknoten simultan betroffen, wä-
re eine Kettenreaktion unmöglich und das Ausmaß des Ausfalls ungleich geringer
gewesen.
Zum anderen ist es keine Überraschung, dass der AT&T-Bug auf einen Fehler
innerhalb des Codes zur Fehlerbehandlung zurückgeht. Im Gegensatz zur Haupt-
funktionalität wird der Fehlerbehandlungscode in der Praxis nur vergleichsweise
wenig und unter Umständen gar nicht getestet. Die Gründe hierfür sind vielfältig
und gehen unter anderem auf die Schwierigkeit zurück, ein bestimmtes Fehlersze-
nario unter Laborbedingungen gezielt herbeizuführen. Erschwert wird die Situation
durch die schier gigantische Anzahl an erdenklichen Fehlerszenarien, die Compu-
tersysteme dieser Größenordnung aufweisen.
War der AT&T-Fehler einfach die Konsequenz aus der nicht mehr kontrollierba-
ren Komplexität heutiger Computersysteme und damit im Vorfeld unvermeidbar?
Die Antwort ist ein klares Nein. Sowohl die Wahl eines restriktiven Sprachstan-
dards, wie z. B. MISRA-C, als auch die Durchführung eines C0 -Überdeckungstests
hätten den Fehler im Vorfeld vermieden. Auf die Details dieser Techniken werden
wir in den Kapiteln 3.1.2 bzw. 4.4.2 zurückkommen.
An dieser Stelle wenden wir uns erneut der NASA zu. Ein Blick in die vergleichs-
weise kurze Geschichte der US-amerikanischen Raumfahrt zeigt, dass nicht nur die
Mariner-Mission unter einem schlechten Stern stand. Neben ihren unbestreitbaren
Erfolgen musste die NASA im Laufe der Raumfahrtgeschichte weitere Rückschläge
hinnehmen, von denen viele auf das Versagen der eingesetzten Software zurückge-
führt werden konnten. So ging auch das Mars-Surveyor-’98-Programm unter dem
Auge einer breiten Öffentlichkeit als spektakulärer Rückschlag in die Geschich-
te der US-amerikanischen Raumfahrt ein. Im Rahmen der Mission wurden zwei
Raumsonden zum Mars geschickt, um dessen atmosphärische Bedingungen detail-
liert zu erkunden. Während eine der Sonden, der Polar Lander, auf der Marsober-
fläche aufsetzen sollte, war es die Aufgabe des Climate Orbiters, den roten Planeten
auf einer kreisförmigen Umlaufbahn zu umrunden und aus sicherer Entfernung zu
analysieren.
Der Climate Orbiter wurde am 11. Dezember 1998 erfolgreich in Cape Canave-
ral gestartet und begann seine neun Monate lange Reise zum Mars. Pünktlich am 23.
September 1999 wurde die Anflugphase eingeleitet und wie geplant mit der geziel-
ten Annäherung an die vorberechnete Umlaufbahn begonnen. Die Konzeption sah
vor, die letzte Phase der Annäherung mit Hilfe eines als Aerobraking bezeichneten
Prinzips durchzuführen, das schon zuvor im Rahmen anderer Missionen erfolgreich
eingesetzt wurde. Hierzu tritt die Sonde zunächst, wie in Abb. 2.8 skizziert, in einen
elliptischen Orbit ein, dessen nächster Punkt nur ca. 150 km von der Marsoberfläche
entfernt ist und damit bereits die obersten Schichten der Atmosphäre streift. Durch
40 2 Software-Fehler
Geplanter Eintritt:
19.11.1999
Geplanter Eintritt:
29.10.1999
Geplanter Eintritt:
9.10.1999
Geplanter Eintritt:
23.9.1999
Abb. 2.8 Eintritt des Mars Climate Orbiters der NASA in die Aerobraking-Phase
die atmosphärische Reibung wird die Raumsonde leicht abgebremst, so dass sich die
elliptische Umlaufbahn mit jedem Umlauf langsam an den später einzunehmenden
kreisförmigen Orbit annähert. Nach 57 Tagen ist die Aerobraking-Phase beendet
und die finale Kreisbahn erreicht.
Um 9:06 Uhr trat der Mars Climate Orbiter, wie erwartet, in den Funkschatten
des roten Planeten ein, den er um 9:27 verlassen und den Kontakt wiederherstellen
sollte. Die Kontrollstation wartete jedoch vergeblich auf die Wiederaufnahme des
Funkkontakts und nach kurzer Zeit war klar, dass die Sonde unwiderruflich verloren
war. Später stellte sich heraus, dass sich die Sonde bis auf 57 km der Marsoberfläche
genähert hatte – rund 100 km weniger als vorberechnet. Die hohe atmosphärische
Reibung in dieser niedrigen Höhe ließ den Orbiter in kürzester Zeit in einem hellen
Feuerball verglühen.
Die Analyse der Telemetriedaten brachte den Fehler noch am selben Tag zum
Vorschein. Zur Kurskorrektur beim Landeanflug griff der Orbiter auf eine von Lock-
heed Martin bereitgestellte Lookup-Tabelle zurück, das sogenannte Small forces fi-
le. Lockheed Martin legte die Daten in imperialen Einheiten lbs × s (pound force
seconds) ab, von der NASA wurden die Werte aber, wie international üblich, nach
dem metrischen System als N × s (Newton-Sekunden) interpretiert. Die verwende-
ten Werte waren dadurch um den Faktor 4.45 zu groß, und die Überkompensation
der zu korrigierenden Bahnabweichung drängte den Mars-Orbiter schließlich auf
die fatale Umlaufbahn in nur noch 57 km Höhe.
Der Software-Fehler, der die Mars-Mission in ein Desaster verwandelte, fällt ge-
nau wie der weiter oben beschriebene AT&T-Bug in die große Klasse der Semantik-
fehler. Im direkten Vergleich zeigt sich, dass die Natur beider Fehler trotzdem eine
2.3 Parallelität als Fehlerquelle 41
andere ist. Der AT&T-Bug geht auf die falsche Interpretation eines C-Konstrukts zu-
rück und hat seinen Ursprung damit in der Sprachsemantik. Im Falle des Mars Cli-
mate Orbiters hingegen wurde die Programmiersprache vollständig korrekt verwen-
det. Die Fehlerursache geht auf einen Typkonversionsfehler zurück und ist damit
eine klassische Fehlinterpretation der Programmsemantik. In Abschnitt 3.2 werden
wir auf die Problematik der Datentypkonversion genauer eingehen und Ansätze auf-
zeigen, wie sich Konversionsfehler dieser und ähnlicher Art erfolgreich vermeiden
lassen.
Der Verlust des Mars Climate Orbiters war ein herber Rückschlag für die NASA.
Zum vollständigen Desaster wurde das Projekt schließlich am 3. Dezember 1999,
als auch noch der Mars Polar Lander verloren ging. Gegen 21:10 mitteleuropäischer
Zeit, just im Moment des Eintritts in die Marsatmosphäre, brach der Funkkontakt
für immer ab. Der Grund des Kommunikationsverlusts ist bis heute unbekannt, ge-
nauso wie das Schicksal der Raumsonde. Alle späteren Versuche, den Kontakt mit
dem Polar Lander wiederherzustellen, blieben erfolglos. Die mutmaßliche Ursache
für das Versagen wird, wie im Falle des Climate Orbiters, ebenfalls der Software zu-
geschrieben. Vermutlich wurden durch das Ausfahren der Landebeine Vibrationen
erzeugt, die von der Software als das Aufsetzen der Sonde auf den Mars interpretiert
wurden. Die fatale Fehleinschätzung führte zum sofortigen Abschalten der Brems-
düse, so dass die Sonde mit unverringerter Geschwindigkeit auf der Marsoberfläche
aufschlug und zerschellte.
aktiv bc_sched
wartend aktiv bc_dist
aktiv
aktiv unterbrochen aktiv ASI/MET
125 Millisekunden Zeitfenster
aktiv bc_sched
wartend aktiv bc_dist
aktiv aktiv aktiv
aktiv unterbrochen aktiv ASI/MET
Abb. 2.9 Prioritäteninversion als Ursache unregelmäßiger Resets des Mars-Rovers Sojourner
Analyse vermeiden. Beide werden wir in den Kapiteln 3 und 5 einer genaueren
Betrachtung unterziehen.
Bit- 3 5 7 9 11 13 15 17 19 21 23
position 1 2 4 6 8 10 12 14 16 18 20 22 24
0 . 0 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 0 0 ...
24-Bit Festkommadarstellung
Abb. 2.11 Die Zahl 10−1 besitzt im Binärsystem keine endliche Repräsentation
stellt, permanent das sogenannte Range gate. Dieses beschreibt den Luftkorridor, in-
nerhalb dessen das Zielobjekt als nächstes erscheint. Die Berechnungsvorschrift ist
eine Funktion, die zum einen die Geschwindigkeit des Flugobjekts und zum ande-
ren die Zeit der letzten Radardetektion als Parameter verarbeitet. Der letztgenannte
Parameter wird in Form der Systemzeit entgegengenommen.
Intern wird die Systemzeit als Integer-Wert gespeichert und entspricht der An-
zahl der verstrichenen Zehntelsekunden seit Inbetriebnahme der Abwehrbatterie.
Die Systemzeit wird ständig erhöht, so dass im Laufe des Betriebs immer größere
Absolutwerte gespeichert werden müssen. Vorsorglich wurde der darstellbare Wer-
tebereich so gewählt, dass ein numerischer Überlauf im realen Betrieb faktisch aus-
geschlossen ist.
Da der entsprechende Programmcode zur Berechnung des Range gates die ver-
strichene Zeit in Sekunden erwartet, die Systemzeit aber in Zehntelsekunden vor-
liegt, wird sie vor jeder Berechnung durch die Multiplikation mit dem Faktor 10−1
in das Zielformat konvertiert. Genau an dieser Stelle nahm das Verhängnis seinen
Lauf. Intern verwendet die Patriot-Software eine 24-Bit-Festkommadarstellung. Da
10−1 keine endliche Repräsentation im Binärsystem besitzt, kann der Wert nur an-
genähert werden (vgl. Abb. 2.11). Der hierdurch verursachte Fehler ist nur minimal
und beläuft sich, ausgedrückt im Dezimalsystem, auf ca. 9.5 × 10−7 . Die Patriot-
Software ist jedoch so konzipiert, dass sich der relative Fehler in vollem Maße auf
den Absolutwert der Systemzeit durchschlägt, d. h., der verursachte Fehler nimmt
mit zunehmender Betriebsdauer kontinuierlich zu.
Die Einträge in Tabelle 2.1 fassen die verursachte Verschiebung des Range gates
in Abhängigkeit von der Betriebsdauer zusammen [27]. Ein kritischer Wert wird bei
einem Dauerbetrieb von 20 Stunden erreicht. Ab diesem Zeitpunkt ist das Range ga-
te so weit verschoben, dass sich das Zielobjekt vollständig außerhalb befindet – ein
erfolgreiches Abfangmanöver kann in keinem Fall mehr gelingen. Am 25. Februar
1991 war die betroffene Patriot-Batterie bereits seit 100 Stunden im Dauereinsatz
und damit bereits mehr als 80 Stunden außer Gefecht.
Obwohl es sich bei dem Patriot-Bug um einen klassischen Numerikfehler han-
delt, der in vielen anderen Applikationen in ähnlicher Form vorhanden ist, wurde er
dadurch begünstigt, dass das Patriot-System ursprünglich nicht für die Raketenab-
wehr gebaut wurde. In erster Linie sind die Patriot-Batterien als Boden-Luft-System
zur Abwehr feindlicher Kampfjets konzipiert. In einem solchen Einsatzszenario war
46 2 Software-Fehler
kein Patriot-System jemals zuvor so lange in Betrieb, als die im ersten Golfkrieg
eingesetzten Systeme.
2.5 Portabilitätsfehler
Die Übertragung einer Applikation auf eine andere Hardware-Plattform oder ein
anderes Betriebssystem gehört für gewöhnlich nicht zu den dankbarsten Aufgaben
eines Software-Entwicklers. Zum einen ist die Portierung der Software mit teilweise
erheblichen Anpassungsarbeiten verbunden, deren Auswirkungen bis auf die Archi-
tekturebene herunterreichen können. Zum anderen lässt sich das Verhalten portierter
Programme in der Praxis nur sehr eingeschränkt vorhersagen. In einigen Fällen läuft
die Software auf dem Zielsystem wie gehabt, in anderen fällt die Applikation durch
diffiziles Fehlverhalten auf oder verweigert vollständig seinen Dienst.
Die Gründe hierfür sind vielfältig. Häufig sind es zeitliche Abhängigkeiten, die
über die korrekte Ausführung eines Programms entscheiden. Beispielsweise wer-
den viele Verklemmungsprobleme konkurrierender Prozesse im laufenden Betrieb
nur dadurch vermieden, dass die Prozesse in einer bestimmten zeitlichen Reihenfol-
ge bedient werden. Eine minimale Änderung des Prozess-Schedulers oder auch nur
die Erhöhung der Prozessortaktrate kann dazu führen, dass die Applikation augen-
scheinlich einfriert oder einen Systemabsturz verursacht. Dieses Beispiel zeigt ein
typisches Charakteristikum vieler Portabilitätsfehler: Das Fehlverhalten der Soft-
ware ist oft bereits in der originalen Programmversion angelegt, wirkt sich auf der
Ursprungsplattform aufgrund ihrer spezifischen Gegebenheiten aber nicht aus. Der
Fehler verharrt in diesen Fällen so lange im Verborgenen, bis ihn der Umgebungs-
wechsel sein volles Unheilpotenzial entfalten lässt.
Ein historisches Beispiel für einen Software-Fehler dieser Kategorie liefert uns
der Jungfernflug der Ariane V, der vielen noch in lebendiger Erinnerung sein mag.
Die Ariane V ist die fünfte Generation von Trägerraketen, die unter der Leitung der
European Space Agency (ESA) entwickelt wurde und ursprünglich als Trägersystem
für die Europäische Raumfähre Hermes Verwendung finden sollte (vgl. Abb. 2.12).
Hermes wurde nie gebaut, so dass die Ariane V, wie bereits ihre Vorgängerin Ariane
IV, heute als reines Trägersystem für unbemannte Nutzlasten eingesetzt wird.
2.5 Portabilitätsfehler 47
[Link]
... 1
declare 2
vertical_veloc_sensor: float; 3
horizontal_veloc_sensor: float; 4
vertical_veloc_bias: integer; 5
horizontal_veloc_bias: integer; 6
... 7
begin 8
declare 9
pragma suppress(numeric_error, horizontal_veloc_bias); 10
begin 11
sensor_get(vertical_veloc_sensor); 12
sensor_get(horizontal_veloc_sensor); 13
vertical_veloc_bias := integer(vertical_veloc_sensor); 14
horizontal_veloc_bias := integer(horizontal_veloc_sensor); 15
... 16
exception 17
when numeric_error => calculate_vertical_veloc(); 18
when others => use_irs1(); 19
end; 20
end irs2; 21
den Quelltext der Ariane V. Wie schon im Falle der Ariane IV ist die Software in der
Programmiersprache ADA implementiert. ADA wurde in den Siebzigerjahren ent-
wickelt und ähnelt optisch der Programmiersprache Pascal, verfolgt darüber hinaus
jedoch zahlreiche fortschrittliche Konzepte wie die dynamische Laufzeitüberprü-
fung oder die integrierte Ausnahmebehandlung.
In dem besagten Code-Abschnitt werden zunächst die zwei Float-Variablen
vertical_veloc_sensor und horizontal_veloc_sensor, sowie die zwei
Integer-Variablen vertical_veloc_bias und horizontal_veloc_bias dekla-
riert. Durch den Aufruf der Prozedur sensor_get werden die beiden Float-
Variablen mit den von der Sensorik gemessenen Vertikal- und Horizontalgeschwin-
digkeiten beschrieben. Im nächsten Schritt werden die Float-Werte in vorzeichenbe-
haftete Integer-Zahlen konvertiert und den beiden Bias-Variablen zugewiesen. Bei-
de Integer-Variablen sind 16 Bit breit, so dass im Gegensatz zum Float-Datentyp,
der über einen wesentlich größeren Zahlenbereich verfügt, keine Zahlen größer als
32768 dargestellt werden können. Der Absturz der Ariane V hat seine Ursache da-
mit in einem klassischen Typkonversionsfehler.
Zwangsläufig drängt sich an dieser Stelle die Frage auf, wie ein derart klassi-
scher Programmierfehler die Testphase unbemerkt überstehen konnte. Auch hier ist
die Antwort schnell gefunden und geht auf die historischen Wurzeln des Programm-
abschnitts zurück. Für die Ariane V wurden nicht alle Teile der Software von Grund
auf neu entwickelt. Viele Code-Fragmente, zu denen auch der Programmausschnitt
in Abb. 2.13 gehört, wurden aus der Code-Basis der äußerst zuverlässigen Ariane IV
2.6 Optimierungsfehler 49
übernommen. Insbesondere der für den Fehler verantwortliche Code wurde inner-
halb des Ariane-IV-Programms ausgiebig getestet und konnte seine vermeintliche
Korrektheit in vielen erfolgreich absolvierten Flügen unter Beweis stellen.
Gänzlich unberücksichtigt blieb bei der Code-Übernahme allerdings die Tat-
sache, dass die Horizontalbeschleunigung der wesentlich größer dimensionierten
Ariane V die maximal erreichbare Geschwindigkeit ihres Vorgängermodells um das
Vierfache übertrifft. Mit anderen Worten: Das Risiko eines potenziellen Numerik-
fehlers aufgrund der Inkompatibilität des Float- und Integer-Zahlenbereichs war be-
reits in der Ariane IV vorhanden, führte aber dank der vergleichsweise geringen
Horizontal- und Vertikalgeschwindigkeit zu keiner realen Fehlfunktion. Das Bei-
spiel zeigt wie kaum ein anderes, wie sehr die Korrektheit eines Programms mit
seiner Einbettung in das Gesamtsystem verbunden ist. Diese Erkenntnis hat weiten
Einfluss auf das gesamte Gebiet des Software-Tests, mit dem wir uns in Kapitel 4
im Detail beschäftigen.
2.6 Optimierungsfehler
Die Optimierung von Programmcode dient in den meisten Fällen zur Verbesserung
der Laufzeit- und Speicherplatzeffizienz kritischer Programmabschnitte. Da die Op-
timierung innerhalb eines Software-Projekts in der Regel erst sehr spät durchge-
führt werden kann, muss sie mit entsprechender Vorsicht vorangetrieben werden.
Neben den klassischen Optimierungszielen Laufzeit und Speicherplatz sind viele
Programmierer versucht, durch die Verwendung geeigneter Programmierkniffe auch
die Code-Größe zu minimieren. Hierzu wird durch die geschickte Ausnutzung von
Randbedingungen die vormals allgemein gehaltene Programmstruktur durch eine
maßgeschneiderte und wesentlich kompaktere Variante ersetzt. Damit folgt die Op-
timierung der Code-Größe exakt den Vorgehensweisen, die auch zur Laufzeit- und
Speicherplatzoptimierung zum Einsatz kommen. Dass solche Optimierungen mit
besonderer Vorsicht zu genießen sind, zeigt eine ältere Version des Unix-Werkzeugs
Mail, mit dessen Hilfe E-Mails direkt von der Unix-Kommandozeile entweder ge-
sendet oder empfangen werden können. Entsprechend zweigeteilt präsentiert sich
die Syntax des Werkzeugs:
I Empfangen
mail [-e] [-h] [-p] [-P] [-q] [-r] [ -f file ]
I Senden
mail [-t] [-w] [ -m ... ] <recipient 1> <recipient 2> ...
Die genaue Bedeutung der einzelnen Optionen ist für unsere Betrachtung sekundär.
An dieser Stelle wollen wir uns vor allem mit der Frage beschäftigen, wie die Appli-
kation intern feststellt, ob sie zum Senden oder zum Empfangen von Mails aufgeru-
fen wurde. Ein Blick hinter die Kulissen zeigt, dass die Funktionsweise durch eine
sehr spezifische Analyse der Übergabeparameter bestimmt wird. Anstatt alle Para-
meter der Reihe nach zu analysieren und detailliert auszuwerten, bedient sich der
Autor des Programms eines Tricks. Wie ein gezielter Blick auf die Aufruf-Syntax
50 2 Software-Fehler
zeigt, wird das Programm genau dann zum Empfangen von Mail gestartet, wenn der
letzte Übergabeparameter entweder selbst mit einem Bindestrich eingeleitet wird
oder das zusätzliche Argument der Option -f ist. Der Programmcode der besagten
Mail-Version macht sich dieses Analyseergebnis zunutze und überprüft die Überga-
beparameter wie folgt:
if (argv[argc-1][0] == ’-’ || (argv[argc-2][1] == ’f’)) {
readmail(argc, argv);
} else {
sendmail(argc, argv);
}
Obwohl die If-Abfrage den Sachverhalt augenscheinlich exakt abbildet, ist dem Au-
tor einen diffiziler Denkfehler unterlaufen. Der linke Teil der If-Bedingung ist kor-
rekt und prüft zuverlässig ab, ob es sich bei dem letzten Übergabeparameter um
eine Option handelt oder nicht. Mit dem rechten Teil der If-Bedingung soll über-
prüft werden, ob der vorletzten Übergabeparameter der Option -f entspricht. Genau
hier hat der Autor das Problem überoptimiert, da ausschließlich der zweite Buchsta-
be des vorletzten Parameters verglichen wird. Das Programm müsste sich demnach
überlisten lassen, wenn es mit einer zweielementigen Recipient-Liste aufgerufen
wird, deren erster Empfängername ein „f“ als zweiten Buchstaben besitzt. Mit den
folgenden Argumenten aufgerufen wird die besagte Applikation in der Tat in den
Empfangs- und nicht in den Sendemodus versetzt:
mail Effie Dirk
ßen durchgeführt werden muss und geradezu nach einer Standardlösung schreit. In
diesem Sinne ist auch die durchgeführte Fehlerkorrektur reine Symptombekämp-
fung und weit von einer sauberen Lösung entfernt. In Abschnitt [Link] werden wir
erneut auf das Mail-Beispiel zurückkommen und zwei solide Programmalternativen
kennen lernen.
2038_bug.c
#include <stdio.h> 1
#include <time.h> 2
#include <limits.h> 3
4
int main (int argc, char **argv) 5
{ 6
time_t t = 0x7FFFFFFF - 5; 7
int i; 8
9
for (i=0; i<10; i++) { 10
time_t s = t; 11
printf("%s",asctime(gmtime(&s))); 12
t++; 13
} 14
return 0; 15
} 16
Darunter fallen neben fast allen Unix-basierten Systemen wie Linux und Mac OS X
auch Windows und viele Betriebssysteme aus dem Embedded-Bereich.
Zur Darstellung der Zeit definieren diese Betriebssysteme einen Datentyp
time_t, der die seit dem 1. Januar 1970 verstrichene Zeit in Sekunden repräsen-
tiert. time_t ist in vielen Betriebssystemen als vorzeichenbehaftete 32-Bit-Integer-
Zahl definiert, so dass pünktlich am 19. Januar 2038 um 3:14:08 UTC – exakt
2147483647 Sekunden nach Beginn der Zählung am 1. Januar 1970 – ein nume-
rischer Überlauf eintreten wird.
Die Folgen sind ähnlich unvorhersehbar wie schon im Falle des Millenium-Bugs,
jedoch besteht die berechtigte Hoffnung, dass im Jahr 2038 viele der heute als kri-
tisch einzustufenden Systeme immun gegen den Fehler sein werden. Der Grund
hierfür ist die sich bereits heute vollziehende 64-Bit-Migration der gängigen Be-
triebssysteme. Da der Datentyp time_t dort in der Regel bereits als 64-Bit-Integer-
Zahl dargestellt wird, verschwindet die Überlaufproblematik, sobald die entspre-
chende Applikation auf einem 64-Bit-Betriebssystem neu übersetzt wird. In einigen
Fällen bleibt das Problem jedoch weiterhin bestehen, insbesondere dann, wenn eine
Applikation explizit nur die unteren 32 Bit der entsprechenden time_t-Variablen
ausliest. Folgerichtig kommen wir auch im Falle des 2038-Bugs nicht umhin, alle
relevanten Applikationen im Vorfeld auf dessen Kompatibilität zu untersuchen.
Auf die Frage, ob Ihr Betriebssystem anfällig gegenüber dem 2038-Problem ist
oder nicht, gibt das C-Programm in Abb. 2.14 Auskunft. Zu Beginn wird die Va-
riable t vom Typ time_t deklariert und mit einem Wert knapp unter der 32-Bit-
Überlaufschwelle initialisiert. Innerhalb der sich anschließenden For-Schleife wird
der Wert von t sukzessive um eins erhöht und mit Hilfe der Funktionen gmtime und
asctime zunächst in eine Datums- und Zeitangabe und danach in eine druckba-
2.7 Von tickenden Zeitbomben 53
time_after.c
#define time_after(unknown, known) ((unknown) > (known)) 1
2
void time_sensitive() { 3
unsigned long deadline = jiffies + HZ; 4
/* deadline = aktuelle Zeit plus 1 Sekunde */ 5
6
... 7
8
if (time_after(jiffies, deadline)) { 9
/* Deadline wurde verletzt */ 10
... 11
} else { 12
/* Deadline wurde eingehalten */ 13
... 14
} 15
} 16
Abb. 2.15 Viele Programme sind anfällig für numerische Überläufe, die im Rahmen des Software-
Tests schwer zu entdecken sind. In diesem Fall kommt der Fehler erst nach mehreren Tagen zum
ersten Mal zum Vorschein
Bei jedem Überlauf evaluiert das oben definierte Makro time_after, wie das
folgende Beispiel zeigt, zu einem falschen Ergebnis. Dabei spielt es keine Rolle,
wie nahe beide Zeitpunkte nebeneinander liegen:
time_after(UINT_MAX+1, UINT_MAX)
= time_after(4294967295+1, 4294967295)
= time_after(0, 4294967295)
= (0 > 4294967295)
= FALSE
Die Ursache geht auf die zwar einsichtige, aber zugleich naive Implementierung
des time_after-Makros zurück. Durch die folgende Umformulierung lässt sich
die Überlaufproblematik auf trickreiche Weise lösen, ohne die Laufzeit des Makros
zu verschlechtern:
#define time_after(unknown, known)
((long)(known) - (long)(unknown) < 0)
Angewendet auf das obige Beispiel liefert das Makro trotz des numerischen Über-
laufs jetzt das korrekte Ergebnis:
time_after(UINT_MAX+1, UINT_MAX)
= time_after(4294967295+1, 4294967295)
= time_after(0, 4294967295)
= ((long)4294967295 - (long)0 < 0)
= ((-1) - 0 < 0)
= (-1 < 0)
= TRUE
2.8 Spezifikationsfehler 55
jiffies.h
#define time_after(unknown, known) 1
((long)(known) - (long)(unknown) < 0) 2
#define time_before(unknown, known) 3
((long)(unknown) - (long)(known) < 0) 4
#define time_after_eq(unknown, known) 5
((long)(unknown) - (long)(known) >= 0) 6
#define time_before_eq(unknown, known) 7
((long)(known) - (long)(unknown) >= 0) 8
In der Tat implementiert der Linux-Kernel time_after und eine Reihe ähnlicher
Makros exakt auf diese Weise, so dass ein Überlauf der Variablen jiffies beim
Vergleich zweier nahe beieinander liegender Zeitpunkte keine Probleme verursacht.
Die Makros des Linux-Kernels befinden sich in der Datei linux/jiffies.h, die
auszugsweise in Abb. 2.16 dargestellt ist.
Einen Wermutstropfen gibt es allerdings auch hier zu beklagen. Viele Software-
Entwickler bedienen sich der Makros erst gar nicht und programmieren den Ver-
gleich zweier Zeitpunkte manuell aus. Die Frage, welche Implementierungsvariante
die meisten Programmierer hier bevorzugen, möge sich der Leser selbst beantwor-
ten. Im Zweifel hilft ein Reboot – zumindest alle 49 Tage.
2.8 Spezifikationsfehler
Alle bisher vorgestellten Software-Fehler waren Implementierungsfehler, d. h. ih-
re Ursachen gingen auf eine fehlerhafte Umsetzung der Spezifikation zurück. In der
Tat lassen sich die meisten in der Praxis beobachtbaren Defekte auf Fehler in der Im-
plementierung zurückführen – nicht zuletzt aus dem Grund, dass die Spezifikation
oft nur vage formuliert oder gar nicht vorhanden ist. In anderen Fällen ist bereits die
Spezifikation fehlerhaft. Spezifikationsfehler sind schon deshalb als kritisch einzu-
stufen, da fast alle Methoden der Software-Qualitätssicherung auf die korrekte Um-
setzung der Spezifikation in die Implementierung fokussieren. Für die Erkennung
von Spezifikationsfehlern bleiben diese Methoden außen vor. Insbesondere ist von
dieser Limitierung auch die formale Verifikation betroffen, die als einzige der hier
vorgestellten Technik im mathematischen Sinne vollständig ist. Die Vollständigkeit
bedeutet an dieser Stelle nichts anderes, als dass für ausnahmslos alle möglichen
Eingaben sichergestellt wird, dass die Implementierung der Spezifikation genügt.
Fehler in der Spezifikation liegen damit auch hier konstruktionsbedingt außerhalb
des Leistungsspektrums dieser Verfahren.
Ein klassischer Spezifikationsfehler trat am 14.9.1993 schlagartig in das Licht
der Öffentlichkeit, als der Lufthansa-Flug 2904 bei der Landung auf dem Flughafen
Warschau-Okecie verunglückte. Nach dem Aufsetzen schießt der Airbus A320 über
die Landebahn hinaus und kommt erst durch die Kollision mit einem künstlichen
56 2 Software-Fehler
15:33:48 15:33:57
(769,9 m) (1525,1 m)
Bodenkontakt des Bodenkontakt des
rechten Fahrwerks linken Fahrwerks
Gesamtlänge: 2800 m
15:33:51
LH2904 rast über
(1028,2 m)
die Landebahn
Bodenkontakt des
hinaus
Bugfahrwerks
Erdwall zum Stillstand. Durch den seitlichen Aufprall werden die Treibstofftanks
des Flügels aufgerissen und die Maschine geht kurze Zeit später in Flammen auf.
Trotz der enormen Zerstörung überleben das Unglück 68 Passagiere – für den Ko-
piloten und einen Passagier kommt allerdings jede Hilfe zu spät.
Was war passiert? Um 15:29 befindet sich der Lufthansa-Airbus kurz vor War-
schau und erhält von der Anflugkontrolle die Daten für den Endanflug. Die meteo-
rologischen Bedingungen sind nicht optimal an diesem Tag. Im Bereich des Flug-
hafens setzt starker Regen ein und die Piloten der vorher gelandeten Maschinen
berichten von teilweise erheblichen Scherwinden während des Anflugs. Als Reakti-
on auf die Scherwindmeldung erhöht der Pilot zur Stabilisierung von LH 2904 die
Anfluggeschwindigkeit um 20 Knoten. In Bodennähe wird das Flugzeug durch un-
erwartet hohe Rückenwinde weiter vorangetrieben, so dass sich die Maschine der
Landebahn mit deutlich erhöhter Geschwindigkeit als normal nähert und erst nach
770 Metern um 15:33 und 48 Sekunden mit dem rechten Fahrwerk aufsetzt. Damit
hat die Maschine bereits ein Viertel der nur rund 2800 Meter langen Landebahn
überflogen. 3 Sekunden später setzt das Bugrad auf, den Piloten gelingt es jedoch
nicht, die Schubumkehr zu aktivieren. Erst weitere 6 Sekunden später registriert der
Bordcomputer das Aufsetzen des linken Fahrwerks und damit den Bodenkontakt
der gesamten Maschine. Zu diesem Zeitpunkt bleiben den Piloten nur noch 1275
Meter bis zum Ende der Runway – zu wenig, um den Airbus rechtzeitig zum stehen
zu bringen. Kurz vor dem Ende der Landebahn dreht der Pilot die Maschine quer,
um den Aufprall abzumildern. Abb. 2.17 fasst den zeitlichen Ablauf der Ereignisse
in einer Übersichtsgrafik zusammen.
Doch warum gelang es den Piloten erst so spät, die Schubumkehr zu aktivieren?
Die Ursache geht auf einen Schutzmechanismus des Airbus A320 zurück, der die
Aktivierung der Schubumkehr während des Flugs verhindern soll. Ob sich das Flug-
zeug noch in der Luft befindet oder bereits Bodenkontakt besteht, wird durch das
2.8 Spezifikationsfehler 57
boolesche Signal A/G repräsentiert und von der Airbus-Software durch die Auswer-
tung der folgenden Parameter berechnet:
Der verunglückte Airbus verwendete zur Berechnung des A/G-Signals die folgende
Formel:
A/G = (min(pl , pr ) > 12.000 kg) ∨ (min(vl , vr ) > 72 kts) (2.3)
Der Bodenkontakt ist demnach hergestellt, sobald der Kompressionsdruck beider
Schockabsorber mindestens 12 Tonnen beträgt oder sich die Räder des linken und
des rechten Fahrwerks beide mit mehr als 72 Knoten drehen. Durch die disjunktive
Verknüpfung wird der Bodenkontakt bereits dann erkannt, wenn nur eine der beiden
Bedingungen erfüllt ist.
Im Falle von LH 2904 führte die Kombination von starken Seitenwinden und
großen Wassermengen auf der Landebahn dazu, dass die Airbus-Steuerung erst 9
Sekunden nach dem Aufsetzen des rechten Fahrwerks den Bodenkontakt des Flug-
zeugs registrierte. Zum einen wurde aufgrund der Seitenwinde der geforderte Kom-
pressionsdruck von 12.000 kg erst sehr spät erreicht, zum anderen führte starkes
Aquaplaning dazu, dass das Rad des linken Fahrwerks nicht schnell genug die be-
nötigte Geschwindigkeit aufbauen konnte.
Die gewonnenen Erkenntnisse ließen den Schluss zu, dass die in Gleichung (2.3)
dargestellte Spezifikation ungeeignet ist, um den Bodenkontakt auch bei widrigen
Wetterbedingungen zuverlässig zu erkennen. Die Spezifikation wurde geändert und
heute reicht bei allen Airbus-Flugzeugen vom Typ A320 der Lufthansa-Flotte be-
reits ein Anpressdruck von 2.500 kg aus, um den Bodenkontakt zu erkennen. Der
Crash von LH 2904 würde sich unter den damals herrschenden meteorologischen
Bedingungen mit den geänderten Parametern wahrscheinlich nicht wiederholen.
Ob die neue Parametrisierung ausreicht, um auch bei noch höheren Seitenwin-
den den Bodenkontakt rechtzeitig zu erkennen, steht in den Sternen. Entsprechen-
den Forderungen, das manuelle Setzen des A/G-Signals durch die Piloten zuzulas-
sen, stehen Sicherheitsbedenken entgegen. In Falle von LH 2904 hätte das Unglück
durch die manuelle Aktivierung der Schubumkehr vermieden werden können, aller-
dings ist diese Möglichkeit im Zeitalter terroristischer Bedrohungen mit weiteren
unkalkulierbaren Risiken verbunden. Wird die Schubumkehr während des Flugs ak-
tiviert, bedeutet dies den sicheren Absturz.
An die realen Folgen einer solchen Aktivierung erinnert das Schicksal des Lauda-
Air-Flugs 004 von Bangkok nach Wien. Als am 26.5.1991 während des Steigflugs
die Schubumkehr des linken Triebwerks aufgrund eines technischen Defekts aus-
löst, wird das Flugzeug über Thailand mit 223 Passagieren an Bord regelrecht zer-
rissen.
58 2 Software-Fehler
Tabelle 2.2 Risikobewertung des Pentium-FDIV-Bugs durch Intel (Auszug aus [59])
Impact of failure
Class Applications MTBF
in div/rem/tran
Word processing Microsoft Word, Wordperfect, etc. Never None
Spreadsheets 123, Excel, QuattroPro 27,000 years Unnoticeable
(basic user) (basic user runs fewer than 1000 div/day)
Diese und andere Untersuchungen kamen allesamt zu dem Schluss, dass der
Fehler als unbedenklich einzustufen sei. Trotzdem wurde der öffentliche Druck so
stark, dass sich Intel schließlich gezwungen sah, eine der größten Rückrufaktionen
in der IT-Geschichte zu starten. Zu diesem Zeitpunkt waren bereits ca. 2 Millionen
Pentium-Prozessoren im Alltagseinsatz, von denen jedoch lediglich 10 % an Intel
zurückgesandt wurden. Darüber hinaus mussten schätzungsweise 500 000 Prozesso-
ren aus den eigenen Lagerbeständen und 1 500 000 aus Händlerbeständen vernichtet
werden. Rückblickend wird der finanzielle Schaden des Pentium-FDIV-Bugs auf ca.
475 Millionen US-Dollar geschätzt.
Obwohl der Pentium-Bug kurzfristig auch das Image von Intel ins Wanken brach-
te, war dieser Effekt nicht nachhaltig. Intel hat es verstanden, nach den anfänglich
eher unglücklichen Beschwichtigungsversuchen am Ende geschickt auf den öffent-
lichen Druck zu reagieren. Das Image der Firma Intel ist heute ungebrochen hoch.
Doch was war die Ursache für die falschen Berechnungen des Pentium-
Prozessors und warum traten Rechenfehler nur derart vereinzelt auf? Der Grund
liegt in der Architektur der Fließkommaeinheit, die zur Division zweier Zahlen die
Radix-4-SRT-Division verwendet. Im Gegensatz zu klassischen Divisionseinheiten,
die das Ergebnis in einem iterativen Prozess Nachkommastelle für Nachkomma-
stelle erzeugen, erlaubt die Radix-4-SRT-Division, in jedem Iterationsschritt zwei
Nachkommastellen auf einen Schlag zu berechnen. Die Idee der SRT-Division ist
nicht neu und geht auf die Arbeiten von Sweeney [49], Robertson [223] und To-
cher [256] zurück, die den Divisionsalgorithmus unabhängig voneinander in den
späten Fünfzigerjahren entwickelten.
Eine der Grundideen der SRT-Division besteht darin, das Divisionsergebnis in-
tern nicht im Binärformat darzustellen. An die Stelle der Binärziffern 0 und 1 treten
Koeffizienten mit den Werten −2, −1, 0, 1 oder 2. Der im i-ten Iterationsschritt zu
wählende Wert des Koeffizienten qi wird in Abhängigkeit des Dividenden pi und des
Divisors d aus einer Tabelle ausgelesen. Hierzu verwaltet der Pentium-I-Prozessor
60 2 Software-Fehler
0101000 0
0
0100000 0
0
7 höchstwertige Stellen des Dividenden
0011000 0
0001000
0000000
1111000
1110000
1101000
1100000
1011000
10000
10001
10010
10100
10101
10011
10110
11000
11001
11010
10111
11011
11100
11101
11110
11111
5 höchstwertige Stellen des Divisors
intern eine zweidimensionale Matrix mit 2048 Einträgen, von denen aber nur insge-
samt 1066 verwendet werden (vgl. Abb. 2.18). Zur Adressierung werden die ersten
7 Stellen des Dividenden pi als Zeilenindex und die ersten 5 Stellen des Divisors
d als Spaltenindex verwendet. Entsprechend dem Wertebereich von qi enthält jede
Zelle einen der Werte −2, −1, 0, 1 oder 2.
Der Pentium-FDIV-Bug hat seine Ursache schlicht in fünf falschen Tabellen-
einträgen an der oberen Grenze zwischen dem benutzten und dem unbenutzten Be-
reich. Anstelle des (korrekten) Werts 2 enthalten die Zellen in fehlerhaften Pentium-
Prozessoren den Wert 0. Die Tatsache, dass der Pentium-Bug in der Praxis nur
bei sehr wenigen Zahlenkombinationen auftritt, liegt an der unregelmäßigen Wahr-
scheinlichkeitsverteilung, mit der die einzelnen Zellen ausgelesen werden. Wären
die Zugriffe auf alle Zellen der Matrix statistisch gleichverteilt, wäre der Pentium-
Bug in der Praxis wesentlich häufiger zu beobachten und damit mit hoher Wahr-
scheinlichkeit vor der Auslieferung erkannt und behoben worden.
Aus mathematischer Sicht ist der Pentium-FDIV-Bug heute gut untersucht. Eini-
ge der wichtigsten Erkenntnisse gehen auf die Arbeiten von Alan Edelman zurück,
der die Natur des Pentium FDIV-Bugs in [80] treffend zusammenfasst:
2.10 Fehlerbewertung 61
“The bug in the Pentium was an easy mistake to make, and a difficult one
to catch.”
Alan Edelman [80]
Mit dem Erscheinen der Nachfolgemodelle, dem Pentium Pro und dem Pentium
MMX, gehörte der FDIV-Bug vollends der Vergangenheit an. Sollte dem Divisions-
debakel überhaupt etwas Positives abzugewinnen sein, so ist es wahrscheinlich die
Erkenntnis, dass es keine fehlerfreie Hardware gibt und wahrscheinlich nie geben
wird. Galt die Software damals in der öffentlichen Meinung als die einzige ernst-
zunehmende Quelle für Computerfehler, so hat sich diese Einstellung mit dem Be-
kanntwerden des FDIV-Bugs nachhaltig geändert. Seit die Komplexität von Hard-
ware der Komplexität von Software kaum noch nachsteht, gehört die lange vorherr-
schende Ansicht der fehlerfreien Hardware ins Reich der Visionen.
Auswirkungen hat diese Erkenntnis insbesondere für den Bereich eingebetteter
Systeme. Zur Gewährleistung der mitunter sehr hohen Zuverlässigkeitsanforderun-
gen muss neben der funktionalen Korrektheit der Software-Komponenten und der
physikalischen Beständigkeit der Bauelemente auch die funktionale Korrektheit der
Hardware als Risikofaktor berücksichtigt werden.
2.10 Fehlerbewertung
In den vorangegangenen Abschnitten haben wir einige der prominentesten
Software-Fehler der Computergeschichte kennen gelernt und ihr Wesen genauer
beleuchtet. Deren Auswirkungen waren dabei genauso vielfältig wie deren Ursa-
chen, so dass sich mit dem Begriff des Software-Fehlers nicht nur ein quantitativer
Aspekt, sondern immer auch ein qualitativer verbindet. Zur Verdeutlichung dieses
qualitativen Aspekts betrachten wir die drei C-Fragmente in Abb. 2.19. Jedes Bei-
spiel beschreibt ein typisches Fehlersymptom zusammen mit der beobachteten Auf-
trittswahrscheinlichkeit.
Das erste Programm initialisiert die Variable x zunächst mit dem größten dar-
stellbaren Integer-Wert INT_MAX und beschreibt anschließend die Variable y mit
dem Wert x+1. Aufgrund der Bereichsüberschreitung verursacht die Addition auf
der Mehrzahl der Hardware-Architekturen einen numerischen Überlauf. Legen wir,
wie auf den meisten Architekturen üblich, die Zweierkomplementdarstellung zu-
grunde, so entspricht der Wert von y nach der Zuweisung der kleinsten darstellbaren
Zahl INT_MIN. Kurzum: Der entstandene arithmetische Fehler ist maximal. Weiter
wollen wir für dieses Beispiel annehmen, dass der Fehler in jährlichen Abständen
auftritt.
Das zweite Programm initialisiert die Variable x ebenfalls mit dem Wert
INT_MAX, führt die Addition im Gegensatz zum ersten Beispiel jedoch gesättigt aus.
Hierzu wird der drohende Überlauf mit Hilfe eines zusätzlich eingefügten If-Befehls
abgefangen und die Addition nur dann ausgeführt, wenn der Ergebniswert innerhalb
des Integer-Zahlenbereichs liegt. Im Falle eines drohenden Überlaufs wird der Wert
von x unverändert in die Variable y übernommen. Obwohl das Programm immer
noch ein falsches Ergebnis berechnet, fällt der arithmetische Fehler im Gegensatz
62 2 Software-Fehler
zur ersten Lösung um ein Vielfaches geringer aus. Auch hier wollen wir annehmen,
dass der Fehler jährlich auftritt.
Im dritten Beispiel wird die Variable x innerhalb des If-Zweigs durch 0 dividiert.
Ohne weitere Programmierkniffe löst die Division einen Ausnahme-Interrupt aus,
der das Betriebssystem dazu veranlasst, den laufenden Prozess umgehend zu been-
den. Weiter wollen wir annehmen, dass der Programmabsturz stündlich auftritt.
Die Frage, die wir an dieser Stelle zu beantworten versuchen, lässt sich plakativ
wie folgt formulieren:
„Welcher Fehler ist Ihr größter Feind?“
Haben Sie die Frage für sich bereits beantwortet? Vielleicht gehören auch Sie zu der
großen Gruppe von Befragten, die nicht lange zögert und sich nach kurzer Zeit ein-
deutig auf einen Fehler festlegt. Obwohl die Antworten in der Regel schnell gege-
ben werden, fallen sie für gewöhnlich denkbar unterschiedlich aus. Bei genauerem
Hinsehen ist dieses Ergebnis wenig überraschend, da die korrekte Beantwortung der
gestellten Frage ohne weitere Information im Grunde genommen unmöglich ist. Der
fehlende Parameter ist die Sichtweise, die wir zur Beurteilung der drei vorgestellten
Fehler einnehmen. Je nachdem, ob wir in der Rolle des Software-Entwicklers oder
in der Rolle des Anwenders argumentieren, kommen wir zu völlig unterschiedlichen
Ergebnissen.
Die Kriterien, die wir aus Anwendersicht anlegen, sind offensichtlich. Die
Schwere eines Fehlers steigt proportional mit seiner Auftrittswahrscheinlichkeit und
dem Grad seiner Auswirkung. In der Rolle des Anwenders werden wir deshalb den
im dritten Beispiel beschriebenen Fehler als unseren größten Feind erachten. So-
wohl die Auswirkung in Form des vollständigen Programmabsturzes, als auch die
Auftrittshäufigkeit sprechen eine eindeutige Sprache.
Aus der Sicht des Software-Entwicklers stellt sich die Situation gänzlich an-
ders dar. Die Auftrittswahrscheinlichkeit und der Grad seiner Auswirkung sind
2.10 Fehlerbewertung 63
3.1 Software-Richtlinien
Typischerweise wird der Begriff der Software-Richtlinie synonym für eine Menge
von Konventionen verwendet, die den Gebrauch einer bestimmten Programmier-
sprache in einem Maß regelt, das deutlich über ihre eigenen syntaktischen und se-
mantischen Restriktionen hinausgeht. Der Einsatz von Software-Richtlinien ist in
den meisten Fällen durch einen der folgenden Aspekte motiviert:
I Vereinheitlichung
Die meisten Software-Ingenieure entwickeln im Laufe ihrer Karriere einen
sehr persönlichen Stil im Umgang mit einer bestimmten Programmiersprache.
Nimmt die Individualität der verschiedenen Programmierstile überhand, entsteht
in großen Software-Projekten ein spürbarer Effizienzverlust, der nicht zuletzt
durch die hohe Einarbeitungszeit anderer Programmierer verursacht wird. Der
Einsatz von Software-Richtlinien kann an dieser Stelle helfen, den Quellcode
einheitlich zu verfassen und damit unabhängig von den Vorlieben einzelner Pro-
grammierer zu gestalten. Software-Richtlinien mit dieser Zielsetzung beschrän-
ken sich in den meisten Fällen auf die syntaktischen Aspekte einer Program-
miersprache und nehmen damit die Rolle von Notationskonventionen ein. In Ab-
schnitt 3.1.1 werden wir die gängigen Konventionen im Detail diskutieren.
I Fehlerreduktion
Einige Software-Richtlinien gehen über rein syntaktische Notationsaspekte hin-
aus und definieren feste Regeln für den Umgang mit den einzelnen Sprachkon-
strukten. Im Folgenden wird diese Art der Richtlinie als Sprachkonvention be-
zeichnet. Der Einsatz solcher Konventionen ist mit der Hoffnung verknüpft, die
Anzahl der Software-Defekte durch die Verwendung einheitlicher Lösungsmus-
ter zu verringern und klassische Fallstricke durch das Verbot fehlerträchtiger
Sprachkonstrukte zu umgehen. In Abschnitt 3.1.2 werden wir einige der heute
gebräuchlichen Sprachkonventionen näher untersuchen.
3.1.1 Notationskonventionen
Neben einer Vielzahl firmeninterner Richtlinien haben sich im Laufe der Zeit meh-
rere Notationskonventionen (coding styles) etabliert, die auf der Projektebene (z. B.
Mozilla Coding Style), der Sprachenebene (z. B. Java Coding Style) oder der Be-
triebssystemebene (z. B. Linux Kernel Coding Style) eingesetzt werden. Typische
Notationskonventionen umfassen Regeln für die folgenden Sprachaspekte:
I Auswahl und Schreibweise von Bezeichnern
I Dokumentation
[Link] Notationsstile
Die gängigen Schreibweisen von Bezeichnern lassen sich einer der folgenden Stil-
gruppen zuordnen:
I Pascal Case
Der Bezeichner selbst sowie jedes darin enthaltene Wort beginnt mit einem Groß-
buchstaben. Die einzelnen Wortbestandteile sind ohne Trennzeichen aneinander
gefügt (IntStream, ForegroundColor, ...). Unter anderem wird die Schreibwei-
se in der Programmiersprache Java für die Bezeichnung von Klassen eingesetzt.
I Camel Case
Der Bezeichner beginnt mit einem Kleinbuchstaben, jedes weitere Wort wird
durch einen Großbuchstaben eingeleitet. Die einzelnen Wortbestandteile sind oh-
ne Trennzeichen aneinander gefügt (intStream, foregroundColor, ...). Bei-
spielsweise wird die Schreibweise in der Programmiersprache Java für die Be-
zeichnung von Variablen und Methoden verwendet.
I Uppercase
Der gesamte Bezeichner wird in Großbuchstaben dargestellt. Die einzelnen
Wortbestandteile sind entweder direkt oder mit Hilfe eines Trennzeichens anein-
ander gefügt (INTSTREAM, INT_STREAM, ...). Unter anderem wird die Schreibwei-
se in der Programmiersprache C für die Bezeichnung von Präprozessor-Makros
eingesetzt.
3.1 Software-Richtlinien 67
I Lowercase
Der gesamte Bezeichner wird in Kleinbuchstaben dargestellt. Die einzelnen
Wortbestandteile sind entweder direkt oder mit Hilfe eines Trennzeichens an-
einander gefügt (intstream, int_stream, ...). In der Programmiersprache C
wird die Schreibweise z. B. für die Bezeichnung von Variablen und Funktionen
verwendet.
Das Präfix für kompliziertere Datentypen wird gebildet, indem die Präfixe der ele-
mentaren Datentypen aneinandergereiht werden:
// Long-Pointer auf einen nullterminierten String
68 3 Konstruktive Qualitätssicherung
char *lpszName;
// Globales Array von 3 Floats
float g_rgfVertex[3];
In der Praxis wird die Ungarische Notation nicht immer einheitlich verwendet, so
dass sich die Bedeutungen einzelner Tags von Fall zu Fall unterscheiden können.
Beispielsweise wird das Präfix c nicht nur für Zeichen (Characters), sondern mitun-
ter auch für abgezählte Werte (c = count of items) verwendet. In der Praxis führt die
Mehrdeutigkeit jedoch nur selten zu Problemen, solange die Konvention projektein-
heitlich verwendet wird.
Seit ihrem Erscheinen wird über den Nutzen der Ungarischen Notation genauso
vehement wie kontrovers diskutiert. Die Eigenschaft, den Datentyp einer Variablen
direkt an dessen Namen zu erkennen, mag dem einen oder anderen Programmierer
zwar bequem erscheinen, bringt auf der negativen Seite jedoch erhebliche Risiken
mit sich. Effektiv wird der Datentyp einer Variablen de facto zweimal festgelegt:
zum einen während der Deklaration und zum anderen während der Namensgebung.
Kurzum: Das Vorgehen verstößt damit in eklatanter Weise gegen das Single-Source-
Prinzip. In größeren Software-Projekten ist es nur eine Frage der Zeit, bis der dekla-
rierte und der namentlich codierte Datentyp nicht mehr übereinstimmen. Ohnehin
ist der Zusatznutzen datentyptransparenter Variablennamen im Zeitalter moderner
integrierter Entwicklungsumgebungen (IDEs) mehr als fraglich. Der Datentyp einer
Variablen kann von nahezu allen IDEs automatisch ermittelt und angezeigt werden.
Linus Torvalds fasst die Idee der Ungarischen Notation im Linux Kernel Coding
Style wie folgt zusammen:
3.1 Software-Richtlinien 69
“Encoding the type of a function into the name (so-called Hungarian no-
tation) is brain damaged - the compiler knows the types anyway and can
check those, and it only confuses the programmer. No wonder Microsoft
makes buggy programs.”
Linus Torvalds [257]
Tabelle 3.3 Namenskonvention von Sun Microsystems für die Programmiersprache Java
Kategorie Notation Beispiel
Package Lowercase [Link]
Class Pascal case class NumberCruncher
Interface Pascal case interface Crunchable
Methode Camel case printResult
Variable Camel case age
Konstante Uppercase EARTH_GRAVITY
linux_style.lsp
(defun linux-c-mode () 1
"C mode with adjusted defaults for use with the Linux kernel." 2
(interactive) 3
(c-mode) 4
(setq c-indent-level 8) 5
(setq c-brace-imaginary-offset 0) 6
(setq c-brace-offset -8) 7
(setq c-argdecl-indent 8) 8
(setq c-label-offset -8) 9
(setq c-continued-statement-offset 8) 10
(setq indent-tabs-mode nil) 11
(setq tab-width 8)) 12
“Now, some people will claim that having 8-character indentations makes
the code move far to the right, and makes it hard to read on a 80-character
terminal screen. The answer to that is that if you need more than 3 levels of
intentation, you’re screwed anyway, and should fix your program.”
Linus Torvalds [257]
Neben dem Linux Kernel Coding Style sind im Unix-Umfeld drei weitere Nota-
tionskonventionen angesiedelt, die ebenfalls in Tabelle 3.4 abgebildet sind. Der
BSD-Stil ist auch unter dem Namen Allman-Stil bekannt, benannt nach Eric Allman,
dem Autor zahlreicher BSD-Werkzeuge. Der GNU-Stil wird von der Free Software
Foundation empfohlen und ist die Basis vieler GNU-Projekte. Außerhalb der GNU-
Gemeinde ist der Stil jedoch selten anzutreffen. Der Whitesmith-Stil wird heute nur
noch vereinzelt verwendet und ist nach dem gleichnamigen C-Compiler benannt.
Die Frage, ob eine Notationskonvention den anderen überlegen ist, wird von
verschiedenen Entwicklern unterschiedlich beantwortet. Dass die zuweilen heftig
geführten Diskussionen mitunter glaubenskriegsähnliche Dimensionen annehmen,
zeigt das folgende Zitat:
“First off, I’d suggest printing out a copy of the GNU coding standards,
and not read it. Burn them, it’s a great symbolic gesture.”
Linus Torvalds [257]
Glücklicherweise stellt die Anpassung bestehenden Codes an eine vorgegebene No-
tationskonvention heute kein großes Problem mehr dar. Die meisten der heute im
Feld eingesetzten Entwicklungsumgebungen und Editoren sind in der Lage, Co-
de nach vorgegebenen Regeln selbstständig umzuformatieren. Exemplarisch ist in
Abb. 3.1 die Formatierungseinstellung des Linux Kernel Coding Styles für den
Emacs-Editor wiedergegeben, der sich insbesondere unter Unix-basierten Betriebs-
systemen großer Beliebtheit erfreut.
Alternativ stehen uns heute zahlreiche Werkzeuge zur Verfügung, mit denen be-
liebige Quelltexte flexibel umformatiert werden können. Ein bekanntes Beispiel ist
3.1 Software-Richtlinien 73
in den Linux Kernel Coding Style überführen. Die Option -kr übernimmt mehrere
Standardeinstellungen der Kernighan & Ritchie-Notation, während die Option -i8
die Einrückungstiefe auf 8 Zeichen festsetzt. Torvalds schreibt über die Verwendung
von Indent:
“indent has a lot of options, and especially when it comes to comment re-
formatting you may want to take a look at the manual page. But remember:
indent is not a fix for bad programming.”
Linus Torvalds [257]
Neben dem Erscheinungsbild schränkt der Linux Kernel Coding Style zusätzlich
die Verwendung einzelner Sprachkonstrukte ein. Unter anderem dürfen Präprozes-
sorbefehle wie z. B. #ifdef nicht mehr länger innerhalb eines Funktionsrumpfs
verwendet werden. In der Praxis wird dieses Konstrukt von vielen Programmie-
rern eingesetzt, um den Aufruf einer Unterfunktion in Abhängigkeit externer Pa-
rameter zu verhindern. Eine solche Verwendung zeigt das Programm in Abb. 3.2
(links). Mit wenig Aufwand lässt sich die Implementierung so abändern, dass sie
dem Linux Kernel Coding Styles gerecht wird. Anstatt jeden Aufruf der Funktion
mit einem #ifdef zu versehen, wird die betreffende Unterfunktion, wie in Abb. 3.2
(rechts) gezeigt, bei Bedarf durch eine leere Implementierung ersetzt. Aufgrund der
zusätzlichen Deklaration als Inline-Funktion wird der Aufruf durch den Compiler
vollständig eliminiert, so dass die neue Lösung dem ursprünglichen Programm in
Effizienzgesichtspunkten in nichts nachsteht.
Die beliebige Vermischung von Präprozessordirektiven und Programmcode ist
eine klassische und seit langem bekannte Fehlerquelle der Programmiersprache C,
und die getätigte Einschränkung darf mit Recht als eine der großen Stärken des
Linux Kernel Coding Styles angesehen werden. In Kapitel 7 werden wir auf die-
se Problematik erneut zurückkommen und sehen, wohin die unkontrollierte Ver-
mischung von Präprozessordirektiven und Programmcode letztendlich führen kann.
Dem interessierten Leser sei es an dieser Stelle nicht verwehrt, bereits jetzt einen
vorgreifenden Blick auf Abb. 7.12 zu werfen.
3.1.2 Sprachkonventionen
Während die in Abschnitt 3.1 vorgestellten Notationskonventionen allesamt die
Layout-Aspekte und damit die Syntax eines Programms in den Vordergrund stel-
len, adressieren Sprachkonventionen in erster Linie die semantischen Besonderhei-
ten einer Sprache. Die im Linux Kernel Coding Style enthaltene Vorschrift zur re-
striktiven Verwendung der #ifdef-Anweisung ist eine typische Regel, die in vielen
Sprachkonventionen in ähnlicher Form vorhanden ist. Der im folgenden Abschnitt
74 3 Konstruktive Qualitätssicherung
preproc1.c preproc2.c
void foo(void) 1 #ifdef DEBUG 1
{ 2 inline void foo(void) 2
printf("..."); 3 { 3
} 4 printf("..."); 4
5 } 5
int main(...) 6 #else 6
{ 7 void foo(void) 7
#ifdef DEBUG 8 { 8
foo(); 9 } 9
#endif 10 #endif 10
... 11 11
} 12 int main(...) 12
13 { 13
14 foo(); 14
15 ... 15
16 } 16
im Detail dargestellte MISRA-Regelsatz soll ein Gefühl dafür vermitteln, wie tief
und in welcher Form typische Sprachkonventionen den Umgang mit einer Program-
miersprache reglementieren.
[Link] MISRA-C
Die MISRA-C-Sprachkonvention wurde erstmals 1998 von der Motor Industry Soft-
ware Reliability Association, kurz MISRA, unter dem Namen Guidelines for the Use
of the C Language in Vehicle Based Software vorgestellt [181]. Angespornt von der
zunehmenden Verbreitung der Programmiersprache C im Bereich eingebetteter Sys-
teme, entstand die MISRA-Organisation als Zusammenschluss verschiedener Fir-
men der Automobilbranche. Ziel der MISRA-Sprachkonvention ist die Definition
eines Regelsatzes, der den Software-Entwickler vor den klassischen Programmier-
fehlern und Fallstricken der Programmiersprache C bewahrt. Durch die Vermeidung
fehleranfälliger Programmstrukturen wurde die Erhöhung der Qualität eingebetteter
Software im Allgemeinen und die Qualität von Software im Automobilbereich im
Speziellen angestrebt.
Die im Jahre 1998 publizierte Spezifikation besteht aus insgesamt 127 Regeln,
von denen 93 verpflichtend sind (required rules). Hinter den restlichen 34 Re-
geln verbergen sich Empfehlungen, die in begründeten Ausnahmefällen übergan-
gen werden dürfen (advisory rules). Seit ihrer Einführung erfreut sich die MISRA-
Sprachkonvention steigender Beliebtheit und wird heute nicht nur in der Automo-
bilindustrie, sondern auch in der Avionik und Medizintechnik eingesetzt. Mittler-
weile schreiben viele Hersteller, insbesondere im Automobilsektor, die Beachtung
der MISRA Richtlinien zwingend vor. Mehrere Firmen bieten hierzu Werkzeuge an,
3.1 Software-Richtlinien 75
mit deren Hilfe die Einhaltung der meisten MISRA-Regeln automatisiert überprüft
werden kann.
Neben vielen sinnvollen Programmiervorschriften brachte die erste Definition
von MISRA-C auch einige Probleme mit sich. Einige der Regeln waren so formu-
liert, dass eine werkzeuggestützte Überprüfung nicht möglich ist, andere Regeln
waren nur unpräzise definiert und konnten auf unterschiedliche Art und Weise aus-
gelegt werden. Die Mehrdeutigkeit führte dazu, dass sich verschiedene Werkzeu-
ge uneinheitlich verhielten und eine objektive Bewertung damit nur eingeschränkt
möglich war. Insgesamt wurde hierdurch die Idee, die MISRA-Konformität eines C-
Programms als Qualitätssiegel zu etablieren, deutlich relativiert – wenn nicht sogar
in Frage gestellt.
Mittlerweile liegt die MISRA Sprachkonvention in der Version MISRA-2004
vor [182]. Die zunehmende Verbreitung, weit über den Automobilsektor hinaus,
spiegelt sich auch in der Namensgebung wider: MISRA-2004 firmiert nun unter
dem Titel Guidelines for the Use of the C Language in Critical Systems. Kurz-
um: Der Regelsatz ist heute als ein allgemeiner Standard für sicherheitskritische
Systeme definiert und nicht mehr speziell auf den Bereich der Automobilindustrie
beschränkt.
Der neue Standard eliminiert einige der alten Regeln und fügt mehrere neue hin-
zu, die den neuen MISRA-Regelkatalog jetzt auf insgesamt 141 Regeln anwachsen
lassen. Durch eine komplette Neustrukturierung beseitigt die überarbeitete Versi-
on einige Defizite und Mehrdeutigkeiten des Originalentwurfs. Die 141 Regeln des
MISRA-2004-Standards teilen sich in insgesamt 21 Kategorien auf, die in Tabel-
le 3.5 zusammengefasst sind. Tabelle 3.6 enthält für jede Kategorie exemplarisch
eine Regel und soll auf diese Weise einen tieferen Einblick in die Philosophie und
die Reichweite der Sprachkonvention gewähren. Die ausführlichen Definitionen al-
ler 141 Regeln finden sich zusammen mit einer Reihe von Anwendungsbeispielen
in [182].
76 3 Konstruktive Qualitätssicherung
3.2 Typisierung
Während der Ausführung eines Programms werden in der Regel eine Vielzahl
verschiedener Datenworte verarbeitet, die sich in ihrer Struktur, ihrem Verhalten
und ihrer potenziellen Verwendung erheblich voneinander unterscheiden. Typisier-
te Sprachen bringen an dieser Stelle Ordnung in das Chaos, indem die auftretenden
Daten systematisch kategorisiert und gleichartige Objekte zu einem Datentyp zu-
sammengefasst werden. Fast alle der heute verwendeten Programmiersprachen set-
zen das Prinzip der Typisierung ein – wenn auch nicht immer in derselben Art und
Weise.
Heute gehört die typisierte Programmierung zu den mächtigsten Hilfsmitteln der
konstruktiven Qualitätssicherung. Entsprechend viele der modernen Programmier-
sprachen verfügen über komplexe Typsysteme, die weit über die Bereitstellung ei-
niger vordefinierter Datentypen hinausgehen. In der Vergangenheit wurde der Ty-
pisierung übrigens bei weitem nicht der gleiche Stellenwert zugewiesen wie heute.
3.2 Typisierung 77
Damit ist es nicht verwunderlich, dass Programmiersprachen aus der Frühzeit der
Computertechnik, wie z. B. Lisp, Fortran oder Cobol, anfangs nur über rudimentäre
Typsysteme verfügten.
3.2.1 Typsysteme
Als Typsystem wird derjenige Bestandteil eines Compilers oder einer Laufzeitum-
gebung bezeichnet, der ein Programm auf die korrekte Verwendung der Datenty-
pen überprüft. Die syntaktische Bildung von Ausdrücken wird durch das Typsystem
einer Programmiersprache deutlich eingeschränkt, so dass semantisch inkompati-
ble Konstrukte wie z. B. "41"++ oder &42 bereits zur Übersetzungszeit identifiziert
werden. Neben der automatischen Erkennung von Inkonsistenzen erhöht die Ver-
wendung von Datentypen auch die Lesbarkeit eines Programms. Datentypen dienen
somit nicht ausschließlich zur Codierung semantischer Informationen, sondern be-
sitzen darüber hinaus einen dokumentierenden Charakter.
In objektorientierten Programmiersprachen besteht ein direkter Zusammenhang
zwischen Klassen eines Programms und den Datentypen einer Programmiersprache.
So wird mit jeder neu angelegten Klasse gleichzeitig ein neuer Datentyp erzeugt,
der formal der Menge aller Klasseninstanzen entspricht. Des Weiteren führen die
mitunter komplexen Vererbungsmechanismen objektorientierter Sprachen zur Aus-
bildung einer Hierarchie auf der Ebene der Datentypen. In einigen Sprachen, wie
z. B. Java, wird diese zusätzlich durch eine Reihe elementarer Datentypen ergänzt,
die außerhalb der Klassenhierarchie stehen. Die Typentheorie beschäftigt sich for-
mal mit der Untersuchung von Typsystemen und ist im Laufe der Zeit zu einem der
bedeutendsten Teilgebiete der theoretischen Informatik herangewachsen. Die ma-
thematischen Hintergründe der gebräuchlichsten Typsysteme gelten heute als gut
untersucht [40, 183, 260, 213].
Im Folgenden bezeichnen wir ein Programm als typkonform, falls alle Pro-
grammkonstrukte so aufgebaut sind, dass die verschiedenen Datentypen in einer
konsistenten Beziehung zueinander stehen. Unabhängig von dem zugrunde liegen-
den Programmierparadigma kann die Prüfung der Typkonformität auf zwei grund-
sätzlich unterschiedliche Arten und Weisen erfolgen:
I Statische Typprüfung (static type checking)
Die Typprüfung wird bereits zur Übersetzungszeit durch den Compiler durch-
geführt und alle vorhandene Inkonsistenzen in Form von Fehlermeldungen oder
Warnungen an den Benutzer zurückgemeldet (vgl. Abb. 3.3 links). Nahezu al-
le Hochsprachen führen eine zumindest rudimentäre Typprüfung zur Überset-
zungszeit durch, die sich zwischen den einzelnen Sprachen jedoch erheblich un-
terscheiden kann. Einige Programmiersprachen wie Haskell, ML oder C# 3.0
erlauben sogar die Compilierung von Programmen mit teilweise unvollständigen
Typenangaben [205, 162]. In diesem Fall wird die fehlende Information, soweit
möglich, aus dem aktuellen Kontext hergeleitet (type inference) [213].
Typsystem Typsystem
Compiler Compiler Compiler
Zwischen- Zwischen-
code code
oder die Laufzeitumgebung durchgeführt (vgl. Abb. 3.3 Mitte). Eingesetzt wird
diese Technik insbesondere in Sprachen wie Java oder den diversen Dialekten
des .NET-Frameworks, die das Quellprogramm zunächst in einen Zwischencode
übersetzen und diesen zur Laufzeit innerhalb einer virtuellen Maschine interpre-
tieren.
Zumeist wird die dynamische Typprüfung als Ergänzung zur statischen Prü-
fung eingesetzt (vgl. Abb. 3.3 rechts). Hierdurch lassen sich alle Fehler, die der
statischen Analyse verborgen bleiben, zur Laufzeit abfangen und auf diese Wei-
se z. B. Systemabstürze erfolgreich verhindern. Die Sicherheit fordert an dieser
Stelle jedoch ihren Preis in Form von Laufzeiteinbußen, die durch die wiederkeh-
rend ausgeführten Typisierungstests entstehen. Aus diesem Grund sind dynami-
sche Typprüfungen hauptsächlich in Programmiersprachen der höheren Abstrak-
tionsebenen implementiert, während sich Hardware-nahe Programmiersprachen
zumeist auf die statische Typisierungsprüfung verlassen.
Lösen wir uns für den Moment von der Durchführung der Typisierungsprüfung und
wenden uns stattdessen den zugrunde liegenden Prinzipien zu, so lassen sich die
Typsysteme der heute gängigen Programmiersprachen auf der obersten Ebene wie-
derum in zwei Kategorien einteilen:
I Statische Typisierung (static typing)
In einem statischen Typsystem werden Variablen zusammen mit ihrem Datentyp
im Quelltext deklariert. Einer Variablen kann während ihrer gesamten Lebens-
dauer ausschließlich ein Objekt zugewiesen werden, das mit dem statisch festge-
legten Datentyp kompatibel ist. Klassische Vertreter dieses Paradigmas sind die
Programmiersprachen C, C++, C# und Java sowie die funktionalen Sprachdia-
lekte Haskell und ML (vgl. Abb. 3.4 links).
3.2 Typisierung 79
types.c [Link]
// C-Code: 1
# Python-Code: 1
2
2
int i; // Deklaration 3
# Deklaration entfällt 3
i = 3; // OK 4
i = 3 # OK 4
i = 3.14; // WARNUNG 5
i = 3.14 # OK 5
i = "Luzie"; // FEHLER 6
i = "Luzie" # OK 6
Abb. 3.4 Statische und dynamische Typisierung am Beispiel der Programmiersprachen C und
Python
C, C++ Java, C#
schwach, stark,
Delphi statisch statisch Haskell
Perl Smalltalk
Typisierungsquadrant
(typing quadrant)
gänzlich auf die Möglichkeit, Datentypen mit Hilfe von Type-Casts in einen an-
deren Typ zu konvertieren.
Moderne Sprachen wie Java bedürfen in diesem Zusammenhang ebenfalls ei-
ner genaueren Analyse. Obwohl die Sprachdefinition hier ausdrücklich erlaubt,
den Datentyp eines Objekts mit Hilfe eines Type-Casts zu verändern, wird Ja-
va ebenfalls den stark typisierten Sprachen zugeordnet. Verantwortlich hierfür
zeichnet die Laufzeitumgebung, die während der Programmausführung sicher-
stellt, dass entstehende Inkonsistenzen zuverlässig erkannt werden. Jede Typver-
letzung wird durch eine Ausnahme (exception) behandelt und ein Systemabsturz
hierdurch effektiv vermieden.
Die Einteilungen in schwache und starke Typsysteme sind orthogonal zum Kon-
zept der dynamischen und statischen Typisierung. Jede Programmiersprache lässt
sich hierdurch in eine von vier Klassen einteilen, die zusammen den in Abb. 3.5
dargestellten Typisierungsquadranten bilden.
[Link]
import [Link].*; 1
2
public class FooBar { 3
4
public static void main(String argv[]) { 5
6
// Create list 7
List list = new ArrayList<String>(); 8
[Link](new String("foo")); 9
[Link](new String("bar")); 10
[Link](new Integer(42)); 11
12
// Print list 13
Iterator i = [Link](); 14
while([Link]()) { 15
String item = (String) [Link](); 16
[Link](item); 17
} 18
} 19
} 20
Abb. 3.6 Der nachlässige Umgang mit Datentypen führt nicht selten zu schwerwiegenden Lauf-
zeitfehlern
für die Speicherung beliebiger Objekte. Wie zu erwarten, fordert die Flexibilität an
dieser Stelle ihren Tribut. Durch die Verwendung des Basistyps Object wird dem
Compiler die Typinformation über die gespeicherten Listenelemente vorenthalten
und damit faktisch die Möglichkeit entzogen, Datentypkonflikte zur Übersetzungs-
zeit zu erkennen. Das abgebildete Beispielprogramm verursacht einen solchen Kon-
flikt durch den letzten add-Befehl. Dieser fügt ein Objekt vom Typ Integer ein, wäh-
rend die Ausgabe-Schleife eine Liste erwartet, in der ausschließlich String-Objekte
gespeichert sind. Der Java-Compiler kann den Fehler nicht erkennen und übersetzt
das Programm fehlerfrei. Erst zur Ausführungszeit wird der Typkonflikt erkannt und
das Programm in der dritten Schleifeniteration mit einer ClassCastException ab-
gebrochen:
[Session started at 2006-12-20 09:09:52 +0100.]
foo
bar
Exception in thread "main" [Link]:
[Link] at [Link]([Link])
Eine mögliche Abhilfe stellt die Verwendung von Generics dar, die mit dem JDK
1.5 Einzug in die Programmiersprache Java hielten. Generics geben dem Program-
mierer ein Mittel an die Hand, um von konkreten Datentypen zu abstrahieren. Bei-
spielsweise lässt sich durch die Deklaration
3.2 Typisierung 83
[Link]
import [Link].*; 1
2
public class FooBar { 3
4
public static void main(String argv[]) { 5
6
// Create list 7
List<String> list = new ArrayList(); 8
[Link](new String("foo")); 9
[Link](new String("bar")); 10
[Link](new Integer(42)); 11
12
// Print list 13
Iterator<String> i = [Link](); 14
while([Link]()) { 15
String item = [Link](); 16
[Link](item); 17
} 18
} 19
} 20
Abb. 3.7 Mit Hilfe von Generics kann der Compiler den Typkonflikt zur Übersetzungszeit erken-
nen
eine typensichere Liste deklarieren, die ausschließlich Objekte vom Typ String
aufnehmen kann. Abb. 3.7 zeigt, wie sich das weiter oben diskutierte Java-
Programm mit Hilfe von Generics implementieren lässt. Da der Compiler jetzt
Kenntnis über die Datentypen der gespeicherten Elemente besitzt, kommt das mo-
difizierte Programm ohne einen einzigen Type-Cast aus. Vor allem aber wird die
bestehende Typinkonsistenz jetzt zur Übersetzungszeit und nicht erst zur Laufzeit
bemerkt
[Link]: cannot find symbol
symbol : method add([Link])
location: interface [Link]<[Link]>
[Link](new Integer(42));
Wie das Beispiel zeigt, trägt der sinnvolle Einsatz von Generics dazu bei, die Da-
tentypintegrität der Programmiersprache weiter zu steigern.
Engineering (ISE) entwickelt – zunächst als Werkzeug für den internen Gebrauch.
Einen größeren Bekanntheitsgrad erlangte sie erst im Jahre 1988 durch das Buch
Object-Oriented Software Construction von Bertrand Meyer, dem Gründer der Fir-
ma ISE1 [178, 179]. Eiffel ist als sicheres System entworfen, das nicht nur stark
typisiert, sondern auch frei von Loopholes ist. Kurzum: Die Konzeption des Typ-
systems sieht vor, dass Datentypinkonsistenzen stets erkannt und Systemabstürze
zuverlässig vermieden werden.
Der Vererbungsmechanismus von Eiffel basiert auf einem speziellen Verfeine-
rungskalkül (refinement calculus, [178, 52]), der es ermöglicht, vererbte Routinen
in der abgeleiteten Klasse in gewissen Grenzen umzudefinieren. Die Typkonformität
spielt hierbei eine wesentliche Rolle. Konkret müssen die Parameter und Rückgabe-
werte einer Routine in der abgeleiteten Klasse entweder gleich bleiben oder durch
spezialisiertere (abgeleitete) Datentypen ersetzt werden.
1 ISE wurde später umbenannt und firmiert seit Juli 2002 unter dem Namen Eiffel Software [81]
3.2 Typisierung 85
a
ROOTCLASS MYA
make() foo()
foobar(x : MYA, y : MYA); foobar(a : MYA)
b
MYB
bar()
foobar(b : MYB)
Abb. 3.9 Mit Hilfe des Verfeinerungskalküls lässt sich das Typsystem der Programmiersprache
Eiffel aushebeln
Das Beispielprogramm in Abb. 3.8 demonstriert, wie sich mit Hilfe des Verfeine-
rungskalküls eine Inkonsistenz im Typsystem von Eiffel erzeugen lässt. Insgesamt
werden hierzu 3 Klassen definiert (vgl. Abb. 3.9):
I MYA
Klasse MYA definiert die Routinen foo und foobar. Letztere nimmt ein weiteres
Objekt vom Typ MYA als Übergabeparameter entgegen und ruft für dieses Objekt
die Funktion foo auf.
I MYB
Klasse MYA vererbt die Routinen foo und foobar an die Klasse MYB und fügt
zusätzlich die Routine bar hinzu. Im Zuge der Vererbung von foobar wird der
Datentyp MYA des Übergabeparameters durch den spezialisierten Datentyp MYB
ersetzt. Die Implementierung der Funktion foobar ist in der abgeleiteten Klasse
ebenfalls geändert: Anstelle der Funktion foo wird jetzt die Funktion bar aufge-
rufen, die in jeder Instanz der Klasse MYB vorhanden ist.
I ROOT_CLASS (Wurzelklasse)
Die Wurzelklasse ROOT_CLASS definiert ebenfalls eine Routine foobar, die zwei
Objekte vom Typ MYA entgegennimmt. In der Einsprungsroutine make wird mit
a bzw. b ein Objekt der Klasse MYA und MYB erzeugt und die Routine foobar mit
den vier möglichen Objektkombinationen aufgerufen.
Das Programm durchläuft die statische Typprüfung ohne Beanstandung und wird
durch den Eiffel-Compiler klaglos übersetzt. Obwohl die Implementierung gänzlich
frei von manuellen Typkonversionen ist, führt der letzte Aufruf der Routine foobar
zu einem Systemabsturz. In diesem Fall wird die Routine mit zwei Objekten vom
Typ MYB respektive MYA aufgerufen. Wie das Sequenzdiagramm in Abb. 3.10 zeigt,
versucht das Eiffel-System die Routine bar auf einem Objekt vom Typ MYA auszu-
führen. Der Aufruf läuft an dieser Stelle ins Leere, da die Klasse MYA über keinerlei
86 3 Konstruktive Qualitätssicherung
ROOT_CLASS
make !!a a
!!b b
foobar(a,a)
foobar(a)
foo()
foobar(a,b)
foobar(b)
foo()
foobar(b,b)
foobar(b)
bar()
foobar(b,a)
foobar(a)
bar()
Abb. 3.10 Der letzte Aufruf der Routine foobar führt zu einem Systemabsturz
Routine bar verfügt. Auf dem eingesetzten Testrechner verabschiedet sich das com-
pilierte Eiffel-Programm daraufhin abrupt mit einem Bus error.
speed_limit1.c
#include <stdio.h> 1
2
static int speed_limit() 3
{ 4
// speed limit in mph ("miles per hour") 5
int limit = 65; 6
7
return limit; 8
} 9
10
int main(int argc, char *argv[]) 11
{ 12
// speed limit in kmh ("kilometer per hour") 13
int limit; 14
15
limit = speed_limit(); 16
17
printf("Speed limit = %d kmh\n", limit); 18
return 0; 19
} 20
Abb. 3.11 Durch die simultane Verwendung des Datentyps int bleibt der Typenfehler für den
Compiler unsichtbar
des Mars Climate Orbiters führte und heute zu den bekanntesten Software-Fehlern
der IT-Geschichte zählt (vgl. Abschnitt 2.2).
Im Folgenden wollen wir uns genauer mit der Frage auseinandersetzen, ob die
hauseigenen Bordmittel der Programmiersprache C ausreichen, um Fehler die-
ser Art im Vorfeld zu vermeiden. Eine naheliegende Idee könnte im Einsatz des
typedef-Konstrukts bestehen, mit dessen Hilfe sich neue Datentypbezeichner be-
nutzerdefiniert erzeugen lassen:
typedef <Datentyp> <Typbezeichner>
Wie das entsprechend modifizierte Programm in Abb. 3.12 zeigt, lassen sich die
neuen Bezeichner genauso wie die elementaren Datentypen verwenden. An allen
Stellen, an denen der C-Compiler den Typbezeichner int akzeptiert, lassen sich
auch die alternativen Bezeichner kmh bzw. mph einsetzen.
Gegenüber dem ersten Programm besitzt die modifizierte Variante einen gewalti-
gen Vorteil. Da die Typinkonsistenz jetzt auf den ersten Blick erkannt werden kann,
88 3 Konstruktive Qualitätssicherung
speed_limit2.c
#include <stdio.h> 1
2
typedef int kmh; 3
typedef int mph; 4
5
static mph speed_limit() 6
{ 7
// speed limit in mph ("miles per hour") 8
mph limit = 65; 9
10
return limit; 11
} 12
13
int main(int argc, char *argv[]) 14
{ 15
// speed limit in kmh ("kilometer per hour") 16
kmh limit; 17
18
limit = speed_limit(); 19
20
printf("Speed limit = %d kmh\n", limit); 21
return 0; 22
} 23
Abb. 3.12 Durch die Verwendung des Typedef-Konstrukts wird der Typkonflikt auf den ersten
Blick sichtbar. Nichtsdestotrotz übersetzt der Compiler das Programm fehlerfrei
ist die Chance entsprechend groß, den Fehler im Rahmen eines Reviews (vgl. Ab-
schnitt 5.5.2) oder einer Inspektion (vgl. Abschnitt 5.5.3) frühzeitig zu entlarven.
Durch die Verwendung von typedef ist es uns demnach gelungen, das Qualitäts-
merkmal der Transparenz deutlich zu verbessern.
An dieser Stelle soll mit einem weit verbreiteten Trugschluss über den typedef-
Mechanismus aufgeräumt werden: typedef definiert keinen neuen Datentyp, son-
dern lediglich ein Synonym für einen bereits existierenden. So klein der Unterschied
klingen mag, so weitreichend sind seine Konsequenzen. Insbesondere sind die in
unserem Beispielprogramm definierten Typenbezeichner kmh und mph in Wirklich-
keit nichts weiter als eine andere Bezeichnung für den Datentyp int. Folgerichtig
spielt es überhaupt keine Rolle, ob eine Variable mit Hilfe von int, mph oder kmh
deklariert wurde – sie wird vom Compiler stets gleich behandelt. Aus diesem Wis-
sen heraus verwundert es nicht, dass der Compiler auch das modifizierte Programm
trotz der augenscheinlichen Typinkonsistenz ohne Wenn und Aber übersetzt. Das
Beispiel zeigt eindringlich, dass die Verwendung von typedef durchaus mit Vor-
sicht zu genießen ist und wir uns an dieser Stelle nicht in allzu großer Sicherheit
wiegen dürfen.
Abb. 3.13 deutet eine mögliche Lösung an. In dieser Programmvariante wird der
zu speichernde Integer-Wert künstlich in eine Struktur eingebettet, so dass der C-
3.2 Typisierung 89
speed_limit3.c
#include <stdio.h> 1
2
typedef struct { int value; } kmh; 3
typedef struct { int value; } mph; 4
5
static mph speed_limit() 6
{ 7
// speed limit in mph ("miles per hour") 8
mph limit = { 65 }; 9
10
return limit; 11
} 12
13
int main(int argc, char *argv[]) 14
{ 15
// speed limit in kmh ("kilometer per hour") 16
kmh limit; 17
18
limit = speed_limit(); 19
20
printf("Speed limit = %d kmh\n", [Link]); 21
return 0; 22
} 23
Abb. 3.13 Die Kapselung bewirkt, dass der Compiler beide Datentypen unterschiedlich behandelt
Compiler die Bezeichner kmh und mph als Stellvertreter zweier unterschiedlicher
Datentypen auffasst. Beim Versuch, das geänderte Programm zu übersetzen, gene-
riert der C-Compiler die folgende Warnmeldung:
gcc typedef3.c typedef3.c: In function ’main’:
typedef3.c:19: error: incompatible types in assignment
Auf den ersten Blick erscheint die vorgestellte Lösung als reichlich unsauber. Ein-
elementige Strukturen konterkarieren die Idee eines zusammengesetzten Datentyps
und genau hierfür wurde das struct-Konstrukt ursprünglich geschaffen. Auf den
zweiten Blick zeigt sich jedoch schnell, dass dieses Programm einen Ansatz nach-
bildet, der in objektorientierten Sprachen weit verbreitet ist. Hier lässt sich das Ty-
penproblem elegant lösen, indem sowohl kmh als auch mph durch eine eigene Klasse
repräsentiert werden. Jede einheitenverletzende Wertezuweisung wird auf diesem
Weg effektiv unterbunden. Legen wir die objektorientierte Terminologie zugrunde,
so verbirgt sich hinter einer C-Struktur jedoch nichts anderes als eine methodenlose
Klasse. Damit verfolgt das in Abb. 3.13 dargestellte Programm exakt die gleiche
Lösungsstrategie, die von den meisten objektorientierten Programmiersprachen for-
ciert wird – auch wenn die Sprache C keine Objekte als solche kennt.
90 3 Konstruktive Qualitätssicherung
open
read
write
close
...
Abb. 3.14 Unix-basierte Betriebssysteme hebeln zur Kommunikation mit dem Gerätetreiber das
Typsystem aus
Als erstes Argument nimmt die Funktion einen Dateideskriptor entgegen, der das
angesprochene Zielgerät auswählt. Das zweite Argument bestimmt das spätere Ver-
halten des Gerätetreibers und ist ebenfalls zwingend erforderlich. Die dritte Para-
meterangabe (...) deklariert eine variable Anzahl von Folgeparametern und ist ein
besonderes Merkmal der Programmiersprache C. Bei einem Funktionsaufruf legt
der C-Compiler alle übergebenen Parameter auf dem Stack ab, kümmert sich je-
doch nicht weiter um deren Verbleib. Innerhalb der aufgerufenen Funktion muss der
Programmierer selbst Sorge dafür tragen, dass der Stack-Inhalt korrekt interpretiert
wird.
Im Falle des Gerätetreibers wird zunächst die Integer-Variable request ausge-
wertet und aus dem übergebenen Wert die Signatur der erwarteten Folgeparameter
abgeleitet.
Durch die Deklaration einer typenlosen, variablen Parameterliste wird dem Com-
piler effektiv die Möglichkeit entzogen, die Typkonsistenz der übergebenen Parame-
ter zu überprüfen. Alle Fehler, die durch eine unsaubere Programmierung während
der Parameterübergabe entstehen, lassen sich zur Übersetzungszeit nicht mehr ent-
decken. Kurzum: Die Verwendung einer variablen Parameterliste hebelt das Typ-
system an dieser Stelle vollständig aus.
Eine sauberere Möglichkeit, beliebige Datentypen an eine Funktion zu überge-
ben, ohne sofort das gesamte Typsystem zu umgehen, stellt die Verwendung spe-
zieller Container-Datentypen dar. Ein Beispiel dieser Kategorie ist der Datentyp
VARIANT, der vor allem in den von Microsoft geprägten Sprachen Visual Basic, Vi-
sual C++ und C#, aber auch in alternativen Dialekten wie Delphi vorhanden ist.
VARIANT wirkt wie ein Container für eine Vielzahl fest definierter Datentypen und
ist damit in der Lage, mit jeder Zuweisung den repräsentierten Datentyp chamäleon-
artig zu übernehmen.
Abb. 3.15 demonstriert, wie sich ein entsprechender Datentyp in der Program-
miersprache C definieren lässt. Hinter der Variablen vt verbirgt sich ein einfacher
Integer-Wert, der die Rolle der Typangabe übernimmt. Jeder kapselbare Datentyp
wird durch einen vorher festgelegten Integer-Wert eindeutig charakterisiert. Damit
sind in jedem Objekt neben den Daten auch stets die Typeninformationen persistent
abgelegt. Mit anderen Worten: VARIANT ist ein selbstbeschreibender Datentyp.
Der Bereich zur Speicherung der Nutzdaten wird durch die Variable value de-
finiert. Hierbei handelt es sich um eine Verbundstruktur, die für alle kapselbaren
Datentypen einen eigenen Eintrag vorhält. Der Variablenverbund ist als C-Union
implementiert, so dass alle Strukturelemente überlagernd angeordnet sind und an
der gleichen Speicherstelle beginnen. Die Anzahl der im Speicher belegten Bytes
bleibt hierdurch konstant und wächst insbesondere nicht mit der Anzahl der gekap-
selten Datentypen an.
Leider wird der Datentyp VARIANT in der Praxis nicht immer sinnvoll einge-
setzt. Anstatt die Verwendung auf Szenarien wie die oben skizzierte Treiberkommu-
nikation zu beschränken, wird der Datentyp oft als Allzweckwerkzeug missbraucht.
Eine Erhöhung der Typensicherheit wird auf diese Weise nicht erreicht. Stattdes-
sen wird eine ursprünglich statisch typisierte Programmiersprache faktisch auf eine
dynamisch typisierte Sprache reduziert. Viele Typisierungsfehler, die bei sauberer
92 3 Konstruktive Qualitätssicherung
variant.c
typedef struct { 1
VARTYPE vt; 2
WORD wReserved1; 3
WORD wReserved2; 4
WORD wReserved3; 5
union { 6
LONG lVal; 7
BYTE bVal; 8
SHORT iVal; 9
FLOAT fltVal; 10
DOUBLE dblVal; 11
LONG* plVal; 12
BYTE * pbVal; 13
SHORT * piVal; 14
LONG * plVal; 15
FLOAT * pfltVal; 16
DOUBLE * pdblVal; 17
... 18
} value; 19
} VARIANT; 20
reset.c
// Zur Initialisierung eines Steuergeräts ist die folgende 1
// Funktion direkt aufzurufen: 2
// 3
// Signatur: void reset(void) 4
// Speicheradresse: 0xFFE0 5
// 6
// Der Aufruf erfolgt in drei Schritten: 7
// (1) Deklarieren des Funktions-Pointers fp 8
// (2) Zuweisen der Startadresse 9
// (3) Aufrufen der Funktion über den Funktions-Pointer 10
11
... 12
void (*fp)(); // (1) 13
fp = (void(*)())0xFFE0; // (2) 14
(*fp)(); // (3) 15
... 16
Abb. 3.16 Nicht immer lässt sich die Verwendung von Type-Casts vermeiden
I Invarianten
I Zusicherungen
modulo.e
feature 1
modulo(a, b : INTEGER) : INTEGER is 2
-- Berechnet den ganzzahligen Divisionsrest von a / b 3
require 4
a_nonnegative: a >= 0; 5
b_nonnegative: b >= 0; 6
b_not_zero: b /= 0; 7
local 8
q : INTEGER; 9
r : INTEGER; 10
do 11
from 12
q := 0; 13
r := a; 14
invariant 15
a = q * b + r 16
variant r 17
until 18
r < b 19
loop 20
q := q + 1; 21
r:= r - b; 22
end 23
Result := r; 24
25
ensure 26
Result >= 0 and Result < b 27
end 28
Für die Angabe von Vorbedingungen hält jede Eiffel-Routine einen separaten
Abschnitt vor, der mit dem Schlüsselwort require eingeleitet wird. Jede Vorbedin-
gung hat die Form eines booleschen Ausdrucks und wird zur eindeutigen Identifi-
kation mit einem zusätzlichen Bezeichner versehen. In entsprechender Weise wird
die Routine mit einem speziellen Abschnitt beendet, der alle Nachbedingungen ent-
hält und durch das Schlüsselwort ensure markiert ist. Jede korrekt implementierte
Eiffel-Routine garantiert die Gültigkeit der Nachbedingungen immer dann, wenn
alle Vorbedingungen erfüllt sind.
Abb. 3.17 demonstriert die Verwendung von Vor- und Nachbedingungen am Bei-
spiel der Modulo-Berechnung zweier ganzer Zahlen a und b. Mit Hilfe der drei Vor-
bedingungen legt die Routine fest, dass nur dann ein korrektes Ergebnis garantiert
wird, falls beide Operanden nicht negativ sind und der Divisor zusätzlich ungleich 0
ist. Die Nachbedingung drückt aus, dass sich der berechnete Divisionsrest innerhalb
des halboffenen Intervalls [0; b[ befinden muss. Hierzu wird die Variable Result
referenziert, die in jeder Eiffel-Routine existiert und der Speicherung des Rückga-
bewerts dient.
3.3 Vertragsbasierte Programmierung 95
3.3.2 Invarianten
Neben der Spezifikation von Vor- und Nachbedingungen unterstützt die vertragsba-
sierte Programmierung die Angabe von Invarianten. Auf der obersten Ebene werden
in Eiffel Klasseninvarianten und Schleifeninvarianten unterschieden.
I Klasseninvarianten
Hierbei handelt es sich um boolesche Bedingungen, die permanent, d. h. nicht nur
an einer bestimmten Position innerhalb des Programms, gelten müssen. Die ty-
pischen Anwendungen von Klasseninvarianten umfassen die Überwachung des
Wertebereichs einer Variablen sowie die Sicherstellung der Konsistenz seman-
tisch korrelierter Objekte. Das folgende Programmfragment demonstriert die
Verwendung einer Invarianten am Beispiel der Implementierung einer einfachen
Listenstruktur:
invariant
count_positive: count >= 0;
empty_invariant: empty implies count = 0;
Während die erste Invariante fordert, dass eine Liste niemals eine negative An-
zahl an Elementen enthalten kann, stellt die zweite sicher, dass eine leere Liste
keine Elemente enthält. Beide Forderungen besitzen den Charakter einer Kon-
sistenzbedingung und müssen während der gesamten Laufzeit des Programms
gelten.
I Schleifeninvarianten
Diese speziellen Konstrukte dienen zur Überwachung von Schleifen. Spezifiziert
wird eine Schleifeninvariante durch das Schlüsselwort invariant, gefolgt von
einer oder mehreren Bedingungen, die sowohl vor der ersten Ausführung als auch
nach jeder Schleifeniteration erfüllt sein müssen. Schleifeninvarianten spielen
insbesondere im Bereich der Software-Verifikation eine bedeutende Rolle und
werden uns in Abschnitt 6.2 im Rahmen der formalen Deduktion erneut beschäf-
tigen.
Zusätzlich gestattet Eiffel die Angabe von Schleifenvarianten. Diese werden
mit dem Schlüsselwort variant eingeleitet und können einen beliebigen Wert
referenzieren, der am Ende einer Schleifeniteration stets kleiner, jedoch nie ne-
gativ wird. Mit Hilfe von Schleifenvarianten lässt sich die Schleifenterminierung
während der Laufzeit auf elegante Weise überwachen. Hierzu liest die Laufzeit-
umgebung nach jeder Iteration den Wert der Schleifenvariante aus und signali-
siert einen Fehler, wenn sich deren Wert entweder nicht verringert hat oder in
96 3 Konstruktive Qualitätssicherung
3.3.3 Zusicherungen
Eine Zusicherung ist eine boolesche Bedingung, die an einer beliebigen Stelle ei-
nes Programms platziert werden kann. Anders als eine Invariante, deren Gültigkeit
permanent von der Laufzeitumgebung überwacht wird, wertet der Compiler eine
Zusicherung nur an der Stelle ihres Auftretens aus.
In Eiffel werden Zusicherungen mit dem Schlüsselwort check eingeleitet, in Ja-
va mit dem Schlüsselwort assert. In C und C++ steht das assert-Konstrukt als
Makro zur Verfügung. Wie in Abb. 3.18 am Beispiel der C-Portierung des weiter
oben eingeführten Eiffel-Programms gezeigt, lassen sich Vorbedingungen, Nachbe-
dingungen und Schleifeninvarianten mit Hilfe von Zusicherungen weitgehend nach-
bilden. Alle Konstrukte wurden in entsprechende assert-Makros übersetzt.
Definiert ist das assert-Makro in der Header-Datei assert.h, die in Auszügen
in Abb. 3.19 abgedruckt ist. Wie der Dateiauszug zeigt, verhält sich das assert-
Makro so lange passiv, bis die spezifizierte Zusicherung verletzt wird. In diesem Fall
3.3 Vertragsbasierte Programmierung 97
modulo.c
#include <assert.h> 1
2
int modulo(int a, int b) 3
{ 4
/* Berechnet den ganzzahligen Divisionsrest von a / b */ 5
6
assert(a >= 0); 7
assert(b >= 0); 8
assert(b != 0); 9
10
int q,r; 11
12
q = 0; 13
r = a; 14
15
while (r >= b) { 16
assert(a == q * b + r); 17
18
q++; 19
r -= b; 20
} 21
22
assert(r >= 0 && r < b); 23
return r; 24
} 25
nachbilden. Da neben der sinkenden Lesbarkeit auch die Laufzeit des Programms
negativ beeinflusst wird, kann das assert-Makro das Konzept der Schleifenvarian-
98 3 Konstruktive Qualitätssicherung
assert.h
// Auszug aus assert.h 1
2
#ifdef NDEBUG 3
#define assert(ignore) ((void) 0) 4
#else 5
6
#define __assert(expression, file, lineno) \ 7
(printf ("%s:%u: failed assertion\n", file, lineno), \ 8
abort (), 0) 9
10
#define assert(expression) \ 11
((void) ((expression) ? \ 12
0 : __assert (expression, __FILE__, __LINE__))) 13
#endif 14
ten zwar simulieren, aber in keiner Weise adäquat ersetzen. An dieser Stelle wäre
eine saubere Integration in die Spracharchitektur von C und C++ mehr als wün-
schenswert. Leider ist diese, zumindest mittelfristig, nicht in Sicht.
3.4.1 Software-Redundanz
Eine weit verbreitete Technik zur Erhöhung der Fehlertoleranz ist die künstliche Er-
zeugung von Redundanz. Diese kommt in realen Systemen in den folgenden Aus-
prägungen vor [218, 78]:
3.4 Fehlertolerante Programmierung 99
I Funktionale Redundanz
Das System wird um zusätzliche Funktionen erweitert, die für den regulären Be-
trieb entbehrlich sind und ausschließlich zur Erhöhung der Fehlertoleranz die-
nen. So verfügen viele Kommunikationsnetzwerke über die ergänzende Fähig-
keit, den Datenstrom dynamisch um ausgefallene Vermittlungsknoten herumzu-
leiten [217]. Auch sämtliche Subsysteme, die der Durchführung von Sofware-
Tests dienen, fallen in diese Kategorie.
I Informationelle Redundanz
Die Fehlertoleranz eines Systems wird erhöht, indem die Nutzdaten um zusätzli-
che Informationen angereichert werden. Bereits eine einfache Datenstruktur wie
die doppelt verkettete Liste enthält ein hohes Maß informationeller Redundanz.
Hier lassen sich z. B. alle Rückwärtsreferenzen aus den Vorwärtsreferenzen voll-
ständig rekonstruieren.
Der gesamte Bereich der Datenübertragung fußt ebenfalls auf diesem Prin-
zip. So werden die gesendeten Daten in der Regel um ein oder mehrere Prüf-
bits erweitert, die den Empfänger in die Lage versetzen, eventuell auftretende
Übertragungsfehler in gewissen Grenzen zu korrigieren oder zumindest zu er-
kennen [114, 115, 26, 206, 207].
I Temporale Redundanz
In Systemen dieser Bauart werden die Zeitanforderungen übererfüllt, so dass im
Fehlerfall genug Reserven für eine wiederholte Ausführung zur Verfügung ste-
hen. So arbeitet z. B. die Software zum Abspielen einer Video-DVD schnell ge-
nug, um einen fehlerhaft decodierten Sektor erneut einzulesen, ohne die lücken-
lose Wiedergabe des Videostroms zu unterbrechen.
I Strukturelle Redundanz
In Systemen dieser Bauart werden eine oder mehrere Komponenten mehrfach
ausgelegt. Die strukturelle Redundanz hat ihren Ursprung im Hardware-Entwurf
und gehört heute zu den am häufigsten eingesetzten Mitteln, um die Fehlertole-
ranz eines Systems zu erhöhen. Im Folgenden werden wir uns ausführlicher mit
den verschiedenen Spielarten dieser Technik beschäftigen.
Die Mehrfachauslegung einer Systemkomponente kann auf zwei grundlegend ver-
schiedene Weisen erfolgen und führt uns unmittelbar zu den Begriffen der homoge-
nen und der heterogenen Redundanz:
I Homogene Redundanz
In diesen Systemen werden n Komponenten gleicher Bauart parallel betrieben.
Im Gegensatz zur Hardware unterliegt Software keiner Alterung und verhält sich
– die korrekte Funktion der verwendeten Hardware-Komponenten vorausgesetzt
– stets gleich. Die homogene Mehrfachauslegung von Software-Komponenten
kann damit konstruktionsbedingt zu keiner Verbesserung der Fehlertoleranzei-
genschaften führen. Sie spielt aus diesem Grund ausschließlich im Hardware-
Bereich eine Rolle.
100 3 Konstruktive Qualitätssicherung
I Heterogene Redundanz
In diesen Systemen werden n Komponenten unterschiedlicher Bauart parallel
betrieben. Auf diese Weise wird die Wahrscheinlichkeit von gemeinsam vorhan-
denen Fehlern deutlich reduziert. Um einen möglichst hohen Grad an Hetero-
genität zu erzielen, werden die mehrfach ausgelegten Software- oder Hardware-
Komponenten in der Praxis oft von unabhängigen Zulieferern entwickelt und
getestet. Im Bereich der fehlertoleranten Software kommen ausschließlich hete-
rogen ausgelegte Systeme zum Einsatz.
Als weiteres Unterscheidungsmerkmal der strukturellen Redundanztechniken dient
die Art und Weise, wie die mehrfach ausgelegten Systemkomponenten betrieben
werden. Wir unterscheiden an dieser Stelle statische und dynamische Redundanz:
I Statische Redundanz
Alle mehrfach ausgelegten Systeme arbeiten im Parallelbetrieb und die berechne-
ten Ergebnisse der einzelnen Komponenten werden durch einen nachgeschalteten
Voter verglichen. Dieser führt im einfachsten Fall eine Mehrheitsentscheidung
durch und verwendet für die nachfolgenden Berechnungen das am häufigsten
angetroffene Ergebnis weiter. Arbeiten n Komponenten parallel, so sprechen wir
von einer n-fach-modularen Redundanz (n-modular redundancy). Um eine Mehr-
heitsentscheidung nach dem Ausfall einer Komponente überhaupt noch durch-
führen zu können, werden mindestens 3 nebenläufig arbeitende Systeme benö-
tigt. Systeme dieser Bauart bilden eine TMR-Topologie (TMR = Triple-Modular
Redundancy).
Anders als bei inhärent parallel arbeitenden Hardware-Systemen kann die pa-
rallele Auslegung von Software-Komponenten einen erheblichen Einfluss auf die
Laufzeit ausüben. Werden alle Komponenten durch den gleichen Prozessorkern
ausgeführt, so verlangsamt sich ein n-fach redundantes Software-System minde-
stens um den Faktor n.
Für die Bewertung der Ausfallsicherheit ist zusätzlich zu beachten, dass der
Voter nur einfach ausgelegt ist und damit zur Achillesferse des Gesamtsystems
werden kann. Unter der Annahme, dass alle Komponenten die gleiche Ausfall-
wahrscheinlichkeit besitzen, zeigt das redundant ausgelegte System hierdurch
sogar eine geringere Verfügbarkeit als die singulär verbaute Einzelkomponente.
I Dynamische Redundanz
Systeme dieser Bauart halten Ersatzkomponenten vor, die im Bedarfsfall akti-
viert werden können. Im Gegensatz zur statischen Redundanz ist zu jeder Zeit
immer nur eine Komponente aktiv. Ein so konzipiertes System schont die Res-
sourcen, zeigt jedoch deutliche Schwächen im Bereich der automatischen Feh-
lererkennung. Im Gegensatz zu statischen Systemen, die das Fehlverhalten einer
Komponente durch den einfachen Vergleich der berechnenden Ergebnisse sofort
erkennen können, ist ein dynamisches System auf deutlich kompliziertere Selbst-
diagnosetechniken angewiesen. Viele Systeme mit dynamischer Redundanz sind
daher von vorneherein so konzipiert, dass die Aktivierung von Ersatzkomponen-
ten manuell erfolgen muss. Anders als im Falle der statischen Redundanz wirkt
3.4 Fehlertolerante Programmierung 101
A A
A B
2-aus-3 2-aus-3
A C
A A
A B
1-aus-3 1-aus-3
A C
sich die dynamische Redundanz im Software-Bereich nicht negativ auf die Lauf-
zeit aus.
Insgesamt ergeben sich aus den obigen Überlegungen vier verschiedene Möglich-
keiten der Mehrfachauslegung, die in Abb. 3.20 grafisch gegenübergestellt sind.
In der Praxis kommen Systeme zum Einsatz, die eine Mischung aus dynamischer
und statischer Redundanz forcieren. In Abb. 3.21 ist als Beispiel eines solchen Hy-
bridsystems die Redundanztopologie des Space Shuttles skizziert. Die sicherheits-
kritischen Systeme werden durch insgesamt fünf baugleiche Rechner gesteuert. Vier
davon sind mit identischer Software ausgestattet und bilden zusammen einen vier-
fach ausgelegten homogenen Redundanz-Cluster. Alle Rechner des Clusters laufen
parallel und werden durch einen Voter kontrolliert (statische Redundanz). Fällt einer
der Rechner aus, werden die verbleibenden als 2-aus-3-System weiter betrieben. Zu-
sätzlich steht ein fünfter Rechner zur Verfügung, der mit separat entwickelter Soft-
ware ausgestattet ist und zur Übernahme der Shuttle-Steuerung manuell aktiviert
werden kann. Insgesamt entsteht hierdurch eine zweite Ebene der Absicherung, die
auf dem Prinzip der dynamischen heterogenen Redundanz beruht.
2-fach-Redundanz
nz
(heterogen, dynamisch)
h)
zuletzt von der Schwere der aufgetretenen Fehlfunktion ab. Die folgenden Szenarien
stellen typische Reaktionsstrategien dar:
I Fail-Safe-Reaktion
Auf einen Fehlerzustand wird durch den Wechsel in einen sicheren Zustand rea-
giert (fail safe state). Einige Systeme erreichen ihren sichersten Zustand, indem
sie sich schlicht deaktivieren (Selbstabschaltung), andere setzen im Fehlerfall
spezielle Sicherheitsmechanismen in Gang. Klassische Beispiele aus diesem Be-
reich sind die selbstaktivierende Notbremse eines Personenaufzugs oder die Sitz-
platzverriegelung einer Achterbahn.
An dieser Stelle werfen wir einen erneuten Blick auf die Bordelektronik mo-
derner Kraftfahrzeuge. Für jedes an den CAN-Bus angeschlossene Steuergerät
sieht die Spezifikation einen zweistufig ausgelegten Fail-Safe-Mechanismus vor,
der den Umgang mit defekten Busknoten auf systematische Weise regelt. Wie in
Abb. 3.22 gezeigt, befindet sich jedes Steuergerät zu jedem Zeitpunkt in genau
einem von drei Zuständen:
– Zustand 1: Error active
Dieser Zustand wird im regulären Betrieb eingenommen. Ein Steuergerät darf
sowohl lesend als auch schreibend auf den CAN-Bus zugreifen und quittiert
jeden detektierten Übertragungsfehler durch das Senden einer speziellen Feh-
lerbotschaft (error frame).
den jedoch nicht mehr durch das Senden einer Fehlerbotschaft quittiert. Sollte
der betreffende Busknoten selbst für die gemessenen Fehler verantwortlich
sein, so wird auf diese Weise verhindert, dass der Kommunikationskanal dau-
erhaft mit einer Flut von Fehlermitteilungen überschwemmt wird.
I Selbstreparatur
Systeme dieser Bauart versuchen, die im Rahmen einer Selbstdiagnose ermittel-
ten Inkonsistenzen selbstständig zu beseitigen. Die Möglichkeiten der Selbstre-
paratur sind vielfältig. Neben dem Löschen inkonsistenter Datenstrukturen oder
dem nachträglichen Einsetzen von Standardwerten sind sämtliche Spielarten
komplexer Algorithmen denkbar. Diese reichen bis hin zur semi-autonomen Re-
paratur von Datenbeständen unter Beteiligung des Benutzers. Das Prinzip der
Software-Selbstreparatur wird heute bis hinauf zur Betriebssystemebene ange-
wendet. Beispiele sind die automatische Wiederherstellung der Dateisystemin-
tegrität nach einem Systemabsturz oder die automatische Neuinstallation verse-
hentlich gelöschter Systemkomponenten.
104 3 Konstruktive Qualitätssicherung
Programm Watchdog
Lesen
Reinitialisiere Ja
Zähler = 0? Reset
Zähler
Erniedrigen Nein
I Reaktivierung
Die meisten der in sicherheitskritischen Systemen verbauten Hardware-
Komponenten werden zum Zweck der Selbstüberwachung mit einer zusätzlichen
Watchdog-Logik ausgestattet. Wie in Abb. 3.23 schematisch skizziert, handelt es
sich hierbei um eine separate Schaltung, die periodisch den Inhalt eines Zählers
erniedrigt. Die eigentliche Software hat die Pflicht, den Wert dieses Zählers eben-
falls in periodischen Abständen zu verändern, so dass er im Normalbetrieb nie
den Wert 0 erreicht. Gelingt es der Watchdog-Logik trotzdem, den Zählerstand
auf 0 zu reduzieren, deutet dies auf einen Absturz der Software hin. Der Watch-
dog löst daraufhin einen Hardware-Reset aus und startet das Gerät neu. Mit Hilfe
dieses genauso einfachen wie wirkungsvollen Prinzips gelingt es in der Praxis,
die Verfügbarkeit von Hardware-Komponenten drastisch zu steigern.
In der Vergangenheit wurden verschiedene Versuche unternommen, die
Grundidee des Watchdog-Prinzips auf Software-Systeme zu übertragen. Ein Bei-
spiel ist der Explorer-Prozess des Betriebssystems Windows XP. Der Prozess ist
unter anderem für die Darstellung der Desktop-Oberfläche verantwortlich und
unter normalen Umständen permanent aktiv. Wird der Prozess durch einen Pro-
grammabsturz oder durch den Benutzer beendet, so verschwinden zunächst alle
Icons und Menüs von der Oberfläche. Ein intern verankerter Software-Watchdog
registriert die Terminierung und startet den Prozess neu. Bereits ein paar Sekun-
den später ist die Oberfläche wieder sichtbar und das System zurück im Normal-
betrieb. Im Vergleich mit früheren Windows-Versionen hat es Microsoft mit Hil-
fe des Watchdog-Prinzips geschafft, die Verfügbarkeit der Windows-Oberfläche
deutlich zu erhöhen.
Verfechter formaler Entwurfstechniken stehen der fehlertoleranten Programmierung
nicht selten mit einem gewissen Argwohn gegenüber, schließlich steckt hinter dieser
Philosophie die allzu pessimistische Annahme, dass jedes nichttriviale Programm
Fehler besitzt. Die fehlertolerante Programmierung kommt damit in gewissem Sin-
ne der Akzeptanz der eigenen Unzulänglichkeit gleich. Formale Methoden verfol-
3.4 Fehlertolerante Programmierung 105
gen exakt die gegenteilige Philosophie. So versucht die formale Synthese ein Pro-
gramm durch die Anwendung mathematisch abgesicherter Konstruktionsregeln a
priori fehlerfrei zu erzeugen. Ähnlich stellt sich die Situation im Bereich der forma-
len Verifikation dar. Durch die Anwendung mathematischer Algorithmen wird die
Fehlerfreiheit eines Programms vollständig bewiesen. Das unscheinbare Wörtchen
„vollständig“ hat dabei weitreichende Konsequenzen. Um diese zu gewährleisten,
müssen formale Methoden durchweg auf Algorithmen exponentieller Komplexität
zurückgreifen, die den Einsatz für große Software-Systeme zurzeit ad absurdum
führen. Sollte hier der Durchbruch in naher oder ferner Zukunft noch gelingen, wer-
den wir vielleicht auf die eine oder andere Technik der fehlertoleranten Program-
mierung verzichten können. Nichtsdestotrotz wird deren Bedeutung mit großer Si-
cherheit auch dann noch erheblich sein und weiterhin einen Grundpfeiler für die
Steigerung der Robustheit großer Software-Systeme darstellen.
3.4.3 Ausnahmebehandlung
Der Begriff der Ausnahmebehandlung subsumiert alle Methoden und Vorgehens-
weisen, die einen geregelten Umgang mit spontan auftretenden Fehlersituationen
ermöglichen. Auf der obersten Ebene werden Ausnahmesituationen in geplante
Ausnahmen (Datei nicht gefunden, ungültige Eingabe) und ungeplante Ausnahmen
(Speichermangel, Division durch null) unterschieden.
Einige Sprachen delegieren einen Teil der Ausnahmebehandlung schlicht an
das Betriebssystem. Verursacht beispielsweise eine C-Applikation einen Division-
durch-null-Fehler, so wird dieser durch eine entsprechende Interrupt-Service-
Routine des Betriebssystems behandelt. Die Folgen sind jedem C-Programmierer
bekannt: Eine Division durch null führt zur sofortigen Beendigung des fehlerverur-
sachenden Prozesses.
Andere Sprachen legen die Behandlung dieser Ausnahmesituation in die Hände
des Programmierers. So erzeugt eine Division durch null in einem Java-Programm
zunächst eine sogenannte Ausnahme (exception), die bei Bedarf abgefangen wer-
den kann. Der Software-Entwickler erhält hierdurch die Möglichkeit, mit Hilfe spe-
zieller Fehlerbehandlungsroutinen flexibel auf das eingetretene Fehlerszenario zu
reagieren.
Das Prinzip der Ausnahmebehandlung wurde bereits 1967 mit der Sprache PL/I
eingeführt, die von IBM im Rahmen des System/360-Projekts entwickelt wurde.
Eine weite Verbreitung fand das Exception-Konzept jedoch erst durch die Program-
miersprachen C++ und Java. Hier bildet der Mechanismus unübersehbar eine Kern-
säule der jeweiligen Spracharchitektur.
Um die Vorteile einer systematischen Ausnahmebehandlung zu verstehen, be-
trachten wir die in Abb. 3.24 abgedruckte Implementierung. Dargestellt ist eine ty-
pische Code-Sequenz, die eine Datei öffnet, deren Größe bestimmt und anschlie-
ßend den Inhalt in einen zuvor dynamisch belegten Speicherbereich kopiert. Jede
der nacheinander ausgeführten Operationen kann fehlschlagen und ist in der kon-
ventionellen Implementierung aus diesem Grund durch eine zusätzliche If-Abfrage
abgesichert. Im Fehlerfall wird die Programmausführung unterbrochen und ein fest
106 3 Konstruktive Qualitätssicherung
[Link]
int readFile(String name) { 1
int error_code; 2
<Öffne Datei> 3
if (<Datei erfolgreich geöffnet>) { 4
<Ermittle Dateigröße> 5
if (<Dateigröße erfolgreich ermittelt>) { 6
<Belege Speicher> 7
if (<Speicher erfolgreich belegt>) { 8
<Lese Dateiinhalt ein> 9
if (<Dateiinhalt erfolgreich gelesen>) { 10
error_code = 0; 11
} else 12
error_code = 1; 13
} else 14
error_code = 2 15
} else 16
error_code = 3 17
<Schließe Datei> 18
if (<Datei nicht schließbar>) 19
error_code = 4 20
} else 21
error_code = 5 22
return error_code; 23
} 24
[Link]
void readFile(String name) { 1
try { 2
<Öffne Datei> 3
<Ermittle Dateigröße> 4
<Belege Speicher> 5
<Lese Dateiinhalt ein> 6
<Schließe Datei> 7
} 8
catch (DateiÖffnenFehler ...) { <Fehlerreaktion> } 9
catch (DateiGrößenFehler ...) { <Fehlerreaktion> } 10
catch (SpeicherBelegenFehler ...) { <Fehlerreaktion> } 11
catch (DateiLesenFehler ...) { <Fehlerreaktion> } 12
catch (DateiSchliessenFehler ...) { <Fehlerreaktion> } 13
} 14
3.5 Portabilität
Der Begriff der Portabilität drückt aus, wie leicht ein Software-System in eine an-
dere Umgebung übertragen werden kann. Mit anderen Worten: Die Portabilität um-
schreibt den Grad der Plattformunabhängigkeit eines Software-Systems. In diesem
Zusammenhang interessiert nicht nur, ob ein Programm auf verschiedenen Plattfor-
men per se lauffähig ist, sondern vor allem, wie viele Eingriffe für die Übertragung
in eine noch nicht unterstützte Umgebung notwendig werden. In der Praxis treten
die folgenden Portierungsszenarien auf:
I Architekturportierung
Hinter diesem Begriff verbirgt sich die Portierung eines Software-Systems auf
eine andere Hardware-Plattform. Insbesondere der Wechsel der Prozessorar-
chitektur stellt den Programmierer vor nicht zu unterschätzende Probleme, da
Hardware-nahe Sprachen wie z. B. C oder C++ die primitiven Datentypen direkt
auf das Register-Layout des Prozessors abbilden. Die Bitbreite einer C-Variablen
des Typs long ist in den allermeisten Fällen mit der Registerbreite des Prozes-
sors identisch, so dass sich bei der 64-Bit-Portierung alter 32-Bit-Programme
der Wertebereich vieler Variablen schlagartig ändert. Ein noch größeres Problem
geht auf die Speicherordnung (endianness) eines Prozessors zurück. Warum der
Wechsel von einer Big-Endian- auf eine Little-Endian-Architektur oder umge-
kehrt überhaupt Probleme macht, werden wir in Abschnitt [Link] im Detail dis-
kutieren.
I Betriebssystemportierung
Schwieriger noch als die Portierung auf eine andere Hardware-Architektur ist
in den meisten Fällen der Wechsel des zugrunde liegenden Betriebssystems. So
sind z. B. viele Komponenten einer Windows-Applikation so eng auf das Be-
triebssystem zugeschnitten, dass die Anpassung an andere Systeme wie Linux
108 3 Konstruktive Qualitätssicherung
I Systemportierung
Dieser Begriff fasst alle Szenarien zusammen, in denen ein Programm auf eine
andere Geräteklasse übertragen wird. Ein Beispiel ist die Portierung klassischer
PC-Applikationen auf mobile Endgeräte. Die vielen Varianten des Windows-
Betriebssystems, wie die Media Center Edition, Tablet PC Edition oder die Mo-
bile Edition, sind an dieser Stelle trendweisend. Bei dieser Art der Portierung
kommt erschwerend hinzu, dass die Endgeräte eine riesige Bandbreite bezüglich
der Rechenleistung, des Speicherausbaus, der Energieversorgung und der Benut-
zungsschnittstelle abdecken. Während PC-Betriebssysteme mit dem Anwender
vornehmlich über Tastatur- und Mauseingaben kommunizieren, sind diese Ein-
gabeschnittstellen z. B. auf Mobiltelefonen nicht in der gleichen Form vorhan-
den.
Die Kunst der portablen Programmierung besteht hier in der frühzeitigen Er-
kennung von wiederverwendbaren Code-Modulen, die als kleinster gemeinsa-
mer Nenner in allen Varianten eingesetzt werden können. Auf diese Weise kann
der erhebliche Entwicklungsaufwand von Programmvarianten zumindest partiell
verringert werden.
I Sprachportierungen
Viele ältere Software-Module sind in Programmiersprachen verfasst, die nur
schwer mit modernen Sprachen interagieren. Darüber hinaus geht mit jeder neu-
en Entwicklergeneration mehr und mehr Wissen über antiquierte Sprachen ver-
loren, was die Wartung des alten Programm-Codes wiederum erschwert. Über
kurz oder lang reift der Wunsch, die alten Quelltexte in eine neuen Programmier-
sprache zu übertragen. Dass eine Sprachportierung in der Praxis ein schwieriges
Unterfangen darstellt, beweisen die schier unzähligen COBOL-Programme, die
heute insbesondere im Bankenbereich immer noch ihre Dienste verrichten.
Die wenigsten Systeme, die für eine bestimmte Umgebung entwickelt werden, sind
ad hoc in einer anderen Umgebung lauffähig. Gut programmierte Software zeichnet
sich vor allem dadurch aus, dass eine Anpassung mit wenigen Änderungen in kurzer
Zeit vollzogen werden kann. Die Portabilität praxistypischer Software-Systeme un-
terscheidet sich um Größenordnungen. So wurde z. B. der hohe Portabilitätsgrad des
Linux-Betriebssystems in der Vergangenheit vielfach demonstriert. Waren die ers-
ten Generationen des Linux-Kernels ausschließlich in der x86-Welt zu Hause, un-
terstützt das Betriebssystem heute, wie in Abb. 3.26 gezeigt, nahezu alle gebräuchli-
chen Prozessorvarianten. Neben dem Einsatz auf Servern und Arbeitsplatzrechnern
hat Linux auch im Bereich eingebetteter Systeme Fuß gefasst. So läuft das freie
Betriebssystem heute nicht nur auf PDAs und Mobiltelefonen, sondern auch auf art-
3.5 Portabilität 109
Kernel .95 Kernel 1.2 Kernel 2.0 Kernel 2.2 Kernel 2.4 Kernel 2.6
1992 1995 1996 1999 2001 2003
i386 i386 i386 i386 i386 i386
alpha alpha alpha alpha alpha
mips mips mips mips mips
sparc sparc sparc sparc sparc
m68k m68k m68k m68k
ppc ppc ppc ppc
arm arm arm
s390 s390 s390
sparc64 sparc64 sparc64
cris cris
ia64 ia64
mips64 mips64
parisc parisc
superh superh
sh sh
m68nommu
"PS. […] It is NOT portable (uses 386 task switching etc), h8300
and it probably never will support anything other than ppc64
AT-harddisks, as that s all I have :-(." v850
Linus Torvalds, 1991 um
fremden Geräten wie dem iPod, der in keiner Weise darauf ausgelegt war, neben der
hauseigenen Firmware ein anderes Betriebssystem auszuführen.
Die einfache Portierbarkeit ist nicht nur ein Markenzeichen des Linux-
Betriebssystems, sondern auch der meisten anderen Unix-Derivate. Neben der Si-
cherheit und Stabilität ist die Portabilität ein charakteristisches Merkmal der freien
Unix-Klone FreeBSD, OpenBSD und insbesondere NetBSD. In der Öffentlichkeit
fristen diese Betriebssysteme seit dem Boom von Linux eher ein Schattendasein –
zu Unrecht, wenn marktpolitische Gründe außer Acht gelassen und nur die puren
Leistungsdaten der Betriebssysteme in die Waagschale gelegt werden.
Auch die Portierung von Mac OS X auf die Intel-Plattform – die zweite große
Technologietransition in der Geschichte der Firma Apple – konnte reibungslo-
ser vollzogen werden als von manchen Kritikern befürchtet. Im Herzen von Mac
OS X operiert ein Unix-ähnlicher Kernel (Darwin), der zu großen Teilen eben-
falls auf der BSD-Code-Basis beruht. Trotzdem ist Mac OS X kein reines Unix-
Betriebssystem, da es um viele Komponenten wie z. B. die Aqua-Oberfläche oder
das Cocoa-Framework erweitert wurde. Diese Komponenten haben ihre Wurzeln
nicht in der Unix-Welt und gehen stattdessen auf das Betriebssystem OPENSTEP
der Firma NeXT zurück.
Andere Projekte haben ihre liebe Not mit der Portabilität. Viele große Software-
Systeme adaptieren sich über die Jahre hinweg so eng an eine bestimmte Archi-
110 3 Konstruktive Qualitätssicherung
tektur, dass eine Portierung nur mit extrem hohem Personal- und Kostenaufwand
gestemmt werden kann und in manchen Fällen sogar vollständig scheitert.
Trotzdem stehen wir auch hier dem Problem nicht machtlos gegenüber. Die heu-
te in der Praxis eingesetzten Methoden und Techniken setzen auf drei ganz unter-
schiedlichen Ebenen an, um die Portabilität eines Software-Systems zu erhöhen:
I Portabilität auf Implementierungsebene
[Link] Datentypen
Zu den regelmäßig wiederkehrenden Problemen im Bereich der Architekturportie-
rung gehört die Änderung der prozessorinternen Arithmetikformate – insbeson-
dere die unterschiedliche Bitbreite der CPU-Register schränkt die Portierbarkeit
vieler Programme dramatisch ein. Inwieweit ein Software-System davon betrof-
fen ist, hängt in großem Maße von der eingesetzten Programmiersprache ab. High-
Level-Sprachen wie Java oder Smalltalk abstrahieren bewusst von prozessorinter-
nen Details und sind von der Problematik damit konstruktionsbedingt verschont.
3.5 Portabilität 111
/usr/src/linux
arch
Architekturabhängiger Code
alpha
arm
i386
ppc
...
include
asm-alpha
asm-arm
asm-i386
asm-ppc
...
linux
crypto
Architekturunabhängiger Code
Documentation
drivers
fs
include
init
ipc
kernel
lib
mm
net
scripts
security
sound
usr
Jede Integer-Variable besitzt in Java eine Auflösung von 32 Bit, unabhängig von der
realen Registerbreite. Wie zu erwarten, fordert der Luxus der Datentypabstraktion
seinen Tribut. Da sich eine Integer-Operation nicht mehr in jedem Fall auf eine ein-
zige Assembler-Instruktion abbilden lässt, werden entsprechende Programme auf
manchen Prozessoren deutlich langsamer ausgeführt.
In Low-Level-Sprachen, zu denen unter anderem die verschiedenen Abkömmlin-
ge der Programmiersprache C zählen, ist die Bitbreite der elementaren Datentypen
von Architektur zu Architektur unterschiedlich. Bei diesen Sprachen steht die Lauf-
zeiteffizienz im Vordergrund, so dass sich die Bitbreite der elementaren Datentypen
an den Fähigkeiten des eingesetzten Prozessors orientieren. Entsprechend wenig re-
gelt die Sprachdefinition von C. Für die vier ganzzahligen Datentypen char, short,
112 3 Konstruktive Qualitätssicherung
int und long werden zunächst nur die folgenden, vergleichsweise schwachen Ei-
genschaften garantiert:
I Die Bitbreite von char entspricht der Adressierungsgranularität der Hardware-
Architektur.
I Die Bitbreiten der Datentypen char, short, int und long sind monoton stei-
gend.
Die Adressierungsgranularität eines Prozessors entspricht der Bitbreite eines Daten-
worts, das an einer einzelnen Speicheradresse abgelegt ist. Nahezu alle modernen
Prozessoren adressieren den Hauptspeicher byteweise, so dass die Adressierungs-
granularität und damit die Breite des char-Datentyps stets 8 Bit beträgt. Die zweite
Eigenschaft stellt sicher, dass jeder Datentyp ohne Verluste in den nächstgrößeren
Datentyp konvertiert werden kann. Insbesondere garantiert die C-Sprachdefinition
damit die folgende Ordnungsbeziehung:
LP32 Datenmodell
char 8 Bit
short 16 Bit
int 16 Bit
long 32 Bit
void * 32 Bit
ILP32 Datenmodell
char 8 Bit
short 16 Bit
int 32 Bit
long 32 Bit
void * 32 Bit
P64 Datenmodell
char 8 Bit
short 16 Bit
int 32 Bit
long 32 Bit
void * 64 Bit
LP64 Datenmodell
char 8 Bit
short 16 Bit
int 32 Bit
long 64 Bit
void * 64 Bit
ILP64 Datenmodell
char 8 Bit
short 16 Bit
int 64 Bit
long 64 Bit
void * 64 Bit
I Das LP64-Datenmodell
Durchgängig eingesetzt wird dieses Modell in den 64-Bit-Varianten der heute
gebräuchlichen Unix-Derivate. Darunter fallen auch die 64-Bit-Versionen von
Linux und das im Kern auf BSD-Unix basierende Mac OS X. Im direkten Ver-
gleich mit dem P64-Datenmodell erscheint LP64 als das natürlichere, da es die
elementaren Datentypen char, short, int und long mit verschiedenen Bitbrei-
ten belegt und hierdurch auf die Definition neuer Typen vollständig verzichten
kann. Die Portierung von 32-Bit-Applikationen gestaltet sich hingegen geringfü-
gig schwieriger. Neben der veränderten Bitbreite von Speicherreferenzen muss
auch auf den größeren Wertebereich des long-Datentyps geachtet werden. Das
LP64-Datenmodell wird auch als 4/8/8-Modell bezeichnet.
I Das ILP64-Datenmodell
Im Vergleich zu P64 oder LP64 ist dieses Modell weit weniger verbreitet. Unter
anderem arbeitet die Cray-Plattform nach dem ILP64-Modell. Analog zum P64-
Modell sind die Bitbreiten von Integer- und Long-Variablen identisch, verwen-
den aber beide die volle Breite von 64 Bit. Folgerichtig gibt es keinen elementa-
ren Datentyp für die Repräsentation ganzzahliger 32-Bit-Werte. Abhilfe schafft
hier der int32-Datentyp, der im Jahre 1999 ebenfalls als fester Bestandteil in
den ANSI-C-Standard aufgenommen wurde. Das ILP64-Datenmodell wird auch
als 8/8/8-Modell bezeichnet.
Insgesamt hat das zugrunde liegende Datenmodell einen großen Einfluss auf die Ar-
chitekturportierung einer 32-Bit-Applikation auf einen 64-Bit-Prozessor. Erschwert
wird die Portierung durch eine Reihe von Fehlschlüssen, die auf unzulässigen Ver-
allgemeinerungen der Eigenschaften klassischer 32-Bit-Architekturen zurückgehen.
In der praktischen Arbeit lassen sich die folgenden Fehlinterpretationen regelmäßig
beobachten:
I long und int werden als der gleiche Datentyp betrachtet.
initialize.c initialize_optimized.c
void initialize() 1 void initialize() 1
{ 2 { 2
char a[4]; 3 char a[4]; 3
int i; 4 4
5 *(long *)a = 0; 5
for (i=0; i<4; i++) { 6 6
a[i] = 0; 7 ... 7
} 8 } 8
} 9 9
... 10 10
} 11 11
12 12
In Abb. 3.29 ist ein typisches Programm abgebildet, in dem das vermeintliche
Wissen über die Bitbreite einer Variablen zu Optimierungszwecken eingesetzt wur-
de. Links ist das Originalprogramm und rechts daneben die „verbesserte“ Variante
zu sehen. Die vermeintliche Optimierung beruht an dieser Stelle auf der Hoffnung,
die Array-Initialisierung beschleunigen zu können, indem der elementweise Zugriff
durch eine einzige Zuweisung ersetzt wird. Mit dem Argument der Geschwindig-
keitssteigerung werden solche Konstrukte sowohl im industriellen Umfeld als auch
im Bereich der Open-Source-Software zuhauf eingesetzt. Fälschlicherweise ging
der Programmierer der betreffenden Code-Zeilen aufgrund seiner langjährigen Er-
fahrung davon aus, dass eine Variable vom Typ long eine Breite von 32 Bit besitzt.
Auf den meisten ILP32-Architekturen vollbringt das Programm zuverlässig seinen
Dienst – bis zu dem Tag, an dem die Applikation auf eine 64-Bit-Architektur portiert
wird, die nach dem LP64- oder ILP64-Datenmodell arbeitet.
In der Tat ist die gemachte Annahme über die Bitbreite von long nicht der ein-
zige Fehler, der durch diese Art der Optimierung entsteht. Erkennen Sie den Grund,
weshalb das Programm selbst auf manchen 32-Bit-Architekturen einen Absturz ver-
ursachen wird? Falls nicht, sind Sie an dieser Stelle eingeladen, bereits jetzt einen
Blick auf Abschnitt [Link] zu werfen. Dort werden wir das hier vorgestellte Pro-
gramm erneut aufgreifen und den zweiten großen Portabilitätsfehler im Detail dis-
kutieren.
Eine in der Praxis noch häufiger anzutreffende Fehlerquelle geht auf die Gleich-
behandlung von Integer- und Pointer-Variablen zurück. Auch hierfür zeichnet das
ILP32-Datenmodell verantwortlich, in dem die Typen int und void* die gleiche
Bitbreite besitzen. So manches Problem lässt sich aufgrund dieser Gemeinsamkeit
vermeintlich trickreich lösen – auf Kosten der Portabilität und damit der Zukunft
des Software-Systems. Steht z. B. keine Listen-Datenstruktur für die Verwahrung
von Speicherreferenzen zur Verfügung, wird in der Praxis gerne auf die klassi-
sche Integer-Liste zurückgegriffen. Durch die gleiche Bitbreite gibt es auf Spei-
cherebene keinen Unterschied zwischen den Datentypen – beide lassen sich durch
einen simplen Type-cast verlustfrei ineinander überführen. Insbesondere im Bereich
116 3 Konstruktive Qualitätssicherung
asm-i386/types.h
typedef unsigned char u8; 1
2
typedef signed short s16; 3
typedef unsigned short u16; 4
5
typedef signed int s32; 6
typedef unsigned int u32; 7
8
typedef signed long long s64; 9
typedef unsigned long long u64; 10
11
#define BITS_PER_LONG 32 12
asm-ia64/types.h
typedef unsigned char u8; 13
14
typedef signed short s16; 15
typedef unsigned short u16; 16
17
typedef signed int s32; 18
typedef unsigned int u32; 19
20
typedef signed long s64; 21
typedef unsigned long u64; 22
23
#define BITS_PER_LONG 64 24
bleibt in diesem Modell nach wie vor gültig und der Typisierungsfehler weiterhin
kaschiert.
Was können wir als Programmierer tun, um die Problematik variabler Bitbreiten
zu entschärfen? Eine einfache Möglichkeit besteht in der Verwendung architektur-
unabhängiger Datentypen, die bewusst von dem zugrunde liegenden Datenmodell
abstrahieren und stets eine konstante Bitbreite aufweisen. Als Beispiel betrachten
wir die Header-Datei types.h des Linux-Kernels, die unter anderem die folgen-
3.5 Portabilität 117
stdint.h
typedef signed char int8_t; 1
typedef unsigned char uint8_t; 2
3
typedef short int16_t; 4
typedef unsigned short uint16_t; 5
6
typedef long int32_t; 7
typedef unsigned long uint32_t; 8
9
typedef long long int64_t; 10
typedef unsigned long long uint64_t; 11
12
typedef long intptr_t; 13
typedef unsigned long uintptr_t; 14
den architekturunabhängigen Datentypen definiert: s8, u8, s16, u16, s32, u32, s64,
u64.
Die beiden Code-Fragmente in Abb. 3.30 zeigen die relevanten Auszüge der
Header-Datei in leicht vereinfachter Schreibweise. Links ist die Definition für
die 32-Bit-Plattform i386 und rechts für die 64-Bit-Plattform ia64 dargestellt.
Neben den architekturunabhängigen Datentypen wird zusätzlich die Konstante
BITS_PER_LONG definiert, die stets der Bitbreite einer long-Variablen entspricht.
Auf allen von Linux unterstützten Plattformen ist die Bitbreite einer Long-Variablen
mit der Bitbreite der Prozessorregister identisch.
Mit dem C99-Standard von ANSI-C wurden ebenfalls mehrere neue Datentypen
eingeführt, die über eine architekturunabhängige Bitbreite verfügen. Die Typdefini-
tionen befinden sich in der Header-Datei stdint.h und bilden die neuen Bezeich-
ner in analoger Weise auf die entsprechenden elementaren C-Datentypen ab. Der in
Abb. 3.31 exemplarisch dargestellte Dateiauszug entstammt der 32-Bit-Version von
Mac OS X.
Die zahlreichen neu definierten Datentypen lösen zwar das Problem der variablen
Bitbreiten, tragen jedoch auf der negativen Seite zu einer babylonischen Sprachver-
wirrung ganz neuer Art bei. Welche der neu eingesetzten Datentypen schlussendlich
eingesetzt werden sollten, hängt von der Programmiersprache, dem Betriebssystem
und nicht zuletzt von projektinternen Notations- und Sprachkonventionen ab. Hier
schließt sich der Kreis.
[Link] Speicherordnung
Die Speicherordnung einer Computerarchitektur legt fest, in welcher Reihenfolge
die einzelnen Bytes eines Datenworts im Hauptspeicher abgelegt werden. In Ab-
hängigkeit der Speicherreihenfolge lassen sich die Architekturen moderner Mikro-
prozessoren in zwei Klassen einteilen:
118 3 Konstruktive Qualitätssicherung
7 0 15 8 23 16 31 24
31 24 23 16 15 8 7 0
I Little-Endian-Architekturen
Die Bytes einer Binärzahl werden ihrer Wertigkeit entsprechend in aufsteigen-
der Reihenfolge gespeichert. Das Byte mit der niedrigsten Wertigkeit (Bits 0 -
7) wird zuerst, das Byte mit der höchsten Wertigkeit zuletzt im Speicher abge-
legt. Eingeführt wurde das Format mit dem ersten Mikroprozessor – dem Intel
4004. Die CPU arbeitete intern mit einer Registerbreite von 4 Bit, so dass im
Zuge der Addition zweier langer Datenwörter mehrere Viererpakete sukzessive
addiert werden mussten. Da die Binäraddition stets mit den niedrigstwertigen
Bits beginnt, wurden die Pakete im Speicher in aufsteigender Reihenfolge abge-
legt. Kurzum: Die Idee der Little-Endian-Technik war geboren. Die Registerbrei-
ten moderner Prozessoren beträgt ein Vielfaches der ursprünglich vorhandenen 4
Bit, so dass das serielle Additionsschema der 4004-CPU schon lange der Vergan-
genheit angehört. Trotzdem hält Intel bis heute an der Little-Endian-Architektur
fest.
I Big-Endian-Architekturen
Die Bytes einer Binärzahl werden ihrer Wertigkeit entsprechend in absteigender
Reihenfolge gespeichert. Das Byte mit der höchsten Wertigkeit wird zuerst, das
Byte mit der niedrigsten Wertigkeit (Bits 0 - 7) zuletzt im Speicher abgelegt. Im
direkten Vergleich erscheint die Big-Endian-Architektur als die natürlichere, da
das entstehende Speicherbild der uns vertrauten Notation entspricht. Abb. 3.32
fasst die unterschiedliche Anordnung der Daten grafisch zusammen.
Mit Hilfe des in Abb. 3.33 dargestellten C-Programms lässt sich die Speicherord-
nung eines Computersystems auf einfache Weise bestimmen. Zu diesem Zweck
weist die Implementierung einer short-Variablen den Wert 1 zu und liest den Va-
riableninhalt anschließend byteweise aus. Die zugrunde liegende Speicherordnung
verrät sich unmittelbar durch die vorgefundene Byte-Anordnung. Während die 1 auf
Little-Endian-Architekturen an erster Stelle erscheint, kommt sie auf Big-Endian-
Architekturen erst an zweiter Stelle zum Vorschein. Manch erfahrener C-Entwickler
wird wahrscheinlich die Größe des Programms bemängeln, schließlich lässt sich die
Speicherordnung auch mit einem trickreich formulierten Einzeiler bestimmen. Die
3.5 Portabilität 119
endian.c
#include <stdio.h> 1
2
typedef union { 3
struct { 4
char byte1; 5
char byte2; 6
} b; 7
short word; 8
} endian_test; 9
10
int main(void) { 11
12
endian_test t; 13
[Link] = 1; 14
if (t.b.byte1 == 1 && t.b.byte2 == 0) 15
printf("Little Endian\n"); 16
else if (t.b.byte1 == 0 && t.b.byte2 == 1) 17
printf("Big Edian\n"); 18
else 19
printf("Unknown endianess!?\n"); 20
21
return 1; 22
} 23
deep_thought.h
extern void 1
ask_deep_thought(int *); 2
deep_thought.c
void 1
ask_deep_thought(short *value) 2
{ 3
*value = 42; 4
} 5
main.c
#include <stdio.h> 1
#include "deep_thought.h" 2
3
int main(int argc, char *argv[]) 4
{ 5
int answer = 0; 6
7
ask_deep_thought(&answer); 8
9
printf("The ultimate answer to the " 10
"question of life is %d\n", answer); 11
return 1; 12
} 13
Abb. 3.34 Der hier enthaltene Programmierfehler bleibt auf Little-Endian-Architekturen verdeckt
programm eingebunden. Mit Hilfe des C-Compilers lässt sich das Projekt ohne eine
einzige Fehlermeldung oder Warnung übersetzen:
> gcc -c deep_thought.c
> gcc -o deep_thought deep_thought.o main.c
> ./deep_thought
Doch wie lautet nun die Antwort auf die ultimative Frage des Lebens? Die Anhänger
der Little-Endian-Architektur werden sich freuen – hier ist die Antwort wie erwar-
tet gleich 42. Ausgeführt auf einer Big-Endian-Maschine lautet die Antwort dage-
gen 2752512. Was ist hier passiert? Ein geschärfter Blick auf die Implementierung
zeigt schnell, dass die Funktion ask_deep_thought eine Referenz auf eine Varia-
ble vom Typ short erwartet, das Hauptprogramm jedoch eine Referenz auf eine
Variable vom Typ int übergibt. Auf einer Big-Endian-Architektur wird der Fehler
unmittelbar sichtbar, da die beiden höchstwertigen Bytes des Übergabeparameters
beschrieben werden. Auf einer Little-Endian-Architektur belegen die beiden nied-
rigstwertigen Bytes einer Variablen stets die gleiche Speicheradresse, unabhängig
davon, ob sie als short- oder als int-Variable referenziert wird. In diesem Fall wan-
dert der Ergebniswert sozusagen per Zufall an die richtige Stelle, so dass der Fehler
3.5 Portabilität 121
maskiert bleibt. Auch der C-Compiler selbst hat keinerlei Möglichkeit, den Typkon-
flikt zu entdecken. Ein gezielter Blick auf die Header-Datei ask_deep_thought.h
zeigt, dass der Übergabeparameter bereits hier falsch deklariert wurde.
Wir tun gut daran, das Potenzial des gezeigten Programmierfehlers an die-
ser Stelle nicht zu unterschätzen. Auch wenn die Implementierung auf Little-
Endian-Maschinen den korrekten Wert berechnet, könnte ein findiger Program-
mierer auf die Idee kommen, den Code geringfügig zu optimieren. Da der Wert
der Variablen answer unmittelbar nach ihrer Definition durch den Funktionsauf-
ruf ask_deep_thought überschrieben wird, kann die getätigte Initialisierung mit
ruhigem Gewissen entfallen. Aufgrund des vorhandenen Typkonflikts überschreibt
die Funktion ask_deep_thought den Übergabeparameter aber nur partiell, so dass
mehrere nicht initialisierte Bytes zurückbleiben. Trotzdem besteht eine hohe Wahr-
scheinlichkeit, dass die restlichen Bits zufällig gleich 0 sind und der Fehler in den
meisten Fällen immer noch verdeckt bleibt. Das Fehlerprofil lässt sich damit wie
folgt beschreiben:
I Der Fehler tritt äußerst selten zum Vorschein.
htons() ntohs()
ntohs() htons()
Da alle Intel-Prozessoren auf der Little-Endian Technik beruhen, muss die Byte-
Reihenfolge invertiert werden. In der abgedruckten Version geschieht dies durch
die Verwendung der Swap-Makros __bswap_32 und __bswap_16, die an anderer
Stelle definiert werden.
Die PowerPC-Variante von Mac OS X definiert die Makros in der Datei
/usr/include/sys/_endian.h hingegen wie folgt:
Die übergebenen Werte werden durch die Makros nicht verändert, da die Prozes-
sorarchitektur in diesem Fall auf der gleichen Speicherordnung beruht, wie die Net-
work byte order selbst.
Dass der zu sendende Byte-Strom von vorne herein korrekt angeordnet ist, bringt
an dieser Stelle nicht nur Vorteile mit sich. So ist die Anordnung stets korrekt, un-
abhängig davon, ob der Byte-Strom mit Hilfe der oben eingeführten Makros auf-
bereitet oder unverändert übertragen wurde. Entsprechend groß ist hier das Risiko
für versteckte Fehler, die erst im Zuge einer Architekturportierung zum Vorschein
treten.
3.5 Portabilität 123
“It began upon the following occasion. It is allowed on all hands, that the
primitive way of breaking eggs, before we eat them, was upon the larger
end; but his present majesty’s grandfather, while he was a boy, going to eat
an egg, and breaking it according to the ancient practice, happened to cut
one of his fingers. Whereupon the emperor his father published an edict,
commanding all his subjects, upon great penalties, to break the smaller
end of their eggs. The people so highly resented this law, that our histories
tell us, there have been six rebellions raised on that account; wherein one
emperor lost his life, and another his crown. These civil commotions we-
re constantly fomented by the monarchs of Blefuscu; and when they were
quelled, the exiles always fled for refuge to that empire. It is computed that
eleven thousand persons have at several times suffered death, rather than
submit to break their eggs at the smaller end. Many hundred large volumes
have been published upon this controversy: but the books of the Big-endians
have been long forbidden, and the whole party rendered incapable by law
of holding employments.”
Jonathan Swift [242]
[Link] Datenausrichtung
Der Begriff der Datenausrichtung (data alignment) beschreibt, ob die Startadresse
eines Datenworts in einer wohldefinierten Beziehung zu seiner Größe steht. Grob
gesprochen heißt ein Datenwort ausgerichtet, wenn seine Startadresse ein Vielfa-
ches seiner Größe beträgt. 32-Bit-Integer-Variablen sind demnach genau an den
durch 4 teilbaren Adressen, 16-Bit-Short-Variablen an jeder geraden Adresse und
8-Bit-Character-Variablen an ausnahmslos jeder Speicherstelle ausgerichtet. Ent-
spricht die Größe des untersuchten Datenworts einer Zweierpotenz, so lässt sich die
korrekte Ausrichtung mit einem einzigen Blick auf das Bitmuster der Startadresse
erkennen: Ein 2n -Byte-großes Datenwort ist genau dann ausgerichtet, wenn die n
niedrigstwertigen Bits der Startadresse gleich 0 sind.
124 3 Konstruktive Qualitätssicherung
alignment.c
int32_t foo(int32_t *my_ptr) 1
{ 2
return *my_ptr; 3
} 4
Warum spielt die korrekte Ausrichtung eines Datenworts in der Praxis überhaupt
eine Rolle? Ganz im Sinne dieses Kapitels ist die Antwort abhängig von der zugrun-
de liegenden Rechnerarchitektur. So schreiben einige RISC-Prozessoren zwingend
vor, dass alle im Speicher abgelegten Datenwörter ausgerichtet sein müssen. Der
Versuch, ein nicht ausgerichtetes Datenwort einzulesen, löst auf den entsprechen-
den CPUs einen Prozessor-Interrupt aus, der unter den meisten Betriebssystemen zu
einer sofortigen Beendigung des laufenden Prozesses führt. Andere Architekturen
können ausgerichtete Datenworte deutlich schneller einlesen, so dass falsch ausge-
richtete Daten drastische Laufzeitverschlechterungen zur Folge haben können.
Dass die Geschwindigkeitsverluste in der Tat schmerzliche Ausmaße annehmen
können, demonstriert der Anfang der Neunzigerjahre von der Firma DEC auf den
Markt gebrachte Alpha-Prozessor. Um die Auswirkung zu verstehen, betrachten wir
die in Abb. 3.36 dargestellte C-Funktion. Über die Variable my_ptr nimmt diese
einen Zeiger auf eine Integer-Variable entgegen und liefert den Inhalt der übergebe-
nen Speicheradresse als Ergebnis zurück. Wird das Programm mit den Standardein-
stellungen übersetzt, geht der Compiler von der korrekten Ausrichtung aller Daten-
worte aus und bildet die Return-Anweisung auf einen einzigen Assembler-Befehl
ldl zum Laden des Ergebnisregisters ab (vgl. Abb. 3.36 unten links). Entfällt die
Annahme über die Ausrichtung der Datenworte, muss der Speicherzugriff auf kom-
plizierte Weise abgesichert werden (vgl. Abb. 3.36 unten rechts). In diesem Fall liest
das Assembler-Programm zunächst die beiden ausgerichteten 64-Bit-Wörter ein, die
den gesuchten 32-Bit-Wert zusammen vollständig überdecken. Danach werden mit
3.5 Portabilität 125
Hilfe der extl-Befehle die relevanten Teile isoliert und anschließend mit dem bis-
Befehl wieder zusammengefügt.
sizeof(foo_struct) = 12 sizeof(foo_struct) = 8
structure_padding.c
#include <stdio.h> 1
2
union { 3
struct { 4
char foo; 5
short bar; 6
} a; 7
char b[3]; 8
} foobar; 9
10
int main(void) { 11
12
[Link] = (char)1; 13
[Link] = (short)2; 14
15
printf("%d", (int)foo.b[0]); 16
printf("%d", (int)foo.b[1]); 17
printf("%d", (int)foo.b[2]); 18
printf("%d", (int)foo.b[3]); 19
20
return 1; 21
} 22
? ? ? ?
[Link] = 1; 00000001 ? ? ?
den die Elemente hierdurch nicht mehr länger hintereinander, sondern übereinander
angeordnet.
Effektiv wird hierdurch eine zweite Schnittstelle definiert, mit deren Hilfe by-
teweise auf den durch foo und bar belegten Speicher zugegriffen werden kann.
Konstruktionen dieser Art kommen in der Praxis häufig vor, und als Beispiel mö-
ge der Leser bereits jetzt einen Blick auf die in Kapitel 7, Abb. 7.11 dargestellte
Datenstruktur D3DMATRIX der Microsoft-eigenen 3D-Schnittstelle DirectX werfen.
Das Beispielprogramm beschreibt die Variable foo mit dem 8-Bit-Wert 1 und
die Variable bar mit dem 16-Bit-Wert 2. Aber welche Ausgabe produziert nun das
128 3 Konstruktive Qualitätssicherung
Programm? Die Antwort gibt der ebenfalls in Abb. 3.38 dargestellte Speicheraus-
zug. Die erste Zuweisung beschreibt den Speicher an der Adresse von b[0] und
die zweite Zuweisung den Speicher ab der Adresse von b[2]. Der Inhalt von b[1]
bleibt gänzlich unberührt und bewahrt seinen ursprünglichen Wert.
An dieser Stelle kommen wir auf das C-Beispiel aus Abb. 3.29 zurück. Einen
Fehler haben wir bereits dort erkannt: Der Programmierer hatte die Bitbreite eines
long-Datums als konstant erachtet. Der zweite Fehler ist jedoch deutlich diffizilerer
Natur. Die Elemente des zu initialisierenden Arrays besitzen den Datentyp char, so
dass das Array stets korrekt ausgerichtet ist und hierdurch an einer beliebigen Start-
adresse beginnen kann. Durch den verwendeten Type-cast auf einen long-Pointer
ist das Datenwort nur noch dann ausgerichtet, wenn die Startadresse durch 4 teilbar
ist. Auf einigen Architekturen wird das Programm zu einem Absturz führen, auf an-
deren zumindest verlangsamt laufen. Dass die vermeintlich beschleunigte Variante
sogar eine potenzielle Laufzeitverschlechterung in sich birgt, führt die Optimierung
nicht nur ad absurdum, sondern zeigt zugleich, dass Programmiertricks dieser Art
mit deutlicher Vorsicht zu genießen sind.
[Link] Zeitverhalten
In der Praxis entstehen viele Portabilitätsprobleme durch feste Annahmen über das
Zeitverhalten der zugrunde liegenden Hardware-Architektur, des verwendeten Be-
triebssystems oder des Programms selbst. Die Mehrzahl solcher Fehler lassen sich
auf eine der folgenden Ursachen zurückführen:
I Falsche Annahmen über die Ausführungszeit
Die Ausführungszeit einer einzelnen Funktion ist heute von weit geringerer Be-
deutung als in den frühen Computertagen. In vielen Software-Sparten ist die al-
gorithmische Komplexität einer Implementierung inzwischen wichtiger als die
real gemessene Bearbeitungszeit. Gänzlich anders stellt sich die Situation im
Bereich eingebetteter Systeme dar. Hier spielt die Laufzeitanalyse auf der Ebe-
ne von Taktzyklen immer noch eine zentrale Rolle. Die enge Verzahnung von
Software und Hardware kann hier dazu führen, dass ein Programm mit der 50-
MHz-Variante eines Mikroprozessors tadellos funktioniert, unter der prinzipiell
leistungsfähigeren 60-MHz-CPU jedoch seinen Dienst verweigert. Im Bereich
der Anwender- und Server-Software gehören solche Phänomene weitgehend der
Vergangenheit an.
Abb. 3.39 verdeutlicht die Problematik anhand dreier Threads, die konkurrie-
rend auf die vier Ressourcen A bis D zugreifen. Jede Ressource ist durch ein Se-
maphor geschützt, so dass sich ein Thread zunächst mit Hilfe des lock-Befehls
den exklusiven Zugriff sichern muss. Ist die Ressource schon einem anderen
Thread zugeordnet, blockiert der lock-Befehl die Ausführung so lange, bis die
Ressource von seinem aktuellen Besitzer wieder freigegeben wird. Hierzu steht
die Funktion unlock zur Verfügung. Wird die auf der linken Seite von Abb. 3.39
gezeigte Scheduling-Strategie verwendet, funktioniert das Programm einwand-
frei. Da Thread 2 vor Thread 3 ausgeführt wird, ist Thread 3 zunächst blockiert
und kommt erst wieder zum Zug, nachdem die kritischen Ressourcen von Thread
1 freigegeben wurden.
Eine leichte Änderung der Scheduling-Strategie hat in diesem Beispiel drama-
tische Auswirkungen. Werden die einzelnen Threads, wie in Abb. 3.39 (rechts)
gezeigt, in umgekehrter Reihenfolge bedient, blockieren sich Thread 1 und
Thread 2 in kürzester Zeit gegenseitig. Jeder Thread wartet auf die Ressourcen-
Freigabe des anderen, so dass der gesamte Programmablauf zum Erliegen
kommt. Wir sprechen in diesem Zusammenhang von einer Verklemmung (dead-
lock).
Können Java-Programmierer über viele der weiter oben diskutierten Porta-
bilitätsaspekte nur müde lächeln, so sind sie von der Laufzeitproblematik glei-
chermaßen betroffen. Obwohl die Sprachdefinition einige Regeln festlegt, die die
Ausführungsreihenfolge von Threads an deren Priorität koppeln, ist die genaue
Scheduling-Strategie bewusst nicht im Detail vorgeschrieben. Verschiedene vir-
tuelle Maschinen können das Thread-Scheduling unterschiedlich implementie-
ren – und tun dies auch. Die Problematik zeigt, dass selbst Java-Programme be-
züglich ihrer Portierbarkeitseigenschaften Schranken unterliegen. Trotzdem wä-
re übermäßiger Pessimismus an dieser Stelle übertrieben. Java ist und bleibt eine
der portabelsten Sprachen, die uns heute in der Software-Entwicklung zur Ver-
fügung steht.
Die Funktion schedule_timeout reaktiviert den Thread nach genau 500 Auf-
rufen des Timer-Interrupts. Legen wir für unsere Betrachtung die x86-Plattform
130 3 Konstruktive Qualitätssicherung
lock A; lock D;
lock B; lock C;
lock C; lock D; lock B;
use ABC; use D; use DCB;
unlock C; unlock D; unlock B;
unlock B; unlock C;
unlock A; Thread 1 Thread 2 unlock D; Thread 3
1 2 3 1 2 3
lock A; lock A;
lock D; lock D;
lock B; lock B; Blockiert
use D; Blockiert lock C;
lock C; Blockiert
unlock D; Blockiert
lock D;
use ABC;
unlock C; Beendet Blockiert
lock C;
unlock B;
lock B;
unlock A;
use DCB;
unlock B;
Beendet unlock C;
unlock D;
Beendet
Probleme dieser Art lassen sich leicht vermeiden, indem die Präprozessorkon-
stante HZ verwendet wird. HZ enthält die Anzahl der ausgelösten Timer-Interrupts
pro Sekunde und ist für jede Hardware-Architektur und Kernel-Version individu-
ell definiert. Das obige Programm lässt sich damit auf äußerst einfache Weise in
ein äquivalentes und zugleich portables Programm umschreiben:
set_current_state(TASK_UNINTERRUPTIBLE);
schedule_timeout(5 * HZ); /* wait for 5 seconds */
[Link] Zwischencodes
Der Begriff Zwischencode bezeichnet eine maschinennahe Repräsentation eines
Programms, die von den individuellen Hardware-Merkmalen eines Mikroprozes-
sors abstrahiert. Mit der zunehmenden Rechenleistung moderner Computersysteme
verzichten mehr und mehr Programmiersprachen darauf, die verschiedenen Instruk-
tionen direkt auf den Befehlssatz der eingesetzten CPU abzubilden. Stattdessen wird
der Quelltext in einen Hardware-unabhängigen Zwischencode übersetzt und auf die-
se Weise ein hoher Grad an Portabilität erreicht. Da die erzeugten Instruktionen von
der CPU nicht direkt verarbeitet werden können, wird eine zusätzliche Interpreter-
Software benötigt, die den Zwischencode zur Laufzeit auf den Befehlssatz des Pro-
zessors abbildet. Der gesamte für die Ausführung benötigte Software-Rahmen wird
als Laufzeitumgebung (runtime environment) und der eigentliche Interpreter-Kern
als virtuelle Maschine (virtual machine) bezeichnet. Die Verwendung eines Zwi-
schencodes bietet die folgenden Vorteile:
I Einheitlichkeit
Der erzeugte Zwischencode liegt in einem einheitlichen Format vor, der von den
spezifischen Eigenschaften verschiedener Betriebssysteme und Mikroprozesso-
ren abstrahiert. Um eine Applikation unter einem anderen Betriebssystem oder
einer anderen Hardware-Architektur auszuführen, muss ausschließlich die Lauf-
zeitumgebung angepasst werden – der einmal erzeugte Zwischencode bleibt un-
verändert. Durch die Formatvereinheitlichung wird ein Höchstmaß an Portabili-
tät erzielt, wenngleich die weiter oben skizzierte Laufzeitproblematik weiterhin
besteht.
132 3 Konstruktive Qualitätssicherung
I Sicherheit
Die Laufzeitumgebung bildet eine zusätzliche Abstraktionsschicht zwischen der
auszuführenden Applikation und dem Rest des Computersystems. Die implizit
eingeführte Kapselung ermöglicht, das Verhalten der Applikation weitgehend zu
kontrollieren. So können beliebige Ressourcen gegen ungewollte Zugriffe ge-
schützt oder die physikalischen Hardware-Komponenten problemlos durch virtu-
elle Subsysteme ergänzt werden. Im Zeitalter von Viren, Würmern und Trojanern
kommt diesem Aspekt eine früher ungeahnte Bedeutung zu.
Neben den vielen Vorteilen sollen auch die Nachteile nicht unerwähnt bleiben,
die mit der Einführung einer zusätzlichen Abstraktionsschicht verbunden sind. Da
der Zwischencode von der CPU zur Laufzeit interpretiert werden muss, laufen
entsprechende Programme im Allgemeinen langsamer als ihre direkt in Maschi-
nencode übersetzten Gegenspieler. Die Laufzeitverschlechterung exakt zu quanti-
fizieren ist schwierig, da sie von einer Vielzahl verschiedener Faktoren beeinflusst
wird. Durch die Verwendung immer ausgefeilterer Interpreter-Techniken konnten
die Geschwindigkeitseinbußen in den letzten Jahren drastisch reduziert werden. So
verwenden heute fast alle Laufzeitumgebungen eingebaute Just-in-Time-Compiler
(JIT-Compiler), die wesentliche Vorteile der Interpreter- und der Compiler-Technik
miteinander vereinen. Während der Ausführung zeichnet ein JIT-Compiler verschie-
dene Nutzungsprofile auf und übersetzt häufiger ausgeführte Programmteile dyna-
misch in Maschinencode (On-the-fly-Compilierung). Die Techniken sind heute so
ausgereift und stabil, dass die Geschwindigkeitsproblematik nur noch in den selten-
sten Fällen gegen den Einsatz von Zwischencode spricht. Darüber hinaus bietet der
Einsatz eines Interpreters die Möglichkeit der dynamischen Optimierung. Da der
Zwischencode deutlich mehr semantische Informationen als der äquivalente Ma-
schinencode bewahrt, lassen sich viele Programmoptimierungen durchführen, die
auf der Ebene der Maschineninstruktionen nicht mehr möglich sind. Durch die ge-
schickte Kombination mit den aufgezeichneten Laufzeitprofilen lässt sich die Aus-
führungsgeschwindigkeit des vollständig in Maschinencode übersetzten Programms
in einigen Fällen sogar unterbieten.
Mit der Java-Technologie und dem .NET-Framework basieren zwei der heute
am weitesten verbreiteten Software-Plattformen auf dem Prinzip der interpretier-
ten Programmausführung (vgl. Abb. 3.40). Beide Plattformen übersetzen die Quell-
dateien zunächst in einen Zwischencode, der in der .NET-Terminologie als Com-
mon Intermediate Language (CIL) und in der Java-Welt als Byte-Code bezeichnet
wird. Bei genauerer Betrachtung stehen den vielen Unterschieden in der Namensge-
bung umso mehr technische Gemeinsamkeiten gegenüber. Neben den nahezu iden-
tischen virtuellen Maschinen gibt es für die meisten aus der Java-Welt bekannten
Technologien ein entsprechendes Gegenstück auf .NET-Seite und umgekehrt. Java-
Archiven stehen .NET Assemblies gegenüber und dem Remote-Method-Invocation-
Mechanismus setzt Microsoft die .NET-Remoting-Technik entgegen.
Der wesentliche Unterschied der beiden Plattformen besteht in der Zielsetzung
und dem Verbreitungsgrad. Java wurde mit der Absicht geschaffen, Programme
plattformübergreifend auszuführen. Im Gegensatz hierzu ist das .NET-Framework
von Hause aus sprachenübergreifend ausgerichtet, unterstützt dafür aber ausschließ-
3.5 Portabilität 133
Byte
CIL
Code
lich Windows als Zielsystem. Insgesamt unterscheiden sich die Philosophien beider
Plattformen damit erheblich voneinander. Verfolgt Sun mit der Java-Plattform die
Philosophie „Eine Sprache für viele Plattformen“, so forciert Microsoft mit dem
.NET-Framework die Strategie „Viele Sprachen für eine Plattform“.
Die Idee einer systemübergreifenden Zwischensprache ist keinesfalls neu. Be-
reits in den späten Siebziger- und den frühen Achtzigerjahren erlangte dieses Prinzip
mit dem UCSD P-System eine beachtliche Popularität. Hierbei handelte es sich um
ein portables Betriebssystem, das ähnlich der heute in Java und .NET eingesetzten
Technologie auf der Verwendung von Zwischencode beruhte. Bekannt wurde das
System insbesondere durch UCSD Pascal. Die compilierten P-Code-Programme
liefen ohne Änderung auf einem Apple II, einer Xerox 820 oder einer DEC PDP-11.
Ein ähnliches Konzept wurde auch mit der Sprache Smalltalk verfolgt, die ebenfalls
auf dem Byte-Code-Prinzip aufsetzt und um dieselbe Zeit entstand.
Fremdes
PowerPC Natives
PowerPC
PowerPC PowerPC
Binary
Binary Binary
Binary
Binary Binary
Code-Morphing- Decoder
Software
Optimierer
Code-Generator
Betriebssystem
Prozessor
zessors optimiert wird. Anschließend bildet der Code-Generator die interne Reprä-
sentation auf die Maschineninstruktionen des Zielprozessors ab.
Im Jahre 2001 konnte das Unternehmen Transitive mit der Code-Morphing-
Software Dynamite P/X auf dem Microprocessor-Forum in San José großes Auf-
sehen erregen [258]. Auf einem 1, 4-GHz-Athlon-Prozessor der Firma AMD wurde
eine 1-GHz-PowerPC-CPU emuliert – ein ganzes Jahr bevor der erste in Serie gefer-
tigte PowerPC-Prozessor selbst die 1-GHz-Schallmauer brechen konnte. Unter an-
derem findet sich die Technik der Firma Transitive heute in dem PowerPC-Emulator
Rosetta der Firma Apple wieder. Als Teil der Intel-Varianten von Mac OS X ermög-
licht die Emulations-Software, PowerPC-Applikationen älterer Macintosh-Systeme
auf den neuen Intel-Systemen auszuführen. Die Code-Morphing-Software ist in die-
sem Fall eng mit dem Betriebssystem verzahnt. Sobald eine PowerPC-Anwendung
gestartet wird, aktiviert das Betriebssystem die Rosetta-Emulation unsichtbar im
Hintergrund.
Ein ähnliches Aufsehen erregte die Firma Transmeta mit dem Crusoe-Prozessor,
der als erste CPU die Code-Morphing-Technik direkt in die Mikroprozessorarchi-
tektur integrierte [259]. Der Crusoe-Prozessor ist kompatibel zur x86-Architektur,
zeichnet sich jedoch durch eine deutlich geringere Leistungsaufnahme aus. Der ge-
ringe Stromverbrauch wird unter anderem dadurch erreicht, dass der Crusoe-Kern
intern aus einem sehr einfach aufgebauten VLIW-Kern (VLIW = Very Long Instruc-
tion Word) besteht. Jedes VLIW-Wort – in der Crusoe-Terminologie als Molekül
bezeichnet – ist 128 Bit breit und codiert jeweils 4 Befehle, die parallel zueinander
abgearbeitet werden. Jeder Einzelbefehl wird als Atom bezeichnet.
136 3 Konstruktive Qualitätssicherung
Applikation
Betriebssystem
Bios
VLIW Kern
Code-Morphing-
Software
I Energieeinsparung
Im direkten Vergleich zur komplexen Architektur eines modernen Intel-
Prozessors mutet der VLIW-Kern des Crusoe-Prozessors fast primitiv an. Der
große Vorteil liegt in der deutlich verringerten Leistungsaufnahme, die mit
der gesunkenen Hardware-Komplexität einhergeht. Viele Maßnahmen zur Leis-
tungssteigerung, die moderne Intel-Prozessoren mit Hilfe komplexer und strom-
fressender Hardware-Schaltkreise realisieren, sind im Crusoe-Prozessor in Soft-
ware nachgebildet. Die Prozessoren eignen sich damit insbesondere für den Ein-
satz in mobilen Endgeräten.
I Flexibilität
Software ist jederzeit änderbar und somit auch der Code-Morphing-Anteil der
Crusoe-CPU. Damit ist es theoretisch möglich, den Prozessor so umzuprogram-
mieren, dass anstelle der x86-Instruktionen z. B. der Befehlssatz eines PowerPC-
Kerns nachgebildet wird – der Phantasie sind hier keine Grenzen gesetzt. Positiv
wirkt sich die freie Umprogrammierbarkeit auch auf die Behebung von Fehlern
aus. Jeder Defekt, der im Software-Anteil des Prozessors zu suchen ist, lässt sich
nachträglich im Rahmen eines Firmware-Updates beheben. Ein Fehler wie der
Pentium-FDIV-Bug (vgl. Abschnitt 2.9) hätte so unter Umständen nachträglich
korrigiert werden können.
Dem hohen Innovationsgrad zum Trotz blieb der Markterfolg der Crusoe-
Prozessoren hinter den Erwartungen zurück. Nach mehreren verlustreichen Jahren
stellte Transmeta die Produktion endgültig ein, integrierte jedoch viele Grundkon-
zepte der Crusoe-CPU in den als Nachfolger platzierten Efficeon-Prozessor. Obwohl
die Efficeon-Architektur mehrere vielversprechende Konzepte in sich vereint, war
auch dieser der große Durchbruch bisher verwehrt.
Die nur mäßigen Markterfolge der Crusoe- und Efficeon-Prozessoren täuschen
oft darüber hinweg, dass in der Code-Morphing-Technik ein nicht zu unterschätzen-
des Innovationspotenzial steckt. Sollte sich der heute abzeichnende Trend bezüglich
der Leistungsaufnahme und der Hitzeentwicklung moderner Prozessoren versteti-
gen, so könnte die Zukunft der Code-Morphing-Technologie auf Hardware-Ebene
gerade erst begonnen haben.
[Link] Systemvirtualisierung
Die Systemvirtualisierung verfolgt das Ziel, neben der CPU auch das BIOS, die
Grafik-Hardware sowie sämtliche Peripherie-Komponenten zu emulieren. Als Er-
gebnis entsteht ein vollständig virtualisiertes Computersystem, auf dem sich weitere
Instanzen des Wirtssystems, aber auch Instanzen fremder Betriebssysteme installie-
ren lassen. Auf diese Weise wird es möglich, verschiedene Betriebssysteme paral-
lel zu betreiben (vgl. Abb. 3.43). Für die in der Simulationsumgebung ausgeführte
Software ist die Virtualisierung kaum zu erkennen. Die emulierte Hardware wirkt
für das Gast-Betriebssystem wie eine reale Rechnerumgebung und kann nur durch
progammiertechnische Kniffe als virtuell entlarvt werden.
138 3 Konstruktive Qualitätssicherung
Virtualisierungs-Software
Virtuelle Hardware
Host-Betriebssystem
Host-Prozessor
bleiben die negativen Auswirkungen schadhafter Programme stets auf das Gast-
Betriebssystem beschränkt. So können sich Viren, Würmer und Trojaner zwar in-
nerhalb der Simulationsumgebung in gewohnter Weise ausbreiten, das Wirtssystem
bleibt vor dem Angriff jedoch weitgehend geschützt.
Die Virtualisierung der einzelnen Hardware-Komponenten gelingt in der Praxis
unterschiedlich gut. Lassen sich das BIOS oder die EFI-Firmware vergleichsweise
einfach virtualisieren, gestaltet sich die Emulation moderner Grafikprozessoren als
deutlich schwieriger. Die Leistung der nachgebildeten Grafikkarte bleibt mitunter
deutlich hinter der real eingebauten zurück, so dass z. B. die Ausführung grafikin-
tensiver Spiele in vielen Fällen dem Wirtssystem vorbehalten bleibt.
Nicht nur die Emulation der Grafikkarte kann die Gesamtperformanz des simu-
lierten Systems negativ beeinflussen. Entspricht die CPU der virtuellen Rechnerin-
stanz nicht der des Wirtssystems, so muss die Befehlsausführung Schritt für Schritt
simuliert werden. Die hierfür eingesetzten Verfahren entsprechen in wesentlichen
Teilen der Code-Morphing-Technik aus Abschnitt [Link].
Verwendet die virtuelle und die physikalische Rechnerumgebung die gleiche
CPU, so stellt sich die Situation wesentlich freundlicher dar. Anstatt den Be-
fehlsstrom der Gast-CPU aufwendig zu interpretieren, nutzt die Virtualisierungs-
Software die verschiedenen Privilegstufen (privilege levels) moderner Mikropro-
zessoren geschickt aus.
Die verschiedenen Privilegstufen eines Prozessors werden auch als Ringe be-
zeichnet und bestimmen, mit welchen Rechten der aktuell bearbeitete Befehlsstrom
ausgeführt wird. Die Anzahl der Ringe variiert zwischen den Prozessortypen er-
140 3 Konstruktive Qualitätssicherung
Zuordnung in Windows
0 Windows-Kernel
1,2 Windows-Dienste, Treiber
3 Windows-Anwendungen
Zuordnung in Linux
Ring 0
0 Linux-Kernel (kernel space)
Ring 1
1,2 Nicht verwendet
Ring 2
3 Linux-Anwendungen
Ring 3
3.6 Dokumentation
“«Good morning» he says, «how long ha-
ve you been at that tree?» Not stopping
to look up the lumberjack wheezes out,
«About three hours». «Three hours!» ex-
claims the wise man. Taking a closer look
at the lumberjack’s tool he asks him: «Ha-
ve you stopped to sharpen your saw?»
«Don’t be ridiculous» replies the lumber-
jack, «can’t you see I don’t have time.»”
Stephen R. Covey [60]
An dieser Stelle wenden wir uns einem der prekärsten Kapiteln der Software-
Entwicklung zu: Der Dokumentation. Innerhalb eines typischen Software-Projekts
werden viele verschiedene Dokumente erstellt, die sich ganz unterschiedlichen Ka-
tegorien zuordnen lassen. Die in Abb. 3.46 dargestellte Dichotomie bringt die für
uns wichtigsten Dokumentkategorien in eine hierarchische Ordnung. Auf der ober-
sten Ebene lassen sich externe und interne Dokumente unterscheiden.
I Externe Dokumente
In diese Kategorie fallen alle Dokumente, die an den Kunden ausgeliefert werden
oder von diesem eingesehen werden dürfen. Hierzu gehören neben dem Pflich-
tenheft und den Handbüchern z. B. auch sämtliche Texte der Online-Hilfe.
I Interne Dokumente
Diese Kategorie umfasst alle Dokumente, die für den Kunden nicht zugänglich
sind. Neben der Programmdokumentation fallen die Beschreibungen von Nota-
tionsrichtlinien (Abschnitt 3.1.1) und Sprachkonvention (Abschnitt 3.1.2), aber
auch Marktstudien oder Strategiepapiere in diese Dokumentenklasse.
In diesem Abschnitt legen wir unser Augenmerk mit der Programmdokumentati-
on auf diejenige Kategorie, die aus der Sicht des Software-Entwicklers die größte
Bedeutung besitzt. Die Programmdokumentation unterscheidet sich von allen an-
deren Dokumenten in einem wesentlichen Punkt: Sie muss durch den Software-
Entwickler selbst erstellt werden. Auch wenn die Anfertigung derselben bei den
wenigsten Programmierern Begeisterungsstürme auslöst, gehört sie zu den wichtig-
sten Aufgaben eines Software-Entwicklers.
Von den meisten Programmierern wird die Erstellung der Programmdokumenta-
tion als der Wermutstropfen einer ansonsten schöpferischen Tätigkeit empfunden.
Die Zeit, die ein Software-Entwickler für das Verfassen von Dokumenten und Quell-
textkommentaren aufwenden muss, fehlt zum Programmieren, und da das Projekt
ohnehin spät ist – die pure Statistik rechtfertigt mich zu dieser Unterstellung – wird
die Arbeit allzu schnell als lästiges Übel empfunden. Die Konsequenzen sind fir-
menübergreifend spürbar: Wenn überhaupt, wird zumeist nur stiefmütterlich doku-
mentiert. Der enorme Zeitdruck vieler Programmierer führt zu einer permanenten
Vernachlässigung dieser Tätigkeit und zwingt das Projekt in direkter Weise in einen
142 3 Konstruktive Qualitätssicherung
Software-
Dokumentation
Externe Interne
Dokumentation Dokumentation
Spezi!kations- Implementierungs-
dokumentation dokumentation
3.6.1 Spezifikationsdokumente
Plakativ gesprochen beschreibt die Spezifikation eines Software-Systems, was die
Software tut. Die konkrete Umsetzung, d. h. wie dieses Ziel erreicht wird, spielt an
dieser Stelle noch keine Rolle. Aus diesem Grund ist die Spezifikation in den meis-
ten Fällen in Form externer Dokumente realisiert und nicht Bestandteil der Quell-
texte. Damit eine Spezifikation ihrem Zweck gerecht wird, muss sie den folgenden
Kriterien genügen:
3.6 Dokumentation 143
x7 x6 x5 x4 x3 x2 x1 x0
Legende
Ein / Ausgabe:
n (x7, ..., x0) : Eingabestrom
a Serielle (y7, ..., y0) : Ausgabestrom
s arithmetisch-logische Steuerleitungen:
m Einheit (ALU) n : Negation
d a : Addition
s : Subtraktion
m: Multiplikation
y7 y6 y5 y4 y3 y2 y1 y0 d : Divsion
Die gegebene Beschreibung ist ein typisches Beispiel einer informellen Spezifika-
tion und erfüllt auf den ersten Blick ihren Zweck. Wir wissen nun, was die ALU
tut – genauer gesagt: Wir glauben zu wissen, was die ALU tut. Tatsächlich lässt ein
144 3 Konstruktive Qualitätssicherung
erneuter Blick auf die Beschreibung erste Zweifel aufkeimen, ob es um die Vollstän-
digkeit und Genauigkeit der Spezifikation zum Besten bestellt ist. Spätestens bei der
Umsetzung der umgangssprachlichen Spezifikation in eine Implementierung sieht
sich der Software-Entwickler mit den folgenden Ungereimtheiten konfrontiert:
I Sprachliche Ungenauigkeiten
So schön und elegant sich viele Sachverhalte in natürlicher Sprache formulieren
lassen, so ungenau ist in der Regel das Ergebnis. Beispielsweise ist völlig un-
klar, was die Formulierung „die letzten beiden Eingabewerte“ an dieser Stelle
bedeutet. Ist der aktuelle Eingabewert hier enthalten oder nicht? Auch die For-
mulierung „der Quotient“ lässt einen erheblichen Interpretationsspielraum zu, da
keinerlei Aussage über die Reihenfolge des Divisors und des Dividenden ge-
macht wird. Weitere typische Fehlerquellen sind die Verwendung der Wörter
„und“, „oder“ und „nicht“, die in Abhängigkeit ihrer umgangssprachlichen Ver-
wendung ganz unterschiedliche Bedeutungen in sich tragen.
Von Kritikern informeller Spezifikationen werden diese drei Argumente immer wie-
der angeführt, um die Unzulänglichkeit umgangssprachlicher Beschreibungen zu
3.6 Dokumentation 145
belegen. Trotzdem wäre es falsch, die Vorteile einer verbalen Beschreibung zu igno-
rieren. Keine andere Beschreibungsart ist für uns eingänglicher und damit besser
geeignet, die grundlegenden Ideen und Funktionsweisen eines Software-Systems
zu vermitteln.
I Ausgabe
I Verhalten
−x[0] für n = 1 und a = s = m = d = 0
y[0] =
0 sonst
⎧
⎪
⎪ − x[n + 1] für n = 1 und a = s = m = d = 0
⎪
⎪
⎪
⎪ x[n] + x[n + 1] für a = 1 und n = s = m = d = 0
⎨
x[n] − x[n + 1] für s = 1 und n = a = m = d = 0
y[n + 1] =
⎪
⎪ x[n] × x[n + 1] für m = 1 und n = a = s = d = 0
⎪
⎪
⎪
⎪ x[n] / x[n + 1] für d = 1 und n = a = s = m = 0
⎩
0 sonst
Der Vorteil der überarbeiteten Spezifikation liegt auf der Hand. Durch die Verwen-
dung einer präzisen mathematischen Schreibweise sind nahezu alle Zweideutigkei-
ten der verbalen Beschreibung verschwunden. Trotzdem bleibt die semi-formale
Variante gut lesbar und deren Verständlichkeit nur unwesentlich hinter der verbalen
Beschreibung zurück.
Ein prominentes Beispiel einer semi-formalen Spezifikationssprache ist die Uni-
fied Modeling Language, kurz UML. Die Sprache definiert verschiedene Diagramm-
arten und Richtlinien, mit deren Hilfe die verschiedenen Aspekte eines Software-
Systems einheitlich spezifiziert werden können. Die UML ist ausreichend formal,
um z. B. die automatisierte Synthese von Code-Rahmen zu unterstützen und gleich-
zeitig informell genug, um eine hohe Verständlichkeit zu gewährleisten. Tiefer-
gehende Einführungen in das weite Gebiet der UML finden sich unter anderem
in [148, 10, 11].
146 3 Konstruktive Qualitätssicherung
[Link] Referenzimplementierung
Im Gegensatz zu den bisher vorgestellten Varianten verfolgt die Technik der Refe-
renzimplementierung das Prinzip, die Spezifikation durch ein zweites Programm zu
ersetzen. Das Verhalten des Spezifikationsprogramms wird de jure als korrekt defi-
niert, so dass die Implementierung genau dann als fehlerfrei gilt, wenn sie für alle
Eingaben die Ausgabe des Spezifikationsprogramms produziert.
Die Vorteile dieses Ansatzes sind vielversprechend. Ob sich ein Software-System
für eine konkrete Eingabebelegung korrekt verhält, lässt sich durch das einfache
3.6 Dokumentation 147
I Implementierung
is_prime.c
int is_prime(unsigned int nr) 1
{ 2
int i; 3
4
for (i=2; i<nr; i++) { 5
if (nr % i == 0) { 6
return 0; 7
} 8
} 9
10
return 1; 11
} 12
is_prime.ml
val divides = Define 1
‘divides a b = 2
?x. b = a * x‘; 3
val prime = Define 4
‘prime p = 5
~(p=1) /\ 6
!x. x divides p ==> 7
(x=1) \/ (x=p)‘; 8
|- !x. 9
(prime(x) ==> (is_prime(x)=1) /\ 10
!prime(x) ==> (is_prime(x)=0)) 11
12
Abb. 3.48 Formale Spezifikation eines C-Programms mit Hilfe höherwertiger Logik
Optimierung
Erste
Imp Spec Imp
Optimierung
Zweite
Imp Spec Imp
Optimierung
Dritte
Imp Spec Spec Imp
mentellen Optimierung, die in Abb. 3.49 dem leider immer noch vorherrschenden
Big-Bang-Ansatz gegenübergestellt ist.
Dem Gesagten zum Trotz kann eine Referenzimplementierung in den wenigsten
Fällen eine herkömmliche Spezifikation komplett ersetzen. Mit Hilfe der ausführba-
ren Spezifikation kann zwar für jede Eingabe zweifelsfrei ermittelt werden, wie die
korrekte Ausgabe lautet, nicht aber warum. Auch wenn die Referenzimplementie-
rung als korrekt definiert wird, entspricht deren Ausgabe nicht immer der Vorstel-
lung ihres Entwicklers. Damit unterliegt auch diese Technik deutlichen Grenzen.
Viele Hersteller spezifizieren ihre Produkte bewusst auf mehreren Ebenen, um
die jeweiligen Vorteile der verschiedenen Spezifikationstechniken zu nutzen. So
sind z. B. für die Programmiersprache Java unzählige umgangssprachliche Spezi-
fikationen in Form von Lehrbüchern auf dem Markt. Diese belassen zwar viele
Aspekte im Ungefähren, zum Lernen der Sprache sind sie jedoch mit Abstand die
am besten geeignete Variante. Daneben bietet Sun Microsystems eine Sprachdefini-
tion an, die das Verhalten der Sprache im Detail festlegt, zum Lernen der Sprache
aber gänzlich ungeeignet ist. Als dritte Säule steht eine Referenzimplementierung
für die virtuelle Maschine zur Verfügung, mit deren Hilfe verbleibende Interpretati-
onslücken geschlossen werden können.
An diesem Beispiel wird eine inhärente Eigenschaft deutlich, die alle vorgestell-
ten Spezifikationstechniken miteinander verbindet. Stellen wir die beiden Parameter
Verständlichkeit und Eindeutigkeit gegenüber, so entsteht der in Abb. 3.50 skizzierte
Zusammenhang. Wie die Grafik zeigt, bedingt die Verbesserung des einen Parame-
ters die Verschlechterung des anderen. Mit anderen Worten: Die Verständlichkeit
und die Eindeutigkeit einer Spezifikation sind negativ korreliert. Bei der richtigen
Wahl der Spezifikationssprache ist die Dualität dieser beiden Kriterien in jedem Fall
zu berücksichtigen.
3.6 Dokumentation 149
Informelle
Spezikation
Verständlichkeit
Referenz-
implementierung
Semi-formale
Spezikation
Formale
Spezikation
Eindeutigkeit
3.6.2 Implementierungsdokumente
Ist die Spezifikation die Beschreibung dessen, was ein Programm später zu leisten
hat, so ist die Implementierung gewissermaßen der Weg zum Ziel. In diesem Sin-
ne beschreibt die Implementierungsdokumentation, wie die Spezifikation umgesetzt
wurde. Neben externen Dokumenten gehört zu dieser vor allem auch die Code-
Dokumentation, die in Form von Kommentaren in den Quelltext integriert wird.
In der Praxis wird die Code-Dokumentation nicht selten als redundant an-
gesehen, da das Verhalten eines Programms durch die Quelltexte exakt defi-
niert ist. Dass selbst einfache Programme die weit verbreitete “Our code is our
documentation”-Philosophie ad absurdum führen können, demonstriert die C-
Funktion in Abb. 3.51 auf eindrucksvolle Weise. Um den Nutzen einer sinnvoll
verfassten Code-Dokumentation zu untermauern, wurde das Programm absichtlich
mit einem Fehler versehen. Versetzen Sie sich für den Moment in die Lage eines
Software-Entwicklers, dessen Aufgabe es ist, den Fehler aufzuspüren und zu be-
heben. Ihr fiktiver Vorgänger arbeitet mittlerweile bei der Konkurrenz und hat den
Quellcode aus Zeitgründen ohne jegliche Dokumentation hinterlassen.
Vielleicht haben Sie Glück und treffen im Rest des Quellcodes auf einige Hin-
weise, die auf das angedachte Verhalten zurückschließen lassen. Vielleicht finden
Sie innerhalb des Unternehmens auch den einen oder anderen Mitarbeiter, der sich
noch grob an die Funktion entsinnt. Vielleicht suchen Sie aber auch mehrere Tage
intensiv nach der Fehlerstelle und strecken irgendwann frustriert die Waffen.
Dieses Szenario ist keinesfalls aus der Luft gegriffen. Insbesondere Firmen, die
langlebige Software-Produkte entwickeln, stehen vor dem Problem, Teile ihrer eige-
nen Code-Basis nicht mehr zu verstehen. Mit Methoden des Back engineerings muss
die Bedeutung der hauseigenen Programmquellen aufwendig rekonstruiert werden.
Technisch gesehen spielt es dann keine Rolle mehr, ob der untersuchte Code von der
Konkurrenz oder von ehemaligen Programmierern stammt, die schon längst nicht
150 3 Konstruktive Qualitätssicherung
ack1.c
int ack(int n, int m) 1
{ 2
while (n != 0) { 3
if (m == 0) { 4
m = 1; 5
} else { 6
m = ack(m, n-1); 7
} 8
n--; 9
} 10
return m+1; 11
} 12
Abb. 3.51 Was berechnet diese Funktion und wo steckt der Fehler?
mehr auf der Gehaltsliste des Unternehmens stehen. Die grundlegende Frage, ob
Quellcode überhaupt dokumentiert werden soll, ist damit eine rein rhetorische.
Im Gegensatz hierzu werden die Fragen nach dem optimalen Umfang und der
geeigneten Form der Code-Dokumentation seit jeher konträr diskutiert. So sehen ei-
nige Kritiker in der übermäßigen Verwendung von Kommentaren schlicht den Ver-
such, die Symptome intransparenter Programme zu kaschieren anstatt eine Neuim-
plementierung in Erwägung zu ziehen. Linus Torvalds beschreibt die Situation im
Linux Kernel Coding Style wie folgt:
ack2.c
/* 1
Funktion: int ack (int n, int m) 2
3
Autor: John Doe 4
Date: 01/01/2007 5
6
Revision History: 7
12/15/2006: While-Iteration hinzugefügt. 8
12/01/2006: Funktionsname geändert. 9
*/ 10
11
12
int ack1(int n, int m) 13
{ 14
15
/* Solange die erste Variable nicht null ist... */ 16
while (n != 0) { 17
18
/* Teste zweite Variable auf null */ 19
if (m == 0) { 20
m = 1; 21
} else { 22
/* Hier erfolgt der rekursive Aufruf */ 23
m = ack1(m, n-1); 24
} 25
n--; 26
} 27
/* Liefere Ergebniswert zurück */ 28
return m+1; 29
} 30
Abb. 3.52 Dokumentation aus Selbstzweck fördert den Fehler nicht zum Vorschein
ack3.c
/* Funktion: int ack (int n, int m) 1
Autor: John Doe 2
3
Beschreibung: Berechnet die Ackermann-Funktion 4
Die Ackermann Funktion ist ein Beispiel für 5
eine Funktion, die "berechenbar", aber nicht 6
"primitiv rekursiv" ist. Sie spielt in der 7
theoretischen Informatik eine wichtige 8
Rolle. Die Funktion ist wie folgt definiert: 9
10
(1) ack(0,m) = m+1 [m >= 0] 11
(2) ack(n,0) = ack(n-1,1) [n > 0] 12
(3) ack(n,m) = ack(n-1,ack(n,m-1)) [m,n > 0] 13
14
Hinweis: Die Ackermann-Funktion wächst stärker als 15
z. B. die Exponentialfunktion. Bereits für 16
kleine Eingaben ist das Ergebnis nicht mehr 17
darstellbar. Beispiel: 18
19
ack(3,1) = 13, 20
ack(4,1) = 65533, 21
ack(5,1) = ca. 20.000-stellige-Zahl */ 22
23
int ack(int n, int m) 24
{ 25
while (n != 0) { 26
if (m == 0) { 27
/* Fall (2) */ 28
m = 1; 29
} else { 30
/* Fall (3) */ 31
m = ack(m, n-1); 32
} 33
n--; 34
} 35
/* Fall (1) */ 36
return m+1; 37
} 38
Kommentare,
Quelltexte
Metadaten
Quelltexte
Quell- Extraktor
HTML- PDF-
Quelltexte
texte
Quelltexte Formatierer Formatierer
Compiler
HTML- PDF-
Applikation
Dokumentation Dokumentation
haben, versuchen Sie es erneut. Ich bin sicher, jetzt werden Sie innerhalb kurzer Zeit
fündig.2
Wägen wir die Zeit, die zum Hinschreiben des Kommentars benötigt wird, ge-
gen den Aufwand ab, den die Fehlersuche in der undokumentierten Variante aus
Abb. 3.51 oder der falsch dokumentierten Variante aus Abb. 3.52 verursacht, wird
der Nutzen einer sinnvoll verfassten Dokumentation unmittelbar einsichtig.
[Link] Dokumentationsextraktion
Häufig entsteht der Wunsch, Quelltextkommentare in externe Dokumente einfließen
zu lassen. Entsprechend gibt es heute für nahezu alle Programmiersprachen leis-
tungsfähige Werkzeuge zur Dokumentationsextraktion. Hierzu werden die Quell-
texte automatisch nach Kommentaren durchsucht und beispielsweise als PDF-
Dokument oder in Form verlinkter HTML-Dokumente aufbereitet (vgl. Abb. 3.54).
Die heute verfügbaren Werkzeuge sind in ihrer Bedienung so einfach und in ih-
rer Leistungsfähigkeit so fortgeschritten, dass sich eine Verwendung geradezu auf-
zwingt. Die Aufgabe des Programmierers erstreckt sich an dieser Stelle lediglich
auf das Einfügen spezieller Kommentarschlüssel (tags), die das Extraktionswerk-
zeug mit den notwendigen Metainformationen versorgen.
Typische Werkzeuge zur Dokumentenextraktion sind JavaDoc von Sun Micro-
systems und das .NET-Äquivalent XmlDoc von Microsoft. JavaDoc ist fester Be-
standteil des Java 2 Software Development Kits (J2SDK) und das mit Abstand am
häufigsten verwendete Extraktionswerkzeug im Java-Umfeld. Eine gute Übersicht
2 Ein Blick auf die Definition der Ackermann-Funktion zeigt, dass der rekursive Aufruf
ack(m, n-1) falsch ist und in Wirklichkeit ack(n, m-1) lauten muss.
154 3 Konstruktive Qualitätssicherung
I .NET
Tag Bedeutung
<example> Anwendungsbeispiel
<exception> Ausgelöste Ausnahme
<include> Einfügen externer Dokumentation
<param> Name und Funktion eines Parameters
<remarks> Bemerkungen
<returns> Rückgabewert
<see> Verweis
<summary> Kurzbeschreibung
<value> Name und Bedeutung eines Attributs
über dessen Leistungsfähigkeit eröffnet ein Blick auf die Java-Online-Hilfe, die
komplett mit diesem Werkzeug erstellt wurde.
Innerhalb des Quelltextes werden JavaDoc-Kommentare mit der Zeichensequenz
/** eingeleitet und mit */ abgeschlossen. Da gewöhnliche, mit der Zeichensequenz
/* eingeleitete Kommentare, von JavaDoc vollständig ignoriert werden, ist der Pro-
grammierer in der Lage, den zu extrahierenden Dokumentationsanteil nach Belie-
ben zu kontrollieren. Innerhalb eines Kommentarblocks können verschiedene Tags
spezifiziert werden, die in JavaDoc allesamt mit dem At-Symbol (@) beginnen (vgl.
Tabelle 3.7 oben).
Microsoft hat im Zuge der .NET-Einführung ein eigenes Dokumentationsformat
definiert, das auf der XML-Syntax basiert (vgl. Tabelle 3.7 unten). Die speziellen
Kommentare werden in C# durch /// eingeleitet und dadurch von den Standard-
kommentaren unterschieden. Diese beginnen nach wie vor mit zwei Schrägstrichen.
Die unterschiedlichen Erscheinungsbilder der beiden Dokumentationsformate sind
in Abb. 3.55 gegenübergestellt.
Die Erfahrung zeigt, dass die Software-Dokumentation eine Zukunftsinvestition
darstellt und auf lange Sicht zu einer Zeit- und Kostenreduktion führt. Wie uns das
Beispiel der Ackermann-Funktion deutlich vor Augen geführt hat, ist es noch wich-
tiger, das Richtige in der richtigen Form zu dokumentieren. Eine einfache Antwort
3.6 Dokumentation 155
I JavaDoc
/** 1
Bestimmt die Position eines Strings in einer Liste. 2
3
Mit Hilfe dieser Funktion wird die Position eines 4
Strings in der angegebenen String-Liste bestimmt. 5
Die Funktion setzt voraus, dass die String-Liste 6
sortiert ist. Für die Positionsbestimmung in einer 7
unsortierten Liste steht die Funktion 8
unsortedIndexOf zur Verfügung. 9
10
@param s Zu suchender String 11
@param sl Sortierte String-Liste 12
@param idx In diesem Objekt wird der Index 13
gespeichert, falls der String gefunden 14
wurde. 15
@retval TRUE wenn der String gefunden wurde. 16
@retval FALSE wenn der String nicht gefunden wurde. 17
@see indexOf 18
@see sort 19
@see sorted 20
*/ 21
I .NET
/// <remarks> 1
/// Mit Hilfe dieser Funktion wird die Position eines 2
/// Strings in der angegebenen String-Liste bestimmt. 3
/// Die Funktion setzt voraus, dass die String-Liste 4
/// sortiert ist. Für die Positionsbestimmung in einer 5
/// unsortierten Liste steht die Funktion 6
/// unsortedIndexOf zur Verfügung. 7
/// </remarks> 8
/// <seealso cref="indexOf"/> 9
/// <seealso cref="sort"/> 10
/// <seealso cref="sorted"/> 11
/// <param name="s">Zu suchender String</param> 12
/// <param name="sl">Sortierte String-Liste</param> 13
/// <param name="idx">In diesem Objekt wird der Index 14
/// gespeichert, falls der String gefunden wurde. 15
/// </param> 16
/// <retval name="TRUE"> wenn der String gefunden wurde. 17
/// </retval> 18
/// <retval name="FALSE"> wenn der String nicht gefunden 19
/// wurde.</retval> 20
/// <summary>Bestimmt die Postion eines Strings in einer 21
/// String-Liste.</summary> 22
auf die Frage, wie die optimale Dokumentation auszusehen hat, existiert an dieser
Stelle nicht. Einige Lehrbücher versuchen, den Software-Entwicklern mit Inhalts-
vorgaben, Formatvorlagen und Checklisten unter die Arme zu greifen und auf diese
Weise gleichzeitig zu einer Vereinheitlichung des Erscheinungsbilds beizutragen.
Die Praxiserfahrung zeigt jedoch, dass eine derartige Mechanisierung die Anferti-
gung von Dokumentation häufig zum Selbstzweck degradiert.
Auf der anderen Seite ist die hohe Rendite, die durch eine gute Code-
Dokumentation erzielt werden kann, mit kaum einer anderen Technik der konstruk-
tiven Qualitätssicherung zu erreichen. Leider, und das ist die Crux bei der Geschich-
te, wird die Rendite erst nach mehreren Jahren fällig, so dass es auch weiterhin durch
die Flure so mancher großer und kleiner quartalsgetriebener IT-Unternehmen hallen
wird: “Documentation? We don’t have time for that.”
Kapitel 4
Software-Test
4.1 Motivation
Der Software-Test ist die mit Abstand am häufigsten eingesetzte Technik der analy-
tischen Qualitätssicherung und bestimmt wie keine andere die tägliche Arbeit eines
jeden Software-Entwicklers. Im direkten Vergleich mit den anderen Methoden der
Software-Qualitätssicherung entstammt der Software-Test dabei einer erstaunlich
simplen Idee: Das zu untersuchende Programm wird mit Hilfe eines Satzes konkre-
ter Eingabedaten ausgeführt und das gemessene Ist-Ergebnis anschließend mit dem
vorher ermittelten Soll-Ergebnis abgeglichen.
Nichtsdestotrotz präsentieren sich die Ansichten darüber, was der Begriff des
Software-Tests im Einzelnen bedeutet, in der Praxis als unerwartet weit gefächert.
So definiert das IEEE Standard Glossary of Software Engineering Terminology den
Begriff Software-Test beispielsweise wie folgt:
“Test: (1) An activity in which a system or component is executed under
specified coditions, the results are observed or recorded, and an evaluation
is made of some aspect of the system or component.”
IEEE Standard Glossary of Software Engineering Terminology [137]
Diese Definition stellt klar den handwerklichen Aspekt des Software-Tests in den
Vordergrund, schließlich beschreibt der Ausdruck „specified conditions“ an dieser
Stelle nichts anderes als einen konkreten Testfall, mit dem das untersuchte Pro-
gramm ausgeführt wird. Andere Definitionen fassen den Begriff des Software-Tests
deutlich weiter. So abstrahieren Craig und Jaskiel vollständig von dem handwerkli-
chen Charakter und siedeln den Begriff auf der Prozessebene an:
Prüfebene
Abnahmeebene
Systemebene
Integrationsebene
Unit-Ebene
Testfall
Funktional Black-box
Operational White-box
Temporal Gray-box
Prüfkriterium Prüfmethodik
Der hohe Stellenwert, den diese Definition dem Software-Test mit Recht zubilligt,
wird insbesondere durch den neu eingeführten Begriff der Testware unterstrichen.
Testumgebung und Testfälle werden zu einem festen Bestandteil der entwickelten
Software und stehen auf Augenhöhe mit den später an den Kunden ausgeliefer-
ten Komponenten. Die realen Gegebenheiten in großen Software-Projekten unter-
streichen diese Sichtweise. So übersteigt in vielen Projekten das Code-Volumen der
Testware, d. h. derjenige Anteil, der ausschließlich dem Zweck des Software-Tests
dient, das Code-Volumen der ausgelieferten Produktkomponenten bei weitem.
4.2 Testklassifikation
Als allgegenwärtiges Instrument der Software-Qualitätssicherung werden Software-
Tests in allen Phasen des Entwicklungsprozesses durchgeführt. Die einzelnen Tests
lassen sich bezüglich der folgenden Merkmale in verschiedene Klassen einteilen
(vgl. Abb. 4.1):
In den nächsten Abschnitten werden alle drei Merkmalsräume einer genaueren Be-
trachtung unterzogen.
4.2 Testklassifikation 159
Programmstruktur
Programmorientiert Funktionsorientiert
Modul-
Unit-Test
ebene
t
Entwicklungsphase
4.2.1 Prüfebenen
Der Einteilung in Prüfebenen liegt zum einen die Programmstruktur des untersuch-
ten Software-Systems und zum anderen die zeitliche Entwicklungsphase, in der ein
Test durchgeführt wird, zugrunde. Was genau mit Hilfe eines einzelnen Testfalls
abgeprüft wird, spielt in dieser Betrachtung nur eine untergeordnete Rolle. Jeder
Testfall lässt sich einer der folgenden vier Klassen zuordnen:
I Unit-Tests
I Integrationstests
I Systemtests
I Abnahmetests
Abb. 4.2 stellt die vier verschiedenen Teststufen grafisch gegenüber.
[Link] Unit-Tests
Mit dem Begriff Unit wird eine atomare Programmeinheit bezeichnet, die groß ge-
nug ist, um als solche eigenständig getestet zu werden. Diese recht vage formu-
lierte Begriffsdefinition lässt einen erheblichen Spielraum zu und in der Praxis un-
terscheiden sich zwei atomare Programmeinheiten oft beträchtlich in ihrer Größe.
Hier können sich Units von einzelnen Funktionen, über Klassen, bis hin zu Klassen-
oder Modulverbünden in Form von Paketen und Bibliotheken erstrecken. Aus die-
sem Grund wird der Unit-Test nicht selten auch als Modultest oder noch allgemei-
ner als Komponententest bezeichnet. Wird die Methodik des Unit-Tests auf größere
Klassen- oder Modulverbünde angewendet, so geht der Testprozess fast nahtlos in
den Integrationstest über (Abschnitt [Link]). Die klare Trennlinie, die in der Litera-
tur zwischen den beiden Testarten gezogen wird, ist in der Praxis an vielen Stellen
durchbrochen.
Um den abstrakten Begriff des Unit-Tests mit Leben zu füllen, betrachten wir
die in Abb. 4.3 abgedruckte Implementierung der Methode binarySearch von Sun
160 4 Software-Test
[Link] (Auszug)
/** 1
* Searches the specified array of ints for the specified 2
* value using the binary search algorithm. The array 3
* <strong>must</strong> be sorted (as by the <tt>sort</tt> 4
* method, above) prior to making this call. If it is not 5
* sorted, the results are undefined. If the array contains 6
* multiple elements with the specified value, there is no 7
* guarantee which one will be found. 8
* 9
* @param a the array to be searched. 10
* @param key the value to be searched for. 11
* @return index of the search key, if it is contained in the 12
* list; otherwise, <tt>(-(<i>insertion point</i>) - 1) 13
* </tt>. The <i>insertion point</i> is defined as the 14
* point at which the key would be inserted into the list: 15
* the index of the first element greater than the key, or 16
* <tt>[Link]()</tt>, if all elements in the list are 17
* less than the specified key. Note that this guarantees 18
* that the return value will be >= 0 if and only if 19
* the key is found. 20
* @see #sort(int[]) 21
*/ 22
23
public static int binarySearch(int[] a, int key) { 24
int low = 0; 25
int high = [Link]-1; 26
27
while (low <= high) { 28
int mid = (low + high) >> 1; 29
int midVal = a[mid]; 30
31
if (midVal < key) 32
low = mid + 1; 33
else if (midVal > key) 34
high = mid - 1; 35
else 36
return mid; // key found 37
} 38
return -(low + 1); // key not found. 39
} 40
Microsystems. Als Methode der Klasse ArrayUtils ist sie offizieller Bestandteil
des Collection frameworks der Java-2-Plattform [107, 277, 188].
Die Methode binarySearch wird mit einem geordneten Array a sowie einem
Schlüssel key aufgerufen und überprüft, ob das Element key in a enthalten ist. Wur-
de der Schlüssel erfolgreich lokalisiert, wird dessen Position als Ergebnis zurückge-
geben. Andernfalls liefert die Funktion einen negativen Wert zurück und der Betrag
4.2 Testklassifikation 161
binarySearch(..., 80)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
low mid high
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
Symmetrische Grenzen
des Rückgabewerts entspricht der Position, an der das Element eingefügt werden
müsste, um die Ordnung des Arrays nicht zu zerstören. Auf diese Weise lässt sich
die Methode nicht nur für die schnelle Suche eines Elements, sondern auch für das
sortierte Einfügen weiterer Elemente verwenden.
Wie der Name der Methode unverwechselbar andeutet, macht sie sich die Tech-
nik der binären Suche zunutze – einem klassischen Suchalgorithmus auf geordneten
Mengen [228, 229]. Die Idee der binären Suche besteht darin, die Array-Elemente
als geordnetes Intervall aufzufassen, dessen linke bzw. rechte Grenze durch die Va-
riable low bzw. high repräsentiert wird. Das Intervall wird nun in der Mitte geteilt
und anhand des mittleren Elements mid geprüft, ob sich der gesuchte Wert in der
linken oder der rechten Intervallhälfte befindet. Anschließend wird die Suche auf
einem der Teilintervalle rekursiv wiederholt. Die Suche bricht ab, sobald das Ele-
ment an der Stelle mid gefunden wurde oder das Suchintervall keine Elemente mehr
enthält. Die binäre Suche ist ein Spezialfall des mathematischen Prinzips der Inter-
vallschachtelung und wird in der Literatur dementsprechend auch als Intervallsuche
bezeichnet.
Abb. 4.4 demonstriert die einzelnen Iterationen der binären Suche anhand ei-
nes konkreten Beispiels. Das gesuchte Element ist in dem gegebenen Array nicht
vorhanden und der Algorithmus terminiert erst, sobald das Suchintervall zur leeren
Länge degradiert. In unserem Beispiel enthält das Intervall in der vierten Iteration
keinerlei Elemente mehr.
Die binäre Suche zeichnet sich vor allem durch ihre positive Laufzeiteigenschaft
aus, da das Suchintervall aufgrund der sukzessiven Halbierung mit exponentieller
Geschwindigkeit schrumpft. Hierdurch terminiert die binäre Suche spätestens nach
logarithmisch vielen Iterationen und ist der linearen Suche damit deutlich überlegen.
162 4 Software-Test
[Link]
import [Link].*; 1
2
public class BinarySearchUnitTest { 3
4
public static void main (String args[]) { 5
6
boolean success = true; 7
int[] a = { 10, 12, 22, 28, 30, 51, 52, 60, 8
62, 81, 82, 89, 90, 92, 93, 99 }; 9
10
// Test 1: Element ’89’ is at position 11 11
success &= ([Link](a, 89) == 11); 12
13
// Test 2: Element ’99’ is at position 15 14
success &= ([Link](a, 99) == 15); 15
16
// Test 3: Element ’100’ must not be found! 17
success &= ([Link](a, 100) < 0); 18
19
if (success) 20
[Link]("Success"); 21
else 22
[Link]("Failure"); 23
} 24
} 25
Abb. 4.5 Ein einfacher, wenn auch ungeschickter Unit-Test für die Funktion binarySearch
Ein typischer Unit-Test für die Methode binarySearch ist in Abb. 4.5 darge-
stellt. Die Testroutine erzeugt zunächst ein Integer-Array mit 16 Elementen und
führt im Anschluss daran mehrmals eine binäre Suche mit unterschiedlichen Schlüs-
seln durch. Der Erfolg oder Misserfolg der Testfalldurchführung wird in der Varia-
blen success festgehalten.
Die gezeigte Implementierung ist aus mehreren Gründen nicht optimal. So wirkt
die Testroutine wie mit der heißen Nadel gestrickt – sie folgt weder einem standar-
disierten Testschema noch ist das Ergebnis für den Software-Entwickler besonders
informativ. Insbesondere im Fehlerfall liefert der Testtreiber keinerlei verwertbare
Informationen für die Fehlersuche. Der Entwickler muss hier selbst in mühevoller
Kleinarbeit bestimmen, welcher der Testfälle mit welchem Ergebnis fehlgeschlagen
ist. In Abschnitt 8.3 werden wir die Problematik aufgreifen und zeigen, wie sich
Unit-Tests mit Hilfe spezieller Frameworks deutlich komfortabler formulieren und
zugleich automatisiert auswerten lassen.
Ein zweiter, nicht minder wichtiger Kritikpunkt betrifft die Auswahl der Testfäl-
le. Neben der verhältnismäßig geringen Anzahl sind sie darüber hinaus so ungünstig
gewählt, dass sich der Kontrollfluss innerhalb der Methode binary_search für je-
den Testfall gleicht. Entsprechend hoch ist die Wahrscheinlichkeit, dass bestimmte
4.2 Testklassifikation 163
[Link] Integrationstest
Nach dem Unit-Test bildet der Integrationstest die nächsthöhere Abstraktionsstu-
fe und wird immer dann eingesetzt, wenn einzelne Programmmodule zu größeren
Software-Komponenten zusammengesetzt werden. Der Integrationstest stellt dabei
sicher, dass die Komposition der separat getesteten Programmkomponenten eben-
falls wieder ein funktionsfähiges System ergibt. In der Praxis kommen die folgenden
Integrationsstrategien zum Einsatz:
I Big-Bang-Integration
Hinter dem Begriff der Big-Bang-Integration verbirgt sich keine Integrationsstra-
tegie im eigentlichen Sinne. Sämtliche Module werden zunächst vollständig ent-
wickelt und anschließend auf einen Schlag integriert. Die Big-Bang-Integration
besitzt zwei gravierende Nachteile.
Zum einen kann mit der Integration erst dann begonnen werden, wenn die
Entwicklung des letzten Teilmoduls erfolgreich abgeschlossen wurde. Fehler,
die nur im Zusammenspiel verschiedener Komponenten zum Vorschein treten,
können hierdurch erst spät im Projektverlauf entdeckt werden. Darüber hinaus
treten bei der Integration nicht selten unvorhergesehene Probleme auf, die sich
nur durch aufwendige Architekturänderungen beseitigen lassen. Durch eine spä-
te Integration werden diese Probleme zu einem unkalkulierbaren Risiko in der
Projektplanung.
Zum anderen erweist sich die Big-Bang-Integration in der Praxis als schwer
handhabbar. Da alle Teilkomponenten gleichzeitig integriert werden, lässt sich
die Ursache eines beobachteten Fehlers nur mühsam lokalisieren. Im Gegensatz
zu den diversen inkrementell ausgelegten Strategien, die in jedem Teilschritt nur
eine begrenzte Anzahl neuer Teilmodule hinzufügen, wird der Debug-Aufwand
dramatisch erhöht.
Den gravierenden Nachteilen zum Trotz gehört die Big-Bang-Integration im-
mer noch zu den in der Praxis am häufigsten eingesetzten Integrationstechniken.
Ein Vorteil der Strategie soll an dieser Stelle jedoch nicht unerwähnt bleiben.
Da die Integration nicht schrittweise erfolgt, sind alle Komponenten zum Zeit-
punkt der Integration bereits vollständig ausprogrammiert. Hierdurch entfällt das
Schreiben von Testtreibern und Platzhaltern, die bei der inkrementellen Integrati-
on als Stellvertreter für noch fehlende Komponenten eingesetzt werden müssen.
I Strukturorientierte Integration
Die Programmmodule werden inkrementell zu einem Gesamtsystem zusammen-
gefügt. Die Reihenfolge, in der die einzelnen Teilkomponenten integriert wer-
164 4 Software-Test
den, richtet sich dabei nach den strukturellen Abhängigkeiten, die zwischen den
einzelnen Modulen auf Architekturebene bestehen. Im Speziellen werden die fol-
genden strukturorientierten Integrationsstrategien unterschieden:
– Bottom-Up-Integration
Die Integration beginnt mit den Basiskomponenten, d. h. mit denjenigen Mo-
dulen, die keine Abhängigkeiten zu anderen Modulen besitzen. Wird ein
Schichtenmodell oder eine Baumbeschreibung der Programmstruktur zugrun-
de gelegt, so bilden die zuerst integrierten Komponenten die unterste Schicht
bzw. die Blätter des Baums. Sind alle Basiskomponenten integriert, so wer-
den die Komponenten der nächsten Schicht bearbeitet. Generell gilt, dass eine
Komponente erst dann integriert wird, wenn alle benutzten Komponenten be-
reits hinzugenommen sind.
Solange das auf diese Weise konstruierte Software-System noch unvoll-
ständig ist, werden die noch nicht integrierten Komponenten durch Testtrei-
ber ersetzt. Ein Testtreiber (driver) ist dabei nichts anderes als ein temporäres
Programmgerüst, das bestimmte Aspekte der später eingesetzten Implemen-
tierung simuliert. Testtreiber sind in aller Regel funktional unvollständig und
damit bewusst so ausgelegt, dass sie nur für ganz bestimmte Testszenarien das
spätere Programmverhalten korrekt simulieren.
– Top-Down-Integration
Die Top-Down-Integration entsteht aus der Bottom-Up-Strategie durch das
einfache Umkehren der Integrationsrichtung. Die Integration beginnt mit den
Modulen der obersten Software-Schicht und wird anschließend mit den Mo-
dulen der weiter unten liegenden Schichten fortgesetzt. Die Basiskomponen-
ten werden zuletzt integriert und damit das Gesamtsystem komplettiert.
Während der Integration werden noch nicht integrierte Komponenten
durch einen Platzhalter ersetzt (stub). Im Gegensatz zu einem Testtreiber, der
die darunter liegenden Software-Komponenten selbst aufruft, bildet ein Stub
die Funktionalität einer Komponente nach. In aller Regel sind Stubs funktio-
nal unvollständig, so dass nur für ganz bestimmte Eingabekombinationen die
richtigen Ergebniswerte zurückgeliefert werden. Im direkten Vergleich mit ei-
nem Testtreiber erweist sich die Programmierung eines Stubs in den meisten
Fällen als deutlich schwieriger, so dass die Top-Down-Integration mehr Ent-
wicklungsaufwand abverlangt als eine vergleichbare Bottom-Up-Integration.
Nicht selten stehen Software-Entwickler der Strategie daher ablehnend gegen-
über. Aus Benutzersicht bringt die Top-Down-Strategie jedoch nicht zu unter-
schätzende Vorteile mit sich, da bereits sehr früh ein prototypisches Modell
der Software entsteht, das sich äußerlich kaum von dem Endprodukt unter-
scheidet.
– Outside-In-Integration
Die Outside-In-Strategie versucht, die Vorteile der Bottom-Up- und der Top-
Down-Strategie miteinander zu kombinieren. Hierzu werden sowohl die Mo-
dule der obersten als auch die der untersten Architekturschicht zuerst inte-
4.2 Testklassifikation 165
Bottom-up-Integration Top-down-Integration
Modul 0 Modul 0
Integrationsrichtung
Integrationsrichtung
Modul 1 Modul 1
Modul 2 Modul 2
Outside-in-Integration Inside-out-Integration
Modul 0 Modul 0
Integrationsrichtung
Integrationsrichtung
Modul 1 Modul 1
Modul 2 Modul 2
– Inside-Out-Integration
Die Integration beginnt in der Mitte der Komponentenhierarchie und wird so-
wohl nach unten als auch nach oben fortgesetzt. Die Strategie wird in der
Praxis nur selten eingesetzt, da sie kaum zusätzliche Vorteile bietet, auf der
anderen Seite aber die Nachteile der Bottom-Up- und der Top-Down-Strategie
in sich vereint.
I Funktionsorientierte Integration
Genau wie im Falle der strukturorientierten Integration werden die Teilkompo-
nenten hier inkrementell zu einem Gesamtsystem zusammengefügt. Dagegen
wird die Auswahlreihenfolge nicht mehr durch die Programmstruktur, sondern
durch funktionale oder operative Kriterien bestimmt. Die folgenden Integrati-
onsstrategien lassen sich unterscheiden:
[Link] Systemtest
Mit dem Systemtest wird begonnen, sobald alle Teilkomponenten eines Software-
Systems erfolgreich integriert sind. Im Gegensatz zum Unit- bzw. Integrationstest
wird das System als Ganzes getestet und auf die Einhaltung der im Pflichtenheft
spezifizierten Eigenschaften überprüft.
Auf den ersten Blick scheint der Integrationstest nahtlos in den Systemtest über-
zugehen, schließlich bewegt sich das getestete System mit jeder integrierten Kom-
ponente einen Schritt näher auf das Gesamtsystem zu. In der Tat unterscheiden sich
beide jedoch in einem zentralen Punkt, der zugleich einen klaren Schnitt zwischen
4.2 Testklassifikation 167
I Unvorhergesehene Fehler
Erfahrungsgemäß treten viele Fehler erst im Zusammenspiel aller Komponenten
auf und bleiben damit dem Unit-Test und in weiten Teilen auch dem Integrati-
onstest verborgen.
I Unklare Anforderungen
In der Praxis sind die Anforderungsbeschreibungen oft nur vage formuliert oder
gar nicht vorhanden. Je größer der Interpretationsspielraum, desto schwieriger
wird es, das korrekte Verhalten eines Software-Systems zweifelsfrei festzustel-
len.
I Testumgebung
Systemtests werden vorzugsweise in einer Umgebung ausgeführt, die so weit wie
möglich der des Kunden entspricht. Der Aufwand für den Aufbau einer solchen
Umgebung ist insbesondere im Bereich eingebetteter Systeme erheblich.
I Eingeschränkte Debug-Möglichkeiten
In der Release-Version eines Software-Systems sind viele der ehemals vorhande-
nen Debug-Möglichkeiten entweder deaktiviert oder nicht mehr vorhanden. Tritt
ein Fehler auf, so ist die Ursachenanalyse nur noch in sehr begrenztem Umfang
möglich.
I Eingeschränkte Handlungsfähigkeit
In den späten Entwicklungsphasen sind die Möglichkeiten zur Fehlerbehebung
ebenfalls stark eingeschränkt. Ist der Systemtest bereits angelaufen, verbieten
sich beispielsweise größere Architekturänderungen aus Gründen der Risikoab-
schätzung von selbst. Bildlich gesprochen können Fehler ab jetzt nur noch mit
mikrochirurgischen Eingriffen korrigiert werden, um das Risiko einer weitrei-
chenden Fehlereinpflanzung klein zu halten.
In der Praxis wird der Systemtest oft in mehreren Phasen durchgeführt. Insbesonde-
re im Bereich sicherheitskritischer Systeme erfolgt der Test inkrementell. In jeder
Phase wird sowohl der Testaufwand als auch das eingegangene Risiko schrittweise
erhöht.
168 4 Software-Test
Systemtest
Abnahmetest
Abb. 4.7 Typische Phasen des System- und Abnahmetests eines Kfz-Steuergeräts
Als Beispiel sind in Abb. 4.7 die typischen Phasen dargestellt, die im Rahmen des
Systemtests eines Kfz-Steuergeräts nacheinander durchlaufen werden. Bevor das
Steuergerät unter realen Bedingungen in einem Kraftfahrzeug getestet wird, werden
zunächst mehrere Labortests durchgeführt. Verhalten sich alle Teilkomponenten wie
erwartet, so wird das Gerät in einer Simulationsumgebung in Betrieb genommen,
die sich aus Sicht des Steuergeräts als reale Fahrzeugumgebung präsentiert. Alle
elektronischen Komponenten, mit denen das Steuergerät über die fahrzeuginternen
Bussysteme kommuniziert, werden innerhalb der virtuellen Umgebung in Software
nachgebildet (Restbussimulation).
Sobald alle Labortests bestanden sind, beginnt die Erprobung in einem realen
Fahrzeug – dem sogenannten Testträger. Zunächst erfolgen mehrere Kurzfreiga-
ben, die der Funktionsprüfung des Steuergeräts unter realen Bedingungen dienen.
Je nachdem, wie stark das getestete System die Fahrsicherheit beeinflusst, wird die
Kurzfreigabe entweder auf einer speziell gesicherten Prüfstrecke oder im regulä-
ren Straßenverkehr durchgeführt. Sind auch die Kurzfreigaben ohne Fehler absol-
viert, erfolgt die eigentliche Freigabe in Form einer oder mehrerer längerer Testfahr-
ten. Neben dem reinen Funktionstest kommen in dieser Phase weitere Prüfkriterien
hinzu, wie z. B. das Verhalten unter extremen Witterungsbedingungen (Eis, Schnee
etc.), besonderen Straßenverhältnissen oder bei Dauerbelastung.
[Link] Abnahmetest
Genau wie der Systemtest verfolgt auch der Abnahmetest das Ziel, die Leistungspa-
rameter des erstellten Software-Systems mit den Vorgaben des Pflichtenhefts abzu-
gleichen. Trotzdem unterscheiden sich die beiden Teststufen in zwei wesentlichen
Aspekten:
I Liegt der Systemtest noch vollständig im Verantwortungsbereich des Herstellers,
so wird der Abnahmetest unter Federführung des Auftraggebers durchgeführt.
In einigen Fällen wird die Abnahme sogar vollständig durch den Kunden selbst
abgewickelt.
4.2 Testklassifikation 169
I Der Abnahmetest findet in der realen Einsatzumgebung des Kunden statt. Wenn
nicht bereits im Systemtest geschehen, werden die Programmläufe spätestens
jetzt mit authentischen Daten des Auftraggebers durchgeführt.
Der Abnahmetest besitzt neben der fachlichen Bedeutung auch eine nicht zu unter-
schätzende juristische Relevanz, schließlich entscheidet dessen Ausgang am Ende
eines Projekts über den Grad der Vertragserfüllung. Entsprechend sorgfältig muss
bei der Erstellung der Abnahmekriterien, der Testdurchführung sowie der Doku-
mentation und Auswertung der Ergebnisse vorgegangen werden.
Die Frage nach dem optimalen Umfang und den zu prüfenden Inhalten werden
aus verschiedenen Perspektiven traditionell unterschiedlich beantwortet. Insbeson-
dere dann, wenn es nur einen einzigen Auftraggeber gibt, tendieren Hersteller zu
kurzen Abnahmetests, schließlich ist ein schlechter Ausgang derselben mit zusätz-
lichem Aufwand und Kosten verbunden. Im Gegensatz hierzu ist der Auftraggeber
aus verständlichen Gründen an möglichst umfangreichen Abnahmetests interessiert.
In vielen Fällen wird die Kundenseite bereits in den späten Phasen des System-
tests eingebunden, so dass die Grenze zwischen System- und Abnahmetest an dieser
Stelle verwischt. So ist es beispielsweise nicht ungewöhnlich, dass der Auftragge-
ber beim Systemtest eines Kfz-Steuergeräts bereits an Freigabefahrten teilnimmt,
die formal dem Systemtest zugeordnet werden. Die eigentliche Abnahme erfolgt am
Ende des Projekts im Rahmen eines Dauertests. Hierzu wird eine komplette Fahr-
zeugflotte mit dem entwickelten Steuergerät ausgestattet und die Funktionstüchtig-
keit in zahlreichen simultan durchgeführten Dauerfahrten unter Beweis gestellt (vgl.
Abb. 4.7).
Die frühzeitige Einbeziehung des Kunden bietet mehrere Vorteile. Zum einen
ist es auf diese Weise leichter, das Gesamtsystem unter den realen Gegebenheiten
und Anforderungen des Auftraggebers zu testen. Zum anderen kann eine Teilab-
nahme des Produkts bereits im Zuge des Systemtests geschehen und die Anzahl
der später zu wiederholenden Abnahmetests damit effektiv verringert werden. Auf
der negativen Seite erhöht eine zu frühe Einbeziehung des Kunden das Risiko des
unfreiwilligen Technologietransfers.
Im Falle eines Software-Produkts, das den Massenmarkt bedient, stellt sich die
Situation wiederum völlig anders dar. Hier entfällt ein individueller Abnahmetest
schon aufgrund der Anonymität des Kunden. An die Stelle des Abnahmetests tritt
ein spezieller Feldtest, der sich über eine oder mehrere Alpha- und Beta-Phasen
erstreckt.
I Alpha-Tests
Alpha-Tests finden in der Anwendungsumgebung des Herstellers statt. Das Sys-
tem wird durch repräsentativ ausgesuchte Anwender erprobt und die Ergebnis-
se an die Entwickler zurückgeliefert. Neben der Untersuchung der funktionalen
Aspekte des fertigen Produkts können Alpha-Test-ähnliche Szenarien auch in
frühen Projektstadien gewinnbringend eingesetzt werden. Viele Firmen betrei-
ben zur Optimierung der Benutzungsschnittstelle speziell konzipierte Usability-
Labore, in denen die geplanten Schnittstellen frühzeitig von ausgewählten An-
wendern evaluiert werden.
170 4 Software-Test
I Beta-Tests
Im Gegensatz zu Alpha-Tests finden Beta-Tests in der Umgebung des Kunden
statt. Da das Produkt ab jetzt in seiner authentischen Zielumgebung betrieben
wird, erweist sich die Fehlersuche als deutlich schwieriger. Zum einen ist der
Kunde beim Testen der Software im Wesentlichen auf sich alleine gestellt, zum
anderen fehlen die herstellerseitig vorhandenen Debug-Möglichkeiten.
In großen Software-Projekten wird der Beta-Test häufig inkrementell durch-
geführt und in jeder Phase die Anzahl der beteiligten Tester erweitert. Gut be-
obachten ließ sich das Vorgehen unter anderem in der Beta-Phase von Windows
Vista im Jahre 2006. So war die Beta-1 des Produkts zunächst ausschließlich
für Mitglieder des MSDN-Entwicklerprogramms, Technet-Mitglieder und spezi-
ell registrierte Benutzer zugänglich. In einer späteren Entwicklungsphase wurde
das Beta-Programm dann auf die breite Öffentlichkeit ausgedehnt.
4.2.2 Prüfkriterien
Der zweite Merkmalsraum zur Klassifizierung von Software-Systemen erstreckt
sich über die inhaltlichen Aspekte eines Testfalls – ganz im Gegensatz zu der bisher
betrachteten Einteilung, der vor allem die zeitliche Abfolge der Testfälle unterein-
ander, wie auch die Abstraktionsebene zugrunde lag, auf der die einzelnen Tests
durchgeführt werden. Inhaltlich lassen sich die im Laufe eines Projekts durchge-
führten Software-Tests auf der obersten Ebene in drei Kategorien einteilen (vgl.
Abb. 4.8).
I Trivialtests
Im Rahmen eines Trivialtests wird ein Programm mit Eingaben aufgerufen, die
4.2 Testklassifikation 171
Software-
Test
besonders einfach strukturiert sind und in der Praxis mit hoher statistischer Wahr-
scheinlichkeit zu Fehlern führen. Beispiele solcher Tests sind die Sortierung ei-
ner leeren Liste, die Drehung eines Objekts um 0 Grad oder die Streckung eines
Vektors um den Faktor 1. Technisch gesehen sind Trivialtests nichts anderes als
spezielle Grenzwerttests, die in Abschnitt 4.3.2 genauer unter die Lupe genom-
men werden.
I Crashtests
Als Crashtests werden spezielle Funktionstests bezeichnet, die versuchen, den
Absturz des untersuchten Software-Systems herbeizuführen. Durch die geziel-
te Suche nach entsprechenden Schwachstellen wird insbesondere die Robustheit
eines Software-Systems deutlich gesteigert. Crashtests sind insbesondere im Be-
reich sicherheitskritischer Systeme von zentraler Bedeutung.
I Kompatibilitätstests
Diese Art von Software-Tests verfolgen das Ziel, verschiedene Verträglichkeits-
aspekte des erstellten Produkts zu überprüfen. So wird mit Hilfe von Kompa-
tibilitätstests unter anderem sichergestellt, dass ein Software-System auch auf
anderen Hardware- oder Softwareplattformen lauffähig ist, sich an vereinbarte
Schnittstellen- und Formatstandards hält und keine negativen Einflüsse auf ande-
re, parallel betriebene Programme ausübt.
I Zufallstests
Im Gegensatz zu allen anderen vorgestellten Techniken wird das untersuchte
Software-System nicht mit gezielt konstruierten Eingabedaten, sondern mit zu-
fällig erzeugten Werten betrieben. Da sich die generierten Testfälle nur selten
172 4 Software-Test
I Ergonomietests
Im Rahmen von Ergonomietests wird die Benutzbarkeit eines Software-Systems
überprüft. Hierzu gehören insbesondere die zahlreichen Aspekte der Benut-
zungsschnittstelle wie z. B. die Struktur der Menüführung, die Gestaltung der
Dialoge oder die Ergonomie der verwendeten GUI-Elemente. Im weiteren Sin-
ne fallen auch Prüfkriterien wie die Verständlichkeit der Installationsprozedur
sowie der Aufbau des Hilfesystems und der Dokumentation in den Bereich der
Software-Ergonomie.
I Sicherheitstests
Die Unbedenklichkeit eines Software-Systems wird mit Hilfe von Sicherheits-
tests nachgewiesen. Hierzu gehören Tests, mit denen die Vertraulichkeit der ge-
speicherten Daten sichergestellt oder das Software-System auf eventuell vorhan-
dene Sicherheitslecks untersucht werden kann (security tests). Im Bereich sicher-
heitskritischer Systeme muss zusätzlich gewährleistet werden, dass im Regelbe-
trieb keine Gefahr für Leib und Leben besteht (safety tests).
I Laufzeittests
Im Rahmen von Laufzeittests werden die vereinbarten Zeitanforderungen mit
konkreten Messungen überprüft (stopwatch test). Laufzeitmessungen erfolgen
in der Praxis auf allen Software-Ebenen. Tests zur Messung der Ausführungs-
geschwindigkeit einzelner Funktionen oder Methoden sind genauso typisch wie
Tests, die sich mit der Zeitmessung vollständiger Geschäftsvorgänge beschäfti-
gen.
I Lasttests
Mit Hilfe von Lasttests wird untersucht, wie sich ein Software-System in den
Grenzbereichen seiner Spezifikation verhält. So muss vor der Eröffnung eines
elektronischen Warenhauses in verschiedenen Testreihen sichergestellt werden,
dass die Server den anfallenden Datenverkehr auch bei hoher Kundenlast noch
in ausreichendem Maße verarbeiten können.
Zur Durchführung eines Lasttests wird zunächst eine geeignete Testumge-
bung aufgesetzt, mit der sich verschiedene Lastprofile reproduzierbar generie-
ren lassen. Anschließend wird anhand verschiedener Lastprofile überprüft, ob
sich das Software-System auch im Grenzlastbereich noch immer im Rahmen
seiner Spezifikation bewegt. Bei komplexen Transaktionssystemen ist der Auf-
bau einer entsprechenden Testumgebung mit einem erheblichen Aufwand ver-
bunden, so dass Lasttests mit zu den kostspieligsten Maßnahmen der Software-
Qualitätssicherung gehören und eine dementsprechend intensive Planung erfor-
dern.
I Stresstests
Operational entspricht der Stresstest dem Lasttest, allerdings wird der spezifi-
zierte Grenzbereich hier bewusst überschritten. Eine entsprechende Last kann
entweder durch eine weitere Steigerung der zu verarbeitenden Daten (Überlast-
test) oder durch die Wegnahme von Ressourcen künstlich erzeugt werden (Man-
geltest). Stresstests verfolgen mehrere Ziele: Zum einen werden die reale Be-
lastungsgrenzen der Software experimentell ausgelotet und zum anderen wird
beobachtet, ob bzw. wie das System nach dem Wegfall der Überlast wieder in
den Normbetrieb zurückfindet (stress recovery).
4.2.3 Prüftechniken
Der dritte Merkmalsraum zur Klassifizierung von Software-Tests erstreckt sich über
die verschiedenen Methoden und Techniken, die zur Konstruktion eines Testfalls
zum Einsatz kommen. Auf der obersten Ebene lassen sich Software-Tests diesbe-
züglich in drei Kategorien einteilen:
I Black-Box-Tests
Für die Konstruktion von Testfällen wird ausschließlich das Ein- und Ausga-
174 4 Software-Test
Testfälle werden aus Testfälle werden aus Testfälle werden aus der
der Anforderungs- und der inneren Anforderungs- und
Schnittstellenbeschreibung Programmstruktur Schnittstellenbeschreibung
hergeleitet. systematisch hergeleitet. unter Kenntnis der inneren
Programmstruktur
hergeleitet.
beverhalten der Software herangezogen (vgl. Abb. 4.9 links). Die innere Code-
Struktur bleibt in diesem Fall gänzlich unberücksichtigt. Zu den gängigen Kon-
struktionsverfahren gehören unter anderem der Äquivalenzklassentest, die Grenz-
wertbetrachtung sowie das paarweise Testen. In Abschnitt 4.3 werden diese
Techniken ausführlich behandeln.
I White-Box-Tests
Im Gegensatz zu Black-Box-Tests basieren White-Box-Tests auf der inneren
Struktur eines Programms (vgl. Abb. 4.9 Mitte). Je nachdem, welche Aspekte des
Programm-Codes zur Testfallkonstruktion herangezogen werden, sprechen wir
von kontrollflussorientierten oder datenflussorientierten White-Box-Tests. Bei-
de werden in Abschnitt 4.4 einer genaueren Betrachtung unterzogen.
I Gray-Box-Tests
Die Testfälle werden nach den Prinzipien des Black-Box-Tests konstruiert. Der
Software-Tester verschafft sich jedoch zunächst einen Überblick über die interne
Programmstruktur und lässt dieses Wissen in die Testfallkonstruktion einfließen
(vgl. Abb. 4.9 rechts). Folgerichtig fallen alle Black-Box-Tests, die der Entwick-
ler eines Moduls selbst erstellt hat, automatisch in diese Kategorie. Die Art und
Weise, wie mit dem Wissen über die Code-Struktur die Testfallauswahl beein-
flusst wird, ist nicht formal festgelegt. Anders als im Fall der White-Box-Tests
werden die Testfälle damit nicht systematisch aus der Programmstruktur herge-
leitet.
Abschließend sind in Tabelle 4.1 die drei vorgestellten Merkmalsräume gegenüber-
gestellt. Die Tabelle enthält für jedes Prüfkriterium eine separate Spalte und für
jede Prüfebene und Testtechnik eine separate Zeile. In jeder Zeile sind diejenigen
4.3 Black-Box-Testtechniken 175
Kompatibilitätstest
Komplexitätstest
Installationstest
Sicherheitstest
Ergonomietest
Funktionstest
Laufzeittest
Zufallstest
Trivialtest
Stresstest
Crashtest
Lasttest
Unit-Ebene
Integrationsebene
Systemebene
Abnahmeebene
Black-Box-Technik
White-Box-Technik
Prüfkriterien mit einem Kreuz markiert, die typischerweise auf der entsprechenden
Prüfebene oder mit der entsprechenden Testtechnik durchgeführt werden. In der
Praxis sind die vorgefundenen Korrelationen zwischen den einzelnen Merkmals-
räumen nicht immer so stark ausgeprägt, wie die Tabelle auf den ersten Blick sug-
gerieren mag. So spricht in speziellen Anwendungsszenarien beispielsweise nichts
dagegen, einen Laufzeittest auf der Basis einer White-Box-Analyse zu konstruieren,
auch wenn dieses Vorgehen für die Mehrheit dieser Testfälle nicht üblich ist.
Nachdem in diesem Abschnitt ein wenig Ordnung in die facettenreiche Land-
schaft des Software-Tests gebracht werden konnte, wenden wir uns nun ausführ-
lich den verschiedenen Verfahren und Techniken zur Testfallkonstruktion zu. In
Abschnitt 4.3 werden wir zunächst einen Blick auf die gebräuchlichen Black-Box-
Testtechniken werfen. Im Anschluss daran folgt in Abschnitt 4.4 eine ausführliche
Beschreibung der uns heute zur Verfügung stehenden White-Box-Techniken.
4.3 Black-Box-Testtechniken
4.3.1 Äquivalenzklassentest
Der Äquivalenzklassentest verfolgt das Ziel, mit einer möglichst geringen Anzahl
von Testfällen eine möglichst hohe Testabdeckung zu erreichen. Die Durchführung
erfolgt in zwei Schritten:
I Äquivalenzklassenbildung
Im ersten Schritt wird der Wertebereich aller Eingangsvariablen in zwei disjunkte
Partitionen (Äquivalenzklassen) aufgeteilt. Zwei konkrete Belegungen der Ein-
gabevariablen fallen genau dann in dieselbe Äquivalenzklasse, wenn beide von
dem untersuchten Software-System intern mit hoher Wahrscheinlichkeit in der
gleichen Art und Weise bearbeitet werden.
176 4 Software-Test
I Testfallkonstruktion
Aus jeder Äquivalenzklasse wird eine beliebige Variablenbelegung ausgewählt
und in Form eines konkreten Testfalls in die Testdatenbank aufgenommen. An-
schließend wird das Software-System wie üblich getestet, indem für jeden Test-
fall zunächst der Sollwert bestimmt und mit dem anschließend ermittelten Ist-
Wert abgeglichen wird.
der Klasse [Link]. Als Eingabe nimmt die Methode abs eine (vorzei-
chenbehaftete) long-Variable entgegen und berechnet daraus den Absolutwert. Die
übergebene Zahl wird in Abhängigkeit des Vorzeichens entweder unverändert oder
negiert zurückgegeben. In Java besitzt jede long-Variable stets eine Breite von 64
Bit, so dass insgesamt 264 verschiedene Testfälle existieren. Das vollständige Testen
mit allen möglichen Eingabekombinationen ist damit von vorne herein zum Schei-
tern verurteilt – selbst wenn ein Test nur eine einzige Mikrosekunde in Anspruch
nimmt, hätten wir erst in rund 1000 Jahren Gewissheit, ob die Methode wirklich für
alle Eingaben korrekt arbeitet.
Mit Hilfe des Äquivalenzklassentests kann die Anzahl der durchzuführenden
Tests auf ein Minimum beschränkt werden. Werfen wir einen erneuten Blick auf
die Verhaltensbeschreibung der Funktion abs, so führt diese in direkter Weise zu
einer Partitionierung des Wertebereichs in zwei disjunkte Äquivalenzklassen. Al-
le negativen Zahlen fallen in die eine, alle positiven Zahlen in die andere Klasse.
Einzig die Zuordnung der Null gestaltet sich an dieser Stelle nicht ganz so einfach.
Ohne Einsicht in die konkrete Implementierung haben wir keine Möglichkeit zu er-
kennen, ob die Null als negative oder als positive Zahl behandelt wird. Insgesamt
ergeben sich für dieses Beispiel damit die beiden in Abb. 4.10 dargestellten Partitio-
nierungsmöglichkeiten. Das Beispiel offenbart an dieser Stelle bereits eine zentrale
Eigenschaft der Äquivalenzklassenbildung: Die Partitionierung ist im Allgemeinen
nicht eindeutig.
Dem allgemeinen Vorgehen des Äquivalenzklassentests folgend, wird die Metho-
de abs mit einem beliebig wählbaren Wert aus jeder Partition getestet. Die Anzahl
der Testfälle wird hierdurch effektiv auf 2 beschränkt. Wahrscheinlich wären Sie
auch ohne ihr gerade erworbenes Wissen nicht auf die Idee gekommen, die Methode
abs mit jeder der 264 möglichen Eingaben auszuführen. Die Wahrscheinlichkeit ist
an dieser Stelle sehr hoch, dass Sie für dieses kleine Beispiel ganz ähnliche Testfälle
konstruiert hätten – ganz einfach intuitiv, ohne auf den formalen Apparat des Äqui-
valenzklassentests auch nur einen Gedanken zu verschwenden. In der Tat wenden
die meisten Software-Entwickler das gedankliche Grundmuster des Äquivalenzklas-
sentest implizit an, ohne sich dessen überhaupt bewusst zu sein. Liefert beispiels-
weise der Aufruf abs(5) das richtige Ergebnis, so lehrt die Programmiererfahrung,
dass auch der Aufruf abs(6) mit hoher Wahrscheinlichkeit den korrekten Wert be-
4.3 Black-Box-Testtechniken 177
I Erste Partitionierungsmöglichkeit
Äquivalenzklasse 1 Äquivalenzklasse 2
I Zweite Partitionierungsmöglichkeit
Äquivalenzklasse 1 Äquivalenzklasse 2
[Link]
public class Team { 1
2
int points; 3
int goals; 4
5
/** Bestimmt den Sieger unter zwei Mannschaften. 6
7
@param team1 Referenz auf das erste Team (Team A) 8
@param team2 Referenz auf das zweite Team (Team B) 9
*/ 10
11
public static Team champion(Team team1, Team team2) { 12
... 13
} 14
} 15
sen sich die oberen drei und die unteren drei Äquivalenzklassen zu jeweils einer
einzigen Partition verschmelzen – schließlich spielt die Anzahl der geschossenen
Tore nur dann eine Rolle, wenn beide Teams die gleiche Punktanzahl aufweisen.
Insgesamt reduziert sich die Anzahl der Äquivalenzklassen damit auf nur noch 5
(vgl. Abb. 4.12 rechts).
Die beiden bisher vorgestellten Beispielprogramme besitzen eine entscheidende
Gemeinsamkeit: Für die jeweils betrachteten Methoden gab es keinerlei ungültige
Eingabewerte. In realen Programmen sind viele Methoden jedoch nur partiell de-
finiert, d. h. der Definitionsbereich der einzelnen Übergabeparameter ist eine echte
Teilmenge des Wertebereichs des zugrunde liegenden Datentyps. Als Beispiel be-
trachten wir die Java-Methode season in Abb. 4.13. Als Eingabe nimmt season
eine Variable vom Typ int entgegen, dessen Wert als numerische Monatsangabe
interpretiert wird (1 = Januar, 12 = Dezember). Die Methode berechnet daraus die
4.3 Black-Box-Testtechniken 179
season.c
static final String season(int month) { 1
if (month <= 2) { 2
// January, February 3
return "WINTER"; 4
} else if (month >= 3 && month <= 5) { 5
// March, April, May 6
return "SPRING"; 7
} else if (month >= 6 && month <= 8) { 8
// June, July, August 9
return "SUMMER"; 10
} else if (month >= 9 && month <= 11) { 11
// September, November 12
return "FALL"; 13
} else { 14
// December 15
return "WINTER"; 16
} 17
} 18
I Vollständige Partitionierung
Die Äquivalenzklassenbildung bezieht auch die Eingabewerte außerhalb des er-
laubten Wertebereichs mit ein. Hierzu werden die ungültigen Eingabekombina-
tionen zu einer oder mehreren Äquivalenzklassen zusammengefasst und wie ge-
wohnt behandelt. Macht die funktionale Beschreibung keine Aussage über die
konkreten Rückgabewerte, wird über entsprechende Testfälle zumindest abge-
prüft, dass keine undefinierten Werte zurückgeliefert werden oder der Funktions-
aufruf keinen Systemabsturz verursacht. Eingesetzt wird dieses Vorgehen insbe-
sondere im Bereich sicherheitskritischer Systeme.
Für die Beispielfunktion season ergeben sich die in Abb. 4.14 dargestellten Äqui-
valenzklassen. Die partielle Partitionierung führt zu insgesamt 5 Testfällen. Wird
stattdessen der vollständige Wertebereich partitioniert, so werden durch die Hinzu-
nahme der Ränder zwei zusätzliche und damit insgesamt 7 Testfälle erzeugt.
180 4 Software-Test
I Partielle Partitionierung
... -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ...
Abgeleitete Testfälle:
season(1), season(3), season(5), season(10), season(11)
I Vollständige Partitionierung
... -1 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 ...
Abgeleitete Testfälle:
season(1), season(3), season(5), season(10), season(11),
season(-5), season(31)
4.3.2 Grenzwertbetrachtung
Die Grenzwertbetrachtung ist eine Erweiterung des Äquivalenzklassentests und ba-
siert auf der exakt gleichen Partitionierungsstrategie. Beide Verfahren unterschei-
den sich lediglich in der Art und Weise, wie aus den gebildeten Partitionen kon-
4.3 Black-Box-Testtechniken 181
season2.c
static final String season(int month) { 1
if (month == 3) return "SPRING"; // March 2
if (month == 4) return "SPRING"; // April 3
if (month == 5) return "SPRING"; // May 4
if (month == 6) return "SUMMER"; // June 5
if (month == 7) return "SUMMER"; // July 6
if (month == 8) return "SUMMER"; // August 7
if (month == 9) return "FALL"; // September 8
if (month == 10) return "FALL"; // October 9
if (month == 11) return "FALL"; // November 10
return "WINTER"; // January, February, December 11
} 12
krete Testfälle abgeleitet werden. Anstatt einen beliebigen Wert aus jeder Partiti-
on zu wählen, fordert die Grenzwertbetrachtung, dass die Testfälle nach bestimm-
ten Regeln aus dem Randbereich der Äquivalenzklassen entnommen werden. Für
ein- und mehrdimensionale Äquivalenzklassen gelten die folgenden Konstruktions-
regeln (vgl. Abb. 4.16):
I Eindimensionale Äquivalenzklassen
Es wird jeweils der innere und der äußere Wert des unteren und des oberen Rands
der Äquivalenzklasse als Testfall verwendet.
I Mehrdimensionale Äquivalenzklassen
Es wird jeweils das Randwerte-Tupel sowie alle Tupel, für die ein einzelner Wert
außerhalb der Äquivalenzklasse liegt, als Testfall verwendet.
Die Grenzwertbetrachtung mehrdimensionaler Äquivalenzklassen sollte stets mit
Bedacht geschehen, um die Anzahl der erzeugten Testfälle nicht unnötig in die Höhe
182 4 Software-Test
zu treiben. Alle Testfälle, die außerhalb der Äquivalenzklasse liegen, sind so kon-
zipiert, dass genau einer der Parameter außerhalb der Klasse liegt. Grundsätzlich
spricht nichts dagegen, die Funktion zusätzlich mit Eingabewerten zu testen, von
denen gleich mehrere außerhalb der Äquivalenzklasse liegen. Statistisch gesehen
decken die zusätzlichen Testfälle jedoch fast ausschließlich solche Fehler auf, die
auch durch einen der anderen Testfälle zum Vorschein kommen. Das gleiche gilt für
Testfälle, die vollständig im Innern einer Äquivalenzklasse liegen. Auch hier ist die
Wahrscheinlich gering, dass bisher unentdeckte Fehler aufgespürt werden.
An dieser Stelle wollen wir einen zweiten Blick auf Abb. 4.16 werfen und uns der
Frage widmen, wie stark die Anzahl der Testfälle mit der Dimension der Äquiva-
lenzklasse steigt. Im Folgenden bezeichne n die Dimension der Äquivalenzklasse.
Wir betrachten zunächst die Anzahl der Testfälle, die für einen einzelnen Grenz-
punkt einer n-dimensionalen Klasse erzeugt werden. Neben dem Testfall, der im
Inneren der Äquivalenzklasse liegt, wird für jeden der n Übergabeparameter ein
Testfall außerhalb der betrachteten Partition gebildet. Folgerichtig entstehen für je-
den Grenzpunkt exakt n + 1 Testfälle. Da eine n-dimensionale Äquivalenzklasse 2n
Grenzpunkte besitzt, werden insgesamt 2n (n + 1) Testfälle erzeugt.
In der Praxis liegt die Anzahl der Testfälle jedoch in vielen Fällen deutlich dar-
unter. Zum einen grenzen viele Äquivalenzklassen direkt aneinander, so dass sich
viele der Testfälle überschneiden. Zum anderen sind manche Äquivalenzklassen zu
einer oder mehreren Seiten geöffnet und verfügen hierdurch über eine reduzierte
Anzahl an Grenzpunkten.
Als Beispiel betrachten wir erneut die weiter oben eingeführte Methode
champion der Java-Klasse Team (Abb. 4.11). Führen wir eine Grenzwertbetrachtung
auf den 5 Äquivalenzklassen der Methode durch, so entstehen die in Abb. 4.17 mar-
kierten Testfälle. Aufgrund zahlreicher Überschneidungen reduziert sich die rein
rechnerische Anzahl von 5 · 22 (2 + 1) = 60 auf nur noch 28 Testfälle.
Die Anzahl der Testfälle reduziert sich weiter, wenn die konstruierten Äquiva-
lenzklassen zu einer oder mehreren Seiten geöffnet sind. Hierzu betrachten wir er-
neut die Methode binarySearch aus Abschnitt [Link]. Die Methode nimmt zwei
Übergabeparameter entgegen und führt zur Bildung einer einzigen Äquivalenzklas-
4.3 Black-Box-Testtechniken 183
0 0
[Link] [Link]
minInt minInt
maxInt maxInt
se mit zwei Dimensionen (vgl. Abb. 4.18). Die Klasse ist nach rechts geöffnet, da
die Länge des zu durchsuchenden Arrays a nach oben faktisch nur durch die Größe
des Hauptspeichers begrenzt wird.
In der Tat stehen wir an dieser Stelle vor einem Dilemma. Lassen wir die Test-
fälle am rechten Rand der Äquivalenzklasse einfach weg, so bleiben im Zuge der
Grenzwertbetrachtung nur noch diejenigen Testfälle übrig, in denen das Array a
keine Elemente enthält. Kurzum: Im Falle offener Partitionsgrenzen führt kein Weg
vorbei, die Äquivalenzklasse künstlich zu beschränken. Wir sprechen in diesem Fall
von einer künstlichen Grenzinduktion.
Obwohl uns mit der Grenzwertbetrachtung ein leistungsfähiges Instrument zur
gezielten Generierung von Testfällen zur Seite steht, ist die Technik nicht auf je-
de Funktion oder Methode anwendbar. So setzt die Bestimmung der Grenzpunkte
einer Äquivalenzklasse zwingend voraus, dass der zugrunde liegende Wertebereich
geordnet ist. In allen bisher betrachteten Beispielen war dies in der Tat der Fall.
Das Beispiel in Abb. 4.19 zeigt jedoch, dass diese Eigenschaft keinesfalls immer
gegeben ist. Bei dem abgebildeten Programmbeispiel handelt es sich um eine leicht
modifizierte Variante der Methode season aus Abb. 4.13. Im Gegensatz zur Origi-
nalimplementierung nimmt die Neuimplementierung den aktuellen Monat in Form
eines Strings und nicht mehr als numerischen Wert entgegen. Die Bildung der Äqui-
valenzklassen ist zwar immer noch ohne Probleme möglich, allerdings lassen sich
für diese keine klaren Grenzen mehr bestimmen. Anders als im Falle der Integer-
Variablen besitzt die Menge der String-Objekte keine natürliche Ordnung mehr.
season()
static final String season(String month) { 1
2
// "January" -> "Winter" 3
// "February" -> "Spring" 4
// "March" -> "Spring" 5
// ... 6
// "December" -> "Winter" 7
8
... 9
} 10
Abb. 4.19 Die Grenzwertbetrachtung stößt bei ungeordneten Wertebereichen an ihre Grenzen
[Link]
public class Ringbuffer { 1
2
int[] a = {0, 0, 0, 0, 0, 0, 0, 0 }; 3
int readPtr = 0; 4
int writePtr = 0; 5
6
public void write(int value) { 7
a[writePtr] = value; 8
writePtr = (writePtr + 1) % 8; 9
if (writePtr == readPtr) 10
readPtr = (readPtr + 1) % 8; 11
} 12
13
public int read() { 14
int result = a[readPtr]; 15
if (readPtr != writePtr) 16
readPtr = (readPtr + 1) % 8; 17
return result; 18
} 19
} 20
a[4] a[3]
a a[4] a[3]
a a[4] a[3]
a
Schreibzeiger Lesezeiger
Zum anderen lässt sich die Datenstruktur besonders einfach und effizient implemen-
tieren. Einige Mikroprozessorarchitekturen stellen sogar spezielle Adressierungsar-
ten zur Verfügung, die speziell für die Programmierung solcher Datenstrukturen
ausgelegt sind. Mit Hilfe der Ringpufferadressierung wird die stets wiederkehren-
de Modulo-Berechnung direkt in Hardware und damit ohne zusätzliche Rechenzeit
durchgeführt.
Mit Hilfe von Zustandsübergangsgraphen (endliche Automaten) lassen sich Sys-
teme dieser oder ähnlicher Art auf natürliche Weise modellieren. Ein solcher Auto-
mat besteht aus einer endlichen Menge von Zuständen und Übergängen. In welchem
Zustand sich ein Software-System befindet, wird durch die augenblickliche Bele-
gung der internen Variablen bestimmt. Die Übergänge eines Zustandsübergangs-
graphen werden durch gerichtete Pfeile markiert. In jedem deterministischen Pro-
gramm sind die Zustandsübergänge an Bedingungen geknüpft, die in Form von Kan-
tenmarkierungen in den Graphen eingetragen werden (vgl. Abb. 4.22).
I Zustand Leer
Der Ringpuffer enthält keine Elemente. Ein leerer Ringpuffer wird repräsentiert,
indem sowohl der Schreib- als auch der Lesezeiger das gleiche Element refe-
renzieren. Mit Hilfe der Methode write kann ein Element in den Ringpuffer
eingefügt werden. In diesem Fall wird das Element an der aktuellen Position des
Schreibzeigers abgelegt und dieser anschließend um eins erhöht. Ein Lesezugriff
ist in diesem Zustand nicht erlaubt.
I Zustand Befüllt
Der Ringpuffer wurde mit Elementen befüllt, die maximale Speicherkapazität
aber noch nicht erreicht. Mit Hilfe der Methode read kann ein Element aus dem
Ringpuffer ausgelesen werden. In diesem Fall wird das Element an der aktuellen
Position des Lesezeigers zurückgelesen und dieser anschließend um eins erhöht.
186 4 Software-Test
read / write
write write
read read
read
Fehler
Genau wie im Falle eines leeren Ringpuffers können mit der Methode write
weitere Elemente hinzugefügt werden.
I Zustand Voll
Die Kapazität des Ringpuffers ist erschöpft. Wird ein weiteres Element hinzuge-
fügt, so erreicht der Schreibzeiger die Position des Lesezeigers. In diesem Fall
wird der Lesezeiger ebenfalls um eins erhöht und das älteste gespeicherte Ele-
ment geht verloren.
Ist Ihnen aufgefallen, dass die maximale Speicherkapazität unseres Beispiel-
ringpuffers damit effektiv nur 7 Elemente und nicht, wie vielleicht erwartet, 8
Elemente beträgt? Der Verlust eines Speicherplatzes wird an dieser Stelle be-
wusst in Kauf genommen, da ein voller Ringpuffer sonst nicht mehr von einem
leeren zu unterscheiden wäre.
Die Idee des zustandsbasierten Software-Tests besteht darin, alle Übergänge zwi-
schen zwei Zuständen mindestens einmal zu überprüfen. Für die Konstruktion der
Testfälle wird der Zustandsübergangsgraph zunächst ausgerollt. Ausgehend von
dem Initialzustand werden hierzu alle Folgezustände rekursiv traversiert und wie
in Abb. 4.23 (oben) gezeigt, in einen Zustandsbaum übersetzt. Die Traversierung
bricht ab, sobald ein bereits besuchter Zustand erneut besucht oder ein Zustand er-
reicht wird, in dem das Programm terminiert (Finalzustand).
Anschließend wird für jedes Blatt des Zustandsbaums ein Testfall abgeleitet. Be-
zogen auf unsere Beispielimplementierung ist durch dieses Vorgehen sichergestellt,
dass die beiden Funktionen read und write in allen Zuständen des Ringpuffers
mindestens einmal ausgeführt werden. Insgesamt entstehen für unser Beispiel 6
Testfälle, die in Abb. 4.23 (unten) zusammengefasst sind.
4.3.4 Use-Case-Test
Alle der bisher betrachteten Konstruktionsverfahren waren bestens geeignet, um
Testfälle für einzelne Methoden oder Klassen abzuleiten. Entsprechend häufig wer-
den diese Verfahren auf der Ebene des Unit- bzw. des Integrationstests eingesetzt. In
4.3 Black-Box-Testtechniken 187
I Zustandsbaum
read 1 read 2
Fehler Leer
Leer write
Befüllt 3
Befüllt
write
read 4
Befüllt
read 5
Befüllt
write
Voll
write
Voll 6
n zu konstruierender Testfall
I Abgeleitete Testfälle
Nr Testeingabe Soll-Ergebnis Soll-Ergebnis
(gelesen) (Puffer-Inhalt)
1 read() Fehler {}
2 write(1), read() {1} {}
3 write(1), write(2) {} {1, 2}
4 write(1), write(2), read() {1} {2}
5 write(1), write(2), write(3) {1} {2, 3, 4, 5, 6, 7}
write(4), write(5), write(6)
write(7), read()
6 write(1), write(2), write(3) {} {2, 3, 4, 5, 6, 7, 8}
write(4), write(5), write(6)
write(7), write(8)
Abb. 4.23 Der zustandsbasierte Software-Test leitet die Testfälle aus dem ausgerollten Zustands-
übergangsgraphen ab
diesem und den folgenden Abschnitten werden wir uns der Systemebene zuwenden
und mit Verfahren beschäftigen, die das zu testende Software-System als Ganzes
betrachten. Dabei wird die Ebene der Implementierungsdetails bewusst verlassen,
und an die Stelle von Methoden und Klassen treten Arbeitsabläufe und vollständige
Anwendungsszenarien.
Use-Case-Diagramme sind eine grafische Beschreibung einzelner Anwendungs-
fälle. Im Gegensatz zu einem Geschäftsprozess, der die systeminterne Umsetzung
einer Anforderung modelliert, wird ausschließlich das nach außen sichtbare Verhal-
ten einer Software-Applikation beschrieben. Mit anderen Worten: Ein Use-Case-
Diagramm beschreibt ein komplexes System aus Kundensicht und lässt die interne
Umsetzung bewusst unberücksichtigt.
188 4 Software-Test
Buch ausleihen
Buch ausleihen
<verwendet> <verwendet>
Student Ausweis Konto Bibliothekar
überprüfen überprüfen
<erweitert> <erweitert>
I Use Case
Ein Use Case setzt sich aus einer Reihe von Aktionen zusammen, die, nacheinan-
der ausgeführt, das gewünschte Verhalten erzeugen. Ein Use Case wird stets von
einem Akteur angestoßen und führt zu einem festgelegten Ergebnis. Als Darstel-
lungssymbol wird die Ellipse verwendet.
I Beziehungen
Typische Systeme bestehen aus mehreren Use cases, die untereinander in
Beziehung stehen. So verwendet (importiert) der Use case Buch auslei-
hen das Verhalten der Use cases Ausweis überprüfen und Konto überprüfen
(Include-Beziehung). Erweitert werden diese durch die Use cases Konto sperren
und Ausleihwunsch zurückweisen (Extend-Beziehung). Anders als die Include-
Beziehung, die das Verhalten eines anderen Use cases stets importiert, drückt
eine Erweiterung aus, dass ein Use case in einigen Fällen um das Verhalten eines
anderen ergänzt wird, in anderen dagegen nicht.
I Akteure
Die Systeminteraktion wird mit Hilfe von Akteuren modelliert, die jeweils mit
einer ganz bestimmten Rolle identifiziert werden (Student, Bibliothekar etc.).
Akteure und reale Personen sind durch die Rollendefinition zwei voneinander
getrennte Begriffe. Insbesondere entsprechen zwei Akteure nicht zwangsläufig
zwei verschiedenen realen Personen. Die semantischen Beziehungen zwischen
den Akteuren und den entsprechenden Use cases werden in Form von Assozia-
tionen in das Diagramm eingetragen.
Der Use-Case-basierte Black-Box-Test fordert, dass alle möglichen Sequenzen ei-
nes Use-Case-Diagramms durch mindestens einen Testfall abgedeckt werden. Die
Testfallkonstruktion erfolgt in zwei Schritten:
I Schritt 1
Zunächst werden die Eingabewerte bestimmt, die eine bestimmte Ablaufsequenz
innerhalb des Use-Case-Diagramms auslösen. Am Ende dieses Schritts steht eine
Partitionierung der Eingangsbelegungen, die wir im Zuge der Äquivalenzklas-
senbildung bereits in Abschnitt 4.3.1 kennen gelernt haben.
I Schritt 2
Anschließend werden aus den ermittelten Werteintervallen konkrete Eingabe-
belegungen ausgewählt. Auch hier ist die Wahrscheinlichkeit groß, Fehler an
den Rändern der Äquivalenzklassen zu finden. Damit bietet sich die in Ab-
schnitt 4.3.2 vorgestellte Grenzwertanalyse als aussichtsreiches Instrument für
die Testfallkonstruktion an.
Der Use-Case-basierte Test eignet sich insbesondere zur Durchführung von System-
und Abnahmetests. Der Rückgriff auf konkrete Anwendungsfälle garantiert zum
einen, dass ein System im Normalbetrieb zuverlässig arbeitet. Zum anderen wer-
den bei der Systemspezifikation auch wichtige Ausnahmesituationen mit Hilfe von
Use-Cases beschrieben. Beispiele typischer Ausnahmesituationen sind das Verhal-
ten eines Systems bei Ressourcenknappheit oder die Abarbeitung von Notfallproze-
190 4 Software-Test
Ausweis Benutzerdaten
gültig abrufen
Konto Konto
㱹
aktiv sperren
Konto Buch
im Plus 㱸
ausleihen
Maximale Anzahl 㱸
㱹
Bücher entliehen Negation ODER UND
I Entscheidungstabelle (Reduziert)
Bedingungen
N J J J J J Ausweis gültig
– N N J J J Konto aktiv
– N J N J J Konto im Plus
– – – – N J Limit erreicht
Aktionen
N J J J J J Daten abrufen
J J N J N N Konto sperren
N N N N J N Buch ausleihen
für jeder Spalte ermittelt wird, welche der Aktionen unter den gegebenen Bedin-
gungen ausgelöst werden. Tabelle 4.2 (oben) zeigt die entsprechend konstruierte
Entscheidungstabelle des Bibliotheksbeispiels.
Sobald die Entscheidungstabelle vollständig aufgebaut ist, wird für jede Spal-
te ein separater Testfall abgeleitet. Hierzu werden die Eingabedaten so gewählt,
dass die Bedingungen der entsprechenden Spalte erfüllt sind. Anschließend wird
der Testfall ausgeführt und die ausgelösten Aktionen gegen die zuvor ermittelten
Sollwerte abgeglichen. Die Zweiteilung der Entscheidungstabelle in Bedingungen
und Aktionen erfüllt damit einen ganz praktischen Zweck: Für jede Spalte definiert
der obere Abschnitt (Bedingungen) die Eingabe und der untere Abschnitt (Aktio-
nen) die Sollwerte des auszuführenden Testfalls.
In der Praxis werden Entscheidungstabellen in den wenigsten Fällen vollständig
erzeugt. Die Gründe hierfür sind vielfältig. Da die Anzahl der theoretisch mögli-
chen Kombinationen exponentiell mit der Anzahl der Bedingungen steigt, muss die
Tabellengröße ab einer gewissen Anzahl von Bedingungen schon aus Komplexi-
tätsgründen reduziert werden. Des Weiteren können sich die Bedingungen gegen-
seitig beeinflussen und die Anzahl sinnreicher Kombinationen hierdurch begrenzen.
In anderen Fällen überschattet der Wert einer Bedingung alle anderen, so dass die
Anzahl der Testfälle reduziert werden kann, ohne die Testabdeckung merklich zu
verschlechtern.
192 4 Software-Test
Mac OS X Opera
Kon&guration
Ausprägung
Auch das Beispiel der Buchausleihe enthält ein gehöriges Maß an Redundanz.
Ist z. B. der Benutzerausweis ungültig, so wird der Ausleihprozess erst gar nicht
initiiert. Die Werte der restlichen Bedingungen wirken sich in diesem Fall nicht
mehr auf die ausgelösten Aktionen aus. Dementsprechend kann die vollständig ex-
pandierte Entscheidungstabelle durch eine reduzierte Variante ersetzt werden, die
in Tabelle 4.2 (unten) dargestellt ist. Die mit „–“ markierten Felder spielen für die
Testkonstruktion keine Rolle und dürfen beliebige Werte annehmen.
den allermeisten Fällen zwei einzelne Faktoren identifizieren, die für das Fehlver-
halten verantwortlich sind. Nur in wenigen Fällen müssen drei oder mehr Faktoren
in einer ganz bestimmten Kombination zusammenspielen, um ein Systemversagen
herbeizuführen. In Tabelle 4.3 sind alle Ausprägungspaare zusammengefasst, die
als potenzielle Ursache für ein etwaiges Fehlverhalten unserer Web-Applikation in
Frage kommen.
Der paarweise Software-Test verfolgt einen sehr pragmatischen Ansatz: Wenn
wir schon nicht in der Lage sind, alle möglichen Konfigurationen eines Software-
Systems zu testen, wollen wir als Minimalkriterium sicherstellen, dass jedes Aus-
prägungspaar durch einen entsprechenden Testfall abgedeckt wird. Hierdurch wird
die Anzahl der Testfälle deutlich verringert, ohne ein einziges Ausprägungspaar zu
ignorieren. Eine Testfallmenge mit dieser Eigenschaft heißt paarweise vollständig.
Wie in Tabelle 4.4 demonstriert, lassen sich die 18 möglichen Konfigurationen
(links) auf 9 reduzieren (rechts), ohne das Paarkriterium zu verletzen. Egal welches
Ausprägungspaar sie auch wählen – in der rechten Tabelle werden sie stets einen
passenden Testfall finden. Damit ist für unsere Web-Applikation mit 9 Testfällen
sichergestellt, dass
I jeder Web-Browser unter jedem Betriebssystem und
Tabelle 4.4 Der vollständige und der paarweise vollständige Software-Test im Vergleich
Vollständiger Test Paarweiser vollständiger Test
Betriebssystem Modus Browser Betriebssystem Modus Browser
1 Windows Online Explorer 1 Windows Online Explorer
2 Windows Online Firefox 2 Windows Offline Firefox
3 Windows Online Opera 3 Windows Online Opera
4 Windows Offline Explorer 4 Linux Online Firefox
5 Windows Offline Firefox 5 Linux Offline Opera
6 Windows Offline Opera 6 Linux Offline Explorer
7 Linux Online Explorer 7 Mac OS X Online Opera
8 Linux Online Firefox 8 Mac OS X Offline Explorer
9 Linux Online Opera 9 Mac OS X Online Firefox
10 Linux Offline Explorer
11 Linux Offline Firefox
12 Linux Offline Opera
13 Mac OS X Online Explorer
14 Mac OS X Online Firefox
15 Mac OS X Online Opera
16 Mac OS X Offline Explorer
17 Mac OS X Offline Firefox
18 Mac OS X Offline Opera
Ein orthogonales Feld ist ein zweidimensionales Array von ganzen Zahlen mit
der folgenden Eigenschaft: Werden zwei beliebige Spalten ausgewählt, so findet
sich dort jedes der möglichen Kombinationspaare mindestens einmal wieder.
Orthogonale Felder sind keine Erfindung des Computerzeitalters. Bereits Mitte
des achtzehnten Jahrhunderts beschäftigte sich der Schweizer Mathematiker Leon-
hard Euler ausführlich mit solchen und ähnlichen Gebilden. Orthogonale Felder
zählen heute zu den gut verstandenen mathematischen Strukturen [50, 117, 212].
Für die folgenden Betrachtungen vereinbaren wir für die Bezeichnung orthogo-
naler Felder die abkürzende Standardschreibweise Lr (mc ) [55]. Hierin bezeichnen r
die Anzahl der Zeilen (rows) und c die Anzahl der Spalten (columns). Der Parame-
ter m entspricht der Anzahl der verschiedenen Werte, die ein einzelnes Element des
Arrays annehmen kann.
Orthogonale Felder besitzen die wohlwollende Eigenschaft, dass jede Permuta-
tion der Zeilen oder Spalten erneut zu einem orthogonalen Feld führt. Die Bezeich-
nung Lr (mc ) ist somit nicht eindeutig und steht stellvertretend für eine Vielzahl von
Feldern. Als Beispiel sind in Abb. 4.27 zwei Varianten des Arrays L9 (34 ) dargestellt.
Orthogonale Felder sind eng verwandt mit den lateinischen Quadraten. Ein la-
teinisches Quadrat der Ordnung n ist eine n × n-Matrix mit der Eigenschaft, dass
in jeder Spalte und Zeile genau eines von n Symbolen auftritt. In Abb. 4.28 sind
Vertreter der Ordnungen 3, 4 und 5 exemplarisch gegenübergestellt. Lateinische
Quadrate sind bereits seit dem Mittelalter bekannt und erlebten in jüngster Zeit im
4.3 Black-Box-Testtechniken 195
1 2 3 4 1 2 3 4
1 2 3 1 3 1 1 1 1 1
2 2 2 3 1 2 1 2 2 2 Anzahl
Spalten
3 1 1 1 1 3 1 3 3 3
4 1 3 3 2 4 2 1 2 3 L9(34)
5 2 1 2 2 5 2 2 3 1
Werte-
6 3 2 1 2 6 2 3 1 2 Anzahl bereich
7 3 1 3 3 7 3 1 3 2 Zeilen
8 1 2 2 3 8 3 2 1 3
9 3 3 2 1 9 3 3 2 1
Zusammenhang mit dem Logikrätsel Sudoku eine wahre Renaissance. Bei genauerer
Betrachtung verbirgt sich hinter einem Sudoku-Rätsel nichts anderes als ein lateini-
sches Quadrat der Ordnung 9, das sich aus 9 lateinischen Quadraten der Ordnung 3
zusammensetzt.
Aus jedem lateinischen Quadrat der Ordnung n lässt sich ohne Umwege ein or-
thogonales Array der Form Ln2 (n3 ) ableiten. Hierzu erzeugen wir für jedes Feld
(i, j) des lateinischen Quadrats einen Tabelleneintrag (i, j, k) mit
Angewendet auf die lateinischen Quadrate in Abb. 4.28 entstehen die drei in Tabel-
le 4.5 dargestellten Felder. Die n2 Tabelleneinträge besitzen die Eigenschaft, dass
die Wertepaare (i, j) alle möglichen Kombinationen genau einmal annehmen. Das
gleiche gilt für die Wertepaare (i, k) und ( j, k). Mit anderen Worten: Die erzeugten
Felder sind allesamt orthogonal.
Damit können wir das Vorgehen des paarweisen Software-Tests wie folgt fest-
halten:
196 4 Software-Test
I Schritt 1
Zunächst wird die Anzahl der Freiheitsgrade (= c) und die maximale Anzahl der
Ausprägungen (= k) bestimmt. Basierend auf diesen Parametern wird ein ortho-
gonales Array der Form Ln (kc ) als Basis für die Testfallkonstruktion gewählt.
Unsere Web-Applikation besitzt 3 Freiheitsgrade mit jeweils 3 bzw. 2 Ausprä-
gungen. Das in Tabelle 4.5 links dargestellte Feld ist somit ausreichend, um eine
paarweise vollständige Testmenge zu konstruieren.
I Schritt 2
Sobald das passende orthogonale Feld bestimmt ist, wird jeder Freiheitsgrad ei-
ner beliebigen Spalte und jede Ausprägung einer beliebigen Zahl aus dem In-
tervall {1, . . . , k} zugeordnet. Anschließend wird aus jeder Zeile eine separa-
te Testkonfiguration abgeleitet (vgl. Tabelle 4.6). Die mathematische Struktur
4.3 Black-Box-Testtechniken 197
I Schritt 3
Ist die Anzahl der Ausprägungen für verschiedene Freiheitsgrade unterschied-
lich, so sind die Konfigurationen in einigen Zeilen nur partiell definiert. Im Falle
der Web-Applikation enthält die mittlere Spalte der Zeilen 3, 6 und 9 immer noch
den Wert 3 (vgl. Tabelle 4.6 Mitte). Um die Testmenge zu vervollständigen, wer-
den die Werte durch eine beliebige Ausprägung ersetzt (vgl. Tabelle 4.6 rechts).
Keinesfalls dürfen wir die entsprechenden Zeilen einfach löschen. In diesem Fall
würde z. B. das Ausprägungspaar (Windows, Opera) verschwinden und die Ei-
genschaft der paarweisen Vollständigkeit verloren gehen.
Die mit Hilfe des orthogonalen Felds L9 (33 ) erzeugte Konfigurationsmenge (Tabel-
le 4.6) entspricht exakt derjenigen Menge, die wir bereits am Anfang dieses Ab-
schnitts in Tabelle 4.4 ohne weitere Herleitung eingeführt haben.
In der Praxis kann die ermittelte Konfigurationsmenge oft noch weiter reduziert
werden, da sich gewisse Ausprägungskombinationen gegenseitig ausschließen. So
wird beispielsweise der Internet Explorer von Microsoft heute nur noch für das fir-
meneigene Windows-Betriebssystem entwickelt. Hierdurch entfallen die Ausprä-
gungspaare (Linux, Explorer) und (Mac OS X, Explorer). Während der Testfallge-
nerierung werden solche Abhängigkeiten zunächst ignoriert und ungültige Konfigu-
rationen erst am Ende aus der Testmenge entfernt.
198 4 Software-Test
I Back-to-Back-Test
Diese Spielart des diversifizierenden Software-Tests greift die Idee der heteroge-
nen Redundanz auf, die bereits in Abschnitt 3.4.1 ausführlich untersucht wurde.
Für die korrekte Durchführung eines Back-to-Back-Tests werden n verschiedene
Implementierungen der gleichen Software gegeneinander getestet. Hierzu wer-
den die Programmvarianten parallel mit den gleichen Testfällen stimuliert und
die berechneten Ergebnisse anschließend miteinander verglichen (vgl. Abb. 4.29
links).
Alle eingesetzten Implementierungen basieren auf der gleichen Anforde-
rungsbeschreibung, werden jedoch von verschiedenen, unabhängig voneinander
operierenden Teams entwickelt. Hierbei wird eine maximale Heterogenität zwi-
schen den Entwicklergruppen angestrebt, um die Gefahr gemeinsamer Fehler zu
begrenzen. Die Wahrscheinlichkeit, dass zwei völlig unabhängig voneinander
entwickelte Software-Systeme gemeinsame Fehler enthalten, ist größer als auf
den ersten Blick vermutet. Die Gründe bilden ein breites Spektrum und reichen
von Compiler-Inkompatibilitäten über Fehler in gemeinsam genutzten Subsyste-
men bis hin zu Spezifikationslücken, die von mehreren Teams auf die gleiche Art
und Weise fehlinterpretiert werden.
Aufgrund des hohen Aufwands wird der Back-to-Back-Test hauptsächlich für
Code-Module eingesetzt, die entweder sehr hohen Sicherheitsanforderungen un-
terliegen oder mit konventionellen Mitteln nur schwer getestet werden können.
Eine besondere Rolle spielt hierbei eine spezielle Variante des Back-to-Back-
Tests, in dem die Referenzimplementierung in Form eines funktional äquivalen-
ten Prototyps realisiert ist. So werden z. B. viele Programme aus dem Bereich
der digitalen Signalverarbeitung getestet, indem die Rechenergebnisse mit den
Signalverläufen spezieller Simulationsmodelle abgeglichen werden. Die Erstel-
lung solcher prototypischer Modelle erfolgt heute weitgehend werkzeugunter-
stützt (siehe z. B. [116, 66, 38, 77, 267, 273]).
I Regressionstest
Ein Regressionstest wird durchgeführt, nachdem die bestehende Version eines
Software-Systems im Zuge der Weiterentwicklung oder Fehlerkorrektur verän-
dert wurde. Die durchgeführten Tests sollen sicherstellen, dass die Veränderun-
gen des Quellcodes keine negativen Auswirkungen auf die bestehende Funk-
tionalität zeigen. Mit anderen Worten: Fehler, die sich im Rahmen der Weiter-
4.3 Black-Box-Testtechniken 199
Back-to-back-Test Regressionstest
A A
Team A Team A
= =
Testdatenbank Testdatenbank
B A'
Weiter-
Team B entwicklung
I Mutationstest
Hinter dem Begriff des Mutationstests verbirgt sich kein Testverfahren im eigent-
lichen Sinne. Stattdessen haben wir es hier mit einer diversifizierenden Technik
zu tun, die zur Bewertung anderer Testverfahren dient. In Abschnitt 4.5.2 kom-
men wir auf die Durchführung von Mutationstests genauer zu sprechen.
4.4 White-Box-Testtechniken
Im Gegensatz zu den Black-Box-Techniken, die zur Testfallkonstruktion ausschließ-
lich auf die funktionale Beschreibung eines Software-Systems zurückgreifen, basie-
ren White-Box-Tests auf der Analyse der inneren Programmstrukturen. Folgerich-
tig sprechen wir in diesem Zusammenhang auch von Strukturtests, die sich auf der
obersten Ebene in zwei Klassen einteilen lassen:
4.4 White-Box-Testtechniken 201
I Kontrollflussorientierte Strukturtests
Pfadüberdeckung
Mehrfache
Strukturierte
McCabe-Überdeckung Bedingungs-
Pfadüberdeckung
überdeckung
Minimale mehrfache
Boundary-Interior
Zweigüberdeckung Bedingungs-
Pfadüberdeckung
überdeckung
Legende
Einfache
A B Anweisungs-
Bedingungs-
überdeckung
überdeckung
„A enthält B
I Datenflussorientierte Strukturtests
Legende
All du A B
„A enthält B
I Kontrollflussorientierte Tests
Tests dieser Kategorie ziehen für die Konstruktion der einzelnen Testfälle aus-
schließlich die internen Berechnungspfade eines Programms in Betracht. Die
Beschaffenheit der verarbeiteten Daten spielt für die Testfallkonstruktion dage-
gen keine Rolle. Zu den bekanntesten Vertretern gehören die Anweisungsüber-
deckung (C0 -Test), die Zweigüberdeckung (C1 -Test) und die Pfadüberdeckung.
Zusätzlich sind in der Praxis die diversen Varianten der Bedingungsüberdeckung
sowie die McCabe-Überdeckung von Bedeutung (vgl. Abb. 4.30 oben).
I Datenflussorientierte Tests
Für die Testfallkonstruktion werden zusätzliche Kriterien herangezogen, die sich
mit der Beschaffenheit der manipulierten Daten beschäftigen. Hierzu werden
aus den Variablenzugriffen eines Programms spezifische Nutzungsprofile erzeugt
202 4 Software-Test
und die gewonnenen Informationen für die Wahl geeigneter Testfälle verwendet.
Im direkten Vergleich mit den kontrollflussbasierten Testverfahren erfordern die-
se Kriterien eine tiefergehende Analyse der Programmsemantik und sind dement-
sprechend schwerer zu erfüllen. Abb. 4.30 (unten) fasst die wichtigsten datenflus-
sorientierten Strukturtests in einer Übersicht zusammen.
I Strukturanalyse
Aus dem Programmtext wird zunächst der Kontrollflussgraph extrahiert, der die
internen Berechnungspfade eines Programms auf Funktions- oder Modulebene
modelliert. Per definitionem verfügt jeder Kontrollflussgraph über genau einen
Eintrittsknoten (in) und einen Austrittsknoten (out), die zusammen die Schnitt-
stelle nach außen bilden. Jeder innere Knoten des Kontrollflussgraphen entspricht
einem einzigen Befehl oder einem sequenziell durchlaufenen Befehlsblock.
I Testkonstruktion
Ist der Kontrollflussgraph vollständig aufgebaut, werden daraus die einzelnen
Testfälle abgeleitet. Die Testfallmenge wird dabei so gewählt, dass ein zuvor
festgelegtes Überdeckungskriterium erfüllt wird. Je nachdem, ob die Topologie
der Ausführungspfade oder die Beschaffenheit der manipulierten Daten in den
Vordergrund gerückt wird, entsteht ein kontrollflussorientierter oder ein daten-
flussorientierter White-Box-Test.
I Testdurchführung
Sind alle Testfälle konstruiert, so werden diese wie gewohnt ausgeführt und die
gemessenen Ist-Ergebnisse mit den zuvor ermittelten Soll-Ergebnissen abgegli-
chen. Die Testdurchführung verläuft damit stets gleich – unabhängig davon, ob
es sich um einen Black-Box- oder einen White-Box-Test handelt.
4.4.1 Kontrollflussmodellierung
Der Konstruktion des oben angesprochenen Kontrollflussgraphen fällt bei der
Durchführung eines White-Box-Tests eine zentrale Rolle zu. Als Beispiel betrachten
wir den in Abb. 4.31 dargestellten Kontrollflussgraphen der C-Funktion manhattan,
die uns für die weiteren Betrachtungen als fruchtbares Beispiel dienen wird. Die
Funktion nimmt zwei Übergabeparameter a und b entgegen und berechnet daraus
die Summe d(a, b) = |a| + |b|. Hierzu werden zunächst die Vorzeichen der Para-
meter a und b überprüft und im Falle eines negativen Vorzeichens in eine positive
Zahl gewandelt. Anschließend wird die Summe der erzeugten Werte gebildet und als
Ergebnis zurückgeliefert. Der Name der Funktion deutet an, dass sie auf einfache
Weise für die Berechnung der Manhattan-Distanz
zweier Punkte (x1 , y1 ) und (x2 , y2 ) eingesetzt werden kann, indem die Differenzen
der jeweiligen x- und y-Koordinaten als Übergabeparameter verwendet werden.
4.4 White-Box-Testtechniken 203
manhattan(a,b)
manhattan.c in 0
// Eingabe: a:int, b:int 1
// Ergebnis: |a| + |b| 2
3 1 if
a<0
int manhattan(int a, int b) 4
{ 5
2 a0
a = -a
if (a < 0) 6
a = -a; 7
if (b < 0) 8 3 if
b<0
b = -b; 9
return a+b; 10 b = -b 4 b0
} 11
out 5 return a + b;
manhattan2(a,b)
in 0
manhattan2.c
// Eingabe: a:int, b:int 1 1 if
a<0
// Ergebnis: |a| + |b| 2
3 2 a0
a = -a;
int manhattan2(int a, int b) 4
{ 5
if (a < 0) 6 3 if
b<0
a = -a; 7
if (b < 0) 8 _tmp = a-b; 4 b0
return a-b; 9
return a+b; 10
5 _tmp = a+b;
} 11
6 return _tmp;
I Knotenmarkierte Kontrollflussgraphen
In diesen Graphen werden Verzweigungsbedingungen ebenfalls den Knoten
zugeordnet. Knotenmarkierte Graphen besitzen insgesamt eine geringere Be-
deutung als kantenmarkierte – nichtsdestotrotz bilden sie die Grundlagen ei-
niger White-Box-Tests. Ein Beispiel ist der Required-k-Tupel-Test, der in Ab-
schnitt 4.4.8 genauer betrachtet wird.
Die Aufteilung in kanten- und knotenmarkierte Graphen ist nicht das einzige Unter-
scheidungsmerkmal von Kontrollflussgraphen. Ein weiteres betrifft die Beschaffen-
heit der Knoten, die zur Modellierung verzweigungsfreier Befehlssequenzen ver-
wendet werden. Die folgenden drei Fälle werden diesbezüglich unterschieden:
I Expandierte Kontrollflussgraphen
Diese Graphen enthalten für jeden Befehl einen separaten Knoten. Da die meis-
ten White-Box-Tests verzweigungsfreie Befehlsblöcke als Einheit betrachten,
werden vollständig expandierte Kontrollflussgraphen zu Testzwecken nur selten
eingesetzt.
I Teilkollabierte Kontrollflussgraphen
In diesen Graphen werden zwei oder mehrere sequenzielle Befehle in einem
einzigen Knoten zusammengefasst. Eine spezielle Variante teilkollabierter Kon-
trollflussgraphen werden wir in Abschnitt 4.4.8 im Zusammenhang mit dem
Required-k-Tupel-Test kennen lernen.
4.4 White-Box-Testtechniken 205
0 in 0 in
1 if 1 if
B B ¬B
X 2 ¬B X 2 3 Y
3 out 4 out
0 in 0 in
1 while 1 X
B B
X 2 ¬B 2 while
¬B
3 out 3 out
I Kollabierte Kontrollflussgraphen
Diese Graphen zeichnen sich dadurch aus, dass verzweigungsfreie Befehlsblöcke
vollständig in einem einzigen Knoten zusammengefasst werden. Folgerichtig
existieren in einem kollabierten Graphen nur noch solche Knoten, die über mehr
als einen Nachfolger verfügen. Kollabierte Kontrollflussgraphen sind die kom-
pakteste und gleichzeitig am häufigsten verwendete Datenstruktur im Bereich
des White-Box-Tests.
0 in 0 in 0 in
1 if 1 if 1 if
B ¬B B ¬B
Y1; B ¬B
Kantenmarkiert
0 in 0 in 0 in
Y1;
Knotenmarkiert
4.4.2 Anweisungsüberdeckung
Der Anweisungsüberdeckungstest (statement coverage) ist das schwächste der hier
vorgestellten Testverfahren und in allen anderen Überdeckungstests als Teilmenge
enthalten. Die Testmenge wird dabei so gewählt, dass alle Knoten des Kontroll-
flussgraphen durchlaufen und damit alle Anweisungen des untersuchten Programms
mindestens einmal ausgeführt werden. Die Anweisungsüberdeckung wird in der Li-
teratur auch als C0 -Test bezeichnet.
4.4 White-Box-Testtechniken 207
0 I Testfälle
manhattan(-1,-1)
1
2
I Überdeckung
3
{0, 1, 2, 3, 4, 5}
Abb. 4.33 zeigt die Konstruktion einer C0 -Testmenge für die weiter oben einge-
führte Beispielfunktion manhattan. Wird die Funktion mit zwei negativen Werten
aufgerufen, so werden alle Knoten des Kontrollflussgraphen nacheinander durch-
laufen. Folgerichtig ist ein einziger Testfall ausreichend, um das Kriterium der An-
weisungsüberdeckung zu erfüllen.
Damit macht das Beispiel zugleich auf eine eklatante Schwäche des C0 -Tests
aufmerksam: Für viele Programmkonstrukte lässt sich das Kriterium mit einer ver-
gleichsweise kompakten Testfallmenge erfüllen, in der wichtige Fälle unberücksich-
tigt bleiben. So haben wir in unserem Beispiel das Überdeckungskriterium erfüllt,
ohne die Funktion mit nur einem einzigen positiven Wert auszuführen – entspre-
chend gering fällt die zu erwartende Fehlererkennungsquote aus. In [102] konnte
die Anweisungsüberdeckung nur magere 18 % der Fehler eines Software-Systems
aufdecken.
Aufgrund der augenscheinlichen Limitierungen wird der C0 -Test in der Literatur
oft als unzulängliches Element der Software-Qualitätssicherung gegeißelt – zu Un-
recht, wie die folgenden Beispiele zeigen werden. Die geringe Testabdeckung darf
nicht darüber hinweg täuschen, dass sich die Anweisungsüberdeckung in der Praxis
häufig als ein schwer zu erreichendes Ziel erweist. Wunsch und Wirklichkeit klaffen
an dieser Stelle weit auseinander.
Das Programm in Abb. 4.34 soll Ihnen einen kleinen Vorgeschmack auf die Pro-
bleme geben, die in der Praxis zu lösen sind. Anders als in unserem pathologischen
Eingangsbeispiel werden die ersten drei Return-Befehle nur im Falle einer eintre-
tenden Ausnahmesituation ausgeführt. Für die Durchführung eines vollständigen
Anweisungsüberdeckungstests müssen die verschiedenen Fehlerszenarien künstlich
herbeigeführt werden. Lässt sich eine nicht vorhandene Datei noch vergleichsweise
einfach simulieren, so sind im Falle der dynamischen Speicherbelegung schon grö-
ßere Kunstgriffe nötig. Der Aufwand nimmt insbesondere dann stark zu, wenn der
208 4 Software-Test
hard_to_test.c
// Übungsaufgabe: 1
// Führen Sie einen vollständigen C0-Test durch! 2
3
uint8_t *hard_to_test(...) 4
{ 5
// Get file properties 6
if (stat(filename, &fileProperties) != 0) { 7
return NULL; // (1) 8
} 9
10
// Open file 11
if (!(file = fopen(filename, "r"))) { 12
return NULL; // (2) 13
} 14
15
// Allocate memory 16
if (!(data = (uint8_t *)malloc(fileProperties.st_size))) { 17
return NULL; // (3) 18
19
return data; 20
} 21
Abb. 4.34 In der Praxis ist die Durchführung von C0 -Tests schwierig
bugs.h (Auszug)
static void __init check_fpu(void) 1
{ 2
... 3
4
/* Test for the divl bug.. */ 5
__asm__("fninit\n\t" 6
"fldl %1\n\t" 7
"fdivl %2\n\t" 8
"fmull %2\n\t" 9
"fldl %1\n\t" 10
"fsubp %%st,%%st(1)\n\t" 11
"fistpl %0\n\t" 12
"fwait\n\t" 13
"fninit" 14
: "=m" (*&boot_cpu_data.fdiv_bug) 15
: "m" (*&x), "m" (*&y)); 16
stts(); 17
if (boot_cpu_data.fdiv_bug) 18
printk("Hmm, FPU with FDIV bug.\n"); 19
} 20
ist es ein unkalkulierbares. Aus diesem Grund ist die Durchführung eines vollständi-
gen C0 -Tests in diesen Anwendungsgebieten häufig fest vorgeschrieben. So fordert
beispielsweise der Standard RTCA DO-178B für Software-Anwendungen in der
Luftfahrt, dass eine Anweisungsüberdeckung für alle Software-Module der Kritika-
litätsstufe C durchgeführt werden muss. Hierzu zählen alle Software-Komponenten,
deren Ausfall zu einer bedeutenden, aber nicht kritischen Fehlfunktion führen kann.
4.4.3 Zweigüberdeckung
Die Zweigüberdeckung fordert, dass jede Kante des Kontrollflussgraphen von min-
destens einem Testfall durchlaufen werden muss. Um das Kriterium zu erfüllen,
müssen die Testfälle so gewählt werden, dass jede Verzweigungsbedingung min-
destens einmal wahr und mindestens einmal falsch wird. Da hierdurch alle Kno-
ten ebenfalls mindestens einmal besucht werden müssen, ist die Anweisungsüber-
deckung in der Zweigüberdeckung vollständig enthalten. In der Literatur wird die
Zweigüberdeckung auch als C1 -Test bezeichnet.
Abb. 4.36 demonstriert, wie sich das Kriterium für die Beispielfunktion
manhattan erfüllen lässt. Insgesamt wird die Funktion zweimal ausgeführt, mit
jeweils wechselnden Vorzeichen der Operanden. Anders als im Falle der Anwei-
sungsüberdeckung müssen wir jeden Parameter mindestens einmal mit einem posi-
tiven und einem negativen Wert belegen.
In der Literatur wird die Zweigüberdeckung oft als Minimalkriterium für die
Durchführung eines White-Box-Tests beschrieben [8, 163]. Wie schon im Falle
210 4 Software-Test
I Testfälle
0 0
manhattan(-1,1)
1 1 manhattan(1,-1)
2 2
I Überdeckung
3 3
{0, 1, 2, 3, 5}
4 4 {0, 1, 3, 4, 5}
5 5
4.4.4 Pfadüberdeckung
Die Pfadüberdeckung ist die mit Abstand mächtigste White-Box-Prüftechnik, be-
sitzt aufgrund ihrer immensen Komplexität aber nur eine äußerst geringe Praxisbe-
deutung. Das Kriterium wird erst dann vollständig erfüllt, wenn für jeden möglichen
Pfad, der den Eingangsknoten des Kontrollflussgraphen mit dem Ausgangsknoten
verbindet, ein separater Testfall existiert.
Abb. 4.37 fasst die verschiedenen Möglichkeiten zusammen, den Eingangs- und
Ausgangsknoten der Beispielfunktion manhattan miteinander zu verbinden. Da je-
de If-Bedingung die Anzahl der Pfade verdoppelt, ergeben sich insgesamt 4 ver-
schiedene Möglichkeiten.
Die praktische Untauglichkeit der Pfadüberdeckung wird besonders deutlich,
wenn sie auf Programme angewendet wird, die neben Verzweigungen auch Schlei-
fenkonstrukte enthalten. Als triviales Beispiel eines solchen Programms ist in
Abb. 4.38 eine Schleife abgebildet, die in der i-ten Iteration den Wert des Array-
Element a[i] überprüft. In Abhängigkeit des ausgelesenen Werts wird entweder
die Funktion foo oder die Funktion bar aufgerufen. Da sich die Anzahl der Pfa-
4.4 White-Box-Testtechniken 211
I Testfälle
0 0 0 0
manhattan(-1,-1)
1 1 1 1 manhattan(-1,1)
manhattan(1,-1)
2 2 2 2
manhattan(1,1)
I Überdeckung
3 3 3 3
{0, 1, 2, 3, 4, 5}
4 4 4 4 {0, 1, 2, 3, 5}
{0, 1, 3, 4, 5}
{0, 1, 3, 5}
5 5 5 5
in 0
foobar.c
void foobar(int *a) 1 for (...) 1
{ 2
for (int i=0; i < 512; i++) { 3
if (a[i]) 4 2
foo(); 5
else 6 if
bar(); 7 (a[i])
3 4
} 8
} 9 foo(); bar();
out 5
Abb. 4.38 Selbst für kleine Programme stößt die Pfadüberdeckung schnell an ihre Grenzen
– Äußere Pfade
Die Testfälle durchlaufen das Programm auf Pfaden, die den Schleifenkörper
nicht betreten. Für Schleifen fester Lauflänge sowie für While-Schleifen, die
erst am Ende des Schleifenkörpers die Wiederholungsbedingung auswerten,
ist diese Testfallgruppe leer.
I Strukturierte Pfadüberdeckung
Die Verallgemeinerung des Boundary-Interior-Tests führt uns auf direktem Weg
zur strukturierten Pfadüberdeckung [127, 128, 129, 244]. Anstatt nur die ersten
4.4 White-Box-Testtechniken 213
0 0 0 0
1 1 1 1
2 2 2 2
1. Ausführung
3 4 3 4 3 4 3 4
1 1 1 1
2 2 2 2
2. Ausführung
3 4 3 4 3 4 3 4
1 1 1 1
5 5 5 5
Variante 1 Variante 2
if ((A && !B) || (!A && B)) 1 if (A) 1
{ 2 if (!B) 2
foo(); 3 goto foo: 3
} 4 if (!A) 4
5 if (B) 5
6 goto foo: 6
7 return; 7
8 foo: foo(); 8
in 0
in 0
if (A) 1
if (...) 1
2 if (!B)
2 foo();
if (!A) 3
3
4 if (B)
out 4
out 6 5 foo();
Abb. 4.40 Obwohl beide Code-Fragmente exakt die gleiche Programmlogik implementieren, führt
die Zweigüberdeckung zu einer vollständig anderen Testfallmenge
4.4.5 Bedingungsüberdeckung
Unter den drei vorgestellten elementaren Überdeckungskriterien besitzt die Zweig-
überdeckung den größten Stellenwert, schließlich stellt sie einen Kompromiss zwi-
schen der minimalistischen Anweisungsüberdeckung und der unmöglich zu errei-
chenden Pfadüberdeckung dar. Nichtsdestotrotz ist die Aussagekraft der Zweigüber-
deckung in ihrer Reinform mitunter recht begrenzt, wie das Beispiel in Abb. 4.40
nahe legt.
Abgebildet sind zwei Programmbeispiele, die exakt die gleiche Kontrollflusslo-
gik implementieren. Der einzige Unterschied zwischen beiden Code-Fragmenten
besteht in der Implementierung der If-Abfrage. Während die gesamte Verzwei-
gungsbedingung in der linken Variante in eine einzelne If-Bedingung gepackt wird,
verwendet die rechte Implementierung vier verschiedene Abfragen. Aufgrund der
unterschiedlichen Programmstrukturen fallen auch die Kontrollflussgraphen gänz-
lich unterschiedlich aus. Führen wir einen vollständigen C1 -Test durch, so benöti-
gen wir für die linke Implementierung lediglich zwei Testfälle, um alle Kanten zu
durchlaufen. Für die rechte Implementierung fordert der C1 -Test die Erzeugung von
4.4 White-Box-Testtechniken 215
Einfache Bedingungsüberdeckung
Minimale Mehrfachbedingungsüberdeckung
Mehrfachbedingungsüberdeckung
A = 0, B = 0
„Alle Wahrheitskombinationen A = 0, B = 1
müssen getestet werden A = 1, B = 0
A = 1, B = 1
vier Testfällen. Kurzum: Für funktional identische Programme kann die Zweigüber-
deckung gänzlich unterschiedliche Ergebnisse liefern.
Abhilfe schaffen an dieser Stelle die verschiedenen Spielarten der Bedingungs-
überdeckung. Neben der Struktur des Kontrollflussgraphen beziehen diese Kriterien
zusätzlich die logische Struktur der einzelnen If-Bedingungen in die Testkonstruk-
tion mit ein. In der Praxis spielen die drei in Abb. 4.41 dargestellten Bedingungs-
überdeckungskriterien eine wichtige Rolle:
I Einfache Bedingungsüberdeckung
Das Kriterium ist vollständig erfüllt, wenn alle atomaren Prädikate mindestens
einmal beide Wahrheitswerte annehmen. Ein atomares Prädikat kann eine Varia-
ble oder auch das Ergebnis eines Funktionsaufrufs sein. Die If-Bedingung unse-
res Beispielprogramms enthält die atomaren Bedingungen A und B, so dass zwei
Testfälle ausreichen, um die einfache Bedingungsüberdeckung zu erfüllen. Zu
beachten gibt es an dieser Stelle, dass das Überdeckungskriterium ausschließlich
eine Forderung bezüglich der atomaren Prädikate aufstellt und keinerlei Aussage
über den resultierenden Wahrheitswert der If-Bedingung selbst macht.
I Minimale Mehrfachbedingungsüberdeckung
Das Kriterium ist vollständig erfüllt, wenn alle atomaren und alle zusammen-
gesetzten Prädikate mindestens einmal beide Wahrheitswerte annehmen. Diese
Variante ist stärker als die einfache Bedingungsüberdeckung – insbesondere sind
die beiden Testfälle A = 0, B = 1 und A = 1, B = 0 nicht ausreichend, um die
minimale Mehrfachüberdeckung sicherzustellen. So nehmen zwar die zusam-
mengesetzten Prädikate (A && !B) und (!A && B) beide Wahrheitswerte an,
die gesamte If-Bedingung selbst ist jedoch niemals falsch. Um das Kriterium
216 4 Software-Test
I Mehrfachbedingungsüberdeckung
Das Kriterium ist vollständig erfüllt, wenn alle Wahrheitskombinationen der ato-
maren Prädikate getestet wurden. In der Konsequenz bedeutet diese Forderung,
dass für jede If-Bedingung mit n atomaren Prädikaten 2n Testfällen zu generieren
sind. Die If-Bedingung in unserem Beispielprogramm enthält nur zwei atomare
Prädikate, so dass insgesamt vier Testfälle erzeugt werden müssen. Angewendet
auf reale Programme kann die Mehrfachüberdeckung schnell zu einer kombi-
natorischen Explosion führen. Entsprechend gering ist die praktische Relevanz
dieses Kriteriums.
4.4.6 McCabe-Überdeckung
Eine weitere Variante des kontrollflussorientierten White-Box-Tests geht auf die
Mitte der Siebzigerjahre publizierten Arbeiten von Thomas J. McCabe zurück. Die
Testkonstruktion basiert auf der Zerlegung des Kontrollflussgraphen in eine mini-
male Menge von Elementarpfaden (basic paths). Entsprechend wird die McCabe-
Überdeckung in der Literatur auch als Basic-Path-Test bezeichnet.
Für die folgenden Betrachtungen bezeichnet |E| die Anzahl der Kanten (edges)
und |N| die Anzahl der Knoten (nodes) des Kontrollflussgraphen. Nummerieren wir
die Kanten mit 1, . . . , |E|, so lässt sich jeder Pfad als Element des Vektorraums N|E|
darstellen. Die i-te Komponente des Vektors gibt an, wie oft der Pfad die i-te Kante
des Kontrollflussgraphen durchläuft. Eine Menge von Elementarpfaden p1 , . . . , pn
besitzt die Eigenschaft, dass sich jeder geschlossene Pfad – hierunter fallen alle
Pfade, deren Start- und Endknoten zusammenfallen – als Linearkombination der
Elementarpfade darstellen lässt. Mit anderen Worten: Für jeden geschlossenen Pfad
p existieren Konstanten k1 , k2 , . . . , kn mit
p = k1 · p1 + k2 · p2 + . . . + kn · pn . (4.2)
manhattan(a,b) manhattan(a,b)
0 0
1 1
1 1
2 2
2 4 2 4
3 3 8
3 3
5 5
4 7 4 7
6 6
5 5
0 0 0
1 1 1
1 1 1
2
2 2 4 2 4
3
3 3 3
5
4 7 4 4 7
6
5 5 5
⎛ ⎞ ⎛ ⎞ ⎛ ⎞
1 1 1
⎜1⎟ ⎜0⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1⎟ ⎜0⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
p1 = ⎜ 0 ⎟ p2 = ⎜ 1 ⎟ p3 = ⎜ 1 ⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜0⎟ ⎜1⎟ ⎜0⎟
⎝0⎠ ⎝1⎠ ⎝0⎠
1 0 1
Mit Hilfe der ermittelten Vektoren lässt sich der Pfad {0, 1, 2, 3, 4, 5} beispiels-
weise wie folgt darstellen:
⎛ ⎞ ⎛ ⎞ ⎛ ⎞ ⎛ ⎞
1 1 1 1
⎜1⎟ ⎜1⎟ ⎜0⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1⎟ ⎜1⎟ ⎜0⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
{0, 1, 2, 3, 4, 5}= ⎜ ⎟ ⎜ ⎟ ⎜
ˆ ⎜0⎟ = 1·⎜0⎟+1·⎜1⎟−1·⎜1⎟ ⎟ ⎜ (4.5)
⎟
⎜1⎟ ⎜0⎟ ⎜1⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎝1⎠ ⎝0⎠ ⎝1⎠ ⎝0⎠
0 1 0 1
0 0 0
1 1 1
1 1 1
2 2
2 2 2 4
3 3
3 3 3
5
4 4 7 4 7
6
5 5 5
⎛ ⎞ ⎛ ⎞ ⎛ ⎞
1 1 1
⎜1⎟ ⎜1⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1⎟ ⎜1⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
p1 = ⎜ 0 ⎟ p2 = ⎜ 0 ⎟ p3 = ⎜ 1 ⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜1⎟ ⎜0⎟ ⎜0⎟
⎝1⎠ ⎝0⎠ ⎝0⎠
0 1 1
I Im zweiten Schritt wird für jeden Elementarpfad ein Testfall erzeugt. Die Anzahl
der erzeugten Testfälle entspricht damit stets der um eins erhöhten zyklomati-
schen Zahl des Kontrollflussgraphen. Die Erhöhung um eins trägt der zusätzlich
hinzugefügten Kante Rechnung, um aus graphentheoretischer Sicht einen stark
zusammenhängenden Graphen zu erzeugen.
Für unsere Beispielfunktion manhattan erfüllen die folgenden Testfälle das Über-
deckungskriterium von McCabe:
manhattan(-1,1) → {0, 1, 2, 3, 5}
manhattan(1,-1) → {0, 1, 3, 4, 5}
manhattan(1,1) → {0, 1, 3, 5}
Stellen wir das Überdeckungskriterium nach McCabe den weiter oben eingeführ-
ten Kriterien gegenüber, so erweist es sich als vergleichsweise stark. Insbesondere
erfüllt jede McCabe-Testmenge automatisch das Kriterium der Zweig- und somit
auch das der Anweisungsüberdeckung.
Eine wichtige Eigenschaft der Elementarpfade soll an dieser Stelle nicht ver-
schwiegen werden: Die Menge der Graphen, die zusammen eine Basis bilden, ist
im Allgemeinen nicht eindeutig bestimmt. In Abb. 4.44 ist eine weitere Menge von
Elementarpfaden dargestellt, die zusammen ebenfalls eine Basis bilden. Mit Hilfe
220 4 Software-Test
der abgebildeten Menge lässt sich z. B. der Pfad {0, 1, 3, 4, 5} wie folgt darstellen:
⎛ ⎞ ⎛ ⎞ ⎛ ⎞ ⎛ ⎞
1 1 1 1
⎜0⎟ ⎜0⎟ ⎜1⎟ ⎜1⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜0⎟ ⎜0⎟ ⎜1⎟ ⎜1⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
ˆ ⎜1⎟ = 1·⎜1⎟+1·⎜0⎟−1·⎜
{0, 1, 3, 4, 5}= ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎜0⎟
⎟ (4.6)
⎜1⎟ ⎜0⎟ ⎜1⎟ ⎜0⎟
⎜ ⎟ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟
⎝1⎠ ⎝0⎠ ⎝1⎠ ⎝0⎠
0 1 0 1
Damit erfüllt auch die folgende Menge das Überdeckungskriterium von McCabe:
manhattan(-1,-1) → {0, 1, 2, 3, 4, 5}
manhattan(-1,1) → {0, 1, 2, 3, 5}
manhattan(1,1) → {0, 1, 3, 5}
4.4.7 Defs-Uses-Überdeckung
Im Gegensatz zu allen bisher betrachteten Überdeckungskriterien, die ausschließ-
lich den Kontrollfluss eines Programms in Betracht ziehen, leiten die Defs-Uses-
Kriterien die auszuführenden Pfade aus dem Datenfluss ab. Hierzu werden die Va-
riablenzugriffe des untersuchten Programms analysiert und mit verschiedenen Da-
tenflussattributen versehen. Die vergebenen Attribute teilen die Variablenzugriffe in
drei Klassen ein:
res = 1.0;
De nition
while( i > 0) {
res *= x ;
i--;
}
p-use
if( n < 0)
res = 1/ res ; c-use
De nition
return res ;
}
return res; 9
I All Definitions
Die Testfälle durchlaufen für jede Definition einer Variablen einen definitions-
freien Pfad zu mindestens einem p-Use oder c-Use. Die folgende Testmenge er-
füllt das All-Defs-Kriterium für die Beispielfunktion pow:
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,1) → {0, 1, 3, 4, 5, 6, 5, 7, 9}
Das Überdeckungskriterium ist vergleichsweise schwach und wird von den meis-
ten der anderen Kriterien subsumiert.
I All c-Uses
Die Testfälle durchlaufen für jede Definition einer Variablen einen Pfad zu allen
definitionsfrei erreichbaren c-Uses. Die folgende Testmenge erfüllt das All-c-
Uses-Kriterium für die Beispielfunktion pow:
pow(x,0) → {0, 1, 3, 4, 5, 7, 9}
pow(x,1) → {0, 1, 3, 4, 5, 6, 5, 7, 9}
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,2) → {0, 1, 3, 4, 5, 6, 5, 6, 5, 7, 9}
I All p-Uses
Die Testfälle durchlaufen für jede Definition einer Variablen einen Pfad zu allen
definitionsfrei erreichbaren p-Uses. Die folgende Testmenge erfüllt das All-p-
Uses-Kriterium für die Beispielfunktion pow:
pow(x,0) → {0, 1, 3, 4, 5, 7, 9}
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,2) → {0, 1, 3, 4, 5, 6, 5, 6, 5, 7, 9}
Auch hier gilt, dass das Kriterium im Allgemeinen deutlich stärker ist als das
All-Definitions-Kriterium, dieses jedoch nicht in jedem Fall einschließt. Enthält
ein Programm z. B. überhaupt keine prädikative Nutzung, so ist das All-p-Uses-
Kriterium bereits ohne einen einzigen Testfall erfüllt.
I All Uses
Die Testfälle durchlaufen für jede Definition einer Variablen einen Pfad zu allen
definitionsfrei erreichbaren p- und c-Uses. Die folgende Testmenge erfüllt das
All-Uses-Kriterium für die Beispielfunktion pow:
pow(x,0) → {0, 1, 3, 4, 5, 7, 9}
224 4 Software-Test
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,1) → {0, 1, 3, 4, 5, 6, 5, 7, 9}
pow(x,2) → {0, 1, 3, 4, 5, 6, 5, 6, 5, 7, 9}
Das All-Uses-Kriterium ist damit genau dann erfüllt, wenn sowohl das All-p-
Uses- als auch das All-c-Uses-Kriterium erfüllt ist.
I All c, some p
Die Testfälle erfüllen das All-c-Uses-Kriterium. Existiert zu einer Definition kei-
ne berechnende Nutzung, so wird zusätzlich mindestens ein definitionsfreier Pfad
zu einer prädikativen Nutzung hinzugenommen. Die folgende Testmenge erfüllt
das All-c-Some-p-Kriterium für die Beispielfunktion pow:
pow(x,0) → {0, 1, 3, 4, 5, 7, 9}
pow(x,1) → {0, 1, 3, 4, 5, 6, 5, 7, 9}
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,2) → {0, 1, 3, 4, 5, 6, 5, 6, 5, 7, 9}
I All p, some c
Die Testfälle erfüllen das All-p-Uses-Kriterium. Existiert zu einer Definition kei-
ne prädikative Nutzung, so wird mindestens ein definitionsfreier Pfad zu einer
berechnenden Nutzung hinzugenommen. Die folgende Testmenge erfüllt das All-
Defs-Kriterium für die Beispielfunktion pow:
pow(x,0) → {0, 1, 3, 4, 5, 7, 9}
pow(x,-1) → {0, 1, 2, 4, 5, 6, 5, 7, 8, 9}
pow(x,2) → {0, 1, 3, 4, 5, 6, 5, 6, 5, 7, 9}
zunächst zwei wichtige Hilfsmengen ein, die für beliebige Kontrollflussgraphen mit
der Knotenmenge S = {s0 , . . . , sn } definiert sind:
I dcn(x, si )
Die Menge dcn(x, si ) ist für alle Knoten si ∈ S definiert, in denen die Variable x
definiert wird. Die Menge enthält alle Knoten s j ∈ S, die x berechnend verwenden
und von si über einen definitionsfreien Pfad erreichbar sind.
I dpn(x, si )
Die Menge dpn(x, si ) ist für alle Knoten si ∈ S definiert, in denen die Variable x
definiert wird. Die Menge enthält alle Kanten (s j , sk ) mit s j , sk ∈ S, die x prädi-
kativ verwenden und von si über einen definitionsfreien Pfad erreichbar sind.
Im Falle der Beispielfunktion pow berechnen sich die Mengen dcn(x, si ) und
dpn(x, si ) wie in Tabelle 4.9 dargestellt. Für jede Variablenzuweisung enthält die
Tabelle eine separate Zeile. Die erste Spalte enthält einen entsprechenden Eintrag
dcn(x, si ) bzw. dpn(x, si ). Die zweite Spalte beinhaltet die definitionsfrei erreichba-
ren Knoten und Kanten, in denen eine berechnende bzw. prädikative Nutzung der
betrachteten Variablen x stattfindet. In der dritten Spalte ist für jeden Endknoten s j
der zweiten Spalte ein entsprechender Pfad angegeben, der den Startknoten si defi-
nitionsfrei mit s j verbindet. Die Wahl des Pfadstücks in der dritten Spalte ist nicht
eindeutig. Insbesondere in Programmen mit bedingten Schleifen können unendlich
viele Pfade existieren, die si definitionsfrei mit s j verbinden.
An dieser Stelle gilt es zu beachten, dass Pfadsegmente auftreten können, die auf-
grund von semantischen Abhängigkeiten der Variablenbedingungen niemals durch-
laufen werden können. So drückt die Knotensequenz 4, 5, 7, 8 für unser Beispiel aus,
dass die While-Schleife übersprungen wird und gleichzeitig n < 0 ist. Der Rumpf
der While-Schleife wird jedoch nur im Fall n = 0 übersprungen – im Widerspruch
zur Eigenschaft von n, einen negativen Wert zu besitzen. Pfadsegmente, die durch
keinen Testfall durchlaufen werden können, sind in der Tabelle durchgestrichen und
können für die weiteren Betrachtungen bedenkenlos ignoriert werden.
Mit Hilfe der aufgestellten Tabellen für die Mengen dcn und dpn sind wir in der
Lage, die oben eingeführten Überdeckungskriterien auf elegante Weise umzuformu-
lieren:
I All-Defs-Kriterium
Eine Menge T von Pfaden erfüllt das All-Defs-Kriterium, falls für jede Zeile i
ein Pfadstück aus der i-ten Zeile von Tabelle dcn oder der i-ten Zeile von Tabelle
dpn in den Pfaden aus T enthalten sind.
I All-c-Uses-Kriterium
Eine Menge T von Pfaden erfüllt das All-c-Uses-Kriterium, falls alle Pfadstücke
aus Tabelle dcn in den Pfaden aus T enthalten sind.
I All-p-Uses-Kriterium
Eine Menge T von Pfaden erfüllt das All-p-Uses-Kriterium, falls alle Pfadstücke
aus Tabelle dpn in den Pfaden aus T enthalten sind.
226 4 Software-Test
I All-Uses-Kriterium
Eine Menge T von Pfaden erfüllt das All-Uses-Kriterium, falls alle Pfadstücke
aus Tabelle dcn und Tabelle dpn in den Pfaden aus T enthalten sind.
I All-c-Some-p-Kriterium
Eine Menge T von Pfaden erfüllt das All-c-Some-p-Kriterium, falls alle Pfad-
stücke aus Tabelle dcn in den Pfaden aus T enthalten sind. Ist die dcn-Menge der
i-ten Zeile leer, so ist mindestens ein Pfadstück aus der i-ten Zeile der dpn-Menge
in Pfaden aus T enthalten.
I All-p-Some-c-Kriterium
Eine Menge T von Pfaden erfüllt das All-p-Some-c-Kriterium, falls alle Pfad-
stücke aus Tabelle dpn in den Pfaden aus T enthalten sind. Ist die dpn-Menge der
i-ten Zeile leer, so ist mindestens ein Pfadstück aus der i-ten Zeile der dcn-Menge
in Pfaden aus T enthalten.
Anhand der umformulierten Kriterien lässt sich nun leicht überprüfen, dass die wei-
ter oben konstruierten Testfälle auch wirklich das jeweils zugrunde liegende Über-
deckungskriterium erfüllen.
Für die systematische Konstruktion sind die aufgestellten Tabellen der Mengen
dcn und dpn allerdings nur bedingt geeignet. Die Anzahl der erzeugten Testfälle
hängt maßgeblich von den in der dritten Spalte aufgeführten Pfaden ab. Sind die
4.4 White-Box-Testtechniken 227
foo(1) → {0, 2, 3, 4, 6}
foo(-2) → {0, 1, 3, 5, 6}
I Möglichkeit 2
foo(2) → {0, 2, 3, 5, 6}
foo(-1) → {0, 1, 3, 4, 6}
Beide Möglichkeiten lassen wichtige Pfadstücke außer Acht. Im ersten Fall werden
die Segmente {1, 3, 4} und {2, 3, 5} nicht berücksichtigt, im zweiten Fall bleiben
die Segmente {1, 3, 5} und {2, 3, 4} außen vor.
Abhilfe schafft die Alle-Definitions-Nutzungspfade-Überdeckung (kurz All-du-
Überdeckung). Dieses Kriteriums fordert, dass nicht nur ein, sondern alle definiti-
onsfreien Pfade von einer Definition zu einer Nutzung durchlaufen werden müssen.
Eine Ausnahme bildet die Behandlung von Schleifen, um das Kriterium in der Pra-
xis handhabbar zu machen. Um das Alle-Definitions-Nutzungspfade zu erfüllen,
sind jetzt 4 verschiedene Testfälle notwendig:
foo(1) → {0, 2, 3, 4, 6}
foo(-2) → {0, 1, 3, 5, 6}
foo(2) → {0, 2, 3, 5, 6}
foo(-1) → {0, 1, 3, 4, 6}
4.4.8 Required-k-Tupel-Überdeckung
Die Required-k-Tupel-Überdeckung basiert ebenfalls auf der Analyse des Daten-
flusses eines Programms und geht auf die Anfang der Achtzigerjahre publizierten
Arbeiten von Simeon C. Ntafos zurück [192, 193]. Im direkten Vergleich mit den im
letzten Abschnitt eingeführten Defs-Uses-Kriterien unterscheidet sich die Required-
k-Tupel-Überdeckung in den folgenden Punkten:
228 4 Software-Test
all_def.c
void foo(int x) 1 foo(x)
{ 2
int y,z; 3 0
4 x<0 x0
if (x < 0) 5
y = 2; // x ist negativ 6 y=2 1 2 y=1
else 7
y = 1; // x ist positiv 8
3
9
odd(x) even(x)
if (odd(x)) 10
z = 2; // x ist ungerade 11
z=2 4 5 z=1
else 12
z = 1; // x ist gerade 13
14
6
return y+z; 15
} 16
return y+z;
{[d1 (x1 ), u2 (x1 )], [d2 (x2 ), u3 (x2 )], ..., [dk−1 (xk−1 ), uk (xk−1 )]} (4.7)
4.4 White-Box-Testtechniken 229
in 0 int fsm()
fsm.c
int fsm() 1
{ 2 1 state = INITIAL;
int state, accept; 3
char c; 4
5 2 c = next_character();
state = INITIAL; 6
do { 7
c = next_character(); 8 3 if (c != EOF)
if (c != EOF) { 9
accept = 10 accept =
next_state(c, &state); 11 4 next_state
} 12
(c, &state);
} while (c != EOF); 13 5 while (c != EOF);
return accept; 14
} 15
out 6 return (accept);
von k − 1 Zugriffspaaren. Im ersten Zugriff wird die Variable xi definiert (di (xi ))
und im zweiten Zugriff referenziert (ui+1 (xi )) – die Nutzung ui+1 (xi ) wird dabei
definitionsfrei von di (xi ) erreicht. Jede Definition di (x j ) und Nutzung ui (xk ) findet
in dem gleichen Knoten ni statt. Innerhalb einer k-dr-Sequenz müssen die durch-
laufenen Knoten paarweise verschieden sein, die auftretenden Variablen dürfen sich
hingegen wiederholen.
Für die Durchführung eines Required-k-Tupel-Tests werden zunächst die i-dr-
Sequenzen für alle i mit i ≤ k gebildet und im Anschluss daran zur Testfallkon-
struktion verwendet. Die Anzahl und die Beschaffenheit der erzeugten Testfälle
hängt damit eng mit der konkreten Wahl der Konstanten k zusammen, wobei der
Required-(k + 1)-Tupel-Test den Required-k-Tupel-Test einschließt.
Zur Demonstration der Testkonstruktion betrachten wir das Beispiel in Abb. 4.48.
Die Funktion fsm ist eine vereinfachte Variante des in [48] eingeführten Beispiel-
programms und implementiert die Funktionsweise eines endlichen Automaten. Im
Initialzustand startend wird in jeder Iteration der Do-While-Schleife ein Zeichen
c eingelesen und mit Hilfe der Funktion next_state der Folgezustand sowie die
Akzeptanzbedingung accept berechnet. Die Schleife wird beendet, sobald der Ein-
gabestrom versiegt. In diesem Fall liefert die Funktion next_character den Wert
EOF (end of file) zurück.
Aus dem Kontrollflussgraphen lassen sich die folgenden k-dr-Interaktionen ab-
leiten:
I 2-dr-Interaktionen
I 3-dr-Interaktionen
Für k > 3 sind keine k-dr-Interaktionen mehr vorhanden. An dieser Stelle wol-
len wir unser Augenmerk explizit auf den Knoten 4 legen. Die Variable state
wird in diesem Knoten definiert und in der Schleifeniteration einer referenzieren-
den Nutzung unterzogen. In den oben aufgeführten k-dr-Interaktionen taucht das
Paar [d4 (c), u4 (c)] jedoch nicht auf, da für jedes Tupel [di (. . .), u j (. . .)] explizit die
Beziehung i = j gefordert wird.
Die exakten Eigenschaften, die eine Menge von Testfällen erfüllen muss, um dem
Required-k-Tupel-Kriterium zu genügen, werden in der Literatur unterschiedlich
definiert. Allen Definitionen ist gemein, dass die erzeugten Ausführungspfade alle
vorhandenen k-dr-Interaktionen beinhalten müssen. Zusätzliche Kriterien stellen die
Ausführung von Schleifen sicher. Wird die in [48] gegebene Definition zugrunde
gelegt, so wird das Required-k-Tupel-Kriterium von jeder Testmenge erfüllt, die
das Programm auf den folgenden Pfaden durchläuft:
{in, 0, 1, 2, 3, 4, 5, 2, 3, 5, 6, out}
{in, 0, 1, 2, 3, 4, 5, 6, out}
{in, 0, 1, 2, 3, 5, 6, out}
Die Testfälle enthalten für jedes der oben abgeleiteten k-dr-Interaktionen ein Pfad-
segment, das die entsprechenden Zuweisungen und Referenzen definitionsfrei ver-
bindet. Zusätzlich stellt der erste Testfall sicher, dass die Do-While-Schleife iteriert
wird.
4.5 Testmetriken 231
Ein genauer Blick auf die erzeugten Testfälle zeigt, dass das Required-k-Tupel-
Kriterium das All-Defs-Kriterium nicht subsumiert. Der Grund liegt auch hier wie-
der in der Tatsache, dass Zuweisungen nicht getestet werden müssen, falls eine Nut-
zung ausschließlich in demselben Knoten stattfindet. In unserem Beispiel wird der
in Knoten 4 zugewiesene Wert der Variablen state ausschließlich in demselben Kno-
ten referenzierend verwendet. Um das All-Defs-Kriterium zu erfüllen, müssten die
konstruierten Testfälle daher zusätzlich das Pfadsegment {4, 5, 2, 3, 4} durchlaufen.
4.5 Testmetriken
Mit Hilfe von Testmetriken werden verschiedene Aspekte des Software-Tests quan-
titativ erfasst. Der abstrakte Begriff der Güte einer Testmenge oder eines Testverfah-
rens wird auf diese Weise zu einem greifbaren Maß. In typischen Projekten werden
Testmetriken eingesetzt, um die folgenden Fragen zu beantworten:
4.5.1 Überdeckungsmetriken
In Abschnitt 4.4 haben wir die verschiedenen Varianten des White-Box-Tests als
mehrstufige Verfahren kennen gelernt. Jeder einzelne wird durch ein bestimmtes
Überdeckungskriterium definiert, das es mit der anschließend konstruierten Testfall-
menge zu erfüllen gilt. Soweit die Theorie. In der Praxis wird bei der Durchführung
eines White-Box-Tests äußerst selten eine vollständige Testabdeckung erreicht. Ins-
besondere haben die sehr kurzen, in Abschnitt 4.4.2 angeführten Beispiele gezeigt,
dass selbst die vergleichsweise primitive Anweisungsüberdeckung in der Praxis nur
selten zu 100 % erfüllt werden kann.
Genau an dieser Stelle kommen die verschiedenen Überdeckungsmetriken ins
Spiel. Diese messen den Grad der erreichten Testabdeckung und geben uns ein Mit-
tel an die Hand, um die Güte einer gegebenen Testfallmenge quantitativ zu erfassen.
Da in der industriellen Software-Entwicklung fast ausschließlich die Anweisungs-
und Pfadüberdeckung eine praktische Rolle spielen, werden zur Datenerhebung ty-
pischerweise die folgenden beiden Überdeckungsmetriken zugrunde gelegt:
Anzahl der überdeckten Knoten
MC0 = · 100 [ % ] (4.8)
Anzahl der Knoten
232 4 Software-Test
Intra-Modulbewertung Inter-Modulbewertung
100 %
65 % 80 % 25 %
80 %
Modul Modul Modul
Überdeckung
A B C
Modul Abnahme-
A kriterium
Abgeleitete Entwicklungspriorität:
erfüllt
Modul Modul Modul
C A B
0%
Zeit
I Inter-Modulbewertung
Neben der absoluten Bewertung einer einzigen Testfallmenge erlauben Über-
deckungsmetriken den direkten Vergleich zwischen zwei oder mehreren
Software-Modulen. Hierzu wird die Testabdeckung aller Module separat ge-
messen und dasjenige Modul mit der schwächsten Abdeckung ermittelt (vgl.
Abb. 4.49 rechts). Mit Hilfe der gewonnenen Information lassen sich nicht nur
Schwachstellen in der Testumgebung aufdecken, sondern auch die zur Verfügung
stehenden Ressourcen zielgerichtet einplanen.
Bei der Erhebung von Überdeckungsmetriken wird der Software-Entwickler in der
täglichen Arbeit durch zahlreiche Werkzeuge unterstützt. Ein bekannter Vertreter
4.5 Testmetriken 233
manhattan.c
#include "stdio.h" 1
#include "stdlib.h" 2
3
int manhattan(int a, int b) 4
{ 5
if (a < 0) 6
a = -a; 7
if (b < 0) 8
b = -b; 9
return a+b; 10
} 11
12
int main(int argc, char **argv) 13
{ 14
if (argc != 3) { 15
printf("Usage: %s a b\n", argv[0]); 16
exit(1); 17
} 18
manhattan(atoi(argv[1]), atoi(argv[2])); 19
exit(0); 20
} 21
aus dem Bereich der Open-Source-Software ist der Profiler Gcov, der als Teil der
GNU Compiler Collection (GCC) eine weite Verbreitung besitzt.
Um das Funktionsprinzip von Gcov zu verstehen, betrachten wir das in Abb. 4.50
dargestellte C-Programm. Mit der Funktion manhattan treffen wir auf einen alten
Bekannten, der uns bereits weiter oben als Demonstrationsobjekt für die verschie-
denen Überdeckungskriterien diente. Die zusätzlich hinzugefügte Funktion main
nimmt zwei Integer-Werte als Kommandozeilenparameter entgegen und reicht diese
direkt an die Funktion manhattan weiter. Mit Hilfe von Gcov erfolgt die Durchfüh-
rung einer Überdeckungsanalyse in drei Schritten (vgl. Abb. 4.51):
I Compilieren
Das Beispielprogramm wird über den Aufruf
gcc -fprofile-arcs -ftest-coverage manhattan.c
Map le
.gcno
.c
gcc [Link] gcov .gcov
.cpp
Quelltext Coverage
.gcda report
Data le
– -fprofile-arcs
Diese Option veranlasst den Compiler, zusätzlichen Runtime code in die er-
zeugte Programmdatei einzufügen. Der zusätzliche Code bewirkt, dass das
übersetzte Programm während der Ausführung Kontrollflussinformationen
sammelt und diese im Hintergrund in einer Log-Datei mit der Endung .gcda
aufzeichnet.
I Ausführen
Im Anschluss an die Übersetzung werden die einzelnen Testfälle der Reihe nach
ausgeführt. Für unsere Beispielbetrachtung gehen wir von der Abarbeitung der
folgenden Testfälle aus:
> manhattan 1 1
> manhattan 2 2
> manhattan 1 -1
Der durch die Option -fprofile-arcs hinzugefügte Code bewirkt, dass am En-
de der ersten Programmausführung die gesammelten Kontrollflussdaten in ei-
ne Log-Datei mit dem Namen [Link] geschrieben werden. Mit jeder
weiteren Ausführung wird die Datei automatisch aktualisiert, so dass die Infor-
mationen der vorangegangenen Programmausführungen nicht verloren gehen.
I Analysieren
Sind alle Testfälle ausgeführt, werden die erfassten Daten ausgewertet. Für unser
Beispielprogramm wird die Analyse mit dem Befehl
gcov manhattan.c
gestartet. Gcov liest die während der Compilierung bzw. Ausführung erzeugten
Dateien [Link] und [Link] ein und generiert hieraus die
folgende Ausgabe:
4.5 Testmetriken 235
[Link]
-: 0:Source:manhattan.c 1
-: 0:Graph:[Link] 2
-: 0:Data:[Link] 3
-: 0:Runs:3 4
-: 0:Programs:1 5
-: 1:#include "stdio.h" 6
-: 2:#include "stdlib.h" 7
-: 3: 8
-: 4:int manhattan(int a, int b) 9
3: 5:{ 10
3: 6: if (a < 0) 11
#####: 7: a = -a; 12
3: 8: if (b < 0) 13
1: 9: b = -b; 14
3: 10: return a+b; 15
-: 11:} 16
-: 12: 17
-: 13:int main(int argc, char **argv) 18
3: 14:{ 19
3: 15: if (argc != 3) { 20
#####: 16: printf("Usage: %s a b\n", argv[0]); 21
#####: 17: exit(1); 22
-: 18: } 23
3: 19: manhattan(atoi(argv[1]), atoi(argv[2])); 24
3: 20: return 0; 25
-: 21:} 26
-: 22: 27
gcov manhattan.c
File ’manhattan.c’
Lines executed:75.00% of 12
manhattan.c:creating ’[Link]’
gcov_fool.c
#include "stdio.h" 1
#include "stdlib.h" 2
3
int manhattan(int a, int b) 4
{ 5
if (a < 0) a = -a; 6
if (b < 0) b = -b; 7
return a+b; 8
} 9
10
int main(int argc, char **argv) 11
{ 12
manhattan(1, 1); 13
exit(0); 14
} 15
Abb. 4.53 Bei der Verwendung von Gcov ist Vorsicht geboten. Die berechnete Zeilenüberdeckung
ist nicht nur schwächer als die Anweisungsüberdeckung, sondern auch anfällig gegenüber Forma-
tierungsänderungen
mittelten Überdeckungsmaße muss jedoch stets mit Bedacht erfolgen. Der Grund
hierfür liegt in der Eigenschaft von Gcov, alle Anweisungen einer Programmzeile
als Einheit zu betrachten. Mit anderen Worten: Gcov berechnet eine Zeilenüber-
deckung. Dieses Kriterium ist nicht nur schwächer als das der Anweisungsüber-
deckung, sondern auch äußerst sensitiv gegenüber Formatänderungen. Als Beispiel
ist in Abb. 4.53 eine alternative Variante unseres Beispielprogramms dargestellt, in
der die Funktion manhattan ein einziges Mal mit den Werten 1, 1 aufgerufen wird.
Die Implementierung der Funktion ist mit der ursprünglichen Variante funktional
identisch – es wurden lediglich die beiden If-Bedingungen syntaktisch umforma-
tiert. Da die bedingten Zuweisungen jetzt in der gleichen Zeile wie die If-Bedingung
selbst stehen, werden diese als Einheit betrachtet und unabhängig vom logischen
Wert der If-Bedingung stets als ausgeführt betrachtet. Folgerichtig ermittelt Gcov
für unsere modifizierte Programmvariante eine Abdeckung von 100 %:
File ’manhattan2.c’
Lines executed:100.00% of 7
manhattan2.c:creating ’[Link]’
Java sowie alle Dialekte des .NET-Frameworks. Neben der Verwendung als Ein-
zelapplikation fügt sich das Werkzeug nahtlos in die Entwicklungsumgebungen Vi-
sual Studio der Firma Microsoft oder die WebSphere Application Studio Workbench
der Firma IBM ein. Eine Integration in die freie Entwicklungsumgebung Eclipse
wird ebenfalls unterstützt.
Abb. 4.54 zeigt die grafische Windows-Oberfläche während der Analyse der
Beispielfunktion manhattan. PureCoverage unterscheidet vier verschiedene Über-
deckungsstufen, die unter anderem zu einer unterschiedlichen Einfärbung des ana-
lysierten Quelltextes führen:
I Hit lines (Blau)
Alle Anweisungen in der betreffenden Programmzeile wurden mindestens ein-
mal ausgeführt.
4.5.2 Mutationstest
Im Gegensatz zu den herkömmlichen Testtechniken dient der Mutationstest nicht
primär der Suche nach Software-Defekten. Stattdessen haben wir es hier mit einer
Technik zu tun, die uns erlaubt, verschiedene Testverfahren quantitativ zu bewerten.
Der Mutationstest basiert auf dem Prinzip der Fehlerinduktion. Im Kern dieser
Verfahrensweise steht die Erzeugung sogenannter Mutanten. Diese entstehen aus
dem Originalprogramm, indem an zufällig ausgewählten Stellen künstliche Fehler
eingefügt werden. Wir sprechen in diesem Zusammenhang von einer Mutations-
bzw. Fehlertransformation. Die meisten der in der Praxis verwendeten Mutations-
transformationen lassen sich einer der folgenden Fehlerklassen zuordnen:
I Konstantenfehler
Im Zuge der Fehlertransformation werden eine oder mehrere Konstanten des Ori-
ginalprogramms geändert. Typische Transformationen inkrementieren oder de-
krementieren den ursprünglichen Wert um einen festen Betrag oder skalieren die
Konstante mit einem zufälligen Faktor.
I Variablenfehler
In diese Klasse fallen alle Transformationen, die durch die Manipulation ei-
ner Variablenreferenz entstehen. Wird eine Variable auf der linken Seite eines
Gleichheitszeichens editiert, so sprechen wir von einem Zuweisungsfehler.
I Arithmetikfehler
Transformationen dieser Kategorie umfassen unter anderem den Austausch zwei-
er arithmetischer Verknüpfungen (Verknüpfungsfehler) sowie die Verfälschung
des Ergebnisses einer arithmetischen Operation um einen konstanten Wert (Off-
setfehler).
I Logikfehler
Diese Kategorie umfasst alle Transformationen, die zu einer falschen Verwen-
dung der logischen Wahrheitswerte True und False führen. Entsprechende Mu-
tanten lassen sich bilden, indem der Inhalt einer booleschen Variablen geändert
wird oder die If- und Else-Abschnitte eines Konditionalbefehls durch das Inver-
tieren der Verzweigungsbedingung miteinander vertauscht werden.
Als Beispiel sind in Abb. 4.55 vier Mutationen der Funktion manhattan dargestellt.
I Der erste Mutant enthält einen Konstantenfehler in der ersten Verzweigungsbe-
dingung. Hier wurde der Vergleichswert 0 durch −1 ersetzt.
4.5 Testmetriken 239
I Konstantenfehler I Variablenfehler
manhattan_error1.c manhattan_error2.c
int 1 int 1
manhattan(int a, int b) 2 manhattan(int a, int b) 2
{ 3 { 3
if (a < -1) a = -a; 4 if (b < 0) a = -a; 4
// "-1" anstelle "0" 5 // "b" anstelle "a" 5
if (b < 0) b = -b; 6 if (b < 0) b = -b; 6
return a+b; 7 return a+b; 7
} 8 } 8
I Arithmetikfehler I Logikfehler
manhattan_error3.c manhattan_error4.c
int 1 int 1
manhattan(int a, int b) 2 manhattan(int a, int b) 2
{ 3 { 3
if (a < 0) a = -a; 4 if (a < 0) a = -a; 4
if (b < 0) b = -b; 5 if (b <= 0) b = -b; 5
return a-b; 6 // "<=" anstelle "<" 6
// "-" anstelle "+" 7 return a+b; 7
} 8 } 8
I Der zweite Mutant enthält einen Variablenfehler. Anstelle von a wird in der ers-
ten Verzweigungsbedingung die Variable b referenziert.
I Der dritte Mutant enthält einen Arithmetikfehler. Als Ergebniswert wird nicht
die Summe, sondern die Differenz von a und b zurückgegeben.
I Der vierte Mutant enthält einen Logikfehler in Form einer geänderten Verknüp-
fung. In der zweiten Verzweigungsbedingung wird der Vergleichsoperator <= an-
stelle von < verwendet.
Der Mutationstest existiert in verschiedenen Ausprägungen, insbesondere werden
in der Praxis eine starke und eine schwache Variante unterschieden. Beide werden
wir in den folgenden Abschnitten einer genaueren Betrachtung unterziehen.
Vergleichender Mutationstest
A'A'
A'
T1 = {manhattan(-1,-1), manhattan(1,1)}
T2 = {manhattan(-1,1), manhattan(1,-1)}
Führen wir die Testfälle von T1 und T2 auf den mutierten Varianten der Funktion
manhattan aus, so erhalten wir das folgende Ergebnis:
Prädizierender Mutationstest
M
Muttations
ti
Mutations-
Anzahl transformation Anzahl
identi"zierter = Testfälle = gefundener
Mutanten Fehler
Spezi"kation
A'
A'A'
Erzeugte Mutanten
Anzahl Fehler Gefundene Fehler
Identi"zierte Mutanten
diejenigen Fehlertransformationen eine Rolle spielen, die eine nach außen hin sicht-
bare Diskrepanz zwischen dem Originalprogramm und dem abgeleiteten Mutanten
erzeugen.
Schließen wir alle äquivalenzerhaltenden Transformationen aus unserer Betrach-
tung aus, so identifiziert die Testfallmenge T1 insgesamt 66 % aller Mutanten. T2
kommt sogar auf eine Identifikationsquote von 100 %. Eine Testfallmenge mit die-
ser Eigenschaft heißt adäquat. Für unsere Beispielfunktion war es ein Leichtes, eine
adäquate Testfallmenge zu erzeugen – lediglich 2 Testfälle reichten hierfür aus. Für
industrietypische Software-Systeme ist die Erzeugung einer solchen Testfallmenge
aufgrund der extrem hohen Komplexität jedoch kaum noch möglich.
Vergleichen wir die weiter oben eingeführten Fehlertransformationen mit rea-
len Software-Fehlern, so weisen letztere in vielen Fällen eine deutlich komplexere
Struktur auf. Der schwache Mutationstest basiert an dieser Stelle auf der Annahme,
dass eine Testmenge, die einen Großteil der Elementarfehler identifiziert, auch kom-
plexe Fehler in gleichem Maße aufdecken kann. In der Literatur wird dieser Zusam-
menhang als Kopplungseffekt beschrieben [69]. In der Tat legen einige empirische
Untersuchungen die Existenz eines starken Kopplungseffekts nahe [194, 195].
Das Anwendungsspektrum des vergleichenden Mutationstests ist nicht auf den
Vergleich zweier Testfallmengen beschränkt. Immer dann, wenn die betrachteten
Mengen systematisch konstruiert wurden, lässt die Maßzahl einen direkten Rück-
schluss auf die Leistungsfähigkeit des zugrunde liegenden Testkonstruktionsverfah-
rens zu [102].
Eine abweichende Spielart des starken Mutationstests ist dessen prädizierende
Variante. Diese erlaubt, die Gesamtzahl der in einem Software-System vorhandenen
Fehler in gewissen Grenzen abzuschätzen (vgl. Abb. 4.57). Ausgehend von einer
Testfallmenge T wird die Software wie gewöhnlich überprüft – die Testfälle wer-
den mit Hilfe des Originalprogramms ausgeführt und die produzierte Ausgabe mit
der Spezifikation abgeglichen. Festgehalten wird die Anzahl der real gefundenen
242 4 Software-Test
Fehler. Anschließend wird der starke Mutationstest durchgeführt und die prozentua-
le Anzahl der identifizierten Mutanten gemessen. Der ermittelte Prozentwert dient
als Gütemaß der Testfallmenge, mit dem sich die Gesamtzahl der real vorhandenen
Defekte wie folgt abschätzen lässt:
Erzeugte Mutanten
Anzahl Fehler ≈ Gefundene Fehler ×
Identifizierte Mutanten
Die Anzahl der gefundenen Fehler in Gleichung (4.10) bezieht sich stets auf die
im Programmtext vorhandenen, voneinander unabhängigen Fehlerstellen, die mit
einem oder mehreren der betrachteten Testfälle identifiziert werden konnten. Insbe-
sondere reicht es an dieser Stelle nicht aus, nur die Anzahl fehlgeschlagener Test-
fälle zu zählen – Doppelzählungen wären die Folge.
Auch wenn die Möglichkeit einer Fehlervorhersage verlockender nicht sein
könnte, muss die Interpretation der approximierten Maßzahl stets mit Bedacht er-
folgen. Zum einen liefert Abschätzung im Sinne von Gleichung (4.10) nur für große
Mutations- und Testmengen statistisch relevante Informationen. Zum anderen ba-
siert die Fehlerabschätzung auf der expliziten Annahme, dass die zugrunde liegende
Testfallmenge künstlich induzierte und reale Fehler mit gleicher Wahrscheinlichkeit
identifiziert.
(a < 0) = (a ≤ 0) (4.10)
4.6 Grenzen des Software-Tests 243
Testfall Testfall
C C' C C'
Mutations- Mutations-
transformation transformation
Gleichung (4.10) ist gleichbedeutend mit a = 0. Mit anderen Worten: Der schwache
Mutationstest verlangt, dass die Funktion mit Testfällen aufgerufen wird, in denen
der Parameter a mindestens einmal den Wert 0 besitzt. Dabei ist es unerheblich,
dass sich die betrachtete Fehlertransformation in keiner Weise auf das nach außen
sichtbare Programmverhalten auswirkt. Der schwache Mutationstest ist damit nur
begrenzt für die Leistungsbewertung einer Testfallmenge geeignet, da hier die Iden-
tifikation realer, d. h. nach außen erkennbarer Fehler im Vordergrund steht. Stattdes-
sen besitzt der schwache Mutationstest den Charakter einer Überdeckungsmetrik –
eine wesentliche Eigenschaft, die zu einer klaren Abgrenzung zwischen der starken
und der schwachen Testvariante führt.
I Programmkomplexität
Schon für sehr kleine Programme nimmt die Anzahl der möglichen Eingabe-
244 4 Software-Test
I Mangelnde Werkzeugunterstützung
Unter den zahlreichen Werkzeugen zur Software-Entwicklung finden sich nur
wenige, die den Programmierer bei der Konstruktion von Software-Tests unter-
stützen. Die meisten Applikationen aus diesem Bereich beschränken sich auf
die automatisierte Durchführung, auf die wir in Abschnitt 8.2 im Detail zurück-
kommen werden. Bei der Testfallkonstruktion ist der Software-Entwickler immer
noch weitgehend auf sich alleine gestellt.
I Fehlende Management-Unterstützung
Nicht selten wird der Software-Test von Seiten des Managements nur stiefmütter-
lich behandelt oder sogar vollständig in die Verantwortung der Entwickler gelegt.
Entsprechend häufig wird er als eine Entwicklungsaktivität unter vielen betrach-
tet und in der Planung nicht explizit berücksichtigt. Erschwerend kommt hinzu,
dass der Software-Test zu den weichen Tätigkeiten gehört, für die eine Zeitab-
schätzung ohnehin nur schwer durchführbar ist.
I Zeitprobleme
Die meisten Software-Entwickler beklagen einen Mangel an Zeit, der ihnen für
die Durchführung von Software-Tests zur Verfügung steht. Neben der oft nur
spärlichen Unterstützung von Seiten des Managements ist ein Teil des Problems
auch hausgemacht: Der oft grenzenlose Optimismus veranlasst viele Program-
mierer dazu, den Testaufwand wieder und wieder zu unterschätzen. Der knappe
Terminplan sorgt in vielen Projekten für den Rest. Da der Testaufwand eine va-
riable Größe ist, fällt er dem Rotstift nicht selten zuerst zum Opfer. Genug Zeit
zum Testen bleibt dann nur noch, wenn das Projekt optimal verläuft – nur leider
ist dies so gut wie nie der Fall.
hard_to_test1.c hard_to_test2.c
float foo(unsigned char x) 1 float bar(unsigned short x) 1
{ 2 { 2
unsigned char i = 0; 3 unsigned char i = 0; 3
4 4
while (i < 6) { 5 while (i < 6) { 5
i++; 6 i++; 6
x /= 2; 7 x /= 2; 7
} 8 } 8
return 1.0 / (x-i); 9 return 1.0 / (x-i); 9
} 10 } 10
Abb. 4.59 Der vorhandene Programmfehler ist mit den Mitteln des klassischen Software-Tests
kaum zu finden
des Software-Tests kann ausschließlich das Vorhandensein von Fehlern gezeigt wer-
den, niemals aber die Korrektheit eines Programms.
Glenford Myers verdeutlich in seinem Buch The Art of Software Testing auf
plakative Art und Weise, wie hoffnungslos die Durchführung eines vollständigen
Software-Tests in der Praxis ist:
“Consider attempting an exhaustive black-box test of a C++ compiler. Not
only would you have to create test cases representing all valid C++ pro-
grams ([...]), but you would have to create test cases for all invalid C++
programs ([...]) to ensure that the compiler detects them as being invalid.”
Glenford J. Myers [187]
Bereits in kleinen Projekten ist der Software-Test nur noch eine partielle Methode
für die Suche nach Fehlern – selbst mit riesigen Testdatenbanken decken die ausge-
führten Testfälle nur einen Bruchteil des gesamten Wertebereichs ab. Wie weitma-
schig das Netz des Software-Tests wirklich ist, sollen die beiden Beispielprogramme
in Abb. 4.59 verdeutlichen.
Das linke Programm nimmt einen 8-Bit-Integer-Wert x entgegen und dividiert
diesen sukzessive durch 2. Nach genau 6 Iterationen bricht die Schleife ab und
1
gibt den Wert x−6 an den Aufrufer zurück. Beachten Sie bei der Programmanalyse,
dass innerhalb der While-Schleife eine Integer-Division durchgeführt und damit das
Zwischenergebnis in jedem Schritt auf den nächsten ganzzahligen Wert abgerundet
wird. Folgerichtig existieren für jeden Rückgabewert stets mehrere Eingabewerte,
die das gleiche Endergebnis erzeugen.
Während das linke Programm klaglos seine Dienste verrichtet und für jeden der
256 möglichen Eingabewerte das korrekte Ergebnis berechnet, führen manche Ein-
gaben, z. B. der Wert 402, in der rechts abgebildeten Implementierungsvarianten zu
einer Division durch 0. Die Implementierung verwendet zwar intern den gleichen
Algorithmus, allerdings wurde die Bitbreite des Eingabeparameters x auf 16 Bit er-
weitert. Von den jetzt 216 = 65536 möglichen Eingaben gibt es genau 64, die eine
Division durch 0 provozieren. Keine der in diesem Kapitel eingeführten Testtechni-
ken ist in der Lage, die Kombinationen systematisch herzuleiten. Die Chance, den
246 4 Software-Test
Die Code-Analyse umfasst alle Methoden und Techniken, die a posteriori sicher-
stellen, dass ein Software-System den geforderten Qualitätsanforderungen genügt.
Alle in diesem Kapitel untersuchten Techniken sind statischer Natur, d. h., die Über-
prüfung erfolgt durch eine Sichtung der Quelltexte, ohne diese in ein ausführbares
Programm zu übersetzen. Die statische Code-Analyse kann automatisiert oder ma-
nuell erfolgen und wird im Gegensatz zu den Verfahren der konstruktiven Quali-
tätssicherung stets auf bereits erstellte Software-Komponenten angewendet. In den
folgenden Abschnitten werden wir die für die Praxis wichtigsten Analysetechniken
im Detail kennen lernen.
5.1 Software-Metriken
Software-Metriken geben uns ein leistungsfähiges Hilfsmittel an die Hand, um be-
stimmte Aspekte eines Software-Systems systematisch und quantitativ zu erfassen.
Um deren weitreichende Bedeutung im Bereich der Software-Entwicklung verste-
hen und einordnen zu können, besinnen wir uns für einen Moment an die in Kapi-
tel 1 postulierten Qualitätsmerkmale zurück. Die äußeren Merkmale der Funktiona-
lität, Zuverlässigkeit, Effizienz und Benutzbarkeit wurden durch die inneren Merk-
male der Übertragbarkeit, Änderbarkeit, Testbarkeit und Transparenz komplettiert.
Der Umgang mit den verschiedenen Qualitätsmerkmalen erweist sich in der Pra-
xis als unterschiedlich schwierig. Im positiven Sinne sticht an dieser Stelle das Kri-
terium der Effizienz hervor, das sich in den meisten Fällen exakt spezifizieren und
messen lässt. Für andere Merkmale, wie z. B. die Benutzbarkeit eines Software-
Systems, gestaltet sich bereits die Formulierung der Anforderungen als schwierig.
So sind die Ergonomieeigenschaften einer Applikation zwar subjektiv zu spüren,
aber nur schwer zu quantifizieren. Ähnliches gilt für das Kriterium der Transpa-
renz. Die objektive Beurteilung, inwieweit der untersuchte Programmcode dieses
Kriterium erfüllt, ist ohne die Vereinbarung fester Kenngrößen unmöglich.
Genau an dieser Stelle kommen Software-Metriken ins Spiel, mit deren Hilfe
bestimmte Kenngrößen eines Software-Systems quantitativ erfasst werden können.
Objektivität Ökonomie
Robustheit Korrelation
Vergleichbarkeit Verwertbarkeit
Die in den vergangenen Jahren postulierten Metriken beschränken sich dabei nicht
auf die Qualitätsparameter des Produkts Software, sondern decken in gleichem Ma-
ße den Bereich des Software-Managements ab. Beispiele sind die Bestimmung des
Projektfortschritts oder der Mitarbeiterzufriedenheit. Trotz ihrer Unterschiede die-
nen die Metriken beider Kategorien am Ende dem gleichen Zweck: Durch die quan-
titative Erfassung der gemessenen Kenngrößen werden vormals unsichtbare Aspek-
te der Software sichtbar und auf einer objektiven Ebene vergleichbar. Software-
Metriken bilden damit die Grundlage, um steuernd in die Software-Entwicklung
eingreifen zu können – sei es auf der Ebene des Projektmanagements oder der Pro-
duktentwicklung. Tom DeMarco fasst die Bedeutung von Software-Metriken wie
folgt zusammen:
“You can’t manage what you can’t control, and you can’t control what you
don’t measure. To be effective software engineers or software managers, we
must be able to control software development practice. If we don’t measure
it, however, we will never have that control.”
Tom DeMarco [67]
I Robustheit
Die Ergebnisse müssen belastbar sein, d. h., die Wiederholung der Messung muss
für jede Kenngröße das gleiche Ergebnis liefern. Die Erfüllung des Objektivitäts-
kriteriums ist hierfür eine notwendige, aber keine hinreichende Bedingung.
5.1 Software-Metriken 249
I Vergleichbarkeit
Zu jeder Zeit müssen verschiedene Messungen der gleichen Kenngröße zueinan-
der in Relation gesetzt werden können. Hierdurch werden die erhobenen Para-
meter gruppen- oder produktübergreifend vergleichbar und gleichzeitig die Vor-
aussetzungen geschaffen, um Software-Metriken zu Steuerungszwecken gewinn-
bringend einsetzen zu können.
I Ökonomie
Die Erhebung einer Software-Metrik muss kostenökonomisch durchgeführt wer-
den können. Lässt sich eine Maßzahl automatisiert berechnen, so reduzieren
sich die Ausgaben im Wesentlichen auf die Anschaffungskosten entsprechender
Werkzeuge. Andere Messgrößen lassen sich nur mit erheblichem Personaleinsatz
bestimmen und verursachen auf diese Weise beträchtliche Mehrkosten.
I Korrelation
Eine Software-Metrik korreliert umso stärker, je eher die Messergebnisse einen
soliden Rückschluss auf die überwachte Kenngröße zulassen. Für einige Quali-
tätsparameter, wie die Laufzeiteffizienz eines Programms, lassen sich korrelie-
rende Metriken vergleichsweise einfach formulieren. Für andere Parameter, wie
die bereits oben angeführte Transparenz, kann eine starke Korrelation nur sehr
eingeschränkt garantiert werden.
I Verwertbarkeit
Der Einsatz einer Software-Metrik macht in der Praxis nur dann Sinn, wenn un-
terschiedliche Messergebnisse das zukünftige Handeln unterschiedlich beeinflus-
sen. Das Kriterium der Verwertbarkeit klingt trivial, wird in der Praxis jedoch
vielfach konterkariert. Die in einigen Firmen zum Selbstzweck degradierte Erhe-
bung von Daten ist eine der Hauptursachen für die Skepsis, die viele Program-
mierer diesem Thema entgegenbringen.
In den folgenden Abschnitten werden wir die wichtigsten der in der Vergangenheit
vorgestellten Software-Metriken genauer beleuchten. Dabei beschränken wir uns
zunächst auf die Messung von Kenngrößen, die das Produkt Software selbst betref-
fen. Aspekte des Software-Managements, für die Metriken ebenfalls eine wichtige
Rolle spielen, sind Bestandteil von Kapitel 9 und werden dort erneut aufgegriffen.
Die NCSS-Metrik (NCSS = Non Commented Source Statements) ist eine Wei-
terentwicklung der LOC-Metrik. Die NCSS-Kennzahl berechnet sich ebenfalls aus
der Summe der Quellcode-Zeilen, ignoriert im Gegensatz zu LOC jedoch sämtliche
Kommentare. In vielen Fällen wird die Metrik zur Bestimmung des Dokumentati-
onsgrads eines Software-Moduls eingesetzt, indem der ermittelte NCSS-Kennwert
durch den LOC-Kennwert geteilt wird.
So einfach sich die LOC- und NCSS-Metrik berechnen lassen, so mäßig ist ihre
Aussagekraft. Insbesondere hat der verwendete Programmierstil einen maßgebli-
chen Einfluss auf die gemessenen Kennzahlen, da bereits die einfache Umforma-
tierung eines Programms die LOC- und NCSS-Messwerte erheblich verändern. Mit
anderen Worten: Beide Metriken können das Kriterium der Vergleichbarkeit nur un-
zureichend erfüllen.
Zur Verbesserung der Aussagekraft wurden in der Vergangenheit andere Zähl-
techniken herangezogen, die sich mehr an der inneren Struktur des Programms und
weniger an dessen visueller Repräsentation orientieren. Die folgenden Beispiele
sind eine kleine Auswahl entsprechender Metriken:
I Zähle alle Zeilen, die ausführbaren Code enthalten.
Faktor
15 15
...
10 10
...
6
5 5.0 5.0 5.0
4.5 4.5
4 4.0
3.5
3 3.0 3.0 3.0
2.5
2
1 1.0
0
FORTRAN
Assembler
PROLOG
Modula 2
Smalltalk
PASCAL
COBOL
ALGOL
BASIC
LISP
APL
PL/I
Ada
C
5.1.2 Halstead-Metriken
1977 postulierte Maurice Howard Halstead mehrere aufeinander aufbauende Metri-
ken, die unmittelbar aus der lexikalischen Struktur des untersuchten Programmtex-
tes hergeleitet werden [110, 111]. Die Berechnung und Interpretation der Kenn-
größen basieren dabei auf empirischen Ergebnissen, die insbesondere einen Zu-
sammenhang zwischen der Programmkomplexität und der Auftrittscharakteristik
verschiedener lexikalischer Elemente herstellen. Hierzu teilt Halstead die einzel-
nen Programmelemente auf der obersten Ebene in Operatoren und Operanden (vgl.
Abb. 5.3) ein. Operatoren umfassen alle von einer Programmiersprache bereitge-
stellten Konstrukte wie Schlüsselwörter, vordefinierte Operatoren oder Präprozes-
252 5 Statische Code-Analyse
Quell-
text
Vorbereitung:
Aufteilung der
Operatoren Operanden
Quellcode-Elemente
in Operatoren und
Schlüsselwörter Operanden Bezeichner
Operatoren Konstanten
... ...
Abb. 5.3 Messung der textuellen Komplexität mit Hilfe der Halstead-Metriken
Als Beispiel betrachten wir die beiden in Abb. 5.4 dargestellten C-Programme zur
Berechnung des größten gemeinsamen Teilers (ggT) zweier positiver, ganzer Zah-
len. Beide Algorithmen basieren auf dem Prinzip der Wechselwegnahme, das der
griechische Mathematiker Euklid bereits um 300 vor Christus formulierte [252].
Die erste Programmvariante implementiert den klassischen Algorithmus. Dieser re-
duziert den jeweils größeren Operanden solange um den kleineren, bis beide densel-
ben Wert erreichen. Sobald die Iteration abbricht, ist der größte gemeinsame Teiler
gefunden. Die zweite Implementierung entspricht der heute üblichen Form. Der Al-
gorithmus ersetzt die Subtraktion durch die Modulo-Division und terminiert für die
meisten Zahlenpaare deutlich schneller.
Als Grundlage für die Berechnung der Halstead-Metriken sind die unterschiedli-
chen Operatoren und Operanden zusammen mit deren Auftrittshäufigkeiten tabella-
risch aufgelistet. Zusammengehörige Operatoren, wie z. B. die Schlüsselwörter do
und while, sowie öffnende und schließende Klammern werden jeweils als ein ein-
ziger Operator gezählt. Ebenso unterscheidet Halstead nicht zwischen runden und
geschweiften Klammern, da beide der Gruppierung von Programmausdrücken die-
nen.
Aus den Tabelleneinträgen lassen sich die vier Basisgrößen direkt extrahieren.
Für die Implementierung ggt1 erhalten wir die folgenden Werte:
5.1 Software-Metriken 253
Variante 1 Variante 2
int ggt1(int x, int y) 1 int ggt2(int x, int y) 1
{ 2 { 2
while (x != y) { 3 int r; 3
4 4
if (x > y) 5 do { 5
x -= y; 6 r = x % y; 6
else 7 x = y; 7
y -= x; 8 y = r; 8
} 9 } while (y != 0); 9
return x; 10 return x; 10
} 11 } 11
Operatoren Operatoren
int (3×) (), {} (5×) int (4×) (), {} (4×)
, (1×) while (1×) , (1×) do while (1×)
!= (1×) if else (1×) != (1×) = (3×)
> (1×) -= (2×) % (1×) ; (6×)
return (1×) ; (3×) return (1×) ggt2 (1×)
ggt1 (1×) Operanden
Operanden 0 (1×) x (4×)
x (6×) y (5×) y (5×) r (3×)
Abb. 5.4 Berechnung des größten gemeinsamen Teilers zweier ganzer Zahlen
η1ggt1 = 11 (5.5)
η2ggt1 =2 (5.6)
N1ggt1 = 20 (5.7)
N2ggt1 = 11 (5.8)
η1ggt2 = 10 (5.9)
η2ggt2 =4 (5.10)
N1ggt2 = 23 (5.11)
N2ggt2 = 13 (5.12)
Für einige der weitergehenden Überlegungen von Halstead ist das Wissen über die
pure Anzahl der im Programmtext auftauchenden Operanden nicht ausreichend. Um
die entsprechenden Kenngrößen ebenfalls berechnen zu können, müssen die vier
Basisparameter um einen weiteren ergänzt werden:
Die Größe η2∗ beschreibt die minimale Anzahl der Operanden, die zur Implemen-
tierung eines bestimmten Algorithmus benötigt werden. Gehen wir davon aus, dass
eine ideale Lösung ohne die Verwendung eines einzigen internen Operanden pro-
grammiert werden kann, so lässt sich die minimale Anzahl der Operanden nach un-
ten durch die Anzahl der Operanden abschätzen, die dem Programm als Eingabe zur
Verfügung gestellt bzw. als Ausgabe zurückgegeben werden. Werden die Halstead-
Metriken für eine einzelne Funktion berechnet, entspricht der Wert von η2∗ exakt
der Anzahl der Funktionsparameter plus eins. Damit ergibt sich für die beiden oben
eingeführten Beispielprogramme die folgende Beziehung:
Basierend auf den Basisgrößen (5.1) bis (5.4) sowie der zusätzlichen Größe (5.13)
lassen sich alle in [111] vorgeschlagenen Metriken ableiten. Die Kenngröße η heißt
die Größe des Vokabulars und entspricht schlicht der Anzahl der verschiedenen Ele-
mentarsymbole, mit denen ein Programm verfasst ist. Entsprechend der Klassifika-
tion der Symbole in Operatoren und Operanden berechnet sich η durch einfache
Addition der Basisparameter η1 und η2 :
η = η1 + η2 (5.15)
Für unsere Beispielprogramme aus Abb. 5.4 erhalten wir das folgende Zwischener-
gebnis:
Die Kenngröße η ∗ entspricht der Größe des Minimalvokabulars. Dieses enthält die
Operanden und Operatoren, die zur Implementierung eines Algorithmus in einer
beliebig wählbaren, fiktiven Programmiersprache mindestens benötigt werden. Hal-
stead verfolgte den Gedanken, dass das einfachste Programm zur Lösung einer be-
stimmten Aufgabe in einer Programmiersprache verfasst sein würde, die den be-
nötigten Algorithmus in Form eines eigens dafür geschaffenen Operators Δ direkt
unterstützt. Unser Programm bestünde dann nur noch aus einer einzigen Zeile, die
einen Operator Δ auf alle Quelloperanden anwendet und das Ergebnis dem Zielope-
randen zuweist. Damit hätte das Programm in etwa die folgende Form:
Die Anzahl der Operatoren in diesem Programm beträgt 2 (Δ , =) und die Anzahl der
Operanden entspricht η2∗ . Somit berechnet sich die Größe des Minimalvokabulars
wie folgt:
η ∗ = η2∗ + 2 (5.19)
Damit gilt:
Als weiteres Maß definiert Halstead die Länge N eines Programms als die Gesamt-
zahl der Vorkommen aller Operanden und Operatoren:
N = N1 + N2 (5.22)
Im Gegensatz zur NCSS-Metrik ist die ermittelte Kenngröße unabhängig von der
Formatierung der Quelltexte und damit als Umfangsmetrik besser geeignet. Trotz-
dem bleiben auch hier einige der Schwierigkeiten bestehen, die wir bereits für die
Metriken LOC und NCSS herausgearbeitet haben. Insbesondere ist der Vergleich
der Maßzahlen für Programme wenig aussagekräftig, wenn diese in verschiedenen
Programmiersprachen verfasst sind.
Als zweite Umfangsgröße definiert Halstead das Volumen eines Programms wie
folgt:
V = N × log2 η (5.23)
Im Gegensatz zur Länge N misst das Volumen V den Programmumfang als die An-
zahl der Bits, die zur Darstellung der Implementierung auf Binärebene nötig sind.
Halsteads Formel liegt die Annahme zugrunde, dass alle Operanden und Operato-
ren mit der gleichen Anzahl Bits codiert werden. Bei einer Vokabulargröße von η
werden für eine zweiwertige Codierung log2 η Bits benötigt. Angewendet auf die
beiden Beispielprogramme ergeben sich die folgenden Maßzahlen:
Die Größe V ∗ wird als Minimalvolumen eines Programms bezeichnet und entspricht
der minimalen Länge einer Implementierung, die in der weiter oben eingeführten
fiktiven Programmiersprache verfasst ist. Es gilt:
Angewendet auf die Gleichungen (5.20) und (5.21) berechnet sich das Minimalvo-
lumen des Euklid’schen Algorithmus wie folgt:
Halstead schlägt vor, das minimale Implementierungsvolumen Vmin mit dem Imple-
mentierungsvolumen V in Bezug zu setzen. Die entstehende Maßzahl drückt aus,
wie weit die real vorliegende Implementierung von der Minimalimplementierung
entfernt ist und misst damit indirekt die Transparenz eines Software-Moduls. Die
berechnete Kenngröße wird mit L bezeichnet und heißt der Level der untersuchten
Implementierung:
V∗
L= (5.28)
V
256 5 Statische Code-Analyse
Im optimalen Fall erreicht die Kenngröße L einen Wert von 1. Allerdings handelt es
sich hier um eine theoretische Grenze, die für ernstzunehmende Programme in der
Praxis ganz offensichtlich nicht erreicht werden kann. Für unsere beiden Beispiel-
programme erhalten wir die folgenden Maßzahlen:
12
Lggt1 = = 0, 104 (5.29)
115
12
Lggt2 = = 0, 088 (5.30)
137
Neben der Definition des Levels postulierte Halstead ein zweites Maß L̂, das ge-
nau wie L einen Rückschluss auf die Transparenz eines Programms zulässt, jedoch
vollständig ohne einen Bezug auf das künstlich wirkende Konstrukt der Minimal-
implementierung auskommt. Dazu stellte er mehrere Überlegungen an, wie sich die
Anzahl und Vorkommen von Operatoren und Operanden auf den Level eines Pro-
gramms auswirken. Nach Halstead steht die Anzahl der Operatoren, die in einer spe-
zifischen Implementierung verwendet werden, in einem umgekehrt proportionalen
Verhältnis zum Programm-Level. Mit anderen Worten: Der Level eines Programms
wird durch jeden zusätzlich verwendeten Operator geringer. Berücksichtigen wir zu-
sätzlich, dass die Anzahl der Operatoren eines Programms mindestens zwei beträgt,
so ergibt sich auf direktem Wege die folgende von Halstead postulierte Proportio-
nalitätsbeziehung:
2
L̂ ∼ (5.31)
η1
Genau wie die zunehmende Anzahl der Operatoren den Programm-Level mindert,
führt auch die mehrfache Verwendung von Operanden zu dessen Verringerung. Die
durchschnittliche Wiederholungsrate von Operanden wird mit Hilfe der Kenngröße
P beschrieben und lässt sich unmittelbar aus den Basisgrößen N2 und η2 berechnen:
N2
P= (5.32)
η2
Der kleinstmögliche Wert von P ist 1 und wird von Programmen erreicht, in denen
jeder Operand exakt einmal auftaucht. Je häufiger ein Operand an verschiedenen
Stellen im Programm verwendet wird, desto größer wird P. Die Proportionalitäts-
beziehung (5.31) lässt sich damit um die folgende Beziehung ergänzen:
1
L̂ ∼ (5.33)
P
Aus den Gleichungen (5.31) bis (5.33) leitet Halstead die folgende Definition der
Kenngröße L̂ ab:
2η2
L̂ = (5.34)
η1 N2
Im Gegensatz zur Gleichung (5.28) berechnet sich L̂ unmittelbar aus den gemes-
senen Basisgrößen einer Implementierung. Für unsere Beispielprogramme erhalten
5.1 Software-Metriken 257
L̂ggt1 = 2×2
11×11 = 4
121 = 0, 033 (5.35)
L̂ ggt2
= 2×4
10×13 = 8
130 = 0, 062 (5.36)
Dggt1 = 1
L = 1
0,104 = 9, 62 (5.38)
D ggt2
= 1
L = 1
0,088 = 11, 36 (5.39)
Neben der Beurteilung einzelner Programme wird die Metrik D mitunter auch als
Maßzahl zur Bewertung ganzer Programmiersprachen eingesetzt. Legen wir eine
typische Programmlösung für eine gewisse Aufgabe zugrunde, so lässt die Kenn-
zahl D einen Rückschluss auf die Schwierigkeit zu, diese mit Hilfe der gewählten
Sprache zu lösen.
Als letzte Metrik betrachten wir den Aufwand (effort), der zum Erstellen bzw.
zum Verstehen eines Programms benötigt wird. Nach Halstead berechnet sich der
Aufwand als das Produkt von Programmvolumen und Schwierigkeitsgrad:
E =V ×D (5.40)
Durch die einfache Umformung von Gleichung (5.40) lässt sich ein bekanntes Phä-
nomen der Software-Entwicklung unmittelbar ableiten:
V V V2
E =V ×D = = V∗ = ∗ (5.43)
L V
V
Folgerichtig steigt der Aufwand, der auf ein Programmmodul verwendet werden
muss, überproportional mit dem Code-Volumen an – ein Ergebnis, das dem subjek-
tiven Empfinden der meisten Programmierer entspricht und in der Vergangenheit in
mehreren empirischen Untersuchungen bestätigt wurde [9].
258 5 Statische Code-Analyse
I Vokabular
η = η1 + η2 (5.44)
I Minimalvokabular
η ∗ = η2∗ + 2 (5.45)
I Länge
N = N1 + N2 (5.46)
I Volumen
V = N log2 η (5.47)
I Minimalvolumen
V ∗ = η ∗ log2 η ∗ (5.48)
I Level
V∗
L= (5.49)
V
2η2
L̂ = (5.50)
η1 N2
I Schwierigkeit:
1
D= (5.51)
L
I Aufwand:
E =V ×D (5.52)
Zum Abschluss sind die verschiedenen Metriken in Tabelle 5.5 in einer Übersicht
zusammengefasst. Die Halstead-Kennzahlen gehören zu den ältesten heute noch
eingesetzten Software-Metriken und bestechen vor allem durch ihre Einfachheit.
Zur Berechnung der Kenngrößen müssen lediglich die oben eingeführten Basisgrö-
ßen aus dem Programmtext extrahiert werden. Da hierzu lediglich eine syntaktische
Programmanalyse durchgeführt werden muss, können alle Maßzahlen mit entspre-
chender Werkzeugunterstützung automatisiert berechnet werden.
Trotzdem sind die Halstead-Metriken nicht frei von Kritik. Eine erste Schwä-
che offenbaren die Kennzahlen im Zusammenhang mit der Klassifizierung der Pro-
grammsymbole in Operatoren und Operanden. Diese kann nicht immer so eindeutig
vollzogen werden, wie in unserem Beispiel. Insbesondere in logischen Program-
miersprachen wie LISP, in denen jeder Ausdruck gleichermaßen als Operand und
als Operator fungieren kann, ist eine solche Aufteilung kaum sinnvoll möglich.
In [84, 226, 214, 243, 56] wird ausführlich auf die Klassifizierungsproblematik ein-
gegangen – Halstead selbst verzichtete in [111] auf die Angabe eindeutiger Klassi-
fizierungsregeln.
Ein anderer Kritikpunkt richtet sich gegen die einfache Beschaffenheit der we-
nigen Basisparameter, auf denen Halstead sein vollständiges Gedankenmodell auf-
5.1 Software-Metriken 259
baut. Einige Experten sind der Meinung, dass die reine Analyse des lexikalischen
Aufbaus nicht ausreicht, um adäquate Aussagen über komplexe Messgrößen wie
Schwierigkeitsgrad oder Aufwand zu treffen [112, 94]. Semantische Eigenschaf-
ten des Kontrollflusses, wie sie z. B. in die zyklomatische Komplexität (vgl. Ab-
schnitt 5.1.3) eingeht, können mit dem entwickelten Instrumentarium nicht erfasst
werden. Nichtsdestotrotz haben die Halstead-Metriken ihren Stellenwert innerhalb
der statischen Code-Analyse gefunden und werden z. B. mit großem Erfolg einge-
setzt, um die Wartbarkeitseigenschaften eines Programmmoduls zu bewerten [270].
5.1.3 McCabe-Metrik
In Kapitel 4 haben wir mit der McCabe-Überdeckung ein klassisches Verfahren zur
Testfallgenerierung kennen gelernt. Eine zentrale Rolle innerhalb des Konstrukti-
onsprozesses spielt die zyklomatische Zahl, die einen direkten Rückschluss auf die
Anzahl der Elementarpfade und damit gleichermaßen auf die Größe der zu kon-
struierenden Testfallmenge gestattet. Wie in Abschnitt 4.4.6 herausgearbeitet, gilt
für die zyklomatische Zahl V (Z) eines stark zusammenhängenden Graphen Z die
folgende Beziehung:
V (Z) = |E| − |N| + 1 (5.53)
|E| und |N| bezeichnen die Anzahl der Knoten und Kanten von Z. An dieser Stelle
gilt es zu beachten, dass Gleichung (5.53) eine Aussage über stark zusammenhän-
gende Graphen macht – eine Eigenschaft, die Kontrollflussgraphen nicht erfüllen.
In Abschnitt 4.4.6 haben wir das Problem durch eine zusätzlich eingefügte Kante
gelöst, die den Endknoten mit dem Startknoten künstlich verbindet. Der so modifi-
zierte Kontrollflussgraph ist stark zusammenhängend, da für zwei beliebige Knoten
stets ein Pfad existiert, der beide miteinander verbindet. McCabe trägt der zusätzli-
chen Kante Rechnung und definiert die zyklomatische Zahl eines Kontrollflussgra-
phen G wie folgt:
V (G) = |E| − |N| + 2 (5.54)
McCabe erkannte nicht nur als Erster die Bedeutung dieser Größe für den Software-
Test, sondern schlug zudem vor, die zyklomatische Zahl als Maß für die Komplexi-
tät eines Software-Moduls einzusetzen [171, 172]. In entsprechender Weise wird der
Wert V (G) als die zyklomatische Komplexität des Kontrollflussgraphen G bezeich-
net. Dass sich die Maßzahl für die Bewertung der Programmkomplexität eignet,
begründet sich unter anderem auf den folgenden Eigenschaften:
I Die zyklomatische Komplexität wird ausschließlich durch die Struktur des Kon-
trollflussgraphen bestimmt. Die Beschaffenheit der in einem Knoten zusammen-
gefassten Befehle hat dagegen keinen Einfluss auf die berechnete Maßzahl.
0 in 0 in
1 if 1 if
B B ¬B
X 2 ¬B X 2 3 Y
3 out 4 out
|E| = 4 |E| = 5
|N| = 4 |N| = 5
0 in 0 in
1 while 1 X
B B
X 2 ¬B 2 while
¬B
3 out 3 out
|E| = 4 |E| = 4
|N| = 4 |N| = 4
Aus der letzten Eigenschaft lässt sich mit einem einfachen induktiven Beweisschluss
eine zentrale Eigenschaft der berechneten Maßzahl ableiten: Die zyklomatische
Komplexität ist stets um eins größer, als die Anzahl der Verzweigungen eines Pro-
gramms – eine Eigenschaft, die sich an den elementaren Kontrollflussgraphen in
Tabelle 5.1 leicht überprüfen lässt. Insgesamt lässt sich die McCabe-Metrik hier-
durch mit rein syntaktischen Mitteln aus dem Quelltext eines Programms ableiten.
5.1 Software-Metriken 261
“It has been interesting to note how individual programmer’s style relates
to the complexity measure. The author has been delighted to find several
programmers who never had formal training in structured programming
but consistently write code in the 3 to 7 complexity range which is quite well
structured. On the other hand, FLOW1 has found several programmers who
frequently wrote code in the 40 to 50 complexity range (and who claimed
there was no other way to do it).”
Thomas J. McCabe, [171]
Abschließend wollen wir die Definition der McCabe-Metrik auf Programme er-
weitern, die sich aus mehreren Unterkomponenten zusammensetzen. Besteht ein
Programm aus n verschiedenen Funktionen, so wird jede als eigenständige Unter-
komponente behandelt und mit einem separaten Kontrollflussgraphen modelliert. In
diesem Fall setzt sich der Kontrollflussgraph G aus einer Menge von n Untergra-
phen G1 , . . . , Gn zusammen, die untereinander keine Verbindungen aufweisen. In
der Terminologie der Graphentheorie sprechen wir in diesem Zusammenhang von
einem Wald. Für einen Wald mit n Komponenten gilt die folgende verallgemeinerte
Definition der zyklomatischen Komplexität:
1 FLOW ist der Name des Analysewerkzeugs, das McCabe für die Metrikberechnung verwendete.
262 5 Statische Code-Analyse
0 1 1
MY_CLASS
1 2 3 2 3
foo();
bar();
foobar(); 2 4 4
G1 G2 G3
Für den Fall n = 1 reduziert sich der Ausdruck auf die uns bisher bekannte Form. Mit
Hilfe elementarer Umformungsschritte lässt sich Gleichung (5.55) darüber hinaus in
eine deutlich handlichere Form bringen:
Damit entpuppt sich die zyklomatische Komplexität v(G) einer Menge G von
Kontrollflussgraphen G1 , . . . , Gn schlicht als die Summe der Einzelkomplexitäten
v(G1 ), . . . , v(Gn ). Für große Software-Systeme geht die Berechnung der McCabe-
Metrik damit äußerst einfach von der Hand – für alle Teilkomponenten werden die
Kennzahlen separat ermittelt und anschließend aufaddiert (vgl. Abb. 5.6).
I Die klassischen Metriken geben keine Antwort auf die Frage, wie mit den Be-
sonderheiten objektorientierter Sprachen umgegangen werden soll. Führen wir
beispielsweise die Bestimmung der LOC- oder NCSS-Maßzahlen wie gewöhn-
lich durch, so bleiben alle geerbten Methodenimplementierungen einer Klasse
unberücksichtigt. Der Vererbungsmechanismus hebelt an dieser Stelle jegliche
Korrelation zwischen der Code-Größe und der Komplexität einer Klasse aus.
Damit erweisen sich die zeilenbasierten Metriken LOC und NCSS im Bereich
objektorientierter Sprachen umso mehr als fragwürdige Kenngrößen.
Um verwertbare Ergebnisse zu liefern, muss eine Metrik die spezifischen Eigen-
schaften objektorientierter Programmiersprachen abbilden. In der Vergangenheit
wurden hierzu mehrere Kenngrößen vorgeschlagen, die sich in Komponentenme-
triken und Strukturmetriken unterteilen lassen.
[Link] Komponentenmetriken
Mit Hilfe von Komponentenmetriken werden die einzelnen Bestandteile eines
Software-Systems separat bewertet. In den meisten Fällen wird eine Teilkompo-
nente mit einer einzelnen Klasse, in vereinzelten Fällen aber auch mit einem ganzen
264 5 Statische Code-Analyse
|A| bezeichnet die Anzahl der Attribute und |M| die Anzahl der Methoden, die in der
untersuchten Klasse neu hinzugefügt oder umdefiniert wurden. Geerbte Attribute
oder Methoden werden nicht berücksichtigt.
5.1 Software-Metriken 265
Die beiden Metriken DOI und NOD gehören zur Gruppe der Vererbungsmetriken
und beurteilen eine Teilkomponente anhand ihrer Eingliederung innerhalb der Klas-
senhierarchie. Die erhobenen Maßzahlen entsprechen der Anzahl der Oberklassen
(DOI) bzw. der Anzahl der Unterklassen (NOD) der untersuchten Komponente. Gut
strukturierte Software-Systeme zeichnen sich unter anderem durch vergleichswei-
se flache Vererbungshierarchien aus. Lange Vererbungsketten, wie sie von vielen
Programmieranfängern gerne erzeugt werden, verschlechtern nicht nur die Code-
Transparenz, sondern auch die Wartbarkeit eines Software-Systems. In der Praxis
lassen sich die DOI- und NOD-Maßzahlen als zuverlässiges Warnsignal für diese
Qualitätsparameter einsetzen.
Neben der Vererbungstiefe spielt für die Komplexitätsbewertung auch die An-
zahl der reimplementierten Methoden eine Rolle. Dieser Wert wird mit der Maßzahl
NORM gemessen, die ebenfalls in die Gruppe der Vererbungsmetriken fällt [167].
Der Begriff Kohäsion beschreibt den Verknüpfungsgrad, der zwischen den ein-
zelnen Bestandteilen einer Klasse, eines Pakets oder einer Bibliothek besteht. Zur
Messung des Kohäsionsgrads einer Klasse bildet die LCOM-Metrik die Differenz
aus der Anzahl der Methodenpaare, die gemeinsame Variablen verwenden, und der
Anzahl der Methodenpaare, die über disjunkte Variablensätze verfügen [42, 43].
Die in [12] durchgeführten empirischen Untersuchungen attestieren den
Vererbungs- und Umfangsmetriken eine praktische Signifikanz für die Qualitäts-
bewertung einer Klasse. Für die Kohäsionsmetrik wurde eine entsprechende Kor-
relation jedoch nicht bestätigt, so dass die praktische Verwertbarkeit der erhobenen
Maßzahl heute in Frage steht. Auch in [122] wird auf die Defizite der LCOM-Metrik
hingewiesen und eine modifizierte Kohäsionsmaßzahl vorgeschlagen.
[Link] Strukturmetriken
Im Gegensatz zu den bisher betrachteten Komponentenmetriken, die auf die Bewer-
tung einer einzigen Klasse ausgerichtet sind, analysieren Strukturmetriken den Klas-
senverbund als Ganzes. Eine wichtige Rolle spielen hier insbesondere die Kopp-
lungsmetriken, die das Zusammenspiel der einzelnen Klassen im Hinblick auf deren
Interaktions- und Vererbungsmuster analysieren. Eine zentrale Rolle in der klassen-
übergreifenden Kopplungsanalyse spielen die Begriffe Fan-In und Fan-Out (vgl.
Abb. 5.7) [275, 186]:
I Fan-In
Als Fan-In einer Klasse C, kurz Fin (C), wird die Anzahl der Klassen bezeichnet,
die direkt auf C zugreifen. Klassen mit einem hohen Fan-In sind in der Regel am
unteren Ende der Software-Architektur angesiedelt, Klassen mit einem niedrigen
Fan-In am oberen Ende.
I Fan-Out
Als Fan-Out einer Klasse C, kurz Fout (C), wird die Anzahl der Klassen bezeich-
net, auf die C selbst direkt zugreift. Klassen mit einem hohen Fan-Out sind in der
Regel am oberen Ende der Software-Architektur angesiedelt, Klassen mit einem
niedrigen Fan-Out am unteren Ende.
266 5 Statische Code-Analyse
B C D
Fan-in = 1 Fan-in = 1 Fan-in = 1
Fan-out = 2 Fan-out = 2 Fan-out = 2
Auf Basis der Fan-In- und Fan-Out-Maßzahlen wurden in der Vergangenheit meh-
rere Metriken entwickelt. So führen Henry und Kafura in [118] das Quadrat des
Produkts aus Fan-In und Fan-Out als strukturelles Komplexitätsmaß einer Klasse C
ein:
vHK (C) = (Fin (C) · Fout (C))2 (5.62)
Henry und Selig erweitern die Definition in [119] um einen zusätzlichen Faktor
vinternal (C), der die innere Komplexität der Klasse C beschreibt:
Die Bestimmung der internen Komplexität vinternal (C) kann mit einer beliebi-
gen Komponentenmetrik erfolgen – beispielsweise mit Hilfe der McCabe-Metrik
(Abschnitt 5.1.3) oder einem Vertreter der diversen Halstead-Maßzahlen (Ab-
schnitt 5.1.2).
Card und Glass schlagen in [39] ein weiteres Komplexitätsmaß vor, das sich aus
der Summe der Daten- und Strukturkomplexität zusammensetzt:
Der linke Summand sCG beschreibt die Strukturkomplexität eines Systems mit n
Klassen C1 , . . . ,Cn und berechnet sich wie folgt:
n
sCG = ∑ Fout (Ci )2 (5.65)
i=1
5.1 Software-Metriken 267
Nach Card und Glass wird die Strukturkomplexität eines Software-Systems damit
ausschließlich durch den Fan-Out der Teilkomponenten bestimmt. Der Fan-In spielt
dagegen keine Rolle.
In die Berechnung der Datenkomplexität dCG geht für jede Teilkomponente Ci
neben dem Fan-Out Fout (Ci ) auch die Anzahl der I/O-Variablen, kurz IO(Ci ), ein:
n
IO(Ci )
dCG = ∑ (5.66)
i=1 out (Ci ) + 1
F
Die Division der Kennzahl vCG durch die Anzahl der Teilkomponenten ergibt die
relative Systemkomplexität vCG . Entsprechend den Gleichungen (5.64) bis (5.66)
berechnet sich die Maßzahl wie folgt:
vCG 1 n n
IO(C)
vCG =
n
=
n i=1∑ Fout (Ci ) + ∑ Fout (Ci ) + 1
2
(5.67)
i=1
Mit vCG liegt eine Strukturmetrik vor, die von der Anzahl der vorhandenen Klas-
sen abstrahiert. Hierdurch liefert die Metrik auch für solche Software-Systeme ver-
gleichbare Werte, die sich in ihrer Größe erheblich unterscheiden.
[Link] Pareto-Diagramme
Hinter diesem Diagrammtyp verbirgt sich eine spezielle Form des Balkendia-
gramms, die sich für die Visualisierung von Häufigkeitsverteilungen über diskreten
Merkmalsräumen eignet. Die Balken eines Pareto-Diagramms sind stets in abstei-
gender Größe angeordnet. Zusätzlich sind diese mit einem Graphen überlagert, der
268 5 Statische Code-Analyse
100 %
gefundene Software-Fehler [%]
Pareto-Prinzip
(80/20-Regel)
Software-Modul
0%
A B C D E F G H I J
50 % 30 % 8% 4% 2% 2% 1% 1% 1% 1%
die Summenhäufigkeiten beschreibt und durch die Addition der i häufigsten Merk-
malsausprägungen gebildet wird.
Zur Verdeutlichung des Gesagten betrachten wir das Pareto-Diagramm in
Abb. 5.8. Das Beispiel beschreibt, wie sich gefundene Fehler auf die einzelnen Mo-
dule eines Software-Systems verteilen. Der diskrete Merkmalsraum erstreckt sich
über die x-Achse und beinhaltet alle Module der betrachteten Software. Der Häu-
figkeitsparameter wird auf der y-Achse eingetragen und entspricht in diesem Fall
der prozentualen Anzahl der Software-Fehler, die dem jeweiligen Modul zugeord-
net werden können. Mit Hilfe des abgebildeten Pareto-Diagramms lassen sich unter
anderem die folgenden, für das Projektmanagement typischen Fragen beantworten:
I Welches Modul besitzt die höchste Fehlerrate?
Merkmal 1
Merkmal 1
Merkmal 1
Merkmal 2 Merkmal 2 Merkmal 2
Positiv Negativ
korrelierte Unkorrelierte
korrelierte
Merkmalswerte Merkmalswerte
Merkmalswerte
Wirkung durch 20 % aller möglichen Ursachen ausgelöst werden [147, 153]. Land-
läufig wird das Pareto-Prinzip daher auch als 80/20-Regel bezeichnet.
Die Gültigkeit der 80/20-Regel ist nicht auf den Bereich der Volkswirtschaftsleh-
re beschränkt. Auch im Bereich der Software-Entwicklung lässt sich die Grundaus-
sage des Pareto-Prinzips empirisch bestätigen – wenn auch mit teilweise abweichen-
den Teilungsverhältnissen. Fenton und Ohlsson untersuchten in [93] die Fehlerver-
teilung in einem Telekommunikationssystem der Firma Ericsson-Telecom. Die Stu-
die kommt zu dem Ergebnis, dass sich rund 60 % aller Fehler auf 20 % der Module
konzentrieren. Ostrand und Weyuker kommen in [198] auf ein ähnliches Ergebnis.
In einem frühen Entwicklungsstadium der untersuchten Software wurden ca. 70 %
der Fehler durch 10 % der Module verursacht. Die von Möller und Paulish in [184]
gemessene Verteilung fällt weniger drastisch aus, bestätigen jedoch den allgemeinen
Trend. Hier wurden 45 % Prozent der Fehler auf 10 % der Module zurückgeführt.
[Link] Streudiagramme
Mit Hilfe von Streudiagrammen (scatter plots) wird die Korrelation zweier sta-
tistischer Merkmale grafisch erfasst. Hierzu werden die Merkmalswerte als (x, y)-
Koordinate interpretiert und als Punkt in das Diagramm eingetragen. Mit der Zeit
entsteht eine Punktwolke, deren Form einen Rückschluss auf die Korrelation der
beobachteten Merkmale erlaubt.
Als Beispiel betrachten wir die drei Streudiagramme in Abb. 5.9. Das linke
Diagramm zeigt eine positive Korrelation zwischen den Merkmalswerten beider
Achsen – die verschiedenen Messpunkte lassen sich durch eine von links unten
nach rechts oben verlaufende Gerade approximieren. Im Gegensatz hierzu sind
die Messwerte des mittleren Diagramms negativ korreliert. Hier bedingen große
Messwerte auf der x-Achse kleine Messwerte auf der y-Achse und umgekehrt. Im
rechten Diagramm sind die Messpunkte gleichmäßig verteilt – ein direkter Zusam-
menhang zwischen der x-Koordinate und der y-Koordinate eines Messwerts lässt
270 5 Statische Code-Analyse
sich hier nicht ableiten. Mit anderen Worten: Es besteht keinerlei Korrelation zwi-
schen den beobachteten Merkmalen.
Streudiagramme lassen sich auf den Vergleich dreier Merkmale erweitern, indem
die Messwerte in ein dreidimensionales Diagramm eingetragen werden. Die räum-
liche Struktur der Punktwolke gibt auch hier Aufschluss über die Korrelation der
gemessenen Parameter. Ab vier Parametern stößt die Korrelationsanalyse mit Hilfe
von Streudiagrammen an ihre Grenzen.
[Link] Kiviat-Diagramme
Werden die Qualitätsmerkmale eines einzelnen Moduls oder eines kompletten
Software-Systems mit mehreren, voneinander unabhängigen Metriken gemessen, so
lassen sich diese in einem Kiviat-Diagramm gemeinsam visualisieren. Ein Kiviat-
Diagramm für die kombinierte Darstellung von n Maßzahlen hat die Grundform
eines Kreisdiagramms, das in n Zonen gleicher Größe aufgeteilt wird [185]. Die
einzelnen Zonen werden durch Achsen getrennt, die im Mittelpunkt entspringen
und gleichmäßig verteilt in alle Richtungen des Diagramms nach außen laufen. Je-
de Achse ist einer separaten Metrik zugeordnet und in jeweils drei Segmente unter-
teilt. Das innere und äußere Segment beschreiben Maßzahlen, die einen zu geringen
bzw. einen zu großen Wert aufweisen. Das mittlere Segment definiert den vor der
Messung festgelegten Normbereich.
Für die gemeinsame Auswertung der erhobenen Qualitätsparameter werden die
Messwerte auf den einzelnen Achsen eingetragen und benachbarte Punkte mitein-
ander verbunden. Auf diese Weise entsteht ein geschlossener Linienzug, der im
optimalen Fall vollständig im Normbereich verläuft. Aufgrund ihrer äußeren Ge-
stalt werden Kiviat-Diagramme auch als Radar- oder Netzdiagramme bezeichnet.
Abb. 5.10 zeigt exemplarisch zwei Kiviat-Diagramme, die fünf der in den vorherge-
henden Abschnitten eingeführten Metriken miteinander kombinieren. Während im
linken Diagramm alle gemessenen Metriken im Normbereich liegen, verlassen im
rechten Diagramm die Maßzahlen der McCabe- und der NCSS-Metrik den zulässi-
gen Bereich. Zum direkten Vergleich zweier Messobjekte können die entsprechen-
den Linienzüge in ein einziges Diagramm eingetragen werden.
Jeder Linienzug eines Kiviat-Diagramms schließt eine Fläche ein, deren Größe
häufig als kumuliertes Qualitätsmaß verwendet wird. Diese Art der Interpretation
ist insbesondere dann sinnvoll, wenn die erhobenen Maßzahlen durch keinen Mi-
nimalwert nach unten begrenzt sind. In diesem Fall bestehen die einzelnen Achsen
ausschließlich aus dem Normbereich und dem äußeren Segment und kleinere Flä-
cheninhalte bedeuten stets bessere Werte. Begrenzt wird die Aussagekraft dieser
Maßzahl lediglich durch die starre Einteilung der Kreisscheibe in Segmente glei-
cher Größe –schließlich gehen durch die gleichmäßige Verteilung der Achsen alle
Messwerte mit der gleichen Gewichtung in die kumulierte Maßzahl ein.
In [211] wurde mit dem Multiple-Metrics-Graph ein gewichtetes Kiviat-
Diagramm vorgeschlagen, das dieses Problem behebt. Im direkten Vergleich mit
der ungewichteten Variante unterscheidet sich der Multiple-Metrics-Graph in zwei
wesentlichen Punkten. Zum einen wird jede Metrik durch ein variables Kreisseg-
5.2 Konformitätsanalyse 271
McCabe McCabe
Hals
Hals
WAC
WAC
tead
tead
LO LO
SS C SS C
NC NC
ment dargestellt, dessen Größe proportional zu deren Bedeutung gewählt wird. Zum
anderen werden die Messwerte nicht mehr auf der Achse selbst, sondern auf der
Mittelachse des jeweiligen Segments eingezeichnet.
Abb. 5.11 zeigt die auf diese Weise modifizierten Kiviat-Diagramme. In den dar-
gestellten Beispielen wurde das Segment der Metrik LOC deutlich verkleinert, so
dass ein hoher Messwert den Flächeninhalt nur noch gering beeinflusst. Die Seg-
mente der McCabe- und der Halstead-Metrik wurde hingegen deutlich vergrößert.
Wird der Normbereich hier verlassen, so wirkt sich die Veränderung überproportio-
nal auf den Flächeninhalt und damit auf das kumulierte Qualitätsmaß aus. Ein zen-
traler Nachteil des Kiviat-Diagramms bleibt jedoch auch in der gewichteten Variante
erhalten: Alle Metriken werden als voneinander unabhängige Merkmale betrachtet.
In der Praxis bestehen zwischen den erhobenen Maßzahlen semantische Abhängig-
keiten, wie die beiden Umfangsmetriken LOC und NCSS besonders deutlich unter
Beweis stellen. Die starke Korrelation zwischen den beiden bleibt in der grafischen
Repräsentation gänzlich unberücksichtigt.
5.2 Konformitätsanalyse
Zu den wichtigsten Anwendungen der statischen Code-Analyse gehört, die Quell-
texte eines Software-Systems auf die Einhaltung vorgegebener Konformitätsregeln
zu überprüfen. Je nachdem, wie der bewusst abstrakt gehaltene Begriff der Konfor-
mität an dieser Stelle interpretiert wird, entstehen ganz unterschiedlichen Spielarten
der statischen Analyse. Auf der obersten Ebene lassen sich diese in zwei Gruppen
aufteilen:
I Syntax-Analyse
Die Syntax-Analyse überprüft die untersuchten Quellen auf die Einhaltung der le-
272 5 Statische Code-Analyse
e
ab
ab
Ha
Ha
cC
cC
lst
lst
M
M
ea
ea
d
d
C
C
WA
WA
LO
LO
C
C
NCSS NCSS
I Semantik-Analyse
Die Semantik-Analyse geht über die rein syntaktische Programmprüfung hinaus
und verfolgt das Ziel, potenziell fehlerhafte oder fragwürdige Programmkon-
strukte zu identifizieren.
Wie in Abb. 5.12 am Beispiel der Programmiersprache C gezeigt, gehen die Syntax-
und die Semantik-Analyse in der Praxis Hand in Hand. Insgesamt sind drei Szena-
rien abgebildet, die gleichzeitig die historische Entwicklung der Compiler-Technik
widerspiegeln. In den frühen Tagen des Übersetzerbaus wurde die Code-Analyse
durch eine separate Applikation durchgeführt. Da der C-Compiler selbst keine ei-
genständige Prüfung durchführte, sprechen wir in diesem Zusammenhang von ei-
ner externen Code-Analyse. Im Gegensatz hierzu verfolgen moderne C-Compiler
die Strategie der internen Code-Analyse und verfügen heute über einen Großteil
der Funktionalität ehemaliger externer Analysewerkzeuge. Die Prüfung findet je-
doch stets nur lokal, d. h. für jede Datei separat statt. Die kombinierte Code-Analyse
vereint beide Ansätze, indem der interne Analysator des Compilers durch externe,
global prüfende Werkzeuge unterstützt wird.
5.2.1 Syntax-Analyse
Jeder Compiler oder Interpreter, der den Programmtext einer rudimentären Vor-
verarbeitung unterzieht, wendet diese Art der Code-Analyse an. Der eingelesene
Quelltext wird anhand der sprachenspezifischen Syntax- und Grammatikregeln auf
Konformität geprüft und der Vorgang gegebenenfalls mit einer Fehlermeldung ab-
gebrochen. Formal wird der Bereich der Syntax-Analyse dem Compilerbau zuge-
ordnet und gehört zu den gut verstandenen Teilgebieten der Informatik [3, 33, 109].
Typische Multi-pass Compiler erzeugen im Rahmen der Syntax-Analyse zunächst
ein Zwischenformat, das anschließend an den Code-Generator zur Erzeugung des
5.2 Konformitätsanalyse 273
I Externe Code-Analyse
Lint C-Compiler
? Korrektur !
Warnings Errors
I Interne Code-Analyse
C-Compiler
Korrektur !
Errors
?
Warnings
I Kombinierte Code-Analyse
Lint C-Compiler
? Korrektur !
Warnings Errors ?
Warnings
Quelltext
.xyz
.l lex [Link].c
Grammatik-
Spezi#kation gcc [Link]
(lex #le)
.y yacc [Link].c
Grammatik-
Spezi#kation Zwischen- Fehler-
(yacc File) format meldung
[Link] Lex-Scanner
Die Lex-Datei definiert mit den Tokens die Elementarsymbole der Eingabesprache
und besteht aus bis zu vier Abschnitten (vgl. Abb. 5.14 links). Innerhalb der ers-
ten beiden lassen sich beliebig viele C- und Lex-Definitionen vereinbaren und in
den restlichen Abschnitten referenzieren. C-Definitionen werden mit den speziellen
Zeichenfolgen %{ und %} umschlossen. Der Hauptteil der Lex-Datei wird durch ei-
ne Reihe von Pattern-Action-Paaren gebildet, die sich jeweils aus einem regulären
Ausdruck sowie einer oder mehrerer C-Anweisungen zusammensetzen. Der letzte
Abschnitt enthält eine beliebige Anzahl weiterer C-Routinen.
Das in Abb. 5.15 dargestellte Beispielprogramm firstlexer.l definiert einen
Scanner, der die jeweils gelesene Zeichenkette nach Dezimalzahlen durchsucht. Ei-
ne Zahl wird durch den regulären Ausdruck [1-9][0-9]* beschrieben und setzt
sich demnach aus einer positiven Führungsziffer sowie einer beliebigen Anzahl wei-
terer Ziffern – jeweils aus dem Zeichenvorrat 0 bis 9 – zusammen. Die restlichen
regulären Ausdrücke weisen den Scanner an, Leerzeichen, die Endemarkierung so-
wie ungültige Eingabezeichen als solche zu erkennen.
Mit Hilfe des Werkzeugs Lex kann aus der Datei firstlexer.l ein vollständi-
ger Token-Scanner generiert werden. Durch den Aufruf
> lex firstlexer.l
5.2 Konformitätsanalyse 275
%{ %{
C-Deklarationen C-Deklarationen
%} %}
Lex-Deklarationen Yacc-Deklarationen
%% %%
Pattern Action Rule Action
Token- Grammatik-
Pattern Action De#nition De#nition Rule Action
Pattern Action Rule Action
%% %%
... ... ... ...
Benutzerde#nierter Benutzerde#nierter
Pattern Action C-Code C-Code Rule Action
firstlexer.l
%% 1
[1-9][0-9]* { printf("NUMBER\n"); } 2
[ \t] { printf("WHITE SPACE\n"); } 3
\n { printf("EOF\n"); } 4
. { printf("UNKNOWN\n"); } 5
%% 6
wird die Datei [Link].c erzeugt, die sich anschließend mit Hilfe des C-Compilers
in eine ausführbare Datei übersetzen lässt:
> gcc -o firstlexer [Link].c -lfl
Der Zusatz -lfl veranlasst den Linker, die Lex-Bibliothek einzubinden. Diese ent-
hält unter anderem eine vordefinierte main-Funktion, die den generierten Scanner
automatisch aktiviert und an den Input-Stream stdin bindet. Mit Hilfe der erzeug-
ten Binärdatei firstlexer lässt sich der Token-Scanner damit interaktiv testen:
> ./firstlexer
42 x 13
NUMBER
WHITE SPACE
UNKNOWN
WHITE SPACE
NUMBER
EOF
276 5 Statische Code-Analyse
[Link] Yacc-Parser
Die Lex-Datei legt die Elementarsymbole der Eingabesprache fest, macht aber kei-
ne Aussage darüber, wie sich die verschiedenen Tokens miteinander kombinieren
lassen. Genau an dieser Stelle kommt die Yacc-Datei ins Spiel. Fassen wir die Lex-
Datei als die Definition des Wortschatzes unserer Sprache auf, so legt die Yacc-Datei
in Form einer Grammatik fest, wie sich einzelne Worte zu wohldefinierten Sätzen
kombinieren lassen.
Genau wie im Fall von Lex besteht eine Yacc-Datei aus bis zu vier Abschnit-
ten (vgl. Abb. 5.14 rechts). Die ersten beiden enthalten eine beliebige Anzahl von
Deklarationen. C-Deklarationen werden auch hier wieder mit der speziellen Zei-
chenfolge %{ und %} voneinander getrennt. Später wird der eingerahmte Abschnitt
eins zu eins in die erzeugte Parser-Datei kopiert. Zu den Yacc-Deklarationen gehört
unter anderem die Angabe der Tokens, die durch den verwendeten Lex-Scanner ge-
liefert werden. Der nächste Abschnitt enthält eine beliebige Anzahl von Grammatik-
regeln, die zusammen die Sprachstruktur definieren. Legen wir die Begrifflichkeit
der formalen Sprachentheorie zugrunde, so fällt Yacc in die Klasse der LALR(1)-
Parser [158, 120].
Als vollständiges Beispiel ist in Abb. 5.16 (oben) eine Grammatik zur Beschrei-
bung einfacher arithmetischer Ausdrücke wiedergegeben, die sich mit Hilfe des
Werkzeugs Yacc direkt in eine C-Datei übersetzen lässt:
> yacc -d calculator.y
Der Aufruf erzeugt die Datei [Link].c und durch die Option -d zusätzlich die Datei
[Link].h. Die generierte Header-Datei enthält einen einzigen Eintrag und weist dem
in der Yacc-Datei verwendeten Token NUMBER eine eindeutige Identifikationsnum-
mer zu:
> cat [Link].h
#define NUMBER 257
Wie in Abb. 5.16 gezeigt, wird die Datei [Link].h innerhalb von calculator.l
eingelesen. Hier dient die Konstante NUMBER als Rückgabewert, sobald ein nume-
rischer Wert als Token erkannt wird. Jetzt genügt die Ausführung von Lex, gefolgt
von einem Aufruf des C-Compilers, um den vollständigen Parser zu erzeugen:
> lex calculator.l
> gcc -o calculator [Link].c [Link].c -ll -ly
Über die Optionen -ll und -ly werden die Lex- und Yacc-Bibliotheken einge-
bunden. Unter anderem enthalten diese auch hier wieder eine Implementierung der
Einsprungsfunktion main, die den Parser direkt startet. Auf diese Weise lässt sich
das Ergebnis sofort über die Kommandozeile testen:
> ./calculator
(32 / 2) + 26
= 42
5.2 Konformitätsanalyse 277
I Yacc-Grammatik
calculator.y
%{ 1
#include "stdio.h" 2
%} 3
4
%token NUMBER 5
%% 6
main: expression { printf("= %d\n", $1); } 7
; 8
9
expression: expression ’+’ expression { $$ = $1 + $3; } 10
| expression ’-’ expression { $$ = $1 - $3; } 11
| expression ’*’ expression { $$ = $1 * $3; } 12
| expression ’/’ expression { $$ = $1 / $3; } 13
| ’-’ expression { $$ = -$2; } 14
| ’(’ expression ’)’ { $$ = $2; } 15
| NUMBER { $$ = $1; } 16
; 17
I Lex-Scanner
calculator.l
%{ 1
#include "[Link].h" 2
extern int yylval; 3
%} 4
%% 5
6
[1-9][0-9]* { yylval = atoi(yytext); return NUMBER; } 7
[ \t] { } /* white spaces are ignored */ 8
\n { return 0; } 9
. { return yytext[0]; } 10
%% 11
> ./calculator
(+4-2)
syntax error
Wie in Abb. 5.17 gezeigt, springt die main-Routine zunächst in die Funktion
yyparse(). Diese ruft in einer Schleife die Funktion yylex() des Lex-Scanners
auf und versorgt sich auf diese Weise mit einem kontinuierlichen Eingabestrom.
Der Yacc-Parser interpretiert die eingelesenen Tokens entsprechend der spezifizier-
ten Grammatikregeln und beginnt mit dem Aufbau eines Syntax-Baums. Der Bear-
beitungsprozess bricht ab, sobald der Ende-Token (EOF) eingelesen wird oder der
verarbeitete Datenstrom die spezifizierten Grammatikregeln verletzt.
278 5 Statische Code-Analyse
main
yyparse() expression
Yacc-Parser $$ = $1 + $3 = 42
expression
( 32 / $$ = $2 = 16
yylex()
2 ) +
expression
26
$$ = 32 / 2 = 16
Die vorgestellten Werkzeuge Lex und Yacc sind keine Erfindungen der letzten
Jahre – beide wurden bereits in den Siebzigerjahren an den Bell Laboratories ent-
wickelt. Stephen C. Johnson machte mit Yacc den Anfang [144], Lex kam wenig
später als gezielte Ergänzung hinzu. Mit dem Erscheinen von Unix V7 im Jahre
1979 wurden Lex und Yacc in den Kreis der klassischen Unix-Werkzeuge aufge-
nommen und seither in mehrere Richtungen weiterentwickelt. Nennenswert sind
die Werkzeuge Flex und Bison. Hierbei handelt es sich um zwei von der Free Soft-
ware Foundation im Rahmen des GNU-Projekts entwickelte und funktional nahezu
identische Alternativen für die Programme Lex und Yacc.
Die Spuren, die der Zahn der Zeit an den Werkzeugen hinterlassen hat, sind heute
kaum noch zu übersehen. Zum einen widerspricht der syntaktische Aufbau der Lex-
und Yacc-Dateien vielen der gegenwärtig verfolgten Architekturprinzipien – insbe-
sondere wirkt die recht unstrukturierte Vermischung von Lex-Yacc-Direktiven mit
nativem C-Code aus heutiger Sicht vergleichsweise hemdsärmelig. Zum anderen ist
das inzwischen allgegenwärtige Prinzip der Objektorientierung sowohl für Lex als
für Yacc ein Fremdwort [143].
Nichtsdestotrotz gehören die Werkzeuge aufgrund ihrer Leistungsfähigkeit im-
mer noch zu den am häufigsten eingesetzten Parser-Generatoren im industriellen
Umfeld. Darüber hinaus sind in den letzten Jahren zahlreiche Alternativen ent-
standen, die viele der geschilderten Defizite beseitigen. Neben den neu entstan-
denen, objektorientierten Varianten Lex++ und Yacc++ [265] wurden zahlreiche
Parser-Generatoren für andere Programmiersprachen implementiert. Zu den be-
kannteren Vertretern gehören unter anderem die Werkzeuge JFlex und CUP [140],
Coco/R [251], ANTLR [7] und JavaCC [139].
5.2 Konformitätsanalyse 279
zwei Regeln definiert, die nur dann berücksichtigt werden, wenn sich der Token-
Scanner aktuell im Zustand F_NAME bzw. M_NAME befindet. Lex ist mit Hilfe des
Zustandsmechanismus in der Lage, primitive Grammatiken auch ohne den Einsatz
von Yacc abzuarbeiten und geht damit aus sprachentheoretischer Sicht deutlich über
die Fähigkeiten eines einfachen Token-Scanners hinaus.
mail.l
%{ 1
#include <stdio.h> 2
3
/* Receiving syntax: 4
mail [-e] [-h] [-p] [-P] [-q] [-r] [ -f file ] */ 5
unsigned receive = 0; 6
7
/* Sending syntax: 8
mail [-t] [-w] [ -m ... ] <recipient 1> ... */ 9
unsigned send = 0; 10
%} 11
12
%s F_NAME 13
%s M_NAME 14
15
%% 16
[ \t] { /* SPACE */ } 17
\n { if (receive == send) 18
printf("Option mismatch\n"); 19
else if (receive == 1) 20
printf("Receiving mail\n"); 21
else 22
printf("Sending mail\n"); 23
} 24
25
-e { receive = 1; } 26
-h { receive = 1; } 27
-p { receive = 1; } 28
-P { receive = 1; } 29
-q { receive = 1; } 30
-r { receive = 1; } 31
-t { send = 1; } 32
-w { send = 1; } 33
-f { BEGIN F_NAME; } 34
-m { BEGIN M_NAME; } 35
<F_NAME>[^ \n]+ { receive = 1; } 36
<M_NAME>[^ \n]+ { send = 1; } 37
[^ \n]+ { send = 1; 38
printf("Recipient = %s\n", yytext); } 39
%% 40
5.2.2 Semantik-Analyse
Im direkten Vergleich mit der Syntax-Analyse, die einen Quelltext ausschließlich
auf die Einhaltung grammatikalischer Bildungsregeln überprüft, beschäftigt sich die
Semantik-Analyse mit der inhaltlichen Bedeutung eines Programms. Die Semantik-
Analyse verfolgt das übergeordnete Ziel, fehleranfällige und generell fragwürdige
Sprachkonstrukte mit Hilfe statischer Analysetechniken zu identifizieren.
Im Gegensatz zum Software-Test, der stets die Ausführung des Programms
mit konkreten Eingabewerten verlangt, wird für die Durchführung einer Semantik-
Analyse ausschließlich der Quelltext der untersuchten Software benötigt. Die Tech-
nik lässt sich hierdurch auf beliebige Code-Fragmente anwenden und setzt kein
lauffähiges System voraus. Wie die weiter unten betrachteten Beispielprogramme
zeigen werden, ist die statische Semantik-Analyse in der Lage, etliche Fehler aufzu-
decken, die mit Hilfe dynamischer Techniken nur schwer zu finden sind. Insgesamt
gehört die Semantik-Analyse aufgrund ihrer einfachen Anwendung und beachtli-
chen Leistungsstärke neben dem klassischen Software-Test zu den Qualitätssiche-
rungstechniken mit der höchsten Praxisbedeutung.
Die Idee der statischen Semantik-Analyse ist nicht neu und deren Entstehung
fest mit der Entwicklungsgeschichte der Programmiersprache C verbunden. Die
ersten C-Compiler wurden allesamt in einer homogenen Rechnerumgebung betrie-
ben und maßen der Syntax- und der Semantik-Analyse noch keine große Bedeu-
tung zu. Es oblag einzig dem Programmierer selbst, für ein syntaktisch und se-
mantisch korrektes Programm zu sorgen. Mit der fortschreitenden Portierung des
Unix-Betriebssystems änderte sich die Situation jedoch auf dramatische Weise. Als
282 5 Statische Code-Analyse
getopt_example.c
#include <unistd.h> 1
#include <stdio.h> 2
3
int main (int argc, char **argv) 4
{ 5
unsigned send = 0, receive = 0; 6
char c; 7
int index; 8
9
while ((c = getopt (argc, argv, "ehpPqrf:twm:")) != -1) { 10
switch (c) { 11
case ’e’: 12
case ’h’: 13
case ’p’: 14
case ’P’: 15
case ’q’: 16
case ’r’: 17
case ’f’: 18
receive = 1; 19
break; 20
case ’t’: 21
case ’w’: 22
case ’m’: 23
send = 1; 24
break; 25
default: 26
printf("Syntax error"); 27
return 0; 28
} 29
} 30
for (index = optind; index < argc; index++) { 31
send = 1; 32
printf ("Recipient = %s\n", argv[index]); 33
} 34
if (receive == send) 35
printf("Option mismatch\n"); 36
else if (receive == 1) 37
printf("Receiving mail\n"); 38
else 39
printf("Sending mail\n"); 40
41
return 0; 42
} 43
I Programm 1 I Programm 2
comparison.c printf.c
#include <stdio.h> 1 #include <stdio.h> 1
2 2
int 3 int 3
main(int argc, char *argv[]) 4 main(int argc, char *argv[]) 4
{ 5 { 5
if (argc = 1) { 6 long value = 42L; 6
/* stuff */ 7 7
} else { 8 printf("%d", value); 8
printf("Usage: ..."); 9 return 0; 9
} 10 } 10
return 0; 11 11
} 12 12
I Programm 3 I Programm 4
arraycopy.c noreturn.c
#include <stdio.h> 1 #include <stdio.h> 1
2 2
int 3 int 3
main(int argc, char *argv[]) 4 main(int argc, char *argv[]) 4
int i = 0; 5 { 5
int p[4] = {1, 2, 3, 4}; 6 if (argc <= 1) { 6
int q[4] = {5, 6, 7, 8}; 7 printf("Usage: ..."); 7
8 } else { 8
while (i < 4) 9 /* stuff */ 9
p[i++] = q[i]; 10 return 0; 10
11 } 11
return 0; 12 } 12
} 13 13
Der ausgegebene Text weist zudem auf eine besondere Eigenschaft des GNU-C-
Compilers hin: Die Fehlermeldung lässt sich unterdrücken, indem der beanstan-
dete Ausdruck in zusätzliche Klammern eingeschlossen wird. Auf diese Weise
hat der Programmierer die Freiheit, gewollte Konstrukte dieser Art explizit als
solche zu kennzeichnen.
I Programm 2: printf.c
In diesem Beispielprogramm wird die Funktion printf der C-
Standardbibliothek verwendet, um den Inhalt der Variablen value auf der
Konsole auszugeben. Die Funktion printf nimmt als erstes Argument einen
Format-String entgegen, gefolgt von einer beliebigen Anzahl weiterer Parameter.
Der Format-String kann neben gewöhnlichen Textfragmenten eine beliebige
Anzahl Platzhalter enthalten, die printf vor der Ausgabe durch die Werte der
zusätzlich übergebenen Parameter ersetzt. Jeder Platzhalter wird mit einem Pro-
zentzeichen eingeleitet und mit einer individuellen Buchstabensequenz ergänzt,
die Auskunft über den Datentyp des einzusetzenden Elements gibt. Dass der
Datentyp des übergebenen Elements mit dem entsprechenden Formatbuchstaben
übereinstimmt, liegt in der Verantwortung des Programmierers und ist eine
typische Fehlerquelle der Programmiersprache C.
In dem abgebildeten Beispielprogramm wird mit der Variablen value ein
Wert vom Typ long übergeben. Nach Tabelle 5.3 sieht C für Werte dieses Typs
den Platzhalter %ld vor. Das abgebildete Beispielprogramm verwendet dagegen,
wie unzählige andere C-Programme an dieser Stelle auch, den Formatbuchstaben
d. In diesem Fall erwartet die Funktion printf eine Variable vom Typ int, der
auf vielen – aber nicht allen – Rechnerarchitekturen mit dem Typ long überein-
stimmt. Der Defekt gehört damit zur großen Gruppe der Portabilitätsfehler, die
wir bereits in Abschnitt 3.5 einer genaueren Betrachtung unterzogen haben.
Im Gegensatz zu Compilern der ersten Generation gleichen moderne Über-
setzer den Format-String mit den Datentypen der tatsächlich übergebenen Pa-
286 5 Statische Code-Analyse
rameter ab und sind damit in der Lage, Inkonsistenzen dieser Art selbstständig
aufzuspüren:
> gcc -c -Wall -ansi -pedantic printf.c
printf.c: In function ’main’:
printf.c:7: warning: format ’%d’ expects type ’int’,
but argument 2 has type ’long int’
Die frühzeitige Erkennung des Format-String-Fehlers ist schon deshalb von Be-
deutung, da er in vielen Fällen keine sichtbaren Symptome zeigt. Insbesonde-
re dann, wenn das Datenmodell der zugrunde liegenden Rechnerarchitektur die
gleiche Bitbreite für int- und long-Variablen verwendet, wird der Fehler zu-
nächst kompensiert. Trotzdem ist die Verwendung des Format-Strings %d für al-
le Arten ganzzahliger Integer-Werte selbst unter erfahrenen C-Programmieren
weit verbreitet. Umso größer ist mitunter das Erstaunen, dass ein bisher stets sta-
5.2 Konformitätsanalyse 287
bil laufendes Programm nach der Portierung auf eine andere Rechnerarchitektur
plötzlich seine Dienste verweigert.
I Programm 3: arraycopy.c
Das dritte Beispielprogramm enthält einen diffizilen Programmierfehler, der
selbst von langjährigen C-Programmieren nicht selten übersehen wird. Das Pro-
gramm deklariert zwei Integer-Arrays und versucht in der sich anschließenden
While-Schleife, die Elemente des Arrays q eins zu eins in das Array p zu kopie-
ren. Obwohl das Programm unter Verwendung der meisten gängigen C-Compiler
korrekt arbeitet, birgt die Zuweisung p[i++] = q[i] ein Kompatibilitätsrisiko
in sich. Die Ursache ist in der C-Spezifikation zu finden, die für Ausdrücke die-
ser Art keine feste Auswertungsreihenfolge festgelegt. In der Konsequenz erfolgt
die Inkrementierung von i entweder vor oder nach dem Zugriff auf das Element
q[i]. Ist ersteres der Fall, so wird das Element p[i] nicht mit dem (korrekten)
Element q[i], sondern mit dem (falschen) Nachfolgeelement q[i+1] beschrie-
ben.
Ausgeführt mit der Option -Wall macht der GNU-C-Compiler abermals auf
den potenziellen Fehler aufmerksam:
> gcc -Wall -ansi -pedantic arraycopy.c
arraycopy.c: In function ’main’:
arraycopy.c:10: warning: operation on ’i’ may be undefined
I Programm 4: noreturn.c
In diesem Programm hat sich ein klassischer Flüchtigkeitsfehler eingeschlichen.
Zunächst wird in der If-Bedingung die Anzahl der Übergabeparameter überprüft
und eine Syntax-Beschreibung ausgegeben, falls das Programm ohne einen ein-
zigen Parameter aufgerufen wurde. Der im Else-Zweig enthaltene Return-Befehl
wird in diesem Fall nicht ausgeführt und das Hauptprogramm ohne die Rück-
gabe eines Ergebniswerts beendet. Wird die fertige Applikation wie gewöhnlich
in der Konsole gestartet, fällt der Fehler zunächst nicht auf – der Rückgabewert
wird in diesem Fall durch die Shell ignoriert. Die Situation ändert sich schlagar-
tig, sobald das Programm innerhalb eines Skripts ausgeführt wird. Hier wird der
Rückgabewert jedes ausgeführten Befehls ausgewertet und die Stapelverarbei-
tung im Falle eines aufgetreten Fehlers abgebrochen. Ohne ein explizites Return
am Ende des Hauptprogramms wird ein (fast) zufälliger Wert zurückgegeben,
mit ebenso (fast) zufälligen Folgen für die Skriptausführung.
Anhand des intern aufgebauten Kontrollflussgraphen ist es für moderne
Compiler ein leichtes, Ausführungspfade zu erkennen, die eine Funktion ohne
Rückgabewert verlassen. Für unser Beispielprogramm produziert der GNU-C-
Compiler beispielsweise die folgende Fehlermeldung:
> gcc -Wall -ansi -pedantic noreturn.c
noreturn.c: In function ’main’:
noreturn.c:11: warning: control reaches end of non-void function
288 5 Statische Code-Analyse
Die vier Beispiele zeigen eindringlich, dass moderne C-Compiler in der Lage sind,
viele Programmierfehler bereits in der Analysephase zu erkennen – vorausgesetzt
Sie lassen die Analyse zu. Wie bereits weiter oben erwähnt, lassen sich alle Pro-
gramme ohne die Angabe zusätzlicher Compiler-Optionen klaglos übersetzen. Um
die Fähigkeiten des Compilers gewinnbringend zu nutzen, sollten Sie insbesondere
auf die Angabe der Option -Wall niemals verzichten.
I Programm 5 I Programm 6
copyfile.c charprint.c
#include <stdio.h> 1 #include <stdio.h> 1
2 #include <string.h> 2
void 3 3
copy(FILE *fin, FILE *fout) 4 int 4
{ 5 main(int argc, char *argv[]) 5
char c; 6 { 6
while ((c=getc(fin))!=EOF) 7 size_t i; 7
putc(c, fout); 8 char *s="Hello, world\n"; 8
} 9 9
10 for (i=strlen(s);i>=0;i--) 10
11 printf("%c\n", s[i]); 11
12 return 0; 12
13 } 13
I Programm 7 I Programm 8
allocate.c undef.c
#include <stdlib.h> 1 #include <stdlib.h> 1
2 2
void 3 int 3
allocate(unsigned size) 4 main(int argc, char *argv[]) 4
{ 5 { 5
char *addr; 6 char *mem; 6
7 7
addr=(char *)malloc(size); 8 if (argc > 1) { 8
} 9 mem = 9
10 (char *)malloc(1024); 10
11 /* stuff */ 11
12 } else { 12
13 /* stuff */ 13
14 } 14
15 free(mem); 15
16 return 0; 16
17 } 17
Abb. 5.21 Die statische Analyse der meisten C-Compiler stößt bei diesen Programmen an ihre
Grenzen
Wie der Deklaration zu entnehmen ist, gibt die Funktion getc einen Ergebnis-
wert vom Typ int zurück, und nicht, wie in unserem Beispielprogramm erwartet,
einen Wert des Typs char. Folgerichtig tritt innerhalb des Beispielprogramms
immer dann ein Problem auf, wenn ein Zeichen mit dem ASCII-Code 0xFF ein-
gelesen wird. Im eingeschränkten Wertebereich des char-Datentyps besitzt die-
ses Zeichen die gleiche Bitrepräsentation wie die Integer-Konstante EOF, so dass
der Kopiervorgang in diesem Fall vorzeitig beendet wird. Mit den Mitteln des
herkömmlichen Software-Tests ist der Fehler nur schwer zu erkennen, da das
ASCII-Zeichen 0xFF z. B. in gewöhnlichen Textdateien überhaupt nicht auftritt.
Mit Hilfe der statischen Semantik-Analyse lassen sich Fehler dieser Art be-
reits im Vorfeld vermeiden. So weist beispielsweise das Analysewerkzeug Splint
durch die folgenden beiden Warnmeldungen vorab auf die vorhandenen Typkon-
flikte hin:
> splint copyfile.c
copyfile.c: (in function copyfile)
copyfile.c:6:13: Assignment of int to char: c = getc(fin)
I Programm 6: charprint.c
Geschrieben wurde dieses Programm mit der Absicht, die Zeichenkette „Hello,
world“ in umgekehrter Reihenfolge auf der Konsole auszugeben. Hierzu wird in
einer For-Schleife zunächst die Indexvariable i mit der Länge der Zeichenkette
initialisiert. Anschließend wird in jeder Iteration das i-te Zeichen dargestellt und
die Indexvariable um eins verringert.
Anstatt die Zeichenkette auszugeben, reagiert das Programm auf dem Test-
rechner unwirsch mit einem Bus error. Die Fehlerursache geht auf die For-
mulierung der Wiederholungsbedingung zurück, die i als vorzeichenbehafte-
ten Integer-Wert interpretiert. Dem Rückgabewert der Funktion strlen entspre-
chend, wurde i jedoch als Variable des Typs size_t deklariert. Ein Blick in die
Header-Datei stddef.h zeigt, dass dieser Datentyp mit
typedef unsigned long size_t;
vorzeichenlos definiert ist, wodurch der Wert der Variablen i niemals negativ
werden kann. Folgerichtig erfüllt das Programm zu jedem Zeitpunkt die Wie-
derholungsbedingung i>=0 und gerät in eine Endlosschleife. Moderne statische
Code-Analysatoren haben im Gegensatz zu den meisten C-Compilern keine Pro-
bleme, den Fehler zu erkennen. So reagiert das Analysewerkzeug Splint unmit-
telbar mit der folgenden Warnmeldung:
> splint charprint.c
...
charprint.c: (in function main)
charprint.c:8:24: Comparison of unsigned value involving zero:
i >= 0. An unsigned value is used in a comparison with zero
5.2 Konformitätsanalyse 291
I Programm 7: allocate.c
Das abgebildete Beispielprogramm besteht aus einer einzigen ausführbaren
Code-Zeile, die dynamisch einen Speicherbereich auf dem Heap reserviert und
die Startadresse in der Variablen addr ablegt. Obwohl die Zeile als solche voll-
kommen korrekt ist, enthält das Programm trotzdem einen klassischen Program-
mierfehler. Da die Sprache C selbst über kein intelligentes Speichermanagement
verfügt, müssen alle dynamisch belegten Speicherbereiche durch einen expli-
ziten Aufruf der Funktion free wieder frei gegeben werden. Die von malloc
zurückgegebene Referenz wird in diesem Beispiel nur in einer lokalen Variablen
gespeichert. Beim Rücksprung aus der Funktion geht der Inhalt aller lokalen Va-
riablen und damit auch die Startadresse des belegten Speicherbereichs unwieder-
bringlich verloren – die Freigabe kann zu einem späteren Zeitpunkt daher nicht
mehr gelingen. Typische Code-Analysatoren sind auch hier in der Lage, den Pro-
grammfehler zu erkennen:
> splint allocate.c
allocate.c: (in function allocate)
allocate.c:8:2: Fresh storage addr not released before return
A memory leak has been detected. Storage allocated locally
is not released before the last reference to it is lost.
allocate.c:7:2: Fresh storage addr created
An dieser Stelle gilt es zu beachten, dass weder der C-Compiler noch der Code-
Analysator prüft, ob der dynamisch belegte Speicher tatsächlich wieder freigege-
ben wird. Die generierte Warnmeldung weist lediglich darauf hin, dass eine Frei-
gabe durch den Verlust der Speicherreferenz nicht mehr möglich ist. Wird die
Variable addr außerhalb der Funktion allocate als globale Variable deklariert,
so verschwindet die Warnmeldung – unabhängig davon, wie mit dem dynamisch
belegten Speicherbereich verfahren wird.
I Programm 8: undef.c
Auch in diesem Programm wird die Funktion malloc eingesetzt, um ein dynami-
sches Speichersegment auf dem Heap zu belegen. Obwohl vor dem Verlassen der
main-Funktion ein Aufruf von free erfolgt, enthält das Programm einen schwer-
wiegenden Fehler. Wird es ohne einen einzigen Übergabeparameter gestartet, so
ist der Wert der Variablen argc gleich 1. Anstelle des If-Zweigs wird in die-
sem Fall der Else-Zweig abgearbeitet und der malloc-Befehl übersprungen. Der
Aufruf der Funktion free erfolgt trotzdem – diesmal jedoch mit einem uninitia-
lisierten Wert der Variablen mem.
Auch in diesem Fall leistet die statische Code-Analyse wertvolle Hilfestel-
lung:
> splint undef.c
undef.c: (in function main)
292 5 Statische Code-Analyse
Der Fehler lässt sich auf einfache Weise korrigieren. Wird der Aufruf von free
nicht mehr unmittelbar vor dem Rücksprung, sondern am Ende des If-Zweigs
aufgerufen, arbeitet das Programm fehlerfrei.
I Programm 9 I Programm 10
unused.c skip.c
int 1 #include <stdio.h> 1
main(int argc, char *argv[]) 2 2
{ 3 void 3
return argc; 4 skip(FILE *file, int n) 4
} 5 { 5
6 for (; n > 0; n--) 6
7 getc(file); 7
8 } 8
I Programm 11 I Programm 12
fallthrough.c bailout.c
#include <stdio.h> 1 #include <stdio.h> 1
2 #include <stdlib.h> 2
void 3 3
foo(char c) 4 static void bailout() 4
{ 5 { 5
unsigned uppercase = 0; 6 exit(0); 6
unsigned keycode = 0; 7 } 7
8 8
switch (c) { 9 int 9
case ’A’: 10 main(int argc, char *argv[]) 10
uppercase = 1; 11 { 11
case ’a’: 12 if (argc <= 1) { 12
keycode = 65; 13 printf("Syntax: ..."); 13
break; 14 bailout(); 14
case ’B’: 15 } else { 15
uppercase = 1; 16 /* stuff */ 16
case ’b’: 17 return 0; 17
keycode = 66; 18 } 18
break; 19 } 19
/* ... */ 20 20
} 21 21
/* stuff */ 22 22
} 23 23
Wie der Text der Warnung bereits andeutet, gibt es für solche Programmkon-
strukte eine einfache Lösung, die gänzlich ohne zusätzlich eingefügte Metakom-
mentare auskommt. Um die Meldung zu eliminieren, reicht es an dieser Stelle
aus, dem Funktionsaufruf einen simplen Void-Cast voranzustellen:
(void)getc(file);
Abhilfe schafft an dieser Stelle erneut ein spezieller Metakommentar, der expli-
zit auf die bewusste Verwendung des Fallthrough-Mechanismus hinweist. Die
Warnmeldung verschwindet, sobald das Programm an der Stelle des ausgelasse-
nen Break-Befehls um den Kommentar /*@fallthrough@*/ ergänzt wird.
Die Gcc-Meldung verschwindet, sobald wir dem Compiler mitteilen, dass es sich
bei der Funktion bailout um eine nicht terminierende Funktion handelt. Zu
diesem Zweck erlaubt der GNU-C-Compiler, die Deklaration bzw. die Definition
einer Funktion um entsprechende Attribute anzureichern. Konkret lässt sich die
weiter oben gezeigte Gcc-Warnung eliminieren, indem die Funktionssignatur um
das Attribut __noreturn__ ergänzt wird:
static void __attribute__((__noreturn__)) bailout()
I Programm 9 I Programm 10
unused.c skip.c
int 1 #include <stdio.h> 1
main (int argc, 2 2
/*@unused@*/ char *argv[]) 3 void 3
{ 4 skip(FILE *file, int n) 4
return argc; 5 { 5
} 6 for (; n > 0; n--) 6
7 (void)getc(file); 7
8 } 8
9 9
I Programm 11 I Programm 12
fallthrough.c bailout.c
#include <stdio.h> 1 #include <stdio.h> 1
2 #include <stdlib.h> 2
void 3 3
foo(char c) 4 static void __attribute__ 4
{ 5 ((__noreturn__)) 5
unsigned uppercase = 0; 6 bailout() 6
unsigned keycode = 0; 7 { 7
8 exit(0); 8
switch (c) { 9 } 9
case ’A’: 10 10
uppercase = 1; 11 int 11
/*@fallthrough@*/ 12 main(int argc, char *argv[]) 12
case ’a’: 13 { 13
keycode = 65; 14 if (argc <= 1) { 14
break; 15 printf("Syntax: ..."); 15
case ’B’: 16 bailout(); 16
uppercase = 1; 17 /*@NOTREACHED@*/ 17
/*@fallthrough@*/ 18 } else { 18
case ’b’: 19 /* stuff */ 19
keycode = 66; 20 return 0; 20
break; 21 } 21
/* ... */ 22 } 22
} 23 23
/* stuff */ 24 24
} 25 25
26 26
Abb. 5.23 Alle False-Positives lassen sich mit minimalen Eingriffen eliminieren
.h .c .h .c .h .c .h .c
gcc gcc
lint
.o .o
ld
Abb. 5.24 Im Gegensatz zu traditionellen C-Compilern erlaubt Lint die Analyse des Quellcodes
über Dateigrenzen hinweg
mals Lint vorbehaltenen Funktionalität verfügen, drängt sich an dieser Stelle un-
mittelbar die Frage auf, ob externe Code-Analysatoren heute immer noch eine Be-
rechtigung besitzen. Auch wenn sich prinzipiell noch striktere Tests in die Compiler
selbst integrieren ließen, ist die Antwort ein klares Ja. Der Grund hierfür geht auf
die primäre Arbeitsweise von Lint zurück, die sich in einem fundamentalen Punkt
von der eines C-Compilers unterscheidet: der dateiübergreifenden Analyse der Pro-
grammquellen.
Ein Grundpfeiler der Programmiersprache C ist die verteilte Compilierung, die
eine ausführbare Datei in einem zweischrittigen Prozess erzeugt. Im ersten Schritt
werden alle Quelldateien nacheinander in eine Reihe von Objektdateien übersetzt
und diese anschließend mit Hilfe des Linkers zu einer ausführbaren Datei ver-
schmolzen (vgl. Abb. 5.24 links). Konstruktionsbedingt ist der C-Compiler dadurch
ausschließlich in der Lage, lokale Inkonsistenzen zu erkennen. Im Gegensatz hier-
zu verfolgt Lint einen globalen Ansatz (vgl. Abb. 5.24 rechts), in dem alle Dateien
eines Projekts simultan analysiert werden. Hierdurch erschließt sich das Werkzeug
ein deutlich größeres Analysepotenzial als traditionell arbeitende C-Compiler und
ist insbesondere in der Lage, Fehler über Dateigrenzen hinweg aufzuspüren.
Um die Leistungsfähigkeit der dateiübergreifenden Analyse zu verdeutli-
chen, betrachten wir das Programm in Abb. 5.25. Die dargestellte Funktion
deep_thought diente uns bereits in Kapitel 3 als fruchtbares Beispiel für den
Vergleich von Big-Endian- und Little-Endian-Architekturen. Wie wir in Ab-
schnitt [Link] herausarbeiten konnten, enthält das Programm eine Typinkonsistenz
5.2 Konformitätsanalyse 299
deep_thought.h deep_thought.c
extern void 1 void ask_deep_thought 1
ask_deep_thought(int *); 2 (short *value) 2
3 { 3
4 *value = 42; 4
5 } 5
main.c
#include <stdio.h> 1
#include "deep_thought.h" 2
3
int main(int argc, char *argv[]) 4
{ 5
int answer = 0; 6
7
ask_deep_thought(&answer); 8
9
printf("The ultimate answer to the " 10
"question of life is %d\n", answer); 11
return 1; 12
} 13
Abb. 5.25 Um die vorhandene Typinkonsistenz zu erkennen, muss die statische Code-Analyse
dateiübergreifend durchgeführt werden
Das Beispiel zeigt, dass sich mit Hilfe statischer Code-Analysatoren Fehler auf-
spüren lassen, die mit traditionellen C-Compilern aufgrund ihrer konzeptuell un-
terschiedlichen Arbeitsweise nicht entdeckt werden können. Trotzdem soll an die-
ser Stelle nicht verschwiegen werden, dass der hier beschriebene Fehler mit ein
wenig Programmierdisziplin leicht hätte vermieden werden können. So gehört es
zur guten Programmierpraxis, die Funktionsprototypen auch in denjenigen Dateien
bekannt zu machen, in denen die deklarierten Funktionen implementiert werden.
Hierdurch bekommt der Compiler die Möglichkeit, die deklarierten mit den tat-
sächlich verwendeten Datentypen abzugleichen. Bezogen auf die Beispielfunktion
deep_thought reicht es aus, am Anfang der Datei deep_thought.c die Zeile
#include "deep_thought.h"
5.3 Exploit-Analyse
Neben der Bedrohung durch klassische Software-Fehler ist im Laufe der letzten
Jahre eine weitere Gefahr für den Betrieb vieler Software-Systeme herangewach-
sen. Die Rede ist von Schwachstellen innerhalb des Programmcodes, die im Nor-
malbetrieb zu keiner Fehlfunktion führen, jedoch mit Hilfe sogenannter Exploits
von fremden Angreifern missbraucht werden können. Hinter einem Exploit verbirgt
sich ein Skript oder Computerprogramm, das die Schwachstelle gezielt ausnutzt,
indem der verwundbare Programmcode mit speziell präparierten Eingabedaten aus-
geführt wird. Das Spektrum der Manipulation, mit dem Software-Entwickler und
-Tester heute fast täglich zu kämpfen haben, reicht von Exploits, die das angegrif-
fene Programm schlicht zum Absturz bringen, bis hin zu Angriffen, die unbemerkt
den Zugang zu fremden Computernetzen ermöglichen [151, 124, 155, 97, 86].
War die Bedrohung von Software-Systemen durch fremde Angreifer in den An-
fängen der Computertechnik so gut wie nicht vorhanden, ist sie seit dem Aufkeimen
der weltweiten Vernetzung allgegenwärtig. Heute vergeht kaum ein Tag, an dem
keine neue Sicherheitslücke gemeldet wird. Insbesondere diejenigen Firmen, de-
ren Geschäfts- oder Vertriebsmodelle vollständig auf das Internet ausgerichtet sind,
sehen sich kaum zu kalkulierenden Risiken ausgesetzt. Mittlerweile besitzt die Ver-
meidung von Sicherheitslücken heute den gleichen Stellenwert wie die Elimination
klassischer Software-Fehler.
5.3 Exploit-Analyse 301
Der Begriff der Exploit-Analyse fasst alle Methoden und Verfahren zusammen,
die zur Aufdeckung potenzieller Schwachstellen eines Software-Systems dienen.
Doch bevor wir uns genauer mit den zur Verfügung stehenden Analysetechniken be-
schäftigen, soll im nächsten Abschnitt zunächst ein kleiner Einblick in die Mittel ge-
währt werden, mit denen ein angreifbares Software-System kompromittiert werden
kann. Am Beispiel eines klassischen Buffer overflows soll exemplarisch aufgezeigt
werden, wie eine heute weit verbreitete Schwachstelle gezielt für einen feindlichen
Angriff ausgenutzt werden kann. Da sich typische Exploits sehr spezifischer Ei-
genschaften der angegriffenen Betriebssystem- und Prozessorarchitektur bedienen,
sind elementare Assembler-Kenntnisse hilfreich, für das grundlegende Verständnis
aber nicht zwingend erforderlich. Für den im Folgenden beschriebenen Angriff wird
die x86-Prozessorarchitektur zugrunde gelegt, die heute den De-facto-Standard im
PC- und Server-Bereich bildet. Das grundlegende Angriffsmuster lässt sich auf die
meisten anderen Prozessorarchitekturen ohne größere Änderungen übertragen.
infinite_loop.c
void foo() { 1
int i; 2
int a[12]; 3
4
for (i=0; i <= 12; i++) 5
a[i] = 0; 6
} 7
Abb. 5.26 Das Beschreiben eines Arrays über seine Grenzen hinweg führt zu einem Pufferüberlauf
(buffer overflow)
wird die Variable i in der letzten Schleifeniteration stets auf 0 zurückgesetzt und
die Schleifenbedingung i<=12 permanent erfüllt. Kurzum: Das Programm gerät in
eine Endlosschleife.
[Link] Stack-Layout
Der Stack selbst ist eines von mehreren Datensegmenten, die ein einzelner Prozess
im Speicher belegt. Auf Unix-ähnlichen Betriebssystemen wie Linux, Solaris oder
Mac OS X, kommen das Textsegment und das Datensegment hinzu:
I Textsegment
Das Textsegment enthält den auszuführenden Programmcode sowie nicht ver-
änderliche Daten. Die belegten Speicherseiten sind typischerweise als read-only
markiert und können nicht verändert werden. Jeder Schreibversuch löst einen
Prozessor-Interrupt aus, der das Betriebssystem in der Regel zu einer sofortigen
Beendigung des laufenden Prozesses veranlasst.
I Datensegment
Das Datensegment befindet sich zwischen dem Text- und dem Stack-Segment.
Neben dem Speicherplatz für global angelegte Konstanten enthält es den BSS-
Bereich und den Heap. Der BSS-Bereich enthält alle mit 0 vorinitialisierten sta-
tischen Daten, während der Heap die dynamisch belegten Speicherbereiche ver-
waltet.
Abb. 5.27 fasst das allgemeine Speicherabbild eines einzelnen Prozesses grafisch
zusammen.
Wächst der Stack- oder Heap-Bereich im Laufe der Programmausführung so
stark an, dass eine Überlappung der einzelnen Segmente droht, so werden die
Speicherbereiche dynamisch vergrößert. Hierzu hält das Betriebssystem den betref-
fenden Prozess an und fügt neuen Speicher zwischen Stack- und Datensegment ein.
Anschließend wird der blockierte Prozess wieder in die Warteschlange der lauffähi-
gen Prozesse eingereiht.
Anders als der Heap, der keine Zugriffsreihenfolge auf die gespeicherten Daten-
elemente vorgibt, arbeitet der Stack nach dem LIFO-Prinzip (Last In, First Out).
5.3 Exploit-Analyse 303
freier
Code Static BSS Heap Stack
Speicher
SP BP
Abb. 5.27 Speicherabbild eines einzelnen Prozesses. Der Stack ist auf den meisten Rechnerarchi-
tekturen in absteigender Speicherrichtung organisiert
Diesem Prinzip folgend wird bei einer Leseoperation stets auf das zuletzt abge-
legte Element zugegriffen. Hierdurch ist für die Verwaltung des Stacks ein einziger
Schreiblesezeiger ausreichend, der als Stapelzeiger oder Stack-Pointer (kurz SP) be-
zeichnet wird. In den meisten Mikroprozessoren wird der Stapelzeiger in einem spe-
ziell dafür vorgesehenen CPU-Register gespeichert. In Abhängigkeit der zugrunde
liegenden Rechnerarchitektur zeigt der Stack-Pointer entweder auf das zuletzt ge-
speicherte Stack-Element oder auf die als nächstes zu beschreibende Speicherstelle.
Für den Zugriff auf die Stack-Elemente verfügen die gängigen CPUs über spe-
zielle PUSH- und POP-Instruktionen. Wird ein Datenwort auf den Stack geschrie-
ben (PUSH-Operation), so wird der Inhalt des SP-Registers dekrementiert. Im Fal-
le des Zurücklesens (POP-Operation) wird der Registerinhalt inkrementiert. Der
Stack wird aufgrund seiner LIFO-Arbeitsweise vor allem für die Durchführung von
Funktionsaufrufen genutzt. Hierzu wird bei jedem Einsprung in eine Unterfunktion
ein logischer Stack-Rahmen (stack frame) auf dem Stack angelegt und bei Verlassen
wieder entfernt. Neben den lokalen Variablen und etwaig vorhandenen Übergabepa-
rametern werden an dieser Stelle auch Informationen zur Wiederherstellung des al-
ten Stack-Zustands abgelegt. Hierzu gehört auch der Inhalt des Instruktionsregisters
vor dem Unterprogrammaufruf. Der Wert wird als Rücksprungadresse verwendet,
sobald die aufgerufene Funktion vollständig abgearbeitet ist.
Viele der gängigen Prozessoren speichern die Startadresse des aktuellen Stack-
Frames in einem separaten CPU-Register zwischen, der als Frame pointer (FP) oder
Base pointer (BP) bezeichnet wird. Die Intel-x86-Architektur hält für die Speiche-
rung des Stack- bzw. Base-Pointers die beiden CPU-Register ESP bzw. EBP vor.
Um den genauen Ablauf eines Funktionsaufrufs zu verstehen, betrachten wir das
Beispielprogramm in Abb. 5.28. Mit Hilfe des Compiler-Aufrufs
> gcc -S -o buffer_overflow.s buffer_overflow.c
304 5 Statische Code-Analyse
buffer_overflow.c buffer_overflow.s
#include "stdio.h" 1 .text 1
#include "string.h" 2 .globl _foo 2
3 _foo: 3
void 4 pushl %ebp 4
foo(char *s) 5 movl %esp, %ebp 5
{ 6 subl $40, %esp 6
char buf[16]; 7 movl 8(%ebp), %eax 7
8 movl %eax, 4(%esp) 8
strcpy(buf, s); 9 leal -24(%ebp), %eax 9
} 10 movl %eax, (%esp) 10
11 call L_strcpy$stub 11
int 12 leave 12
main(int argc, char **argv) 13 ret 13
{ 14 .globl _main 14
foo(argv[1]); 15 _main: 15
return 0; 16 pushl %ebp 16
} 17 movl %esp, %ebp 17
18 subl $24, %esp 18
19 movl 12(%ebp), %eax 19
20 addl $4, %eax 20
21 movl (%eax), %eax 21
22 movl %eax, (%esp) 22
23 call _foo 23
24 movl $0, %eax 24
25 leave 25
26 ret 26
27 .section __IMPORT, 27
28 __jump_table, 28
29 symbol_stubs, 29
30 self_modifying_code 30
31 +pure_ins 31
32 tructions,5 32
33 L_strcpy$stub: 33
34 .indirect_symbol _strcpy 34
35 hlt; hlt; hlt; hlt; hlt 35
36 .subsections_via_symbols 36
Abb. 5.28 Für den Aufruf der Funktion foo wird ein logischer Stack-Frame erzeugt
pushl %ebp
5.3 Exploit-Analyse 305
Abb. 5.29 Speicherabbild des Stacks nach dem Aufruf der Funktion foo
movl %esp,%ebp
subl $40,%esp
Mit Hilfe des pushl-Befehls wird zunächst der aktuelle Base-Pointer auf dem Stack
abgelegt. Mit dem sich anschließenden movl-Befehl wird der Base-Pointer mit dem
aktuellen Wert des Stack-Pointers überschrieben. Damit wird das aktuelle Stack-
Ende als neuer Base-Pointer verankert und ein neuer Stack-Frame erzeugt (vgl.
Abb. 5.29 oben). Der subl-Befehl dekrementiert schließlich den Stack-Pointer um
einen konstanten Wert und schafft auf diese Weise Platz für die Speicherung der
lokalen Variablen.
Die Funktion foo endet mit dem Funktions-Epilog
leave
ret
Der Befehl leave stellt den ursprünglichen Stack-Zustand wieder her und ist zu der
folgenden Befehlssequenz äquivalent:
movl %ebp,%esp
popl %ebp
Der erste movl-Befehl setzt den Stack-Pointer auf den aktuellen Base-Pointer zu-
rück und verwirft damit den zuletzt angelegten Stack-Frame. Anschließend wird
der Wert des Base-Pointers mit Hilfe des popl-Befehls wiederhergestellt.
Schließlich überschreibt der ret-Befehl den Inhalt des Instruktionsregisters mit
der auf dem Stack gespeicherten Rücksprungadresse und setzt damit die Program-
mausführung unmittelbar hinter dem ursprünglich initiierten call-Befehl fort.
306 5 Statische Code-Analyse
[Link] Injektionsvektoren
Abb. 5.29 (unten) zeigt, wie sich der Programmablauf mit Hilfe eines gezielten Puf-
ferüberlaufs manipulieren lässt. Befüllen wir das Character-Array buf über seine
Kapazität hinaus, so wird neben dem gesicherten Base-Pointer insbesondere auch
die Rücksprungadresse überschrieben. Diese Schwachstelle des Stack-Layouts wird
von vielen Exploits für einen Angriff ausgenutzt. Hierzu wird der Puffer buf mit ei-
nem Injektionsvektor befüllt, der den eigentlichen Schadcode enthält. Der Vektor
überschreibt den Stack über seine Grenze hinaus und führt so einen künstlichen
Pufferüberlauf herbei. Die Rücksprungadresse wird dabei gezielt auf die Startadres-
se von buf umgelenkt, so dass beim Verlassen der Funktion nicht mehr länger an
die ursprüngliche Aufrufstelle, sondern direkt an die Startadresse des Schadcodes
gesprungen wird.
Soviel zur Grundidee eines typischen Buffer-Overflow-Exploits. Damit ein sol-
cher Angriff in der Praxis wirklich funktioniert, sind weitere Programmierkniffe
notwendig, die hier nur kurz umrissen werden sollen. Ein erstes Problem entsteht
im Zusammenhang mit der manipulierten Rücksprungadresse. Damit diese auf den
Anfang von buf zeigt, muss dessen absolute Adresse im Speicher bekannt sein.
Hier kommt dem Angreifer die Eigenschaft vieler virtueller Speicherverwaltungen
zugute, die den Stack-Bereich für alle Programme an derselben logischen Adres-
se beginnen lassen. Der mögliche Bereich, in dem sich ein kompromittierter Puffer
befinden kann, ist hierdurch deutlich eingeschränkt. Typische Exploits verwenden
zudem Schadcode, der zusätzlich mit einer Reihe von NOP-Befehlen (no operation)
eingeleitet wird. Hierdurch kann ein Angriff selbst dann erfolgreich durchgeführt
werden, wenn das Stack-Layout des angegriffenen Software-Systems nur grob be-
kannt ist.
Ein weiteres Problem stellt die innerhalb von foo verwendete Funktion strcpy
dar. Da in C alle Strings nullterminiert sind, bricht strcpy ab, sobald ein Null-
byte kopiert wurde. Aus diesem Grund muss der einzuschleusende Schadcode so
programmiert werden, dass der erzeugte Bytestrom keinerlei Nullbytes enthält. Die
grundlegende Idee des Angriffs ändert sich hierdurch jedoch nicht.
Auch die Länge des kompromittierten Puffers kann zum Problem werden. Da der
Schadcode vollständig in den Puffer hineinkopiert wird, ist dessen maximale Länge
deutlich beschränkt. Viele Exploits verwenden daher kurze Injektionsvektoren, die
nichts anderes bewirken, als eine Shell zu starten. Wie der exemplarisch erzeugte
Assembler-Code in Abb. 5.30 zeigt, reichen hierfür wenige Instruktionen aus. Die
neu geöffnete Shell läuft mit den Rechten der kompromittierten Applikation und
kann dazu verwendet werden, weitere Angriffe zu initiieren.
5.3.2 Gegenmaßnahmen
In den letzten Jahren wird in verstärktem Maße versucht, potenzielle Sicherheits-
lecks mit den Mitteln der statischen Code-Analyse aufzuspüren. Die Untersuchung
der Quelltexte kann auf der syntaktischen oder der semantischen Ebene erfolgen.
5.3 Exploit-Analyse 307
shell_code.c shell_code.s
#include "stdio.h" 1 main: 1
2 leal 4(%esp), %ecx 2
void main() { 3 andl $-16, %esp 3
char *cmd[2] = 4 pushl -4(%ecx) 4
{ "/bin/sh", NULL }; 5 pushl %ebp 5
execve(cmd[0],cmd,NULL); 6 movl %esp, %ebp 6
} 7 pushl %ecx 7
8 subl $36, %esp 8
9 movl $.LC0, -12(%ebp) 9
10 movl $0, -8(%ebp) 10
11 movl -12(%ebp), %edx 11
12 movl $0, 8(%esp) 12
13 leal -12(%ebp), %eax 13
14 movl %eax, 4(%esp) 14
15 movl %edx, (%esp) 15
16 call execve 16
17 addl $36, %esp 17
18 popl %ecx 18
19 popl %ebp 19
20 leal -4(%ecx), %esp 20
21 ret 21
Abb. 5.30 Der abgedruckte Schadcode öffnet eine Shell, über die weitere Angriffe erfolgen kön-
nen
Tabelle 5.4 Potenziell gefährliche Funktionen der C-Standardbibliothek und deren sichere Alter-
nativen
Funktion Risiko Bemerkung Alternativen
gets Sehr hoch Generell unsicher fgets
strcpy Hoch Nur sicher bei manueller Längenprüfung strncpy, strlcpy
strcat Hoch Nur sicher bei manueller Längenprüfung strncat, strlcat
sprintf Hoch Nur sicher bei manueller Längenprüfung snprintf
vsprintf Hoch Nur sicher bei manueller Längenprüfung vsnprintf
scanf Hoch Nur sicher bei expliziter Feldgrößenangabe –
sscanf Hoch Nur sicher bei expliziter Feldgrößenangabe –
vsscanf Hoch Nur sicher bei expliziter Feldgrößenangabe –
vfscanf Hoch Nur sicher bei expliziter Feldgrößenangabe –
Die erste Warnmeldung weist auf eine potenzielle Sicherheitslücke hin, die durch
die Verwendung der C-Bibliotheksfunktion strcpy entsteht. Diese überträgt eine
variable Anzahl Bytes von der Startadresse (zweiter Parameter) an die Zieladresse
(erster Parameter). Da die Funktion strcpy keine explizite Möglichkeit der Län-
genbegrenzung vorsieht – der Kopiervorgang bricht nur dann ab, wenn das String-
Ende in Form des Nullbytes kopiert wurde –, ist sie Mitglied einer größeren Grup-
pe potenziell unsicherer C-Funktionen. Neben einigen klassischen Funktionen zur
String-Manipulation finden sich dort auch diverse Vertreter der printf- und scanf-
Funktionsfamilien sowie die Funktion gets wieder (vgl. Tabelle 5.4).
Die Funktion gets verdeutlicht eindringlich, wie sehr die Programmiersprache
C auch heute noch durch ihre historischen Wurzeln geprägt wird. Die in der Header-
Datei stdio.h deklarierte Funktion besitzt die Signatur
char *gets(char *s);
5.3 Exploit-Analyse 309
und liest eine komplette Zeile aus dem Standard-Stream stdio ein. Da sich die ma-
ximale Anzahl der eingelesenen Zeichen in keiner Weise begrenzen lässt, ist die
Verwendung dieser Bibliotheksfunktion immer mit einem Sicherheitsrisiko verbun-
den. Selbst die Online-Beschreibung von gets warnt inzwischen eindringlich vor
der Verwendung dieser Bibliotheksfunktion:
“BUGS Never use gets(). Because it is impossible to tell without knowing
the data in advance how many characters gets() will read, and because
gets() will continue to store characters past the end of the buffer, it is ex-
tremely dangerous to use. It has been used to break computer security. Use
fgets() instead.”
gets(3)-ManPage (Linux)
Dass sich die Funktion gets trotz ihrer Defizite noch immer in der C-Bibliothek
befindet, ist der Existenz vieler alter C-Programme geschuldet, die ausgiebig auf
diese Funktion zurückgreifen. Leider ist das gets-Problem bei weitem kein Einzel-
fall, sondern vielmehr das Symptom einer tieferliegenden Problematik, mit der sich
heute ein beträchtlicher Teil der IT-Industrie konfrontiert sieht. Gemeint ist das in-
zwischen allgegenwärtige Problem der Rückwärtskompatibilität – ein Problem, auf
das wir in Abschnitt 7.2.4 zusammen mit den damit verbundenen Auswirkungen im
Detail zurückkommen werden.
Glücklicherweise existieren für viele der potenziell unsicheren Bibliotheksfunk-
tionen sichere Alternativen (vgl. Tabelle 5.4, rechte Spalte). So kann die in unserem
Beispielprogramm verwendete Funktion strcpy z. B. durch die sichere Variante
strncpy ersetzt werden. In der Header-Datei string.h ist die Funktion mit der
folgenden Signatur deklariert:
size_t strncpy(char *dst, const char *src, size_t size);
Im Gegensatz zu strcpy nimmt strncpy mit dem dritten Parameter size die Grö-
ße des Zielpuffers entgegen und lässt keine Speicherzugriffe außerhalb des spezifi-
zierten Bereichs zu. Des Weiteren wird der duplizierte String-Abschnitt mit einem
Null-Byte abgeschlossen, sofern der Zielspeicher noch mindestens ein weiteres Byte
aufnehmen kann.
Abb. 5.31 zeigt das mit Hilfe von strncpy abgesicherte Beispielprogramm. Wie
erwartet, verschwindet bei einem erneuten Aufruf von Flawfinder der Hinweis auf
das ursprünglich vorhandene Sicherheitsrisiko. Nicht verschwunden ist hingegen
der zweite Warnhinweis. Genau wie im Falle des Originalprogramms beanstandet
Flawfinder die Definition der Variablen buf als Array fester Größe.
310 5 Statische Code-Analyse
strncpy.c
#include "stdio.h" 1
#include "string.h" 2
3
void foo(char *s) { 4
char buf[16]; 5
6
strncpy(buf, s, sizeof(buf)); 7
} 8
9
int main(int argc, char **argv) { 10
foo(argv[1]); 11
return 0; 12
} 13
Abb. 5.31 Durch den Einsatz der sicheren Bibliotheksfunktion strncpy wird das Sicherheitsleck
geschlossen
Damit haben wir eine wesentliche Schwäche aller rein syntaktisch arbeitenden
Exploit-Analysatoren herausgearbeitet. Im Rahmen der Analyse werden zahlreiche
False negatives erzeugt. Anders als im Fall von Lint lassen sich die generierten
Scheinmeldungen nur sehr schwer oder überhaupt nicht eliminieren, so dass sich die
Exploit-Analyse schnell zur nervenstrapazierenden Geduldsprobe entwickelt. Alles
in allem sind rein syntaktisch arbeitende Werkzeug hierdurch nur sehr eingeschränkt
für den produktiven Einsatz zu empfehlen.
Schlimmer noch: Die verwendeten Syntax-Parser sind in vielen Fällen so ein-
fach gestrickt, dass komplizierte Programmkonstrukte nicht mehr korrekt analysiert
werden können. Als Beispiel betrachten wir das in der oberen Hälfte von Abb. 5.32
dargestellte Programm. Zu Beginn wird das C-Makro PRINT definiert. Der Präpro-
zessor substituiert jedes Vorkommen des Makros durch einen entsprechenden Auf-
ruf der Funktion printf und gibt das Ergebnis anschließend an den Compiler wei-
ter. Ohne die Angabe einer speziellen Option wird die Präprozessorausgabe, für den
Benutzer verborgen, im Hintergrund erzeugt. Die Ausgabe wird erst sichtbar, wenn
der Compiler mit der Kommandozeilenoption -E gestartet wird:
> gcc -E fool_flawfinder.c
Der für unser Programm erzeugte Zwischencode ist in Abb. 5.32 (unten) dargestellt.
Ein Blick auf die generierten printf-Befehle zeigt, dass der zweite Aufruf einen
gravierenden Fehler enthält – anstelle eines Format-Strings wird als erstes Argument
die Variable argc übergeben.
Da viele syntaktisch arbeitende Exploit-Analysatoren auf die Expansion von Ma-
kros verzichten, bleibt die fehlerhafte Nutzung der Funktion printf vollständig
verborgen. So moniert flawfinder lediglich die Verwendung von printf in der
Makro-Definition – sämtliche Aufrufe bleiben dagegen ungeprüft:
> flawfinder fool_flawfinder.c
5.3 Exploit-Analyse 311
I Implementierung
fool_flawfinder.c
#include "stdio.h" 1
2
#define PRINT(x) printf (x) 3
4
int main(int argc, char *argv[]) { 5
PRINT ("Number of arguments: "); 6
PRINT (argc); 7
return 0; 8
} 9
I Präprozessor-Ausgabe
gcc -E fool_flawfinder.c
1
... 2
3
int main(int argc, char *argv[]) { 4
printf ("Number of arguments: "); 5
printf (argc); 6
7
return 0; 8
} 9
Angewendet auf das abgesicherte Programm in Abb. 5.31 produziert Splint dagegen
die folgende Ausgabe:
> splint +boundswrite buffer_overflow_strlcpy.c
Splint 3.1.1 --- 30 Dec 2006
Die falsche Verwendung von printf wird jetzt präzise detektiert. Insgesamt sind
semantische Exploit-Analysatoren für den produktiven Einsatz besser geeignet als
rein syntaktisch arbeitende Werkzeuge. Trotzdem wäre jegliche Euphorie an dieser
Stelle verfrüht. Nicht jedes Sicherheitsleck ist so offensichtlich zu erkennen wie in
den hier vorgestellten Beispielen. Entsprechend oft bleiben kritische Programmab-
schnitte sowohl der syntaktischen als auch der semantischen Code-Analyse verbor-
gen. In der Konsequenz können Exploit-Analysatoren den Programmierer in der Ab-
sicherung eines Software-Systems zwar unterstützen, jedoch keinesfalls eine hun-
dertprozentige Sicherheit garantieren. Das größte Potenzial zur Erstellung sicheren
5.4 Anomalienanalyse 313
sign.c even.c
int sign(int x) 1 int even(int x) 1
{ 2 { 2
if (x >= 0) { 3 if (x % 2 == 0) { 3
return 1; 4 return 1; 4
} else { 5 } 5
return -1; 6 if (x % 2 == 1) { 6
} 7 return 0; 7
return 0; 8 } 8
} 9 return 0; 9
10 } 10
5.4 Anomalienanalyse
Im Rahmen der Anomalienanalyse werden die Quelltexte eines Software-Systems
auf ungewöhnliche oder auffällige Anweisungssequenzen untersucht. Die Suche ist
dabei bewusst nicht auf solche Programmkonstrukte beschränkt, die in jedem Fall
ein Fehlverhalten verursachen. Stattdessen steht das Auffinden von Anweisungsse-
quenzen im Vordergrund, die im statistischen Sinne auf einen Fehler hindeuten. Mit
anderen Worten: Nicht jede Software-Anomalie ist tatsächlich ein Software-Fehler.
Auf der obersten Ebene lassen sich Software-Anomalien in Kontrollfluss- und Da-
tenflussanomalien einteilen.
5.4.1 Kontrollflussanomalien
In die Gruppe der Kontrollflussanomalien fallen alle Anweisungssequenzen, die auf
eine Unstimmigkeit im Programmablauf hinweisen. Als Beispiel betrachten wir das
in Abb. 5.33 (links) abgebildete C-Fragment. Die definierte Funktion sign nimmt
eine Integer-Variable als Argument entgegen und bestimmt in der sich anschließen-
den If-Abfrage das Vorzeichen. Da die Funktion sowohl innerhalb des If-Zweigs als
auch innerhalb des Else-Zweigs unmittelbar verlassen wird, kommt der abschlie-
ßende Return-Befehl niemals zur Ausführung. Das Beispielprogramm enthält damit
eine typische Kontrollflussanomalie in Form von unerreichbarem Code. Auch wenn
nicht jede Anomalie dieser Art notwendigerweise zu einem Software-Fehler führt,
ist die Wahrscheinlichkeit außerordentlich hoch, dass der Autor der Funktion ur-
sprünglich einen anderen Programmablauf im Sinn hatte.
Ist eine Kontrollflussanomalie so einfach strukturiert wie in unserem Beispiel,
kann diese mit den Mitteln der semantischen Code-Analyse automatisch aufgespürt
314 5 Statische Code-Analyse
werden. Der Code-Analysator Splint weist mit der folgenden Warnmeldung auf den
potenziellen Fehler hin:
splint sign.c
Splint 3.1.1 --- 30 Dec 2006
Dass sich die Suche nach Kontrollflussanomalien nur schwer automatisieren lässt,
ist nicht zuletzt der Unentscheidbarkeit des Halteproblems geschuldet – ein ge-
nauso fundamentales wie bedeutendes Ergebnis der theoretischen Informatik. Die
Unentscheidbarkeit des Halteproblems bedeutet, dass es keine algorithmische Be-
rechnungsvorschrift geben kann, die für jedes Programm und jede beliebige Ein-
gabe stets korrekt entscheidet, ob der Programmablauf terminiert. In Abschnitt 6.2
werden wir diesem Phänomen im Zusammenhang mit der deduktiven Software-
Verifikation erneut begegnen.
Die Unentscheidbarkeit des Halteproblems hat weitreichende Konsequenzen für
die automatisierte Suche nach Kontrollflussanomalien, da es sich ohne Umwege in
ein äquivalentes Erreichbarkeitsproblem umformulieren lässt. Aus der Unentscheid-
barkeit folgt in diesem Fall unmittelbar, dass es keine algorithmische Berechnungs-
vorschrift geben kann, die für jedes Programm zweifelsfrei entscheidet, ob eine be-
stimmte Programmstelle erreicht werden kann oder nicht. Unsere Möglichkeiten,
nicht erreichbare Code-Abschnitte automatisiert aufzuspüren, sind damit durch die
Berechenbarkeitstheorie fundamental begrenzt.
Nichtsdestotrotz existiert mit der Technik der abstrakten Interpretation ein viel-
versprechender Ansatz für die automatisierte Suche nach gerade solchen Kontroll-
flussanomalien. Natürlich kann auch diese Technik die fundamentalen Ergebnisse
der Berechenbarkeitstheorie nicht aushebeln. Wenn auch nicht alle, so können mit
entsprechenden Analysatoren trotzdem viele in der Praxis auftretende Anomalien
detektiert werden. In Abschnitt 6.4 werden wir auf die Technik der abstrakten Inter-
5.4 Anomalienanalyse 315
pretation zurückkommen und deren Vor- und Nachteile einer genaueren Betrachtung
unterziehen.
5.4.2 Datenflussanomalien
Zur Gruppe der Datenflussanomalien gehören alle Anweisungssequenzen, die un-
stimmige oder zweifelhafte Variablenzugriffe beinhalten. Für die Analyse werden
die Zugriffe auf eine Variable x in drei Kategorien eingeteilt:
Als Beispiel betrachten wir das in Abb. 5.34 dargestellte Programm. Als erste Ak-
tion wird in der Einsprungsfunktion main die Anzahl der übergebenen Komman-
dozeilenparameter überprüft. Wurde das Programm mit mindestens einem Parame-
ter aufgerufen (argc > 1), wird ein 1024 Byte großer Speicherbereich belegt und
der Rückgabewert result mit 1 überschrieben. Die Startadresse des angeforderten
Speichers wird in der Variablen ptr vermerkt und der belegte Bereich am Ende des
Programms mit Hilfe der Funktion free wieder frei gegeben.
Der Datenflussgraph der Funktion main ist auf der linken Seite von Abb. 5.35
dargestellt. Die einzelnen Zustände des Graphen sind mit Attributen versehen, die
jeden Variablenzugriff entsprechend der oben eingeführten Nutzungsszenarien ka-
tegorisieren. Sammeln wir die Datenflussattribute für eine Variable auf dem Weg
316 5 Statische Code-Analyse
anomalie.c
#include <stdlib.h> 1
2
int main(int argc, char *argv[]) 3
{ 4
int result = 0; 5
char *ptr; 6
7
if (argc > 1) { 8
ptr = (char *)malloc(1024); 9
/* stuff */ 10
result = 1; 11
} 12
free(ptr); 13
} 14
Daten&ussgraph
main(int argc, d(argc) I Pfad 1: (0, 1, 2, 4, 5)
char *argv[]) { 0 d(argv)
0 1 2 4 5
int result = 0; d(result) argc d r u
char *ptr; 1 u(ptr) argv d u
result d u
if (argc > 1) 2 r(argc) ptr u ru u
ptr = (char *)
d(ptr) I Pfad 2: (0, 1, 2, 3, 4, 5)
malloc(1024); 3
d(result)
result = 1;
0 1 2 3 4 5
r(ptr)
free(ptr); 4 u(ptr) argc d r u
argv d u
u(argc)
u(argv) result d d u
} 5 u(result) ptr u d ru u
u(ptr)
Abb. 5.35 Jeder Pfad des Datenflussgraphen erzeugt für jede Variable eine individuelle Zugriffs-
signatur
vom Start zu den Endkonten nacheinander auf, so entsteht für jeden Pfad eine in-
dividuelle Zugriffssignatur. Für die beiden Pfade des Beispielgraphen erhalten wir
das auf der rechten Seite in Abb. 5.35 dargestellte Ergebnis.
In nahezu allen Programmen zeigen die konsekutiven Variablenzugriffe typische,
immer wiederkehrende Interaktionsmuster. Hierunter fällt unter anderem die ud-
Interaktion, die z. B. immer dann entsteht, wenn eine Variable deklariert und an-
schließend mit einem bestimmten Wert initialisiert wird. Ein anderes, häufig ange-
5.4 Anomalienanalyse 317
Tabelle 5.5 Unter den 9 möglichen Interaktionsmustern weisen 3 auf eine Datenflussanomalie hin
Muster Beschreibung Anomalie?
dd Variable wird zweimal hintereinander überschrieben Ja
dr Variable wird überschrieben und anschließend verwendet Nein
du Variable wird beschrieben und anschließend gelöscht Ja
rd Variable wird zunächst verwendet und dann überschrieben Nein
rr Variable wird zweimal hintereinander verwendet Nein
ru Variable wird verwendet, dann gelöscht Nein
ud Undefinierte Variable wird überschrieben Nein
ur Undefinierte Variable wird verwendet Ja
uu Undefinierte Variable wird gelöscht Nein
troffenes Muster ist die dr-Interaktion. In diesem Fall wird eine Variable zunächst
beschrieben und anschließend in einer Anweisung oder einer Verzweigungsbedin-
gung ausgewertet. Eine vollständige Liste der Interaktionsmuster ist in Tabelle 5.5
zusammengefasst. Von den 9 möglichen Mustern weisen die folgenden drei auf eine
Datenflussanomalie hin:
I ur-Interaktion
Hinter nahezu jeder ur-Interaktion verbirgt sich ein schwerwiegender Software-
Fehler, da der Wert einer Variablen verwendet wird, die (noch) gar keinen defi-
nierten Wert besitzt. Ein gezielter Blick auf die Zugriffssignaturen unseres Bei-
spielprogramms zeigt, dass ein solches Interaktionsmuster in der Zugriffssigna-
tur der Variablen ptr auftaucht. Auf dem Pfad (0, 1, 2, 4, 5) wird die innerhalb
des If-Zweigs vorgenommene Wertzuweisung nicht ausgeführt und am Ende des
Programms ein undefinierter Wert an die Funktion free übergeben. Nicht ganz
unerwartet reagiert das Programm auf dem Testrechner mit einem Systemab-
sturz.
I du-Interaktion
Liegt eine du-Interaktion vor, so wird eine Variable mit einem neuen Wert be-
schrieben, der anschließend weder einer berechnenden noch einer prädikativen
Nutzung zugeführt wird. In unserem Beispielprogramm existiert für die Variable
result auf beiden Ausführungspfaden eine du-Interaktion: Die Variable wird
beschrieben, der Wert anschließend jedoch nicht verwendet. Die entdeckte Da-
tenflussanomalie offenbart, dass sich ein Flüchtigkeitsfehler in unser Beispiel-
programm einschleichen konnte. Wie der Name zweifelsfrei andeutet, speichert
die Variable result den Ergebniswert der Funktion main. Der für die Rückgabe
erforderliche Return-Befehl ist in unserem Programm jedoch nicht vorhanden.
Obwohl die Mehrheit der du-Interaktionen, wie in unserem Beispiel, durch
handfeste Software-Fehler entstehen, gibt es Situationen, in denen solche Inter-
aktionen absichtlich erzeugt werden. So interagieren z. B. viele Gerätetreiber mit
der angeschlossenen Hardware durch das Beschreiben spezieller Variablen, die
318 5 Statische Code-Analyse
anomalie_corrected.c
#include <stdlib.h> 1
2
int main(int argc, char *argv[]) 3
{ 4
int result; 5
char *ptr; 6
7
if (argc > 1) { 8
ptr = (char *)malloc(1024); 9
/* stuff */ 10
free(ptr); 11
result = 1; 12
} else { 13
result = 0; 14
} 15
return result; 16
} 17
Abb. 5.36 Durch wenige Änderungen lassen sich alle Datenflussanomalien beseitigen
I dd-Interaktion
Eine dd-Interaktion entsteht immer dann, wenn der Wert einer Variablen zwei-
mal hintereinander geändert wird, ohne dass der zuerst geschriebene Wert au-
genscheinlich verwendet wurde. In unserem Beispielprogramm entsteht für die
Variable result eine dd-Interaktion auf dem Pfad (0, 1, 2, 3, 4, 5). Auf diesem
Pfad wird die Variable zum Zeitpunkt ihrer Deklaration mit 0 initialisiert und
anschließend innerhalb des If-Zweigs mit dem Wert 1 überschrieben. Zwischen
beiden Zuweisungen findet weder eine berechnende noch eine prädikative Nut-
zung der Variablen result statt. Im Gegensatz zu den zuvor aufgedeckten Da-
tenflussanomalien handelt es sich im Falle der gefundenen dd-Interaktion nicht
um einen Software-Fehler. Die zweimalige Zuweisung eines Werts auf dem Pfad
(0, 1, 2, 3, 4, 5) ist funktional völlig korrekt und die Programmstruktur in diesem
Fall durch den Programmierer mit Absicht so gewählt.
Abb. 5.36 zeigt, wie sich die drei Datenflussanomalien des Beispielprogramms auf
einfache Weise beseitigen lassen. Die ur-Interaktion wird eliminiert, indem der
free-Befehl in den If-Zweig verschoben und dadurch stets mit einem initialisier-
ten Wert aufgerufen wird. In der korrigierten Programmversion wird die Funktion
main jetzt mit einer Return-Instruktion beendet, so dass auch die du-Interaktion ver-
schwindet. Darüber hinaus wurde die dd-Interaktion durch das Hinzufügen eines
zusätzlichen Else-Zweigs eliminiert. Die Variable result wird nicht mehr während
der Deklaration, sondern erst innerhalb des If- bzw. Else-Zweigs beschrieben. Ob
die Elimination der dd-Interaktion an dieser Stelle Vorteile bringt, ist diskutabel.
5.4 Anomalienanalyse 319
Abb. 5.37 Die korrekte Implemtierung der Funktion manhattan. Alle Pfade sind frei von Da-
tenflussanomalien
Insbesondere haben wir weiter oben herausgearbeitet, dass es sich um keinen wirk-
lichen Software-Fehler handelt. Legen wir den Qualitätsschwerpunkt auf das Kri-
terium der Code-Transparenz, so scheint die ursprüngliche Lösung der anomalie-
befreiten Varianten sogar überlegen zu sein – die ursprüngliche Lösung kommt mit
weniger Instruktionen aus und kann auf den nachträglich hinzugefügten Else-Zweig
vollständig verzichten. Auf der anderen Seite wird die automatisiert durchgeführte
Anomalienanalyse drastisch erschwert, wenn der untersuchte Quellcode eine hohe
Anzahl von Scheinfehlern erzeugt. Wir stehen an dieser Stelle vor einem ähnlichen
Dilemma, dem wir bereits in Abschnitt [Link] im Zusammenhang mit der semanti-
schen Code-Analyse begegnet sind.
Auch wenn das diskutierte Beispiel unter Beweis stellt, dass nicht jede dd-
Interaktion durch einen Software-Fehler verursacht wird, lassen sich viele reale
Fehler durch die gezielte Suche nach dieser Datenflussanomalie aufdecken. Als Bei-
spiel betrachten wir die drei Programmvarianten der Funktion manhattan in den
Abb. 5.37 bis 5.39. Die erste Variante des Programms ist korrekt und, wie ein Blick
auf die Zugriffssignaturen zeigt, gänzlich frei von Datenflussanomalien.
Die Programmvariante in Abb. 5.38 enthält einen Variablenfehler innerhalb des
zweiten If-Zweigs – anstelle der Variablen b wird der Wert der Variablen a über-
schrieben. Durch die Programmänderung wird für die Variable a auf dem Pfad
(0, 1, 2, 3, 4, 5) eine dd-Interaktion erzeugt, so dass ein Blick auf die Zugriffssigna-
320 5 Statische Code-Analyse
turen ausreicht, um den induzierten Fehler mit dem Mittel der Anomalienanalyse
sofort zu entdecken.
Das dritte Beispiel zeigt jedoch, dass wir uns auch hier nicht in allzu großer
Sicherheit wiegen dürfen. In dieser Programmvariante sind die Variablen a und b
in beiden If-Zweigen vertauscht. Anhand der Zugriffssignaturen kann der Fehler
nicht erkannt werden – keine einzige enthält eine Datenflussanomalie. Für diese
Programmvariante erweist sich hingegen der Software-Test als ein leistungsfähiges
Instrument. Wird das Programm mit realen Eingangswerten ausgeführt, so produ-
ziert rund jeder zweite Testfall ein eklatant falsches Ergebnis.
Ein zentrales Problem der Anomalienanalyse haben wir in unseren Betrachtun-
gen bisher außen vor gelassen. Da die Zugriffssignaturen auf konkreten Pfaden ge-
bildet werden, steigt die Anzahl der zu analysierenden Zugriffssignaturen in dem
gleichem Maße wie die Anzahl der möglichen Pfade selbst. Wie wir bereits in Ab-
schnitt 4.4 im Zusammenhang mit den diversen White-Box-Testtechniken heraus-
gearbeitet haben, wird die Anzahl der möglichen Ausführungspfade insbesondere
durch Schleifenkonstrukte exorbitant erhöht.
Glücklicherweise müssen im Falle von Schleifen nicht alle entstehenden Aus-
führungspfade auf Anomalien untersucht werden. Die Begründung hierfür liefert
Tabelle 5.40. Neben den Datenflussanomalien innerhalb der sequenziell ausgeführ-
ten Anweisungsblöcke können Anomalien insbesondere an den Anschlussstellen
5.5 Manuelle Software-Prüfung 321
Abb. 5.39 Inkorrekte Implementierung der Funktion manhattan. Die Anomalienanalyse kann
den Implementierungsfehler in diesem Fall nicht aufdecken
I Schleifenschema
(1); 1
(2); 2
while (...) { 3
(3); 4
(4); 5
} 6
(5); 7
(6); 8
I Anschlussstellen
Iterationen Ausführungspfad Anschlussstellen
0 1 2 5 6 { (2,5) }
1 1 2 3 4 5 6 { (2,3), (4,5) }
nach fest definierten Regeln ab, die sich sowohl auf die inhaltlichen Prüfkrite-
rien als auch auf die Zusammensetzung der Sitzungsmitglieder beziehen. Typi-
sche Walkthroughs werden informell, Reviews und Inspektionen dagegen for-
mell durchgeführt.
5.5.1 Walkthroughs
Im Rahmen eines Walkthroughs wird eine Funktion, ein Algorithmus oder auch ein
ganzes Software-Modul durch zwei oder mehrere Entwickler untersucht und begut-
achtet. Walkthroughs finden oft am Arbeitsrechner des Autors statt und haben den
Charakter eines informellen Gesprächs. Die geschaffene Situation bietet die folgen-
den Vorteile:
I Mehraugenprinzip
Die Erfahrung zeigt, dass mit Hilfe des Mehraugenprinzips viele Fehler aufge-
deckt werden können, die der Autor nach einer gewissen Zeit selbst nicht mehr
erkennt. Kurzum: Das Hinzuziehen einer externen Person wirkt der Fehlerblind-
heit aktiv entgegen. Bei uns allen stellt sich diese Art der Blindheit von selbst
ein, wenn wir über längere Zeit den gleichen Programmcode bearbeiten. Das
Phänomen, Fehler hartnäckig zu übersehen, ist allgegenwärtig und nicht auf den
Bereich der Software-Entwicklung beschränkt. Wahrscheinlich sind auch Sie in
der Lage, Schreibfehler in fremden Texten auf den ersten Blick zu erkennen. Da-
gegen werden ähnliche Fehler in selbst verfassten Schriften mit der Zeit schier
unsichtbar und bleiben dem eigenen Auge auch bei wiederholter Lektüre beharr-
lich verborgen.
324 5 Statische Code-Analyse
I Erzählstil
Durch die Erzählsituation des Autors wird der Blick auf die eigene Software
geschärft. In vielen Fällen reicht die pure Anwesenheit eines externen Zuhö-
rers aus, um die eigene Fehlerblindheit zu beseitigen. Auch in den Labor- und
Praktikumsräumen der hiesigen Hochschulen und Universitäten lässt sich das
Phänomen tagtäglich beobachten. In vielen Fällen, in denen ein Student seinen
Betreuer zur Suche eines versteckten Software-Fehlers zu Rate zieht, wird die
Fehlerursache im Zuge der Erklärung selbst entdeckt. Der Schlüssel zum Erfolg
ist an dieser Stelle der Wechsel in die Erzählerrolle und nicht die hinzugezogene
Expertise.
I Externalisierung
Durch die Präsentation der eigenen Arbeit findet die Software-Entwicklung nicht
mehr länger im Verborgenen statt. Die Externalisierung des inhärent unsicht-
baren Produkts Software hat auf die meisten Entwickler einen disziplinieren-
den Einfluss. Ähnlich dem Künstler, der sein Werk in bestem Glanze präsen-
tieren möchte, wird auch so mancher Programmierer von dem Ehrgeiz getrie-
ben, nicht nur ein funktionierendes, sondern auch ein ästhetisches Stück Softwa-
re zu liefern. Der eigentliche Gewinner der selbst auferlegten Programmierdis-
ziplin ist das Qualitätsmerkmal der Code-Transparenz. Dokumentare entstehen
fast von selbst und auf so manch fragwürdigen Programmiertrick wird freiwillig
verzichtet. Den künstlerischen Aspekt der Software-Entwicklung zu ignorieren
ist ein weit verbreiteter Fehler des Software-Managements, auf den wir in Ab-
schnitt 9.2.5 erneut zu sprechen kommen.
Walkthroughs können zu jeder Zeit durchgeführt werden und sind nicht auf die Be-
gutachtung von Quelltexten beschränkt. Die Bewertung von Konfigurationsdateien,
Testfällen oder Dokumenten ist nach demselben Prinzip möglich. Eng zusammenar-
beitende Entwickler-Teams führen Walkthroughs oft ungeplant durch, um z. B. eine
unmittelbar anstehende Entwurfsentscheidung in die richtige Richtung zu lenken.
Wird die Prüftechnik darüber hinaus kooperativ für die Suche hartnäckiger Pro-
grammfehler eingesetzt, kann die Effizienz der gesamten Arbeitsgruppe deutlich
erhöht werden. In manchen Firmen sind Walkthroughs fest in den Entwicklungs-
prozess integriert und vor der Abnahme eines Software-Moduls zwingend durchzu-
führen.
5.5.2 Reviews
Grob gesprochen verbirgt sich hinter dem Begriff des Reviews eine formalisierte
Variante des Walkthroughs. Die Begutachtung wird anhand spezieller Checklisten
durchgeführt, die Art und Umfang der manuellen Software-Prüfung formal festle-
gen. Abb. 5.41 zeigt den typischen Aufbau einer Checkliste, wie sie in ähnlicher
Form in vielen Unternehmen eingesetzt wird. Aufgrund ihres formalen Charakters
besitzen Reviews die folgenden Vorteile:
I Vollständigkeit
Die Verwendung von Checklisten garantiert eine gewisse Vollständigkeit des
5.5 Manuelle Software-Prüfung 325
I Vergleichbarkeit
Eine ausgefüllte Checkliste dokumentiert den Zustand eines Software-Moduls
auf einer objektiven Ebene. Basiert die Prüfung stets auf denselben Kriterien, so
können verschiedene Module untereinander verglichen werden. Reviews lassen
sich damit sowohl für die Fortschrittsverfolgung als auch für die Qualitätsbewer-
tung einsetzen und erweisen sich als wertvolles Instrument des Projektmanage-
ments.
Auf der negativen Seite bergen Checklisten das Risiko, den Blick auf den unter-
suchten Programmcode über Gebühr einzuengen. Um die Vergleichbarkeit der Be-
wertungen zu erhöhen, werden die einzelnen Prüfpunkte oft sehr detailliert aus-
formuliert. Je präziser die Formulierung gewählt wird, desto mehr degradiert ein
Software-Review zu einem rein mechanischen Prozess. Nimmt die Mechanisierung
überhand, so geht eine wesentliche Stärke der manuellen Software-Prüfung verlo-
ren. Gemeint ist die Möglichkeit, die Prüfung durch pragmatisches Denken und
persönliches Urteilsvermögen zu unterstützen.
Damit ein Review in der praktischen Arbeit seine volle Stärke entfalten kann,
muss die Auswahl der einzelnen Prüfpunkte mit äußerster Sorgfalt erfolgen. Die
exemplarisch dargestellte Checkliste in Abb. 5.41 enthält hierfür vorwiegend posi-
tive, aber auch negative Beispiele. Die meisten der Fragen adressieren semantische
Aspekte des Programms, die mit anderen Methoden nicht zu bewerten sind. Hier
spielt die Review-Technik seine grundsätzliche Stärke aus, komplexe semantische
Zusammenhänge zu analysieren und zu bewerten. Je stärker die einzelnen Prüfpunk-
te auf diese Fähigkeit hin ausgerichtet sind, desto besser ergänzt die Review-Technik
die in den vorhergehenden Abschnitten eingeführten Analysemethoden.
Die Prüfpunkte der ersten beiden Abschnitte (Syntax und Dokumentation) fragen
stattdessen Eigenschaften der Software ab, die nur wenig zur Lösung der dringlichs-
ten Qualitätsprobleme beitragen oder mit einer entsprechenden Werkzeugunterstüt-
zung vollautomatisch untersucht werden können. Eine ungeschickte Wahl der Prüf-
punkte ist aus zweierlei Hinsicht kontraproduktiv für den gesamten Review-Prozess.
Zum einen ist die Zeit, die für ein Review aufgewendet werden kann, durch die
endliche Konzentrationsfähigkeit der Teilnehmer stark begrenzt. Zum anderen sind
viele der wichtigen Fragen vergleichsweise schwierig zu beantworten. Die Möglich-
keit, sich im Rahmen eines Reviews in die zahlreichen Fragen über Einrückungstiefe
und Schreibstile zu flüchten, wird in der Praxis nur allzu gerne angenommen.
Ein Blick in die Entwicklungslabore zahlreicher Software-Firmen bestätigt die
weit verbreitete Tendenz, den Umfang der verwendeten Checklisten mit der Zeit
kontinuierlich zu vergrößern. Auch wenn jede einzelne Erweiterung gut gemeint
und individuell gerechtfertigt ist, führt die übermäßige Ausweitung einer Review-
Sitzung zu einer Verschlechterung der erzielten Ergebnisse. Erfolg und Misserfolg
326 5 Statische Code-Analyse
1. Syntax JA NEIN
Compiliert das Programm ohne Warnungen?
Sind die projektspezifischen Code-Konventionen eingehalten?
Ist nicht erreichbarer Code vorhanden bzw. als solcher markiert?
2. Dokumentation JA NEIN
Sind alle Variablendeklarationen dokumentiert?
Sind alle Funktionsdefinitionen dokumentiert?
Sind alle Klassendefinitionen dokumentiert?
Sind die Einheiten aller numerischen Variablen klar beschrieben?
3. Kontrollfluss JA NEIN
Ist die Terminierungsbedingung jeder Schleife korrekt?
Ist der Code frei von potenziellen Endlosschleifen?
Können rekursive Strukturen einen Stapelüberlauf verursachen?
Ist die Schachtelungstiefe der Kontrollstrukturen akzeptabel?
4. Datenfluss JA NEIN
Werden alle Funktionsparameter überprüft?
Werden Array-Indizes auf gültige Intervallgrenzen überprüft?
Werden alle Variablen vor der ersten Nutzung initialisiert?
Ist der Wertebereich aller numerischen Variablen ausreichend?
5. Fehlerbehandlung JA NEIN
Werden die Intervallgrenzen aller numerischen Werte geprüft?
Werden potenzielle Null-Pointer vor der Verwendung geprüft?
Werden alle ausgelösten Exceptions adäquat behandelt?
Existieren Testfälle für die Auslösung jedes Ausnahmezustands?
6. Dynamische Ressourcenverwaltung JA NEIN
Wird dynamisch belegter Speicher wieder freigegeben?
Werden alle geöffneten Dateien wieder geschlossen?
Werden Referenzen auf veraltete Objekte zeitnah gelöscht?
Wird jedes Objekt nur einmal wieder freigegeben?
7. Konkurrierende Programmierung JA NEIN
Sind alle globalen Variablen Thread-sicher?
Sind alle mehrfach benutzten Objekte Thread-sicher?
Werden alle Semaphore wieder freigegeben?
Ist der Code verklemmungsfrei?
Abb. 5.41 Beispiel einer Checkliste für die manuelle Software-Prüfung
liegen im Bereich der manuellen Software-Prüfung nahe beieinander. Mit den richti-
gen Mitarbeitern und der passenden Intension durchgeführt, können Reviews Wun-
der bewirken. Falsch umgesetzt verkommt die vielversprechende Technik zu einem
bürokratischen Prozedere, ohne die Qualitätskriterien des untersuchten Software-
Systems positiv zu beeinflussen. Insbesondere dann, wenn sich Reviews auf die ma-
5.5 Manuelle Software-Prüfung 327
5.5.3 Inspektionen
Software-Inspektionen betten die manuelle Prüftechnik in einen definierten Prozess
ein und sind damit die formellste Variante der drei vorgestellten Verfahren. Die In-
spektionstechnik geht auf die in den Siebziger- und Achtzigerjahren publizierten
Arbeiten von Michael Fagan zurück und wird in Anlehnung an seine historischen
Wurzeln häufig als Fagan-Inspektion bezeichnet [91, 92]. In der Vergangenheit wur-
de der Inspektionsprozess kontinuierlich verfeinert und in zahlreichen Publikationen
ausführlich dargelegt. Eine tiefergehende Beschreibung gibt [101].
Eine typische Fagan-Inspektion durchläuft bis zu 6 aufeinander aufbauende Pha-
sen, die in Abb. 5.42 grafisch zusammengefasst sind. Die einzelnen Phasen sind
durch die folgenden Inhalte charakterisiert:
I Planungsphase
Eine Software-Inspektion nach Fagan ist prozessgetrieben und dadurch ein ge-
planter Bestandteil des Projektmanagements. Entsprechend erfordert die Ab-
wicklung ein hohes Maß an Organisation, die in der initialen Planungsphase be-
ginnt. Ist ein Software-Modul bereit für die Durchführung einer Inspektion, so
informiert der Autor den Moderator, der anschließend Inspektionstermin, Per-
sonal und Räumlichkeit festlegt. Die Aufbereitung der benötigten Dokumente
übernimmt der Autor.
I Überblicksphase
In dieser Phase werden die verschiedenen, in der Inspektionssitzung eingenom-
menen Rollen verteilt und die ausgewählten Mitarbeiter mit den notwendigen
328 5 Statische Code-Analyse
s-
-
e ng
-
e gs
e gs
g s-
as itu
e s-
as un
as un
un n
e -
as ck
ph rbe
as gs
tz io
ph reit
ph rüf
si ekt
ph rbli
ph nun
ha
rp
e
rb
sp
be
be
ac
a
Vo
Pl
In
Ü
Ü
N
Phase Phase Phase Phase Phase Phase
1 2 3 4 5 6
Eingangs- Ausgangs-
kriterien kriterien
Review-
Dokumente
I Vorbereitungsphase
Die Vorbereitungsphase beginnt, nachdem die Rollen verteilt und alle beteilig-
ten Mitarbeiter mit den erforderlichen Dokumenten versorgt sind. Im Gegensatz
zu einem Walkthrough oder einem Review begutachten die Inspektoren das Un-
tersuchungsobjekt bereits jetzt, d. h., jeder Inspektor bringt bereits eine vorge-
fertigte Fehlerliste in die sich anschließende Inspektionssitzung mit ein. Die in
dieser Phase durchgeführte Begutachtung entspricht in wesentlichen Teilen dem
im vorherigen Abschnitt umrissenen Offline review.
I Inspektionssitzung
In der Inspektionssitzung werden die erarbeiteten Einzelergebnisse zusammen-
getragen und gemeinsam bewertet. Die Sitzung wird mit den folgenden verteilten
Rollen durchgeführt:
– Moderator
Eine Inspektionssitzung nach Fagan wird moderiert durchgeführt. Zu den
Hauptaufgaben des Moderators gehört es, den geregelten Ablauf sicherzu-
stellen und die Dominanz einzelner Inspektoren zu verhindern. Er muss ins-
besondere in der Lage sein, die Diskussion in die richtige Richtung zu lenken,
5.5 Manuelle Software-Prüfung 329
– Gutachter
Der Gutachter hat die Aufgabe, das Inspektoren-Team fachlich durch die ein-
zelnen Programmquellen und Dokumente zu führen. In der englischen Lite-
ratur wird der Gutachter treffend als Reader bezeichnet. Wird beispielswei-
se der Quelltext eines Software-Moduls inspiziert, so geht der Gutachter den
Programmtext Schritt für Schritt durch und erläutert die einzelnen Entwurfs-
entscheidungen und Programmkonstrukte aus eigener Sicht. Im Gegensatz zur
Rolle des Moderators erfordert die Rolle des Gutachters ein hohes Maß an
technischem Sachverstand.
Empirische Untersuchungen legen nahe, dass die Lesegeschwindigkeit, die
der Gutachter selbstständig vorgibt, einen erheblichen Einfluss auf die An-
zahl der erkannten Defekte besitzt. Fagan schlägt in [92] als Richtwert ei-
ne Geschwindigkeit von 500 NCSS (Non Commented Source Statements) für
die Überblicksveranstaltung und 90 NCSS für die Inspektionssitzung vor. Die
gewählte Leserate ist stets ein Kompromiss zwischen Aufwand und Fehlerer-
kennungsrate. Genauere Untersuchungen über diesen Zusammenhang werden
in [101] und [254] beschrieben.
– Autor
Der Autor der untersuchten Dokumente ist während der Inspektionssitzung
anwesend, nimmt aber eine vollständig passive Rolle ein. Insbesondere
schließt Fagan explizit aus, dass der Autor gleichzeitig die Rolle des Mode-
rators, des Protokollführers oder des Gutachters übernimmt. Die passive Teil-
nahme des Autors bewirkt, dass er sein eigenes Werk aus der Perspektive ei-
ner außenstehenden Person wahrnimmt. Auch hier hilft die Externalisierung,
den Blick auf Aspekte zu richten, die in der normalen Arbeitssituation in die-
ser Form verborgen bleiben. Die bewusst passive Rolle des Autors verhindert
überdies, dass sich die Diskussion in Rechtfertigungsversuchen verliert. Un-
terdrückt der Moderator solche Diskussionstendenzen nicht im Ansatz, gerät
die gesamte Inspektionssitzung schnell ins Wanken.
– Protokollführer
Der Protokollführer hat die Aufgabe, einen Prüfbericht zu erstellen und die
erkannten Defekte zu vermerken. Alle erkannten Fehler werden durch die
Inspektoren bezüglich ihrer Schwere gewichtet und hierdurch in verschiede-
ne Kategorien eingeteilt. Zum einen führt die Gewichtung zu einer automa-
tischen Priorisierung der notwendig werdenden Nacharbeiten, zum anderen
entstehen auf diese Weise wertvolle Daten für die Projektverfolgung. Die Rol-
le des Protokollführers kann durch einen eigens hierfür abgestellten Mitarbei-
ter oder durch den Moderator selbst übernommen werden. Insgesamt werden
330 5 Statische Code-Analyse
weitere Inspektoren
Gutachter
Protokollführer
– Weitere Inspektoren
Neben dem Moderator, dem Gutachter und dem Protokollführer, die allesamt
auch die Rolle eines Inspektors innehaben, können weitere Inspektoren hin-
zugezogen werden. Typischerweise wird diese Aufgabe von Entwicklern aus
der Arbeitsgruppe des Autors übernommen, da diese über die nötigen tech-
nischen Kenntnisse verfügen und durch die Teilnahme gleichzeitig zu detail-
liertem Wissen über die Arbeit des Kollegen gelangen können. Wie schon im
Falle der Walkthroughs und Reviews gilt auch hier, dass Vorgesetzte nicht als
Inspektoren in Frage kommen.
Die Anzahl der zusätzlich hinzugezogenen Inspektoren hat einen unmit-
telbaren Einfluss auf die Anzahl der gefundenen Fehler. Trotzdem steigt die
Fehlererkennungsrate nicht linear mit der Gruppengröße. Die Erfahrungswer-
te verschiedener Unternehmen legen nahe, dass die Gruppengröße bei drei bis
sieben Sitzungsteilnehmern ein Optimum erreicht. Größere Gruppen erhöhen
die Anzahl gefundener Defekte nur noch marginal, treiben dafür den ohnehin
hohen Personalaufwand weiter in die Höhe.
Abb. 5.43 fasst die Rollenverteilung einer Fagan-Inspektion grafisch zusammen.
Die richtige Art und Weise der Sitzungsdurchführung ist zentral für deren Erfolg.
Insbesondere dürfen die folgenden beiden Aspekte bei der Durchführung einer
Inspektion niemals außer Acht gelassen werden:
oder drei Fehler nicht hinaus. Der Moderator hat an dieser Stelle die entschei-
dende Aufgabe, den Fokus der Diskussion weg von möglichen Lösungen und
zurück zu den eigentlichen Fehlern zu lenken.
I Nachbearbeitungsphase
In dieser Phase arbeitet der Autor die Fehlerkorrekturen in die untersuchten
Programmmodule und Dokumente ein. Kleinere Fehler werden sofort korri-
giert, größere Modifikationen gehen den Weg über das Änderungsmanagement.
Kommt der Autor zu dem Schluss, dass es sich bei ein oder mehreren beschriebe-
nen Defekten nicht um Fehler handelt, werden die betreffenden Programmstellen
in der Überprüfungsphase erneut begutachtet.
I Überprüfungsphase
Alle in einer Fagan-Inspektion beschlossenen Änderungen werden nachträglich
überprüft. Kleine Änderungen werden in der Regel in einem Vieraugengespräch
zwischen Autor und Moderator abgehandelt. Größere Änderungen werden im
Rahmen einer Follow-Up-Sitzung besprochen, zu der die beteiligen Inspektoren
erneut hinzugezogen werden. Hier wird entschieden, ob die durchgeführten Än-
derungen eine erneute Inspektion des Moduls erforderlich machen.
Zusätzlich wird in der Überprüfungsphase der abschließende Inspektionsbericht er-
stellt. In diesem werden die Inspektionsbefunde zusammengefasst und aufbereitet.
Durch die systematische Erfassung der Ergebnisse helfen Fagan-Inspektionen nicht
nur, die Software-Qualität zu erhöhen, sondern liefern zugleich auch Kennzahlen für
das Projektmanagement. Mit der Überprüfungsphase endet die Fagan-Inspektion.
Werden Inspektionen systematisch und flächendeckend durchgeführt, spielen sie
auch in einem ganz anderen Bereich ihre Stärke aus. Gemeint ist die Suche nach
versteckten Eigenschaften (hidden features). Die Bandbreite an nicht vorgesehe-
ner Zusatzfunktionalität ist riesig. Viele Programmierer ergänzen Software-Systeme
auf eigene Faust, um sich schlicht das eigene Entwicklerleben zu vereinfachen.
Versteckte Kommandos oder die Möglichkeit, nicht für den Endkunden bestimm-
te Debug-Ausgaben zu erzeugen, fallen in diese Kategorie. Andere Programmierer
öffnen sich heimlich Hintertüren, um z. B. die Bedienung der Software während
der Testphase zu erleichtern. Welcher Entwickler wünscht sich nicht, die unentwegt
störende Lizenzüberprüfung einfach abzuschalten?
Die Bandbreite versteckter Änderungen reicht bis hin zur Sabotage. Als Beispiel
ist in Abb. 5.44 eine leicht modifizierte Variante der Methode champion abgebildet,
332 5 Statische Code-Analyse
[Link]
public class Team { 1
2
int points; 3
int goals; 4
5
/** Bestimmt den Sieger unter zwei Mannschaften. 6
7
@param team1 Referenz auf das erste Team (Team A) 8
@param team2 Referenz auf zweite Team (Team B) 9
*/ 10
11
public static Team champion(Team team1, Team team2) { 12
13
#ifdef RELEASE_VERSION 14
if (team1 == DIRKS_TEAM) return team1; 15
if (team2 == DIRKS_TEAM) return team2; 16
#endif 17
18
// Old implementation... 19
... 20
} 21
} 22
Abb. 5.44 Nur die manuelle Software-Prüfung ist in der Lage, „Fehler“ dieser Art zu erkennen
6.1 Motivation
Die Methoden und Techniken der Software-Verifikation stehen auf dem gemeinsa-
men Fundament, die Korrektheit eines Programms mit Hilfe mathematischer Me-
thoden formal zu beweisen. Der mathematischer Charakter dieser Vorgehensweise
bedingt, dass vor der eigentlichen Beweisführung sowohl das Programm als auch
seine Anforderungsbeschreibung in formale Modelle übersetzt werden müssen. Die-
se werden im Folgenden als Implementierung I und Spezifikation S bezeichnet.
Die Aufgabe der Software-Verifikation besteht in dem formalen Nachweis, dass die
Implementierung die Spezifikation erfüllt, geschrieben als I |= S .
Abb. 6.1 fasst die grundlegende Vorgehensweise der Software-Verifikation gra-
fisch zusammen. Aus dem Schaubild geht unmittelbar eine fundamentale Beschrän-
kung dieses Ansatzes hervor: Obwohl die Verifikation aufgrund ihres mathemati-
schen Charakters die mit Abstand präziseste der hier vorgestellten Techniken ist,
wird der formale Beweis ausschließlich auf der Modellebene geführt. In der Konse-
quenz bedeutet diese Beschränkung, dass Fehler, die innerhalb des Formalisierungs-
prozesses entstehen, auch mit diesen Verfahren nicht erkannt werden können. Wir
haben es an dieser Stelle mit einer fundamentalen Grenze aller Verifikationstechni-
ken zu tun, die von einzelnen Verfechtern formaler Methoden nicht selten verges-
sen und mitunter auch schlicht ignoriert wird. Nichtsdestotrotz werden die nächsten
Abschnitte zeigen, dass mit Hilfe formaler Verfahren einige versteckte Fehler auf-
gedeckt werden können, die den anderen vorgestellten Test- und Analysetechniken
gänzlich verborgen bleiben. Richtig eingesetzt kann die formale Verifikation die her-
kömmlichen Testtechniken durchaus gewinnbringend ergänzen – wenn auch nicht
ersetzen.
In diesem Kapitel werden wir mit der Deduktion, der Modellprüfung und der ab-
strakten Interpretation die drei grundlegenden Verifikationstechniken detailliert be-
trachten. Für die Beschreibung der Implementierung und der Spezifikation kommen
in Abhängigkeit des eingesetzten Verifikationsverfahrens gänzlich unterschiedliche
Modelle zum Einsatz. Insbesondere die eingesetzten Spezifikationsmodelle unter-
scheiden sich erheblich in ihrer Ausdrucksstärke und damit auch in der Bandbreite
Programm Verhalten
Reale
Welt
Formalisierung Formalisierung
Implementierung Spezikation
?
Modell-
welt
I Modellprüfung
Die Technik der Modellprüfung geht einen anderen Weg und übersetzt ein Pro-
gramm zunächst in eine Kripke-Struktur, die in ihren wesentlichen Aspekten dem
Aufbau eines endlichen Automaten folgt [156]. Die zu verifizierenden zeitlichen
Eigenschaften werden in einer Temporallogik formuliert und mit Hilfe speziel-
ler Traversierungsalgorithmen nachgewiesen oder widerlegt. Im Gegensatz zur
Deduktionstechnik, die prinzipiell in der Lage ist, das funktionale Verhalten ei-
nes Programms vollständig zu beschreiben, ist die Modellprüfung auf die Verifi-
kation spezieller zeitlicher Eigenschaften beschränkt. Durch die geringere Aus-
drucksfähigkeit der Spezifikationssprache lässt sich die Beweisführung automa-
tisieren, so dass die Technik auch für die industrielle Anwendung interessant
wird. In der Praxis wird die Modellprüfung in sicherheitskritischen Nischenpro-
jekten bereits als unterstützendes Werkzeug eingesetzt, in der breiten Masse der
Software-Entwicklung spielt sie aufgrund der immer noch immensen algorithmi-
schen Komplexität noch keine wesentliche Rolle.
6.1 Motivation 335
PL0 : a → (b ∨ c)
{P}
S
PL1 : ∀x ∃y : f (x) → g(y)
{Q} PLn : ∀P ∃x : ¬P(x, x)
...
I Abstrakte Interpretation
Hinter dem Begriff der abstrakten Interpretation verbirgt sich eine vielverspre-
chende semi-formale Verifikationstechnik, die in den letzten Jahren deutlich an
Popularität gewinnen konnte und zurzeit den Sprung aus dem akademischen Um-
feld in industrielle Entwicklungsprozesse vollzieht. Die abstrakte Interpretation
kombiniert die statische Programmanalyse mit einer Generalisierung des Daten-
bereichs und ermöglicht hierdurch, verschiedene dynamische Eigenschaften ei-
nes Programms zu verifizieren. Für viele Programme lässt sich mit Hilfe der ab-
strakten Interpretation formal beweisen, dass arithmetische Ausdrücke niemals
eine Division durch null verursachen oder Array-Zugriffe stets innerhalb der In-
tervallgrenzen erfolgen.
Das theoretische Fundament der eingesetzten Programmanalyse reicht bis in
die Sechziger- und Siebzigerjahre zurück. Bereits dort wurde die richtungswei-
sende Idee formuliert, ein Programm als eine Menge von Gleichungen aufzu-
fassen. Die Lösung des entstehenden Gleichungssystems entspricht der Menge
von Zuständen, die ein Programm einnehmen kann. Da viele dynamische Ei-
genschaften auf entsprechende Zustandsmengen abgebildet werden können, lässt
sich deren Gültigkeit durch eine einfache Mengenoperation überprüfen und aus
der Schnittmenge im Fehlerfall ein Gegenbeispiel erzeugen.
I Ausdrucksstärke
Die Ausdrucksstärke beschreibt, welche Eigenschaften eines Software-Systems
formal spezifiziert werden können und begrenzt damit auf natürliche Weise die
zu verifizierenden Eigenschaften. Je höher die Ausdrucksstärke eines Verfahrens
ist, desto höher ist auch seine theoretische Leistungsfähigkeit. Auf der negativen
Seite geht die Steigerung mit einem explosionsartigen Komplexitätsanstieg ein-
her, der die Beweisdurchführung überproportional erschwert. Damit kann eine
zu hohe Ausdrucksstärke die praktische Leistungsfähigkeit eines Verfahrens ad
absurdum führen.
Die Deduktion ist die mit Abstand ausdrucksstärkste Verifikationsmethode.
Die Verwendung einer höherwertigen Logik ermöglicht, neben den natürlichen
Zahlen auch alle anderen für die funktionale Beschreibung eines Algorithmus
notwendigen Aspekte formal zu beschreiben. Die Deduktionstechnik versetzt uns
hierdurch in die Lage, die funktionale Korrektheit eines komplexen Algorithmus
vollständig zu verifizieren. Dagegen ist die Ausdrucksstärke der Modellprüfung
und der abstrakten Interpretation deutlich begrenzt, so dass sich diese Verfahren
auf die Verifikation spezieller Eigenschaften beschränken (partielle Verifikation).
I Skalierung
Der Begriff der Skalierung bezeichnet die Eigenschaft eines Verfahrens, auch auf
größere Programme anwendbar zu sein. Die geringe Skalierbarkeit ist das Kern-
problem aller in diesem Kapitel vorgestellten Verfahren und einer der Hauptgrün-
de, warum die Software-Verifikation in der Praxis heute immer noch ein Schat-
tendasein fristet. Vergleichen wir die einzelnen Verfahren untereinander, so er-
geben sich auch hier drastische Unterschiede. Die mit Abstand ausdrucksstärks-
te und damit leistungsfähigste Deduktionstechnik ist zugleich die am wenigsten
skalierende. Dass mit abnehmender Ausdrucksstärke die Skalierbarkeit zunimmt,
lässt sich insbesondere am Beispiel der Modellprüfung und der abstrakten Inter-
pretation beobachten (vgl. Abb. 6.2). Vor allem die abstrakte Interpretation stößt
aufgrund der enorm gestiegenen Rechenkapazität in Leistungsbereiche vor, die
einen industriellen Einsatz ermöglichen. In der Tat konnten sich erste kommer-
ziell vertriebenen Werkzeuge bereits am Markt etablieren.
I Automatisierung
Neben der Skalierbarkeit des Verifikationsverfahrens ist der Grad der Automati-
sierung ein essentielles Kriterium für den Einsatz in realen Software-Projekten.
Die Erfahrung der letzten Jahre hat gezeigt, dass sich manuelle Beweisverfahren
kaum in industrielle Entwicklungsprozesse integrieren lassen. Die Gründe hier-
für sind vielfältig und reichen von der langen Zeitspanne, die zur Durchführung
eines manuellen Beweises benötigt wird, bis zu dem hohen Grad an Expertenwis-
sen, das in der Praxis nicht in ausreichender Form vorhanden ist. Im Gegensatz
zur deduktiven Technik sind die Modellprüfung und die abstrakte Interpretation
automatisierte Verfahren und damit für den industriellen Einsatz besser geeignet
(Push-Button-Verifikation).
6.1 Motivation 337
Deduktion
Ausdrucksstärke
Abstrakte Interpretation
Modellprüfung
Software-Test
Skalierbarkeit
I False positives
Das Verifikationsverfahren beweist die Korrektheit eines in Wirklichkeit fehler-
haften Programms.
In der Praxis werden False positives als schwerwiegender erachtet als False nega-
tives. Viele der heute eingesetzten Verifikationsverfahren sind daher so konstruiert,
dass False negatives von Zeit zu Zeit auftreten, False positives aber in jedem Fall
vermieden werden. Hierdurch ist sichergestellt, dass ein Programm alle verifizierten
Eigenschaften tatsächlich erfüllt. Der Umkehrschluss verliert an dieser Stelle seine
Gültigkeit. So deutet ein fehlgeschlagener Verifikationsversuch nicht mehr zwangs-
läufig auf einen Fehler hin.
Durch die Kombination mit anderen Qualitätssicherungstechniken, wie dem
klassischen Software-Test, lassen sich viele False negatives unmittelbar als solche
identifizieren und so die praktische Anwendbarkeit der formalen Verifikation deut-
lich erhöhen. Das Aufdecken von False positives erweist sich dagegen als deutlich
schwieriger. Im Gegensatz zu einem False negative liefert das Verifikationsverfah-
ren hier keinerlei Anhaltspunkte, die sich gezielt weiter untersuchen lassen.
6.2 Deduktion
6.2.1 Vor- und Nachbedingungen
Deduktive Techniken spezifizieren die Eigenschaften eines Programms in Form von
Vor- und Nachbedingungen. Analog zur klassischen mathematischen Beweisfüh-
rung wird die Gültigkeit der Nachbedingungen durch die Anwendung spezieller
Beweisregeln aus den Vorbedingungen hergeleitet. Sowohl die Vor- und Nachbedin-
gungen, als auch alle geschlussfolgerten Aussagen, werden in einer formalisierten
Sprache beschrieben – der sogenannten Logik. Durch die Hinzunahme der Beweis-
regeln wird eine Logik zu einem Logikkalkül erweitert.
Als Beispiel betrachten wir die in Abb. 6.3 dargestellte Funktion exp zum schnel-
len Potenzieren zweier Integer-Zahlen. Diese nimmt zwei Parameter a und n ent-
6.2 Deduktion 339
fast_exp.c
int exp(int a, int n) { 1
int k = n; 2
int p = a; 3
int y = 1; 4
5
while (k > 0) { 6
if (k % 2 == 0) { 7
p = p * p; 8
k = k / 2; 9
} else { 10
y = y * p; 11
k = k - 1; 12
} 13
} 14
return y; 15
} 16
gegen und berechnet für alle n mit n > 0 das Ergebnis an . Um die theoretischen
Ausführungen an dieser Stelle mit Leben zu füllen, soll die Korrektheit der Funkti-
on mit Hilfe eines Deduktionsbeweises formal verifiziert werden. Hierzu wird über
eine Vorbedingung {P} und eine Nachbedingung {Q} zunächst das Verhalten der
Funktion formal spezifiziert (vgl. Abb. 6.4).
In diesem Beispiel verwenden wir zur Darstellung der Vor- und Nachbedin-
gung sowie für alle berechneten Zwischenergebnisse die gewöhnliche mathema-
tische Notation. Wird die Software-Verifikation rechnergestützt durchgeführt, tritt
an die Stelle der mathematischen Schreibweise eine computerverständliche Spezi-
fikationssprache mit einer klar definierten Semantik. In der Vergangenheit wurden
viele verschiedene Logiken formuliert, die sich allesamt für den Einsatz im Bereich
der deduktiven Verifikation eignen, sich jedoch sowohl in ihrer Ausdrucksfähigkeit
als auch ihrer Beweiskomplexität beträchtlich unterscheiden (vgl. Abb. 6.5). Die
folgenden Logiken spielen im Bereich der Hard- und Software-Verifikation eine
hervorgehobene Rolle:
I Aussagenlogik
Die Aussagenlogik (PL0) ist die mit Abstand einfachste der hier vorgestellten
Logiken. Mit Hilfe aussagenlogischer Ausdrücke können Beziehungen zwischen
atomaren Aussagen formuliert werden, die ihrerseits einen der Wahrheitswerte
Wahr (true) oder Falsch (false) annehmen können. Die klassische Aussagenlo-
gik fällt damit in die große Gruppe der zweiwertigen Logiken. Wie die folgenden
Beispiele zeigen, lassen sich atomare Aussagen mit Hilfe logischer Verknüpfun-
gen rekursiv zu komplexeren Aussagen verbinden:
{P} n≥0
int k = n;
int p = a;
int y = 1;
while (k > 0) {
if (k % 2 == 0) {
p = p * p;
k = k / 2;
} else {
y = y * p;
k = k - 1;
}
}
{Q} y = an
return y;
}
Abb. 6.4 Das Ziel eines Deduktionsbeweises wird mit Hilfe von Vor- und Nachbedingungen for-
muliert
k
gi
Lo
H ral gik
Te kat ik
e
er gik
po lo
g
tig
äd lo
m en
öh lo
er
Pr gen
w
a
i
ss
Au
Entscheidbar + - - -
Natürliche Grenze bzgl.
Semi-Entscheidbar + + + - der Berechenbarkeit
Zeit - - + +
Natürliche Grenze bzgl.
Gleichheit - - - + der Ausdrucksstärke
Natürliche Zahlen - - - +
I Prädikatenlogik
Die Prädikatenlogik erster Stufe (PL1) erweitert die Aussagenlogik um Prädika-
te. Im Gegensatz zu den atomaren Aussagen der PL0 hängt der Wahrheitswert
eines Prädikats von der Belegung einer oder mehrerer Variablen ab. Zusätzlich
stellt die PL1 die beiden Quantoren ∀ (Allquantor) und ∃ (Existenzquantor) zur
Verfügung, mit deren Hilfe komplexe Universal- bzw. Existenzaussagen formu-
liert werden können. Die folgenden Beispiele vermitteln einen Eindruck von der
Aussagekraft der Prädikatenlogik erster Stufe:
I Temporallogik
Für die Modellierung zeitlicher Kausalzusammenhänge wurden in der Vergan-
genheit verschiedene Temporallogiken entwickelt, die auf der Aussagenlogik
bzw. der Prädikatenlogik aufbauen und diese um spezielle temporale Operatoren
342 6 Software-Verifikation
I Höherwertige Logik
Höherwertige Logiken unterscheiden sich von der Prädikatenlogik erster Stufe
durch einen erweiterten Geltungsbereich der Quantoren ∀ und ∃. Im Gegensatz
zur PL1, in der die Quantoren ausschließlich auf Variablen angewendet werden
dürfen, erlauben höherwertige Logiken die Quantifizierung über Prädikate hin-
weg.
Die Verallgemeinerung der Quantifizierungsregeln verleiht den höherwertigen
Logiken eine ungeahnte Ausdrucksstärke, wie das Beispiel der natürlichen Zah-
len eindrucksvoll unter Beweis stellt. Die natürlichen Zahlen lassen sich mit Hil-
fe der 5 Peano-Axiome formalisieren, die bereits seit dem Ende des neunzehnten
Jahrhunderts bekannt sind und auf die Arbeiten des italienischen Mathematikers
Giuseppe Peano zurückgehen. Mit Hilfe der Prädikatenlogik erster Stufe lassen
sich die Axiome nicht beschreiben. Schuld daran ist das Induktionstheorem –
Peanos fünftes Axiom:
Das Induktionstheorem wendet den Allquantor auf Prädikate an und lässt sich da-
her nicht mit den Mitteln der Prädikatenlogik erster Stufe formulieren. Da die al-
lermeisten Korrektheitseigenschaften Aussagen über natürliche Zahlen beinhal-
ten, ist der Einsatz höherwertiger Logiken im Bereich der Software-Verifikation
nahezu unumgänglich. Auch die Vor- und die Nachbedingung unseres Beispiel-
programms machen eine Aussage über die natürlichen Zahlen. Damit erfordert
bereits der vergleichsweise einfache Korrektheitsbeweis der Funktion exp zwin-
gend den Einsatz einer höherwertigen Logik.
Auf der negativen Seite verfügen höherwertige Logikkalküle nur über ein
geringes Automatisierungspotenzial, so dass große Teile eines Korrektheitsbe-
weises manuell durchgeführt werden müssen. Die Berechenbarkeitstheorie setzt
ebenfalls klare Grenzen. Bereits in den frühen Dreißigerjahren konnte der Ma-
thematiker Kurt Gödel formal beweisen, dass höherwertige Logiken die Eigen-
schaft der Semi-Entscheidbarkeit verlieren. Kurzum: Es lassen sich wahre Aus-
sagen formulieren, die sich innerhalb des Logikkalküls nicht als solche beweisen
lassen [189, 238, 104].
prinzip aller deduktiven Verfahren bildet. Innerhalb des Kalküls werden alle Aussa-
gen in Form von Hoare-Tripeln notiert, für die sowohl eine Spaltenschreibweise als
auch eine Zeilenschreibweise existiert:
I Spaltenschreibweise I Zeilenschreibweise
{P}
{P} S {Q} S
{Q}
Die Semantik eines Hoare-Tripels ist wie folgt definiert: Unter der Annahme, dass
die Vorbedingung {P} erfüllt ist, gilt nach der terminierenden Ausführung des Pro-
grammfragments S die Nachbedingung {Q}. Die Korrektheit unseres Beispielpro-
gramms lässt sich mit Hilfe der Hoare-Notation wie folgt formulieren:
{n ≥ 0} y = exp(a,n) {y = an } (6.1)
Die über dem Mittelstrich notierten Aussagen bilden zusammen die Prämisse und
beschreiben die Voraussetzungen, die vor der Anwendung der Regel erfüllt sein
müssen. Die unter dem Mittelstrich notierte Aussage ist die Konklusion, d. h. die
Schlussfolgerung, die aufgrund der Semantik des Logikkalküls aus der Prämisse
abgeleitet werden kann. Tabelle 6.2 fasst die elementaren Beweisregeln des Hoare-
Kalküls zusammen.
Die Zuweisungsregel besagt, dass nach einer Zuweisung der Form x = c; im-
mer dann die Aussage P gilt , wenn vorher P[x ← c] galt. Die Aussage P[x ← c]
geht aus P hervor, indem alle Vorkommen der Variablen x durch den Ausdruck c er-
setzt werden. Als einzige Regel ist die Zuweisungsregel durchweg anwendbar und
unabhängig von der Gültigkeit einer Prämisse.
Mit Hilfe der Kompositionsregel ist es möglich, Aussagen über zusammenge-
setzte Programmabschnitte zu formulieren. Immer dann, wenn die Nachbedingung
eines Programmfragments S1 der Vorbedingung eines anderen Fragments S2 ent-
spricht, lassen sich S1 und S2 zu einer gemeinsamen Programmsequenz vereinen.
Die Fallunterscheidungsregel enthält neben der Vorbedingung P und der Nach-
bedingung Q die boolesche Bedingung B. Durch die Prämisse wird sichergestellt,
dass die Bedingung B zu Beginn des Programmfragments S1 wahr und zu Beginn
von S2 falsch ist. Wird die Fallunterscheidung anhand der booleschen Bedingung B
durchgeführt, so entspricht S1 dem If-Zweig und S2 dem Else-Zweig.
Für die Anwendung der Iterationsregel muss zunächst eine Invariante I bestimmt
werden. I ist eine boolesche Bedingung, die sowohl vor als auch nach der Ausfüh-
rung des Programmfragments S gültig ist. Mit anderen Worten: Die Gültigkeit von
344 6 Software-Verifikation
{True}
Zuweisung
{P[x ← c]} x = c; {P}
{I ∧ B} S {I}
Iteration
{I} while B do S {I ∧ ¬B}
P ⇒ Q, {Q} S {R}
Verstärken der Vorbedingung
{P} S {R}
{P} S {Q}, Q ⇒ R
Abschwächen der Nachbedingung
{P} S {R}
I wird durch S nicht verändert. In der realen Beweisführung stellt uns die Itera-
tionsregel vor nicht zu unterschätzende Probleme, da sich die Bestimmung einer
brauchbaren Invarianten in vielen Fällen als äußerst schwierig erweist.
Mit Hilfe der letzten beiden Regeln können zum einen die Vorbedingungen ver-
stärkt und zum anderen die Nachbedingungen abgeschwächt werden. Eine Verstär-
kung bedeutet, dass eine Aussage {Q} durch eine Aussage {P} ersetzt wird, aus
der {Q} logisch gefolgert werden kann. In entsprechender Weise bedeutet die Ab-
schwächung, dass eine Aussage {Q} durch eine Aussage {R} ersetzt wird, die eine
logische Folgerung von {Q} darstellt. Die Regeln werden immer dann benötigt,
wenn eine Vorbedingung und eine Nachbedingung für die Anwendung der Kompo-
sitionsregel ineinander überführt werden müssen. Die Umkehrung der Regeln gilt
nicht. Sowohl die Abschwächung der Vorbedingung, als auch die Verstärkung der
Nachbedingung wären logisch falsch.
Aus den elementaren Beweisregeln lassen sich spezialisierte Regeln ableiten, wie
z. B. die Regel der einfachen Fallunterscheidung, die einen If-Befehl ohne Else-
Zweig abhandelt:
{P ∧ B} S {Q}, {P ∧ ¬B} ⇒ {Q}
(6.3)
{P} if B then S {Q}
6.2 Deduktion 345
In ähnlicher Weise lassen sich weitere Regeln formulieren, die einen größeren als
den hier vorgestellten Sprachschatz abdecken. So lässt sich die Idee der Iterations-
regel in direkter Weise auf die Do-While-Schleife übertragen. Im Gegensatz zur
reinen While-Schleife wird die Terminierungsbedingung hier erst am Ende einer
Iteration überprüft:
{P} S {I}, {I ∧ B} S {I}
(6.4)
{P} do S while B {I ∧ ¬B}
Durch die Hinzunahme weiterer Regeln ist es prinzipiell möglich, den komple-
xen Sprachschatz heutiger Programmiersprachen abzubilden. Die Verifikation bleibt
dann nicht auf einfache Programme im Sinne unserer Beispielfunktion beschränkt.
Versuche zur Konstruktion umfassender Kalküle, die den gesamten Sprachumfang
einer modernen Sprache wie z. B. C, C++ oder Java abbilden, wurden in der Ver-
gangenheit mehrfach unternommen (siehe z. B. [191, 131]). Aufgrund des großen
Sprachumfangs und der komplizierten bzw. nicht eindeutig definierten Semantik
sind die entstandenen Kalküle jedoch schwer zu handhaben. Viele Arbeiten auf dem
Gebiet der deduktiven Software-Verifikation bestreiten daher einen anderen Weg.
Anstatt das Programm in seiner Originalsprache zu verifizieren, wird der Quelltext
in eine formale Zwischensprache übersetzt, die sich im Sprachumfang auf elemen-
tare Konstrukte beschränkt. Der Weg ist gangbar, da die hier vorgestellten Sprach-
elemente der Zuweisung (=), der Fallunterscheidung (if) und der Schleife (while)
zusammen bereits vollständig sind. Mit anderen Worten: Jeder Algorithmus lässt
sich mit Hilfe dieser drei Grundkonstrukte formulieren.
[Link] Korrektheitsbeweis
Für die Verifikation unseres Beispielprogramms beginnen wir mit der Anwendung
der Zuweisungsregel auf die ersten drei Variableninitialisierungen und fassen die
Zwischenergebnisse mit Hilfe der Kompositionsregel zusammen. Insgesamt erhal-
ten wir das in Abb. 6.6 dargestellte Zwischenergebnis.
Als nächstes wenden wir uns der While-Schleife zu. Die in der Iterationsregel
vorkommenden Terme I und B wählen wir wie folgt:
Die Gültigkeit der Invarianten zu Beginn der While-Schleife ergibt sich aus dem
Zwischenergebnis {P2 } durch die Anwendung der Regel zur Abschwächung der
Nachbedingung:
{P2 } ⇒ (k = n) ∧ (p = a) ∧ (y = 1) ∧ (k ≥ 0) (6.7)
⇒ (pk = an ) ∧ (y = 1) ∧ (k ≥ 0) (6.8)
⇒ (1 × pk = an ) ∧ (y = 1) ∧ (k ≥ 0) (6.9)
⇒ (y × pk = an ) ∧ (k ≥ 0) (6.10)
⇒ {I} (6.11)
346 6 Software-Verifikation
{P} {n ≥ 0}
int k = n;
int p = a;
int y = 1;
while (k > 0) {
if (k % 2 == 0) {
p = p * p;
k = k / 2;
} else {
y = y * p;
k = k - 1;
}
}
{Q} {y = an }
return y;
}
Abb. 6.6 Die Ergebnisse der Zuweisungsregel werden über die Kompositionsregel zusammenge-
fasst
Am Ende der Iteration ist sichergestellt, dass die Schleifenbedingung {B} falsch ist.
Zusammen mit der Invarianten gilt somit {I ∧ ¬B} und die Nachbedingung ergibt
sich durch einfache mathematische Umformung:
Unter der Annahme, dass die Prämisse der Regel erfüllt ist – nur dann dürfen wir
die Iterationsregel überhaupt anwenden –, erhalten wir das in Abb. 6.7 dargestellte
Zwischenergebnis.
Damit die Iterationsregel an dieser Stelle überhaupt angewendet werden darf,
müssen wir die Gültigkeit der Prämisse {I ∧ B}S{I} beweisen. Damit verbleibt das
in Abb. 6.8 dargestellte Beweisziel.
Durch die Anwendung der Fallunterscheidungsregel lässt sich dieses wiederum
in zwei kleinere Beweisziele herunterbrechen, die in Abb. 6.9 dargestellt sind. Deren
6.2 Deduktion 347
{P} {n ≥ 0}
int k = n;
int p = a;
int y = 1;
while (k > 0) {
if (k % 2 == 0) {
p = p * p;
k = k / 2;
} else {
y = y * p;
k = k - 1;
}
}
return y;
}
Gültigkeit ergibt sich ohne Umwege aus der Zuweisungsregel und der folgenden
mathematischen Beziehung:
k
y × (p × p) 2 falls k > 0 ∧ k mod 2 = 0
y× p =k
(6.18)
y × p × pk−1 falls k > 0 ∧ k mod 2 = 0
Damit ist die Korrektheit der Funktion exp mit Hilfe des Hoare-Kalküls formal
verifiziert.
Das Beispiel demonstriert eine wesentliche Eigenschaft der deduktiven
Software-Verifikation: Die Beweisführung steht und fällt mit der richtigen Wahl der
Invarianten {I}. Sobald die „richtige“ Invariante gewählt ist, ergibt sich der Rest des
Beweises mehr oder weniger durch die geradlinige Anwendung der anderen Regeln.
Lässt sich eine passende Invariante für unser Beispielprogramm noch vergleichs-
weise einfach finden, so ist deren Bestimmung für viele aus dem Leben gegriffenen
Programme faktisch kaum noch möglich. Für viele Experten ist die Bestimmung
der Invarianten die Achillesferse der deduktiven Beweistechnik. Die richtige Wahl
erfordert neben einem gewissen mathematischen Gespür eine gehörige Portion an
Metawissen über das zu verifizierende Programm. Aus diesem Grund werden De-
348 6 Software-Verifikation
while (k > 0) {
{I ∧ B} {y × pk = an ∧ k ≥ 0} ∧ {k > 0}
if (k % 2 == 0) {
p = p * p;
k = k / 2;
} else {
y = y * p;
k = k - 1;
}
{I} {y × pk = an ∧ k ≥ 0}
}
return y;
}
while (k > 0) {
if (k % 2 == 0) {
{I ∧ B ∧ B } {y × pk = an ∧ k ≥ 0} ∧ {k > 0} ∧ {k mod 2 = 0}
p = p * p;
k = k / 2;
{I} {y × pk = an ∧ k ≥ 0}
} else {
y = y * p;
k = k - 1;
{I} {y × pk = an ∧ k ≥ 0}
}
}
return y;
}
existieren, das ein Programm p als Argument entgegennimmt und genau dann TRUE
zurückliefert, wenn p terminiert:
boolean halt(p : Program) {
if ( terminates(p) ) return TRUE;
else return FALSE:
}
Aus dem Programm halt lässt sich ein zweites konstruieren, das die Funktion halt
mit sich selbst als Argument aufruft und genau dann in eine Endlosschleife übergeht,
wenn die Funktion halt zu TRUE evaluiert:
void paradoxon() {
if (halt(paradoxon)) while (1);
}
...
....
Unendliches Band
Schreib-/Lesezeiger
Programm
struieren und damit die Unentscheidbarkeit des Halteproblems – wenn auch nur
informell – gezeigt.
Dem 1912 in London geborenen Mathematiker Alan Turing gelang es 1936 als
Erstem, die Unlösbarkeit des Halteproblems formal zu belegen [261, 45]. Turing
führte seinen Beweis mit Hilfe eines universellen Automatenmodells durch, das
heute zu den Grundpfeilern der modernen Berechenbarkeitstheorie zählt. Die Re-
de ist von der Turing-Maschine – einer gedanklich konstruierten Steuereinheit, die
ähnlich einem altertümlichen Tonband einzelne Zeichen lesen und schreiben kann.
Hierzu steht neben einem Schreib-Lese-Kopf ein – per definitionem – unendlich
langes Band zur Verfügung, das in einzelne Felder eingeteilt ist (vgl. Abb. 6.10).
Jedes Feld kann mit genau einem Zeichen beschrieben werden. Der Schreib-Lese-
Kopf wird über einen endlichen Automaten angesteuert, der die auszuführende Ak-
tion aus dem internen Zustand sowie dem Inhalt des adressierten Speicherfelds be-
stimmt. Der Funktionsumfang der Turing-Maschine ist dabei äußerst simpel: Neben
der Veränderung des aktuellen Speicherfelds sowie der schrittweisen Bewegung des
Lesekopfes nach links oder rechts beherrscht die Maschine keine weiteren Opera-
tionen.
Das von Turing vorgeschlagene Gedankenmodell ist für die moderne theoreti-
sche Informatik von unschätzbarem Wert, da die gedachte Maschine trotz ihrer ein-
fachen Struktur ein universelles Berechnungsmodell besitzt. Mit anderen Worten:
Jeder nur erdenkliche Algorithmus lässt sich mit Hilfe einer Turing-Maschine im-
plementieren. Damit übertragen sich die auf der Ebene der Turing-Maschine be-
wiesenen Limitierungen in direkter Weise auf alle heutigen Programmiersprachen
und Rechnertypen. Dieses Ergebnis ist Inhalt der Church’schen These, die nach dem
amerikanischen Logiker Alonzo Church benannt ist und heute eine allgemein aner-
kannte Erkenntnis der Berechenbarkeitstheorie darstellt.
6.3 Modellprüfung
Die Verifikation eines Programms mit dem Mittel der Modellprüfung unterscheidet
sich diametral von einem Deduktionsbeweis. Aufgrund der wesentlich geringeren
Ausdrucksstärke der zugrunde liegenden Logik ist das Anwendungsspektrum der
6.3 Modellprüfung 351
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T1T2 5 6 N1C2
7 8
T2C1 T1C2
6.3.1 Temporallogik
Im Bereich der Modellprüfung werden spezielle Temporallogiken eingesetzt, um die
zu verifizierenden Eigenschaften zu spezifizieren. Die wichtigsten beiden Vertreter
sind die Logik LTL (linear time logic) und die Logik CTL (computation tree logic).
Beide werden in den folgenden Abschnitten kurz vorgestellt:
Viele der Aussagen, die mit Hilfe der Modellprüfung verifiziert werden können,
lassen sich einer der folgenden Eigenschaftsklassen zuordnen:
I Sicherheitseigenschaften
¬(φ ∧ ψ) : Aussage φ und Aussage ψ sind niemals beide gleichzeitig wahr.
I Fairness-Eigenschaften
♦φ : Aussage φ ist unendlich oft gültig.
I Lebendigkeitseigenschaften
(φ → ♦ψ) : Immer wenn φ gilt, dann gilt auch irgendwann wieder ψ.
Die Kombination der verschiedenen Grundschemata erlaubt, selbst komplexe kau-
sale Beziehungen auf eine vergleichsweise kompakte temporallogische Formel ab-
zubilden.
LTL-Formeln werden über unendlichen Pfaden von Zuständen interpretiert. Im
Initialzustand startend, lässt sich jeder Pfad aus der Kripke-Struktur durch die Tra-
versierung der gerichteten Kanten erzeugen. Eine typische Struktur beschreibt auf
diese Weise unendlich viele Pfade, auch wenn sie selbst nur aus endlich vielen Zu-
ständen besteht. Für das Beispiel in Abb. 6.11 ergeben sich unter anderem die fol-
genden Pfadsegmente:
I (0, 1, 4, 7, 2, 6, 0, . . .)
I (0, 1, 4, 7, 2, 5, 8, 1, . . .)
I (0, 2, 5, 8, 1, 4, 7, 2, 6, 0, . . .)
I (0, 2, 6, 0, 2, 6, . . .)
I ...
6.3 Modellprüfung 353
M , 0 |= ◦φ O ...
0 1 2 3 4 5 6
M , 0 |= φ O O O O O O O ...
0 1 2 3 4 5 6
M , 0 |= ♦φ O ...
0 1 2 3 4 5 6
M , 0 |= φ U ψ O O O O ...
0 1 2 3 4 5 6
Genau wie im Falle der LTL erweitert auch die CTL die booleschen Verknüp-
fungen um zusätzliche temporale Operatoren. Im direkten Vergleich wirkt die CTL-
Semantik zunächst komplizierter. In dieser Logik ist jeder temporale Operator mit
einem Existenz- (E) oder einem Allquantor (A) verbunden, der eine Aussage über
die von dem betrachteten Zustand ausgehenden Zweige macht. Dementsprechend
hält die Logik CTL alle temporalen Operatoren in jeweils zwei Varianten vor:
Die Gültigkeit einer CTL-Formel φ in einem bestimmten Zustand s ist wie folgt
definiert:
Tabelle 6.3 fasst die Semantik der CTL-Operatoren grafisch zusammen. Die weiter
oben mit Hilfe der Logik LTL formulierten Aussagen lassen sich mit Formeln der
CTL ebenfalls beschreiben:
I Sicherheitseigenschaften
A¬(φ ∧ ψ) : Aussage φ und Aussage ψ sind niemals beide gleichzeitig wahr.
I Fairness-Eigenschaften
AA♦φ : Aussage φ ist unendlich oft gültig.
I Lebendigkeitseigenschaften
A(φ → A♦ψ) : Immer wenn φ gilt, dann gilt auch irgendwann wieder ψ.
Obwohl die Logik CTL auf den ersten Blick ausdrucksstärker erscheint als ihr Ge-
genspieler LTL, gibt es Aussagen, die in LTL formuliert werden können, nicht je-
6.3 Modellprüfung 355
0 0
O O O
1 2 1 2
3 4 5 6 3 4 5 6
M , 0 |= E♦φ M , 0 |= A♦φ
0 0
O
1 2 1 2
O O O
3 4 5 6 3 4 5 6
M , 0 |= Eφ M , 0 |= Aφ
O O
0 0
O O O
1 2 1 2
O O O O O
3 4 5 6 3 4 5 6
M , 0 |= φ EU ψ M , 0 |= φ AU ψ
O O
0 0
O O
1 2 1 2
3 4 5 6 3 4 5 6
doch in CTL. Da sich viele Algorithmen für die Logik CTL einfacher formulieren
und effizienter berechnen lassen, basieren die meisten der klassischen Modellprü-
fungsverfahren auf dieser Logik. Im nächsten Abschnitt werden wir ebenfalls auf
die CTL als Spezifikationssprache zurückgreifen.
356 6 Software-Verifikation
A A
C1 C2
φ1 = A(T1 → A♦C1 )
φ2 = A(T2 → A♦C2 )
Die Modellprüfung erfolgt in zwei Schritten. Im ersten Schritt werden die Extensi-
onsmengen φ1 und φ2 berechnet und im zweiten Schritt geprüft, ob der Startzu-
stand in den erzeugten Mengen enthalten ist. In der Extensionsmenge einer Formel
φ sind genau diejenigen Zustände enthalten, in denen φ wahr ist. Folgerichtig ist
die untersuchte Eigenschaft genau dann erfüllt, wenn die entsprechende Extensi-
onsmenge den Initialzustand enthält.
Zur Berechnung der Mengen φ1 bzw. φ2 werden zunächst die Syntax-Bäume
von φ1 bzw. φ2 erstellt. Anschließend wird der Baum von den Blättern zur Wur-
zel traversiert und die Extensionsmenge rekursiv für jede Teilformel berechnet.
Abb. 6.12 zeigt die Syntax-Bäume für die beiden Beweisziele φ1 und φ2 . Um die
Menge der Zustände zu berechnen, in denen die Formel φ1 gilt, bestimmen wir
zunächst die entsprechenden Mengen für die atomaren Formeln T1 und C1 . Im An-
schluss daran berechnen wir die Menge A♦C1 und schließlich die Extension von
φ durch die Auswertung der Implikation → sowie des A-Operators.
Die Berechnung der Extensionsmenge wird für die verschiedenen Operatoren
gänzlich unterschiedlich durchgeführt. Für eine Variable a enthält die Extensions-
menge a exakt die mit a markierten Zustände. Ist φ eine Teilformel, die mittels
6.3 Modellprüfung 357
boolescher Konnektive aufgebaut ist, so lässt sich die Berechnung der Extensions-
menge auf eine entsprechende Mengenoperation zurückführen. Es gelten die fol-
genden Beziehungen:
¬φ = φ (6.19)
φ ∧ ψ = φ ∩ ψ (6.20)
φ ∨ ψ = φ ∪ ψ (6.21)
φ → ψ = φ ∪ ψ (6.22)
E◦φ = R • φ (6.23)
A◦φ = R • φ (6.24)
Etwas komplizierter gestaltet sich die Berechnung der Extensionsmengen für die
restlichen temporalen Operatoren. Zu deren Bestimmung müssen die folgenden Fix-
punktgleichungen gelöst werden, die auf Arbeiten von Edmund M. Clarke und E.
Allen Emerson aus den Achtzigerjahren zurückgehen [47, 85]:
und ⊥ bezeichnen den größten bzw. den kleinsten Fixpunkt der nachfolgenden
Funktion. Für die Notation der Fixpunktfunktionen wurde die Schreibweise des
Lambda-Kalküls verwendet. In diesem Zusammenhang steht der Ausdruck λ Z.τ(Z)
für die Funktion f : Z → τ(Z).
Ausgehend von der leeren Menge wird der kleinste Fixpunkt in einem iterati-
ven Prozess berechnet, indem die Gleichung wiederholt angewendet wird, bis sich
die Zustandsmenge nicht mehr ändert. Die Berechnung des größten Fixpunkts wird
analog durchgeführt. Anstelle der leeren Menge wird in diesem Fall die gesamte
Zustandsmenge als Startwert verwendet.
Nach den getätigten Vorarbeiteten sind wir nun in der Lage, die entsprechen-
den Extensionsmengen für unsere Beispielstruktur zu berechnen. Für die atomaren
Formeln T1 und C1 des Beweisziels
358 6 Software-Verifikation
φ1 = A(T1 → A♦C1 )
T1 = {1, 4, 5, 8}
C1 = {3, 7}
Für die Berechnung von A♦C1 beginnen wir mit der leeren Menge und iterieren
die Mengenfunktion
τ(Z) = C1 ∪ R • Z,
bis sich die Zustandsmenge nicht mehr ändert:
τ 1 (0)
/ = {3, 7}
τ 2 (0)
/ = {3, 4, 7}
τ 3 (0)
/ = {1, 3, 4, 7}
τ 4 (0)
/ = {1, 3, 4, 7, 8}
τ 5 (0)
/ = {1, 3, 4, 5, 7, 8}
τ 6 (0)
/ = {1, 3, 4, 5, 7, 8}
Im sechsten Iterationsschritt ist ein Fixpunkt erreicht und die Extensionsmenge da-
mit vollständig bestimmt:
A♦C1 = {1, 3, 4, 5, 7, 8}
Die Behandlung der Implikation reduziert sich nach Gleichung (6.22) auf eine ein-
fache Mengenoperation. Wir erhalten das folgende Ergebnis:
erreichen wir diesen bereits im ersten Schritt. Die Formel φ1 ist damit ausnahmslos
in allen Zuständen der Kripke Struktur aus Abb. 6.11 gültig und die Modellprüfung
6.3 Modellprüfung 359
I T1
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T 1T 2 5 6 N1C2
7 8
T2C1 T1C2
I C1
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T 1T 2 5 6 N1C2
7 8
T2C1 T1C2
I A♦C1
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T 1T 2 5 6 N1C2
7 8
T2C1 T1C2
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T 1T 2 5 6 N1C2
7 8
T2C1 T1C2
I T2
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T1T2 5 6 N1C2
7 8
T2C1 T1C2
I C2
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T1T2 5 6 N1C2
7 8
T2C1 T1C2
I A♦C2
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T1T2 5 6 N1C2
7 8
T2C1 T1C2
N1N2
N2T1 0 N1T2
1 2
N2C1 3 4 T1T2 T1T2 5 6 N1C2
7 8
T2C1 T1C2
erfolgreich abgeschlossen. Abb. 6.13 fasst die während der Berechnung auftreten-
den Extensionsmengen der einzelnen Unterformeln nochmals grafisch zusammen.
Die Modellprüfung der Formel φ2 kann in analoger Weise durchgeführt werden. Die
entsprechenden Extensionsmengen sind in Abb. 6.14 dargestellt.
Ähnlich wie im Falle der Deduktion kann auch die Modellprüfung in der Praxis
nicht in jedem Fall so problemlos durchgeführt werden wie hier gezeigt. Anders als
unser pathologisches Lehrbuchbeispiel vermuten lässt, kann die Größe der auftre-
tenden Zustandsmengen in der Praxis gigantische Ausmaße annehmen. Hierdurch
ist jeder Algorithmus, der auf einer aufzählenden Darstellung der einzelnen Zustän-
de beruht, bereits von vorne herein zum Scheitern verurteilt. Mit anderen Worten:
Ohne weitere Maßnahmen ist der praktische Wert des oben vorgestellten Modell-
prüfungsalgorithmus gleich null.
Erst die symbolische Modellprüfung sorgte Anfang der Neunzigerjahre für einen
kleinen Durchbruch. Indem die berechneten Extensionsmengen nicht mehr aufzäh-
lend, sondern mit Hilfe Binärer Entscheidungsdiagramme (binary decision dia-
grams, kurz BDDs [35]) symbolisch beschrieben wurden, war es auf einen Schlag
möglich, Systeme mit mehr als 1020 Zuständen effizient darzustellen und erfolgreich
zu verifizieren [36]. Die symbolische Modellprüfung konnte hierdurch in Größen-
ordnungen vordringen, die zuvor als undenkbar galten. Trotz dieser Erfolge ist und
bleibt die Speicher- und Laufzeitkomplexität der Modellprüfungsalgorithmen ein
Kernproblem, das die breite industrielle Anwendung bis heute verhindert hat. Im
direkten Vergleich mit der Deduktionstechnik verfügt die Modellprüfung jedoch
über deutlich freundlichere Zukunftsaussichten. Aufgrund der vollautomatischen
Beweisführung lässt sich diese Technik nahtlos in industrielle Entwicklungsprozes-
se integrieren.
zeile wird nach der Methode von Floyd, Park und Clarke eine Gleichung erzeugt,
mit der die Zustandsmengen Si in Beziehung zueinander gesetzt werden. Für ein
Programm mit k Programmzeilen entsteht hieraus ein Gleichungssystem der fol-
genden Form:
S0 = f0 (S0 , . . . , Sk ) (6.31)
S1 = f1 (S0 , . . . , Sk ) (6.32)
...
Sk = fk (S0 , . . . , Sk ) (6.33)
S0,0 = 0/ (6.34)
S1,0 = 0/ (6.35)
...
Sk,0 = 0/ (6.36)
hard_to_test1.c hard_to_test2.c
float foo(unsigned char x) 0 float bar(unsigned short x) 0
{ 0 { 0
unsigned char i = 0; 1 unsigned char i = 0; 1
1 1
while (i < 6) { 2 while (i < 6) { 2
i++; 3 i++; 3
x /= 2; 4 x /= 2; 4
} 4 } 4
return 1.0 / (x-i); 5 return 1.0 / (x-i); 5
} 5 } 5
Abb. 6.15 Für gewisse Eingabewerte erzeugt die rechte Variante einen Division-durch-null-Fehler
in Abschnitt 4.6 die Grenzen des klassischen Software-Tests vor Augen führten.
Auf der linke Seite ist die korrekt arbeitende Variante für 8-Bit-Übergabeparameter
abgebildet. Auf der rechten Seite befindet sich die fehlerhafte Variante für 16-Bit-
Übergabeparameter, die für ausgewählte Eingabewerte eine Division durch 0 durch-
führt. Am Ende von Abschnitt 4.6 stand die Erkenntnis, dass keine der bisher vor-
gestellten Konstruktionstechniken in der Lage war, den Fehler systematisch zu er-
kennen.
Im Folgenden werden wir zeigen, wie sich der Fehler mit Hilfe der statischen
Software-Verifikation auf direktem Wege entdecken lässt. Hierzu transformieren
wir die beiden Beispielprogramme zunächst in ein Gleichungssystem, indem wir
für jede Programmzeile die möglichen Zustandsmengen gegenseitig in Beziehung
setzen. Beschreiben wir den jeweiligen Zustand des Programms mit Hilfe des zwei-
dimensionalen Vektors (i, x), so erhalten wir das folgende Ergebnis:
I Gleichungssystem für die Funktion foo:
x
S4 = {(i, ) | (i, x) ∈ S3 } (6.50)
2
S5 = {(i, x) | (i, x) ∈ S4 , i ≥ 6} (6.51)
Die Schnittmenge ist leer und für die Funktion foo damit formal bewiesen, dass
eine Division durch null in Zeile 5 niemals eintreten kann. Anders erweist sich die
Situation im Falle der Funktion bar. Durch den größeren Wertebereich der Varia-
blen x enthält die Menge S5 deutlich mehr Zustände und führt zu einer nichtleeren
Schnittmenge:
I Start
S0 = S1 = S2 = S3 = S4 = S5 = 0/
I Dritte Iteration
I Sechste Iteration
Die berechnete Schnittmenge lässt einen unmittelbaren Rückschluss auf das Pro-
grammverhalten zu. Genau dann, wenn sich der Übergabeparameter x im Intervall
[384; 447] befindet, verursacht die Funktion einen Division-durch-null-Fehler.
In Bezug auf die Fähigkeit, auch versteckte Programmfehler aufzudecken,
schlägt die abstrakte Interpretation die in Kapitel 4 eingeführten Testkonstruktions-
techniken um Längen. Trotzdem unterliegt auch dieser Ansatz fundamentalen Gren-
zen. So ist die exakte Berechnung der Zustandsmenge nur für eine kleine Gruppe
366 6 Software-Verifikation
I Start
S0 = S1 = S2 = S3 = S4 = S5 = 0/
I Dritte Iteration
I Sechste Iteration
Induzierter
{0,1,2,3} {0,1,2,3}
Operator
Abstraktions- Abstraktions-
domäne domäne
6.4.2 Datenabstraktion
Die abstrakte Interpretation löst das Berechenbarkeitsproblem, indem die Zustands-
mengen nicht exakt bestimmt, sondern lediglich angenähert werden. Hierzu wird,
wie die linke Hälfte von Abb. 6.18 zeigt, der Datenbereich abstrahiert und jede
Programmoperation auf eine induzierte Operation auf den abstrahierten Daten ab-
gebildet. Jedes Element des abstrahierten Bereichs steht für mehrere Elemente des
Originalbereichs und wird für die Programmanalyse als Approximation der exakten
Werte verwendet.
Die rechte Hälfte von Abb. 6.18 demonstriert das Prinzip der Abstraktion am Bei-
spiel der Restklassenarithmetik. Die Originaldomäne besteht aus den natürlichen
Zahlen und der abstrahierte Bereich aus dem endlichen Intervall [0; 3]. Jedes Ele-
ment x des Originalbereichs wird durch die Abstraktionsfunktion α auf ein Element
des abstrahierten Wertebereichs abgebildet. In unserem Beispiel weist die Funktion
α jedem Element x der Originaldomäne das Element x mod 4 zu. Jede in der Origi-
naldomäne ausgeführte Operation lässt sich auf die abstrahierte Domäne übertragen,
indem das Ergebnis ebenfalls modulo-4 gerechnet wird.
Die definierte Abstraktion besitzt zwei bedeutende Eigenschaften. Zum einen
haben wir den vormals unendlichen Wertebereich auf eine endliche Menge redu-
ziert, die nur noch 4 Elemente enthält. Zum anderen gelingt die Reduktion auf eine
Art und Weise, die wichtige Eigenschaften der Originaldomäne erhält. Mit anderen
Worten: Beobachtungen in der Abstraktionsdomäne lassen sich unter bestimmten
Voraussetzungen auf die Originaldomäne zurückübertragen. Um den praktischen
368 6 Software-Verifikation
Nutzen zu demonstrieren, wollen wir uns die Frage stellen, ob die folgende Glei-
chung gilt:
343 × 112 = 420 × 84 (6.64)
Anstatt die Produkte mühsam auszurechnen, betrachten wir zunächst, wie sich die
Gleichung in der wesentlich einfacheren abstrakten Domäne verhält. Nach der Re-
duktion beider Seiten erhalten wir das folgende Ergebnis:
Ein einziger Blick auf Gleichung (6.65) reicht aus, um die Gleichung als falsch zu
entlarven. Da sich die Eigenschaft der Ungleichheit uneingeschränkt auf die Origi-
naldomäne überträgt, folgt sofort, dass die Originalgleichung (6.64) ebenfalls falsch
sein muss. Die umgekehrte Folgerung wäre an dieser Stelle nicht zulässig. Anders
als im Falle der Ungleichheit lässt sich die Eigenschaft der Gleichheit nicht von der
abstrakten Domäne in die Originaldomäne übertragen. Die nur partielle Übertrag-
barkeit von Eigenschaften ist gewissermaßen der Preis, den wir der Abstraktion an
dieser Stelle zollen müssen.
Um die oben geschilderte Fixpunktiteration nach Floyd, Park und Clarke in der
Praxis handhabbar zu machen, wird die Berechnung der Zustandsmengen auf einem
abstrakten Datenbereich ausgeführt. Wird als abstrahierter Datenbereich die Men-
ge der konvexen Polygone im n-dimensionalen Raum gewählt, so lässt sich jede
Zustandsmenge S eines Programms mit n Variablen durch die konvexe Hülle C(S)
approximieren. Entsteht durch die Vereinigung zweier konvexer Polygone im Lau-
fe der Fixpunktberechnung eine nichtkonvexe Struktur, wird das Polygon durch die
Hinzunahme weiterer Zustände wieder in ein konvexes Polygon überführt. Durch
diese Überapproximation ist sichergestellt, dass alle Zustände der nicht exakt bere-
chenbaren Menge S in der Approximation C(S) vollständig enthalten sind. Da die
Menge C(S) neben den Elementen aus S im Allgemeinen auch andere Zustände
enthält, müssen wir bei der Ergebnisinterpretation besondere Vorsicht walten las-
sen. Wie in Abb. 6.19 skizziert, sind an dieser Stelle drei verschiedene Szenarien zu
unterscheiden:
I Fall 1
Die Approximationsmenge C(S) und die Fehlermenge E sind disjunkt. Da es sich
bei der Menge C(S) um eine Überapproximation der Zustandsmenge S handelt,
sind keine Elemente aus S in der Fehlermenge E enthalten. Folgerichtig kann
keiner der fehlerhaften Zustände erreicht werden und das Programm erfüllt die
zu verifizierende Eigenschaft uneingeschränkt.
I Fall 2
Die Approximationsmenge C(S) ist eine Teilmenge der Fehlermenge E. In die-
sem Fall sind mit der Überapproximation C(S) auch alle Zustände von S in der
Fehlermenge enthalten und die zu verifizierende Eigenschaft damit auf jeden Fall
verletzt.
6.4 Abstrakte Interpretation 369
E E E
I Fall 3
Falls sich die Approximationsmenge C(S) und die Fehlermenge E überlappen
und keine der beiden Mengen die andere enthält, lässt das Ergebnis keinerlei
Aussage zu. Sind die Zustände der Schnittmenge auch Elemente der exakten Zu-
standsmenge S, verletzt das Programm die zu verifizierende Eigenschaft. Sind
die Zustände der Schnittmenge in der Überapproximation C(S) enthalten, nicht
jedoch in der Menge S selbst, so lässt sich die Fehlersituation nicht herbeiführen.
In diesem Fall produziert das Verfahren ein False negative. Obwohl das Pro-
gramm vollständig korrekt arbeitet, kann die zu verifizierende Eigenschaft nicht
bewiesen werden.
Anders als die Deduktionstechnik und die Modellprüfung hat die abstrakte Inter-
pretation den Sprung in industrielle Entwicklungsprozesse heute bereits geschafft.
Durch die bewusste Approximation des Zustandsraums skaliert das Verfahren deut-
lich besser als die anderen vorgestellten Techniken. Erkauft wird die hohe Skalier-
barkeit durch den Verlust der Präzision, so dass in einigen Fällen nur partielle Kor-
rektheitsaussagen möglich sind.
Industrielle Werkzeuge, die auf dem Prinzip der abstrakten Interpretation beru-
hen, arbeiten vollautomatisch und lassen sich hierdurch problemlos in industriel-
le Entwicklungsprozesse integrieren. Die verwässerte Präzision spiegelt sich unter
anderem in der Aufbereitung der Endergebnisse wieder. So färbt z. B. das Werk-
zeug Polyspace die untersuchten Programmbereiche mit drei verschiedenen Farben
ein [216]. Grün markierte Programmteile wurden korrekt verifiziert, während rot
eingefärbte Fragmente die untersuchte Eigenschaft mit Sicherheit verletzen. Pro-
grammteile, die sich aufgrund der Approximationsproblematik einer exakten Aus-
sage entziehen, werden grau hinterlegt dargestellt.
Kapitel 7
Software-Lebenszyklus
tik technisch in den Griff zu bekommen ist, werden wir in Kapitel 8 ausführlich
behandeln.
Mit zunehmender Lebensdauer beginnt fast zwangsläufig eine schleichende De-
generation der Code-Basis. Unter anderen sind es die ständig wechselnden Anforde-
rungen, die im Laufe der Zeit viele kleine und große Änderungen an der Quelltext-
basis nach sich ziehen. Wie der stete Tropfen den Stein werden die ehemals soliden
Programmstrukturen immer weiter ausgehöhlt. Unzählige Fehlerkorrekturen tragen
ihren Teil zu der Misere bei.
Bevor wir uns mit den Gründen der altersbedingten Degeneration im Detail be-
schäftigen, wollen wir an dieser Stelle zunächst typische Eigenschaften großer, lang-
lebiger Software-Systeme herausarbeiten. Kurzum, wir suchen die Antwort auf die
plakative Frage: „Wie komplex ist komplexe Software wirklich?“. Die folgenden
Punkte fassen die wichtigsten charakteristischen Eigenschaften solcher Systeme zu-
sammen:
I Code-Basis
Große Software-Systeme verfügen über eine riesige Code-Basis, die leicht aus
mehreren Millionen Zeilen Quelltext bestehen kann. Versionskontrolliert kann
der benötigte Speicherplatz des Code-Repositories die Terabyte-Grenze leicht
durchbrechen. Noch stärker als die Code-Größe wachsen in aller Regel die Se-
kundärdatenbestände. Neben der Produkt- und Projektdokumentation fallen hier-
unter insbesondere auch verwendete Use-Cases, Benchmarks sowie die Testfall-
datenbank.
I Übersetzungszeit
Die Größe der Quellcode-Basis fordert ihren Tribut nicht zuletzt in hohen Über-
setzungszeiten, die bei einem komplexen Software-System durchaus mehrere
Stunden betragen kann. Werden im Anschluss alle Regressionstests ausgeführt,
können weitere Stunden oder gar Tage vergehen. Wie Abschnitt 8.2.2 ausführ-
7.2 Gründe der Software-Alterung 373
I Entwickler-Teams
An großen Software-Projekten können mehrere hundert Programmierer gleich-
zeitig beteiligt sein. Rechnen wir das gesamte Personal, so kommen einige Pro-
jekte spielend in den vierstelligen Bereich. Im Gegensatz zu früher können die
verschiedenen Entwickler-Teams heute weltweit verteilt agieren. Internetbasierte
Versionskontrollsysteme machen es möglich, von überall in der Welt an ein und
derselben Code-Basis zu arbeiten.
I Team-Zusammensetzung
Bedingt durch die hohe Mitarbeiterfluktuation (brain drain) im Bereich der IT-
Berufe überdauern langlebige Software-Systeme die durchschnittliche Verweil-
dauer seiner Entwickler um Längen. Legen wir die durchschnittliche Firmenzu-
gehörigkeit eines US-amerikanischen Software-Entwicklers von ca. drei Jahren
zugrunde, arbeitet an einer 10 Jahre alten Software bereits die vierte Entwickler-
generation.
I Altlasten
Langlebige Software-Systeme sammeln im Laufe der Zeit eine Reihe von Alt-
lasten an. Darunter fallen Programmfragmente, die in antiquierten Sprachen
oder nach veralteten Richtlinien erstellt wurden, genauso wie Bibliotheken und
Schnittstellen, die ausschließlich aus Gründen der Rückwärtskompatibilität wei-
ter unterstützt werden. Manch alternde Software hat im Laufe ihres Lebens die ei-
ne oder andere Firmenfusion überlebt. Die Verschmelzung der eigenen Software
mit der hinzugekauften Technologie führt in vielen Fällen zu einer babylonischen
Vermischung von Sprachen und Konzepten, die ohne Kenntnis der historischen
Entwicklung kaum noch rational nachvollzogen werden kann.
Im nächsten Abschnitt werden wir den Ursachen genauer auf den Grund gehen, die
für die schleichende Degradierung eines Software-Produkts verantwortlich zeich-
nen. Ein wichtiges Anliegen dieses Kapitels ist es, die Darstellung der Software-
Alterung nicht im Ungefähren zu belassen. Aus diesem Grund sind die folgenden
Abschnitte mit zahlreichen Code-Beispielen angereichert, die allesamt aus der Pra-
xis stammen und hinter der Fassade schmucker Benutzungsschnittstellen auch heute
noch ihre Dienste verrichten.
handelt, zeigt ein Blick in den Bereich des Hardware-Software-Co-Designs. Mit den
dort entwickelten Methoden ist es in zunehmendem Maße möglich, in Software rea-
lisierte Funktionalität in Hardware auszulagern und umgekehrt.
Vergleichen wir Hard- und Software in Punkto Flexibilität, so erweist sich die
Software aufgrund ihrer nahezu beliebigen Änderbarkeit als haushoch überlegen.
Um einen Eindruck über die praktischen Auswirkungen zu bekommen, greifen wir
erneut das Beispiel der Kfz-Steuergeräteentwicklung auf. Wie bei jedem anderen
Erzeugnis, das sich über längere Zeit in Produktion befindet, müssen auch hier
in regelmäßigen Abständen kleinere Änderungen vorgenommen werden. Da die
Entwicklung produktionsbegleitend durchgeführt wird, sprechen wir in diesem Zu-
sammenhang von einem running change. Anders als im Fall der Entwicklung oder
Vorentwicklung sind die Anforderungen an den Abnahmeprozess hier deutlich ver-
schärft, da sich jeder unbemerkt einschleichende Fehler unmittelbar und mit schwer
kalkulierbaren Folgen auf die Produktion auswirkt.
Die Zeit, die für eine produktionsbegleitende Änderung aufgebracht werden
muss, hängt maßgeblich davon ab, ob die Hardware oder die Software des Steu-
ergeräts betroffen ist. Eine Hardware-Änderung wird im Automobilbereich mit ca.
6 Monaten veranschlagt, während eine Software-Änderung nur ca. 1 Monat in An-
spruch nimmt. Die eigentliche Code-Modifikation ist nicht selten in nur wenigen
Stunden oder gar Minuten erledigt. Der Rest ist den ausführlichen Test- und Abnah-
meprozessen geschuldet.
Die schier unbegrenzte Änderbarkeit von Software ist Segen und Fluch zugleich.
Im Falle eines Hardware-Systems überlegen sich Kunden und Entwickler zu Pro-
jektbeginn sehr genau, welche Anforderungen das endgültige Produkt später zu
erfüllen hat. Natürlich sind auch hier Anforderungsänderungen keine Seltenheit –
beide Seiten sind sich jedoch sehr bewusst, dass diese nur in engen Grenzen reali-
sierbar sind. Anders sieht die Situation im Bereich der Software aus. Die scheinbar
unbegrenzte Flexibilität führt bei vielen Beteiligten zu dem Bewusstsein, dass sich
Änderungen jederzeit auf’s Neue durchführen lassen. So schallt es durch die Flure
so mancher Software-Firmen: “Hey, it’s software. Everything is possible”. Falsch
ist die Aussage keineswegs: Alles ist möglich – zum Guten und zum Schlechten.
Nahezu alle langlebigen Software-Systeme haben in ihrer Vergangenheit mehre-
re Phasen durchlaufen, in denen die Produktziele geändert und die Anforderungen
entsprechend angepasst wurden. Die häufig getroffene Annahme, dass die zu Pro-
jektbeginn festgelegten Anforderungen konstant bleiben, wird in der Praxis stets
auf’s Neue widerlegt. Kurzum: Um das Phänomen der Software-Alterung zu ver-
stehen, müssen wir die Anforderungen als ein bewegliches Ziel begreifen.
Auch wenn der Änderbarkeit eines Software-Systems keine physikalischen
Grenzen gesetzt sind, wirkt sich die permanente Modifikation mit steigender Le-
bensdauer fatal auf die Code-Qualität aus. Zum einen nimmt die Trägheit eines
Software-Systems durch den wachsenden Umfang permanent zu. Zum anderen be-
ginnt die Struktur und Architektur des Gesamtsystems durch jede Anforderungsän-
derung zu degenerieren. Für die permanente Anpassung der Software-Architektur
fehlen zu Anfang oft die Zeit und Jahre später das Wissen. Nicht selten werden
provisorische Lösungen ohnehin als adäquater erachtet, denn die nächste Anforde-
7.2 Gründe der Software-Alterung 375
[Link] Fallstudie
An dieser Stelle wollen wir auf ein reales Beispiel zurückgreifen, das zeigen wird,
wie selbst kleine Anforderungsänderungen die Struktur einer ehemals geradlinig
entworfenen Software nachhaltig beschädigen können.
Die besagte Datenstruktur stammt aus einem Programmmodul für die Timing-
Analyse integrierter Hardware-Schaltungen und wird zur Modellierung einer physi-
kalischen Leitungsbahn zwischen zwei oder mehreren Zellen eines Silizium-Chips
eingesetzt. Die Timing-Analyse ist eine von mehreren Phasen, die im Rahmen des
Hardware-Entwurfs standardmäßig durchlaufen werden. Moderne integrierte Schal-
tungen setzen sich heute nicht selten aus mehreren Millionen einzelner Logikzellen
zusammen, die untereinander mit mikroskopisch kleinen Leiterbahnen verbunden
sind. Während der Entwicklung werden die einzelnen Schaltvorgänge simuliert und
jede einzelne Leiterbahn im Simulationsmodell durch ein separates Leitungsnetz
dargestellt.
Die Topologie eines solchen Leitungsnetzes wird mit Hilfe einzelner Knoten mo-
delliert, die über Leitungssegmente miteinander verbunden sind. Jedes Leitungsnetz
besitzt genau einen Eingang, mindestens einen Ausgang und beliebig viele innere
Knoten. Die Netz-Topologie entspricht damit einer klassischen Baumstruktur. Die
Wurzel wird durch den Netzeingang gebildet und die Blätter des Baums entsprechen
den Netzausgängen. Um die physikalischen Eigenschaften präzise zu simulieren,
wird jedem Knoten eine elektrische Kapazität und jedem Leitungssegment ein elek-
trischer Widerstand zugeordnet (vgl. Abb. 7.2). Intern wird jeder Netzknoten durch
eine Instanz der Datenstruktur Node repräsentiert, die im unteren Teil von Abb. 7.2
in C-ähnlicher Schreibweise dargestellt ist. Insgesamt wird die Datenstruktur von 3
verschiedenen Entwicklergruppen eingesetzt, die im Folgenden als Team A, Team
B und Team C bezeichnet werden.
Die Timing-Analyse wird in einem zweistufigen Prozess durchgeführt. Im ersten
Schritt wird die Datenstruktur erzeugt. Hierzu wird die benötigte Topologieinfor-
mation zusammen mit den entsprechenden Kapazitäts- und Widerstandswerten aus
einer externen Datenbank extrahiert und in die oben skizzierte Datenstruktur um-
gesetzt. Im zweiten Schritt wird ein rekursiver Traversierungsalgorithmus gestartet,
der sämtliche Knoten von der Wurzel bis zu den Blättern nacheinander abarbei-
tet. In jedem Schritt werden die Parameter Kapazität und Widerstand des besuchten
376 7 Software-Lebenszyklus
I Netztopologie
C8 C9
Eingang
Innerer Knoten N8
Ausgang R89 N9
R68
C5 C6 C7
N5
R56 N6 R67 N7
C1 R25 C2 C3 C4
N1
R12 N2 R23 N3 R34 N4
I Datenstruktur
node.c
struct Node { 1
double cap; /* Kapazität */ 2
double res; /* Widerstand */ 3
list[Node] suc; /* Liste der Nachfolgeknoten */ 4
} 5
Topologie- Traversierungs-
Start
extraktor algorithmus
Erzeugung Zugriff
Team A
Datenstruktur
zur Netz-
repräsentation
...
Team B Team C
...
C3
R23 N3 R35
C1 C2 C5 C6
R12 R56
N1 N2 N5 N6
R24 N4 R45
C4
node2.c
struct Edge { 1
double res; /* Widerstand */ 2
Node *next; /* Nachfolgeknoten */ 3
} 4
struct Node { 5
double cap; /* Kapazität */ 6
list[Edge] suc; /* Liste der ausgehenden Kanten */ 7
} 8
C3
R35
N3 N 3'
R23
C1 C6
C2 C5
N2 N5
N1 N6
R12 R24 R45 R56
N4
C4
also tun, wenn die Datenstruktur nicht angepasst werden kann? Team A entschei-
det sich kurzerhand für die folgende, als Provisorium geplante Lösung: Anstatt die
Datenstruktur zu ändern, wird die Netztopologie vor der Traversierung in eine Er-
satzdarstellung transformiert. Abb. 7.6 zeigt die entstehende Beispielschaltung für
die rekonvergente Struktur aus Abb. 7.4.
In der Ersatzstruktur wurde der zusätzliche Knoten N3 eingefügt, um genug Platz
zur Speicherung aller Widerstandswerte zu schaffen. Um die Gesamtkapazität des
Netzwerks nicht zu verfälschen, wird dem neuen Knoten die Kapazität 0 zugewie-
sen. Da alle anderen Knoten eine zu 0 verschiedene Kapazität besitzen, hat der Tra-
versierungsalgorithmus zugleich die Möglichkeit, die künstlich eingefügten Knoten
zu erkennen und entsprechend zu behandeln. Stößt der Algorithmus auf einen Kno-
ten mit der künstlichen Kapazität 0, so wird allen Nachfolgekanten, wie im Ersatz-
schaubild gezeichnet, der Widerstand 0 zugewiesen. Durch diesen Kunstgriff wird
es möglich, auch rekonvergente Schaltungen mit der ursprünglichen Datenstruktur
darzustellen. Die notwendigen Änderungen beschränken sich auf die aufgebaute
Netztopologie und den Traversierungsalgorithmus – beide sind unter lokaler Kon-
trolle von Team A und die Änderungen können rechtzeitig in den nächsten Release
integriert werden.
So weit so mäßig – bis sich eines Tages die Anforderungen an den Algorithmus
zum zweiten Mal ändern. Diesmal äußert die Kundenseite den Wunsch, auch Netze
mit mehreren Eingängen zu unterstützen (multi-driver nets). Eine entsprechende
Netztopologie mit zwei Eingängen ist in Abb. 7.7 dargestellt. In diesem Beispiel
können die Knoten N1 und N4 wechselweise als Ein- oder Ausgang fungieren.
Enthält ein Netz n Eingänge, so muss der Traversierungsalgorithmus n mal hin-
tereinander ausgeführt werden. In jeder Iteration wird einer der Eingänge als Ein-
gang belassen und alle anderen temporär zu Ausgängen erklärt. Jetzt wird der Auf-
bau unserer Datenstruktur vollends zum Problem, da die Widerstandswerte in Ab-
hängigkeit des aktiven Eingangs in jeweils anderen Knoten abgelegt werden müs-
380 7 Software-Lebenszyklus
C4 C5 R68 C6 C4 C5 R68 C6
R25 R25
N1 N2 N3 N1 N2 N3
C1 C2 C3 C1 C2 C3
sen. Die linke bzw. rechte Seite von Abb. 7.7 zeigt die Inhalte aller Datenfelder, falls
N4 bzw. N1 als Eingang gewählt wird.
Da die Änderung der zugrunde liegenden Datenstruktur immer noch nicht zur
Disposition steht, löst Team A das Problem durch eine Erweiterung des Traversie-
rungsalgorithmus. In Abhängigkeit des Eingangsknotens werden im Rahmen einer
Vorverarbeitung zunächst die Widerstandswerte an die richtige Stelle verschoben
und im Anschluss daran die Traversierung gestartet. Spätestens jetzt ist es höchste
Zeit, die Notbremse zu ziehen und die Grundstruktur vollständig neu zu überdenken.
Ich hoffe, es ist mir gelungen, dem sprichwörtlichen Sand im Getriebe alternder
Software mit diesem Beispiel ein Gesicht zu geben. Wie bereits erwähnt, stammt
die beschriebene Datenstruktur aus der Praxis und ist in ähnlicher Form heute noch
in Verwendung – wann hier die Notbremse gezogen wird, steht allerdings in den
Sternen.
I Das Originalprogramm...
div_zero.c
if (x != 0) 1
if (y != 0) 2
z = (1/x) + (1/y); 3
div_zero1.c div_zero2.c
if (x != 0) { 1 if (x != 0) { 1
if (y != 0) { 2 if (y != 0) { 2
z = (1/x) + (1/y); 3 z = (1/x) + (1/y); 3
} 4 } else { 4
} else { 5 error(); 5
error(); 6 } 6
} 7 } 7
I Typische Industrielösungen...
div_zero3.c div_zero4.c
if (x != 0) 1 if (x != 0) 1
if (y != 0) 2 if (y != 0) 2
z = (1/x) + (1/y); 3 z = (1/x) + (1/y); 3
else error(); 4 if (x == 0 || y == 0) 4
else error(); 5 error(); 5
I Korrekte Lösungen...
div_zero5.c div_zero6.c
if (x != 0 && y != 0) { 1 if (x == 0 || y == 0) { 1
z = (1/x) + (1/y); 2 error(); 2
} else { 3 } else { 3
error(); 4 z = (1/x) + (1/y); 4
} 5 } 5
Abb. 7.8 Die geforderte Programmmodifikation lässt sich nur durch die Anpassung des bestehen-
den Codes sauber implementieren
der Wartbarkeit aus. Auch wenn das hier vorgestellte Beispiel winzig ist und noch
voll kontrollierbar erscheint: Die Erstarrung der bestehenden Struktur – sei es aus
mangelnder Transparenz oder aus anderen Gründen – ist in vielen Fällen der Anfang
vom Ende eines alternden Software-Systems.
Die Programme div_zero5.c und div_zero6.c zeigen, wie sich das Pro-
blem auf saubere Weise lösen lässt. Durch eine geeignete Umstrukturierung der
ursprünglichen Programmstruktur kommen beide Implementierungen ohne eine
Code-Verdopplung oder sonstige Kunstgriffe aus.
[Link] Schnittstellen
Eine weitere Alterserscheinung häufig geänderter Quelltexte ist das empirisch
messbare Anwachsen der internen Modulschnittstellen. Mit der Anzahl der Schnitt-
stellen steigt in gleichem Maße die Anzahl der Aufrufebenen, die eine Funkti-
on durchschnittlich durchläuft, bis die eigentliche Aktion ausgeführt wird. Das in
Abb. 7.9 skizzierte Szenario versucht zu demonstrieren, wie solche Strukturen in
der Praxis entstehen. Den Kern bildet ein beliebiges Programmmodul, das zunächst
von einem einzigen Client verwendet wird. Die Aufrufe erfolgen nicht direkt in das
Modul hinein, sondern über eine wohldefinierte API (application programming in-
terface), die das Modul vollständig umkapselt. Bis hierhin entspricht das Szenario
dem Stand der Software-Technik. Ob Client-Code und Modul objektorientiert, pro-
zedural oder funktional programmiert sind, spielt für die Betrachtung keine Rolle.
Wir nehmen weiter an, dass das Modul schon ein paar Jahre existiert, die
Schnittstelle wenig dokumentiert und die Bedeutungen der bereitgestellten Funk-
tionen nicht unmittelbar einsichtig sind. Weiterhin stellen wir uns einen Software-
Entwickler mit der fiktiven Maßgabe vor, das besagte Modul für die Lösung seiner
eigenen Problemstellung einzusetzen. Bei der ersten Durchsicht der zur Verfügung
stehenden Unterlagen kommen die folgenden Funktionen zum Vorschein, die auf-
grund ihres Namens einen inhaltlichen Bezug zu der benötigten Funktionalität auf-
weisen:
int get_time2();
int get_date_and_time();
int get_precision_time();
An dieser Stelle sei mir ein kurzer Kommentar über die Namensgebung gestattet, da
es dem einen oder anderen Leser bei der Betrachtung der aufgelisteten Bezeichner
sicher kalt den Rücken herunter laufen wird. Unbestritten ist eine Namensgebung
dieser Art nicht nur unverständlich, sondern auch in höchstem Maße inkonsequent.
Trotzdem entwickeln viele Programmierer eine Vorliebe für genau diese Art von
Bezeichnern. Dabei spielt es kaum eine Rolle, ob es sich bei den untersuchten Pro-
grammtexten um die Lösung einer studentischen Laboraufgabe oder die Quellen ei-
nes industriellen Software-Projekts handelt. Hinter den Kulissen von typischem In-
dustriecode spielt sich oft weit mehr ab, als es die polierten grafischen Benutzungs-
oberflächen nach außen hin vermuten lassen. So finden sich in den internen System-
funktionen von Windows 3.1 die folgenden Bezeichner wieder: BEAR35, BUNNY73,
384 7 Software-Lebenszyklus
Client-
Algorith-
Code
mus
Schnittstelle
Alterung
Client-
Code
Client-
Algorith-
Code
mus
Schnittstelle
Schnittstelle Client-
Code
Schnittstelle
Abb. 7.9 Die zwiebelschalenartige Entwicklung interner Schnittstellen ist typisch für viele lang-
lebige Software-Systeme
PIGLET12. Neben PIGLET12, das es bis in die API von Windows 95 geschafft hat,
finden sich dort unter anderem die folgenden Funktionen wieder: BOZOSLIVEHERE,
TABTHETEXTOUTFORWIMPS. Die Liste solcher Namen ließe sich beliebig ergänzen.
Diversen Blog-Einträgen ist zu entnehmen, dass die verantwortlichen Entwickler
durchaus Spaß bei der Namensgebung hatten – eine gewisse Kreativität und Humor
sind den Urhebern sicher nicht abzusprechen. Ob die betroffenen Kunden in An-
betracht der gelieferten Produktqualität den Humor teilen, darf an dieser Stelle mit
Fug und Recht bezweifelt werden.
Kommen wir zurück zu unserem Beispiel. In der Zwischenzeit hat unser Ent-
wickler einige der Funktionen ausgetestet, jedoch schnell festgestellt, dass das Ver-
halten weder transparent ist, noch dass es eine Funktion gibt, die den anvisierten
Zweck exakt erfüllt. Die Schnittstelle ist schon ein paar Jahre alt, der damalige
Programmierer arbeitet mittlerweile für die Konkurrenz und auf eine entsprechen-
de Dokumentation wurde aus Zeitgründen verzichtet. Unser Programmierer ist auf
sich selbst gestellt und findet nach einiger Zeit heraus, dass die folgende Code-
Kombination für ihn funktioniert:
7.2 Gründe der Software-Alterung 385
Da er die Code-Sequenz nicht nur an einer Stelle des neuen Client-Codes benö-
tigt, steht er vor einem neuen Problem. Eine bereits weiter oben als Risiko ent-
larvte Code-Verdopplung möchte er um jeden Preis vermeiden, also entscheidet er
sich für die Kapselung seiner Code-Sequenz in eine neue Funktion mit dem Namen
get_time_safe. Auf diese Weise entsteht nach und nach eine neue, auf der alten
aufbauende Schnittstellenschicht. Leider hat unser Programmierer keine Zeit, die
Funktion zu dokumentieren – ein gutes Gefühl hat er bei seiner Lösung sowieso
nicht.
Mit der Zeit setzt sich dieser Prozess fort. Der nächste Programmierer, der mit
seinem Client-Code an unser Modul andockt, findet jetzt zwei Schnittstellen vor. Es
dauert nicht lange, bis eine Dritte entsteht und so nimmt das Unheil seinen Lauf.
Insgesamt bildet sich mit der Zeit eine Code-Struktur heraus, wie sie in der unteren
Hälfte von Abb. 7.9 dargestellt ist. Den einen oder anderen Leser mag die alters-
bedingte Entwicklung des Schnittstellencodes an die Struktur von Zwiebelschalen
oder die Jahresringe von Baumstämmen erinnern. Der Vergleich ist angebracht und
trifft den Kern des Phänomens auf den Punkt.
Eine Funktion, die, wie oben, im Grunde genommen nur ihre Parameter an eine
oder mehrere Unterfunktionen durchreicht, wird landläufig als Wrapper-Funktion
bezeichnet. In alternden Software-Systemen liegt die Anzahl der neu eingeführten
Aufrufebenen mitunter im zweistelligen Bereich. Die Folgen sind allgegenwärtig:
Neben der Vernichtung der Transparenz sind spürbare Laufzeitverschlechterungen
die Folge. In fast allen alternden Software-Systemen nimmt der prozentuale Anteil
der Wrapper-Funktionen kontinuierlich zu und ist ein gutes Maß für die Messung
des inneren Alters eines Produkts.
der Materie Software verborgen: Software ist inhärent unsichtbar. Die Leistung ei-
nes Programmierers wird dadurch fast ausschließlich an den externen Qualitätsfak-
toren gemessen – im Falle einer Fehlerkorrektur also schlicht an der Tatsache, ob
das Programm anschließend korrekt arbeitet oder nicht. Ob die Code-Struktur unter
dem Eingriff nachhaltig leidet, kann oder will die Managementseite in den meisten
Fällen nicht adäquat beurteilen.
Anhand des in Abb. 7.10 dargestellten Programmbeispiels soll demonstriert wer-
den, wie klassische Kaschierungseffekte in der Praxis entstehen. Das herangezoge-
ne Code-Fragment stammt erneut aus industriellem Programmcode und wurde aus
Gründen der Übersichtlichkeit in vereinfachter Form dargestellt. Das Programm de-
finiert die Funktion first_char_up, die einen String entgegennimmt und das erste
Zeichen in einen Großbuchstaben wandelt. Das Ergebnis wird als Rückgabewert zu-
rückgeliefert. Damit die Funktion ein korrektes Ergebnis berechnet, muss der über-
gebene String einige Randbedingungen erfüllen. Insbesondere darf der Wert kein
Null-Pointer sein und die referenzierte Zeichenkette muss mindestens ein Zeichen
beinhalten. Um die Betrachtung an dieser Stelle nicht zu verkomplizieren, wollen
wir alle Randbedingungen als erfüllt erachten.
Aufgerufen wird die Funktion in den Programmen first_char_up1.c und
first_char_up2.c. Bei genauerer Betrachtung des Client-Codes stellt sich
schnell heraus, dass keine der beiden Varianten korrekt arbeitet. Das Programm
first_char_up1.c gibt den String abc nach dem Aufruf stets frei. Beginnt s mit
einem Kleinbuchstaben, ist die Freigabe korrekt, da die Funktion first_char_up
intern eine Kopie anlegt und der von s referenzierte String nach dem Löschen von
abc immer noch intakt ist. Beginnt s mit einem Großbuchstaben, verzichtet die
Funktion first_char_up auf die Anfertigung eines Duplikats und der free-Befehl
gibt den von s belegten Speicher frei. Ein Programmabsturz ist die wahrscheinliche
Folge, sollte auf den String später erneut zugegriffen werden. Die Programmvariante
first_char_up2.c ist unwesentlich besser, da die dynamisch belegten Speicher-
bereiche überhaupt nicht mehr freigegeben werden. Programmabstürze werden jetzt
effektiv vermieden, allerdings füllt jeder Aufruf der Funktion nach und nach den
Speicher auf.
Offenbar ist die Funktion first_char_up nicht sehr intelligent programmiert.
Ob eine Referenz auf ein String-Duplikat oder den String selbst zurückgeliefert
wird, hängt vom Inhalt des Übergabeparameters ab. Wir haben es mit einem Feh-
ler zu tun, der geradezu nach weiteren Fehlern ruft. Die augenscheinlich einfachste
Lösung bestünde in der Korrektur der Funktion first_char_up. Für den Moment
wollen wir uns dem durchaus realistischen Szenario widmen, dass die Änderung
der Funktion nicht in Frage kommt. Der Code könnte in Form einer Bibliothek
hinzugekauft sein und der Quelltext hierdurch nicht zur Verfügung stehen. Wie ty-
pische Industrielösungen in diesem Fall aussehen können, zeigen die Programme
first_char_up3.c und first_char_up4.c.
Der in first_char_up3.c gewählte Weg ist der vermeintlich sicherste. Alle von
einer externen Funktion zurückgelieferten Strings werden dupliziert und damit ef-
fektiv in eine private Kopie gewandelt. Auf die Freigabe der Strings wird schlicht
verzichtet. Lösungen dieser Art sind nicht selten, da auch Software-Entwickler die
7.2 Gründe der Software-Alterung 387
I Das Originalprogramm...
first_char_up.c
char *first_char_up(char *str) { 1
if (islower(str[0])) { 2
str = strdup(str); 3
str[0] += (’A’-’a’); 4
} 5
return str; 6
} 7
first_char_up1.c first_char_up2.c
void client_code(char *s) 1 void client_code(char *s) 1
{ 2 { 2
char *abc; 3 char *abc; 3
4 4
abc = first_char_up(s); 5 abc = first_char_up(s); 5
/* stuff */ 6 /* stuff */ 6
free(abc); 7 /* never free */ 7
} 8 } 8
I Typische Industrielösungen...
first_char_up3.c first_char_up4.c
void client_code(char *s) 1 void client_code(char *s) 1
{ 2 { 2
char *abc; 3 char *abc; 3
4 4
abc = 5 abc = first_char_up(s); 5
strdup(first_char_up(s)); 6 /* stuff */ 6
/* stuff */ 7 7
} 8 if (islower(s[0])) { 8
9 free(abc); 9
10 } 10
11 } 11
Kunst der Risikoabschätzung beherrschen. Ein free zu viel kann fatale Folgen für
die Software-Stabilität nach sich ziehen. Ein free zu wenig wirkt sich dagegen
kaum auf deren Funktionalität aus. Das einhergehende Speicherproblem ist höchst-
wahrscheinlich eines von vielen und fällt in größeren Projekten noch nicht einmal
auf.
388 7 Software-Lebenszyklus
abgelegte Objekt einen Referenzzähler (reference counter). Jeder neue Verweis er-
höht den Zähler des referenzierten Objekts um eins. Geht ein Verweis z. B. durch
das Verlassen eines Unterprogramms verloren, reduziert sich der Referenzzähler um
den gleichen Betrag. Sobald der Zählerstand den Wert 0 erreicht, wird der entspre-
chende Speicherbereich von der Laufzeitumgebung markiert und zu einem späteren
Zeitpunkt durch den Garbage-Collector freigegeben.
7.2.4 Rückwärtskompatibilität
An der kompletten Neuentwicklung eines Produkts beteiligt zu sein ist der Traum
vieler Software-Entwickler. Anforderungen können ohne Rücksicht auf bestehen-
de Strukturen umgesetzt und neue Ideen nach Belieben eingebracht werden. Kurz-
um: Der Rücken des Software-Entwicklers ist frei von Altlasten, die an jedmögli-
cher Stelle das Programmiererdasein erschweren. Neben den technischen Hürden,
die eine bereits existierende Code-Basis aufbaut, erschweren insbesondere die Ver-
träglichkeitsanforderungen mit früheren Produktversionen das tägliche Arbeitsle-
ben. Die Forderung der weitgehenden Rückwärtskompatibilität führt insbesondere
zwischen Marketing und Verkauf auf der einen Seite und der Entwicklung auf der
anderen Seite zu einem Dissens.
I Entwicklung
Aus der Sicht der Entwicklungsabteilung ist die Rückwärtskompatibilität eines
der größten Hemmnisse in der Evolution eines Software-Systems. Die Fokussie-
rung auf frühere Produktversionen wirkt der Umsetzung neuer Ideen entgegen
und kann die innovative Weiterentwicklung im Extremfall vollständig zum Er-
liegen bringen.
Um eine weitreichende Rückwärtskompatibilität zu gewährleisten, muss ein
Großteil der existierenden Code-Basis beibehalten werden. Schlimmer wiegt,
dass sich neue Ideen nur in dem Umfang realisieren lassen, wie sie mit den
bestehenden Konzepten im Einklang stehen. Die Rückwärtskompatibilität geht
in manchen Fällen sogar so weit, dass augenscheinliche Fehler nicht korrigiert
390 7 Software-Lebenszyklus
d3dmatrix.h
typedef struct _D3DMATRIX { 1
union { 2
struct { 3
float _11, _12, _13, _14; 4
float _21, _22, _23, _24; 5
float _31, _32, _33, _34; 6
float _41, _42, _43, _44; 7
}; 8
float m[4][4]; 9
}; 10
} D3DMATRIX; 11
Was also tun? Beide Seiten verfügen über legitime Argumente für bzw. gegen die
Rückwärtskompatibilität. Die Antwort auf diese Frage ist nicht einfach und eine
pauschale Lösung im Grunde genommen unmöglich. In der Praxis ist der Umfang
der Rückwärtskompatibilität eine Gratwanderung, die sich nur von Fall zu Fall, mit
gesundem Menschenverstand und einer gehörigen Portion Pragmatismus beantwor-
ten lässt. Wir tun gut daran, denjenigen Management- und Entwicklungsprozessen,
die eine einfache Antwort auf diese Problematik versprechen, mit Vorsicht zu be-
gegnen.
Welche Folgen die strikte Einhaltung der Rückwärtskompatibilität für die Code-
Basis haben kann, demonstriert die Grafikschnittstelle Direct3D auf eindringliche
Weise. Direct3D ist ein zentraler Bestandteil von DirectX und die am häufigsten
eingesetzte Grafikschnittstelle unter Microsoft Windows. Insbesondere basieren die
meisten der heute veröffentlichten PC-Spiele auf dieser API. Unter anderem stellt
Direct3D mit D3DMATRIX eine Datenstruktur zur Verfügung, die zur Speicherung
vierdimensionaler Matrizen eingesetzt werden kann. Intensiv genutzt werden solche
Matrizen für die Geometrietransformation dreidimensionaler Objektszenarien.
Abb. 7.11 zeigt das Grundgerüst der Datenstruktur D3DMATRIX. Im Kern befindet
sich eine konventionelle C-Struktur (struct), die sich aus den 16 Matrix-Elementen
_11,. . .,_44 zusammensetzt. Auf der obersten Ebene wird diese zusammen mit ei-
nem 4 × 4-Array in eine Parallelstruktur (union) eingebettet. Im Gegensatz zu einer
herkömmlichen C-Struktur werden die einzelnen Elemente einer Parallelstruktur
nicht hintereinander, sondern überlappend gespeichert. Folgerichtig referenzieren
beispielsweise das Array-Element m[0][0] und das Strukturelement _11 exakt den
gleichen Speicherbereich. Auf diese Weise entstehen zwei verschiedene Schnittstel-
len, um auf die gleichen Matrixelemente zuzugreifen.
7.2 Gründe der Software-Alterung 391
Abb. 7.12 zeigt eine der Umsetzungen der Datenstruktur in DirectX (Version
9.0). Der Zahn der Zeit ist hier kaum zu übersehen. Zahlreiche versionsprüfende
Präprozessorkonstrukte (#ifdef) durchziehen den gesamten Code. Vollends wird
die Code-Transparenz durch den Versuch zerstört, mit weiteren Präprozessordirek-
tiven eine separate Version für die Sprachen C und C++ aus demselben Programm-
code zu erzeugen. In gewissen Grenzen lassen sich die Folgen für die Weiterent-
wicklung sogar quantitativ erfassen: Gehen Sie hierzu die abgebildeten Datenstruk-
turen noch einmal durch und vergleichen Sie die beiden Zeitspannen, die Sie zum
Verständnis der ursprünglichen Struktur aus Abb. 7.11 und der gealterten Struk-
tur aus Listung 7.12 benötigt haben. Das Verhältnis zwischen beiden Werten gibt
Ihnen einen realistischen Eindruck, wie das Phänomen der Software-Alterung den
Entwicklungsprozess nachhaltig lähmt.
Mitunter verlässt die Jagd nach der vollständigen Rückwärtskompatibilität auch
den Boden der Vernunft. Welche Blüten die konservative Produktausrichtung mit-
unter treibt, wird an dem folgenden Beispiel von Joel Spolsky besonders deutlich:
“I first heard about this from one of the developers of the hit game SimCity,
who told me that there was a critical bug in his application: it used memo-
ry right after freeing it, a major no-no that happened to work OK on DOS
but would not work under Windows where memory that is freed is likely
to be snatched up by another running application right away. The testers
on the Windows team were going through various popular applications, te-
sting them to make sure they worked OK, but SimCity kept crashing. They
reported this to the Windows developers, who disassembled SimCity, step-
ped through it in a debugger, found the bug, and added special code that
checked if SimCity was running, and if it did, ran the memory allocator in
a special mode in which you could still use memory after freeing it.”
Joel Spolsky [237]
Das Beispiel bringt erneut zwei Aspekte alternder Software klar zum Vorschein.
Zum einen zeigt es wie kaum ein anderes, dass sich überhöhte Kompatibilitätsan-
forderungen in vielen Fällen nur auf Kosten der Code-Qualität umsetzen lassen.
Zum anderen macht es deutlich, dass viele Software-Fehler eine nahezu infektiöse
Wirkung besitzen.
d3dtypes.h
#ifndef D3DMATRIX_DEFINED 1
typedef struct _D3DMATRIX { 2
#if(DIRECT3D_VERSION >= 0x0500) 3
#if (defined __cplusplus) && (defined D3D_OVERLOADS) 4
union { 5
struct { 6
#endif 7
#endif /* DIRECT3D_VERSION >= 0x0500 */ 8
D3DVALUE _11, _12, _13, _14; 9
D3DVALUE _21, _22, _23, _24; 10
D3DVALUE _31, _32, _33, _34; 11
D3DVALUE _41, _42, _43, _44; 12
#if(DIRECT3D_VERSION >= 0x0500) 13
#if (defined __cplusplus) && (defined D3D_OVERLOADS) 14
}; 15
D3DVALUE m[4][4]; 16
}; 17
_D3DMATRIX() { } 18
_D3DMATRIX( 19
D3DVALUE _m00, D3DVALUE _m01, D3DVALUE _m02, D3DVALUE _m03, 20
D3DVALUE _m10, D3DVALUE _m11, D3DVALUE _m12, D3DVALUE _m13, 21
D3DVALUE _m20, D3DVALUE _m21, D3DVALUE _m22, D3DVALUE _m23, 22
D3DVALUE _m30, D3DVALUE _m31, D3DVALUE _m32, D3DVALUE _m33 23
) 24
{ 25
m[0][0]=_m00; m[0][1]=_m01; m[0][2]=_m02; m[0][3]=_m03; 26
m[1][0]=_m10; m[1][1]=_m11; m[1][2]=_m12; m[1][3]=_m13; 27
m[2][0]=_m20; m[2][1]=_m21; m[2][2]=_m22; m[2][3]=_m23; 28
m[3][0]=_m30; m[3][1]=_m31; m[3][2 =_m32; m[3][3]=_m33; 29
} 30
D3DVALUE& operator()(int iRow, int iColumn) 31
{ return m[iRow][iColumn]; } 32
const D3DVALUE& operator()(int iRow, int iColumn) const 33
{ return m[iRow][iColumn]; } 34
#if(DIRECT3D_VERSION >= 0x0600) 35
friend _D3DMATRIX operator* 36
(const _D3DMATRIX&, const _D3DMATRIX&); 37
#endif /* DIRECT3D_VERSION >= 0x0600 */ 38
#endif 39
#endif /* DIRECT3D_VERSION >= 0x0500 */ 40
} D3DMATRIX; 41
#define D3DMATRIX_DEFINED 42
#endif 43
I Linux-Kernel
LOC Linux 3.6
14 × 106
12 × 106
10 × 106
8 × 106
Linux 0.01
Linux 2.0
6 × 106
Linux 1.2
Linux 1.0
Linux 2.6
4 × 106 Linux 2.4
2 × 106
Linux 2.2
0
1990
1992
1994
1996
1998
2000
2002
2004
2006
2008
2010
2012
Jahr Version Code-Zeilen Jahr Version Code-Zeilen
1991 v0.01 10,239 1999 v2.2 1,800,847
1994 v1.0 176,250 2001 v2.4 3,377,902
1995 v1.2 310,950 2003 v2.6 5,929,913
1996 v2.0 777,956 2012 v3.6 15,868,204
I Windows
LOC
45 × 106
Win XP
40 × 106
35 × 106
30 × 106 Win 2000
25 × 106
20 × 106
15 × 106 Win NT
Win 3.1 Win 98
10 × 106
5 × 10 6 Win 95
0
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
Abb. 7.13 Entwicklung der Quelltextgröße der Betriebssysteme Windows und Linux
schätzte Code-Umfang des lange Zeit beliebten Windows 2000 liegt in der Größen-
ordnung von 30 bis 60 Millionen Zeilen Code. Was für den Optimisten wie Musik
in seinen Ohren klingen mag, treibt so manchem Realisten die Sorgenfalten auf die
Stirn: Mit der Anzahl der Code-Zeilen steigt unweigerlich auch die Anzahl der Pro-
grammfehler – wenn auch nicht proportional.
Mit steigendem Code-Volumen verlagert sich die Arbeit eines einzelnen Ent-
wicklers schleichend zu einer immer punktueller werdenden Tätigkeit. Bereits in
mittelgroßen Software-Projekten sind die Programmquellen für einen einzelnen
Programmierer als einheitliche Entität nicht mehr fassbar. Mit anderen Worten: Die
Code-Größe erscheint unendlich. Streng genommen macht es für einen einzelnen
Entwickler dann keinen Unterschied mehr, ob die Quelltexte aus 30 Millionen, 60
Millionen oder 120 Millionen Programmzeilen bestehen. Für das Zusammenspiel
der punktuell erarbeiteten Komponenten ist die Code-Größe jedoch sehr wohl von
Bedeutung.
Der PC-Magazine-Kolumnist John Dvorak beschreibt die Situation des bereits
erwähnten Betriebssystems Windows 2000 in der April-Ausgabe 1999 wie folgt:
“With a base of 50 to 60 million lines of code that obviously nobody has
a grip on, there has to be an intense feeling of panic within the company.
Silverberg, who brought out Windows 95, is seen as a savior, but he must
know the reality of the situation too. I’ve talked to many ex-Microsoft folk
who all tell me that nobody has a handle on Window’s code. It’s comple-
tely out of control - a hodgepodge of objects and subsystems nobody fully
understands.”
John Dvorak [76]
Was John Dvorak hier beschreibt, trifft die Emotionen fast aller an großen Software-
Projekten mitwirkenden Entwickler auf den Punkt. Mit steigender Code-Größe stellt
sich zunehmend ein Gefühl der Unsicherheit ein. Schleichend verliert der Program-
mierer die Fäden aus der Hand und die Kontrolle über das einstmals so gut verstan-
dene System schwindet.
Allen Spöttern zum Trotz wurde das in der Presse viel gescholtene Windows
2000 zum Erfolg. Noch mehrere Jahre nach dem Erscheinen des Nachfolgers XP
war Windows 2000 in größeren Firmen das am häufigsten eingesetzte Betriebssys-
tem und wurde dort für seine vergleichsweise hohe Stabilität geschätzt.
Software-Großprojekte sind für sich gesehen nichts Neues und seit den frühen
Tagen der Computertechnik allgegenwärtig. Bereits in den Sechzigerjahren arbei-
teten zu Spitzenzeiten mehr als 1000 Mitarbeiter an dem von IBM entwickelten
Betriebssystem OS/360. Insgesamt wurden zwischen 1963 und 1966 ca. 5000 Mit-
arbeiterjahre in Entwurf, Implementierung und Dokumentation investiert [90].
Völlig unterschiedlich erweist sich hingegen die Art und Weise, mit der Projek-
te dieser Größenordnung mittlerweile durchgeführt werden. Im Gegensatz zu Da-
mals können die verschiedenen Entwicklergruppen heute weltweit verteilt agieren.
Mit Hilfe moderner Kommunikationsmedien lässt sich der zwingend erforderliche
Informationsaustausch zumindest aus technischer Sicht weitgehend beherrschen.
Insbesondere versetzen Internet-basierte Versionskontrollsysteme (Abschnitt 8.1)
7.3 Ist die Software-Alterung unumgänglich? 395
die Software-Entwickler in die Lage, ein und dieselbe Code-Basis von beliebigen
Standorten in der Welt gleichzeitig zu bearbeiten. Die technischen Möglichkeiten
sind verlockend und sicher einer der Gründe, warum die weltweite Dezentralisie-
rung im Bereich der Software-Entwicklung in der Vergangenheit so stark vorange-
trieben wurde. Die spürbaren Auswirkungen sind jedoch nicht nur positiver Natur,
schließlich geht mit der Dezentralisierung der Arbeitskräfte auch die Dezentralisie-
rung des projektspezifischen Know-hows einher.
Neben der steigenden Code-Größe wird mit zunehmender Lebensdauer auch die
Mitarbeiterfluktuation zu einer kontinuierlich wachsenden Gefahr für die Quali-
tät großer Software-Systeme. Besonders hart traf es in diesem Zusammenhang die
im kalifornischen Silicon Valley angesiedelten Software-Unternehmen inmitten des
[Link]-Booms. Zu dieser Zeit lag die employee turnover rate bei ca. 30 % pro
Jahr. Mit anderen Worten: Jeder Software-Entwickler wechselte im Durchschnitt
alle drei Jahre seinen Arbeitgeber. Die betroffenen Unternehmen waren durch die
hohe Fluktuation gezwungen, jedes Jahr rund ein Drittel der Belegschaft durch neue
Mitarbeiter zu ersetzen. Die Zahlen haben nicht nur dramatische Auswirkungen auf
die Kosten, sondern auch auf das immaterielle Wissen einer Firma. Für langlebi-
ge Software-Produkte sind die Auswirkungen besonders dramatisch: Legen wir die
durchschnittliche Verweilzeit eines Software-Entwicklers von drei Jahren zugrun-
de, arbeitet an einer 10 Jahre alten Software bereits die vierte Entwicklergeneration.
Mit jeder Generation geht ein bedeutender Teil des angesammelten Know-hows un-
wiederbringlich verloren.
Die Auswirkungen des Wissensverlusts sind in unzähligen Entwicklungsabtei-
lungen deutlich zu spüren. Mit zunehmender Lebensdauer eines Software-Systems
verbringen Programmierer mehr und mehr Zeit damit, das Verhalten der Imple-
mentierung zu erkunden. Für einen Entwickler der vierten oder fünften Generati-
on macht es faktisch kaum noch einen Unterschied, ob es sich bei den untersuchten
Quelltexten um den hausinternen Programmcode oder um eine Implementierung der
Konkurrenz handelt.
I Sensibilität
Eine Grundvoraussetzung, um den Alterungsprozess aufzuhalten, ist die Er-
kenntnis, dass die internen Qualitätsparameter von Software einen Wert an sich
darstellen. Nur wenn es gelingt, Entwicklung und Management gleichermaßen in
dieser Hinsicht zu sensibilisieren, besteht die Chance, die Alterung maßgeblich
zu verzögern. Kosten- und Termindruck sorgen in der Praxis regelmäßig dafür,
dass die externen und die internen Qualitätsparameter gegeneinander abgewogen
396 7 Software-Lebenszyklus
werden müssen. Wer hier permanent auf die für den kurzfristigen Erfolg wich-
tigen externen Qualitätsparameter setzt und die für die langfristige Entwicklung
essentiellen inneren Qualitätsparameter vernachlässigt, stellt frühzeitig die Wei-
chen für eine schnelle Degeneration.
I Investitionsbereitschaft
Die Instandhaltung der inneren Struktur eines Software-Systems ist mit einem
hohen Ressourcen- und Kostenaufwand verbunden. Obwohl bereits in frühen
Produktstadien mit entsprechenden Investitionen begonnen werden muss, zah-
len sich diese erst auf lange Sicht aus. Für viele erscheint die Code-Pflege als
subjektiver Kostentreiber und führt nicht selten zu einer deutlichen Dämpfung
der Bemühungen von Seiten des Managements. Wer investiert schon gerne in die
langfristige Zukunft eines Produkts, wenn heute nicht einmal feststeht, ob dieses
jemals die Entwicklungsabteilung verlassen wird? Andererseits ist die Zukunft
eines Software-Systems früh besiegelt, wenn die inneren Qualitätsparameter von
Beginn an außer Acht gelassen werden.
In den folgenden Abschnitten werden einige der Methoden und Techniken vorge-
stellt, mit deren Hilfe die Lebensdauer von Software deutlich verlängert bzw. der
Alterungsprozess mitunter sogar vollständig gestoppt werden kann – die hierfür not-
wendige Sensibilität und Investitionsbereitschaft vorausgesetzt.
7.3.1 Refactoring
Der Begriff Refactoring bezeichnet die Erneuerung der inneren Strukturen eines
Software-Systems, ohne sein äußeres Verhalten zu verändern. Den einen oder ande-
ren Leser mag diese Beschreibung an klassische Code-cleanups erinnern, die von
den meisten Programmierern in unregelmäßigen Abständen durchgeführt werden.
Die Bedeutung des Refactorings geht jedoch weit darüber hinaus und versteht sich
selbst als das Kernelement einer eigenständigen Programmierphilosophie.
Die Ersten, die das Refactoring mit Nachdruck als Programmierprinzip bewar-
ben, waren Kent Beck und Howard Cunningham. Beide kamen in den Achtziger-
jahren mit der Programmiersprache Smalltalk in Kontakt, die unter anderem durch
extrem kurze Edit-Compile-Run-Zyklen hervorsticht. Die Art und Weise, wie in
der Praxis mit dieser Sprache gearbeitet wird, passte so gar nicht zu der traditio-
nellen Lehre der Software-Technik, die den Entwurf und die Implementierung als
konsekutive Stufen definiert. Permanente Entwurfsänderungen während der Imple-
mentierungsphase widersprechen dem klassischen Vorgehen auf eklatante Weise. In
der Abkehr von der traditionellen Methodik propagierte Kent ein agiles Vorgehens-
modell, das unter dem Namen Extreme Programming, kurz XP, bekannt wurde (vgl.
Abschnitt 9.1.4). Refactoring ist eine tragende Säule der XP-Philosophie.
Bekannt wurde die Refactoring-Technik durch das gleichnamige Buch von Mar-
tin Fowler. Ähnlich dem Prinzip der Entwurfsmuster enthält das Werk einen Ka-
talog verschiedener Refactorings, die besonders fehlerträchtige oder intransparente
Code-Strukturen beschreiben [98]. Für jeden dieser Smells präsentiert Fowler einen
7.3 Ist die Software-Alterung unumgänglich? 397
[Link]
import [Link]; 1
2
public class Vertex3D { 3
4
double x, y, z; 5
6
public static Vertex3D computeNormal 7
(Vertex3D v1, Vertex3D v2, Vertex3D v3) { 8
Vertex3D e1, e2, n; 9
double l; 10
11
e1 = new Vertex3D(); 12
e1.x = v2.x - v1.x; 13
e1.y = v2.y - v1.y; 14
e1.z = v2.z - v1.z; 15
16
e2 = new Vertex3D(); 17
e2.x = v2.x - v3.x; 18
e2.y = v2.y - v3.y; 19
e2.z = v2.z - v3.z; 20
21
n = new Vertex3D(); 22
n.x = e1.y*e2.z - e1.z*e2.y; 23
n.y = e1.z*e2.x - e1.x*e2.z; 24
n.z = e1.x*e2.y - e1.y*e2.x; 25
26
l = n.x*n.x +n.y*n.y + n.z*n.z; 27
l = [Link](l); 28
n.x /= l; 29
n.y /= l; 30
n.z /= l; 31
32
return n; 33
} 34
} 35
Flächenorthogonale
n = e1 e2
Flächennormale
e1=v2-v1 e e2
n= 1
v1=(x1,y1,z1) | e1 e2 |
v2=(x2,y2,z2) e2=v2-v3
y
v3=(x3,y3,z3)
Ob die Erzeugung von Untermethoden sinnvoll ist oder nicht, hängt nicht aus-
schließlich von der Implementierungslänge ab. Lassen sich z. B. keine Fragmente
identifizieren, die einen klar umrissenen und in sich abgeschlossenen Funktionsum-
fang aufweisen, wirkt sich die Ausgliederung einzelner Teile eher kontraproduktiv
aus.
Vielleicht ist Ihnen beim Vergleich beider Programmvarianten aufgefallen, dass
die neu erzeugte Implementierung einen gravierenden Fehler der alten korrigiert.
Im Zuge der Längenbestimmung der Flächenorthogonalen verzichtet die ursprüng-
liche Methode darauf, den Nullvektor gesondert zu behandeln. In diesem Fall führt
die anschließend durchgeführte Skalierung zwangsläufig zu einer Division durch
null. In der Tat fällt der Fehler in der Originalimplementierung aufgrund der nied-
rigen Transparenz kaum auf. In der Neuimplementierung tritt der Fehler hingegen
klar zum Vorschein, da die Skalierung die Hauptfunktionalität der neuen Methode
normalize bildet. Das Beispiel deckt an dieser Stelle einen typischen Nebenef-
fekt des Refactorings auf, der sich in der täglichen Arbeit häufig beobachten lässt:
Die Technik hilft nicht nur, die innere Struktur eines Software-Systems zu erneu-
ern, sondern bringt im gleichen Atemzug viele verborgene Fehler zum Vorschein.
Neben der verbesserten Modularisierung ist für diesen Effekt maßgeblich die ge-
stiegene Code-Transparenz verantwortlich.
An dieser Stelle wollen wir erneut einen Blick auf die zu Anfang gegebene De-
finition des Refactorings werfen. Diese betont ausdrücklich, dass das äußere Ver-
halten eines Software-Systems nicht verändert werden darf. Mit anderen Worten:
Die Durchführung von Refactoring-Schritten muss vollständig getrennt von funk-
tionalen Änderungen erfolgen, zu denen im weiteren Sinne auch jegliche Fehler-
korrekturen gehören. Beck verwendet hierfür die Metapher der „zwei Hüte“. Dieser
zufolge muss ein Programmierer zu jeder Zeit unterscheiden, ob er an der funktio-
nalen Änderung eines Software-Systems oder an dessen Strukturverbesserung ar-
beitet. Bezogen auf unser Beispiel führt die Vorgehensweise dazu, dass zunächst
der Refactoring-Schritt vollzogen werden muss. Erst wenn dieser erfolgreich abge-
schlossen wurde, wird der explizit gewordene Fehler korrigiert.
Die Arbeitsweise reflektiert ein weiteres Kernelement des Refactorings: Alle Än-
derungen werden in möglichst kleinen Schritten durchgeführt. Was erfolgreichen
Programmierern wie eine Selbstverständlichkeit erscheint, wird in der Praxis oft
mit Füßen getreten. Häufig wird die Zusammenfassung vieler Änderungen mit dem
hohen Zeitbedarf begründet, der für die Sicherstellung der Programmkorrektheit
stets auf’s Neue aufgewendet werden muss. Zu bedenken gibt es in diesem Zusam-
menhang, dass mit der Anzahl der simultan durchgeführten Änderungen auch die
Wahrscheinlichkeit eines sich ungewollt einschleichenden Defekts kontinuierlich
zunimmt. In gleichem Maße steigt die Anzahl der Programmstellen, die im Rahmen
der Fehlersuche analysiert werden müssen.
Die Technik des Refactorings löst das Problem durch die Reduktion der Zeit, die
zur Konsistenzprüfung nach jeder Änderung aufgebracht werden muss. In der Praxis
bedeutet dies nichts anderes, als dass jeder Refactoring-Schritt mit dem Schreiben
von Testfällen beginnt, die im Anschluss an jede Änderung automatisiert ausgeführt
400 7 Software-Lebenszyklus
[Link]
import [Link]; 1
2
public class Vertex3D { 3
4
double x, y, z; 5
6
public Vertex3D(double x, double y, double z) { 7
this.x = x; 8
this.y = y; 9
this.z = z; 10
} 11
12
public double length() { 13
return [Link](x*x + y*y + z*z); 14
} 15
16
public Vertex3D normalize() { 17
double l = length(); 18
if (l == 0) l = 1; 19
return new Vertex3D(x/l,y/l,z/l); 20
} 21
22
public static Vertex3D subtract 23
(Vertex3D v1, Vertex3D v2) { 24
return new Vertex3D(v1.x - v2.x, 25
v1.y - v2.y, 26
v1.z - v2.z); 27
} 28
29
public static Vertex3D crossProduct 30
(Vertex3D v1, Vertex3D v2) { 31
return new Vertex3D(v1.y*v2.z - v1.z*v2.y, 32
v1.z*v2.x - v1.x*v2.z, 33
v1.x*v2.y - v1.y*v2.x); 34
} 35
36
public static Vertex3D computeNormal 37
(Vertex3D v1, Vertex3D v2, Vertex3D v3) { 38
Vertex3D e1 = [Link](v2,v1); 39
Vertex3D e2 = [Link](v2,v3); 40
return [Link](e1,e2).normalize(); 41
} 42
} 43
werden. Der initiale Mehraufwand wird in der Praxis zwar von vielen Programmie-
rern skeptisch beurteilt, zahlt sich jedoch später doppelt und dreifach aus. Erinnert
7.3 Ist die Software-Alterung unumgänglich? 401
Sie die Situation an die Holzfäller-Metapher aus Abschnitt 3.6? Wenn ja, so haben
Sie die Refactoring-Philosophie bereits verinnerlicht.
Auch wenn nicht alle Refactorings automatisierbar sind, lassen sich viele
standardisierte Änderungen werkzeuggestützt erledigen. Das erste Refactoring-
Werkzeug – der Smalltalk Refactoring Browser – wurde an der University of Il-
linois at Urbana Champaign von John Brant und Donald Roberts entwickelt und
nimmt dem Software-Entwickler viele Aufgaben ab, die im Zuge des Refactoring
wiederkehrend anfallen [222]. Wird z. B. der Name einer Klasse geändert, so ana-
lysiert das Werkzeug die restlichen Programmquellen und gleicht alle Bezeichner
automatisch an. Im direkten Vergleich mit der manuellen Änderung ist der Produk-
tivitätszuwachs insbesondere in großen Software-Systemen gewaltig.
Heute bieten fast alle gängigen IDEs eine Refactoring-Unterstützung an, deren
Funktionsumfang weit über die des ursprünglichen Smalltalk Refactoring Browsers
hinausgeht. Abb. 7.17 vermittelt einen Eindruck über das diesbezügliche Leistungs-
spektrum der freien Entwicklungsumgebung Eclipse [18, 79].
jeweils das erste bzw. letzte Element bezeichnet, das sich innerhalb des aktuellen
Suchintervalls befindet.
Symmetrische Grenzen besitzen den Nachteil, dass sich leere Intervalle nur auf
unnatürliche Weise darstellen lassen, indem die obere Grenze kleiner gewählt wird
als die untere. Die Bestimmung der Intervalllänge muss ebenfalls mit Bedacht erfol-
gen. Berechnen wir die Anzahl der Elemente als die Differenz der oberen und der
unteren Intervallgrenze, so ist der Ergebniswert um eins zu klein. Wir sprechen in
diesem Zusammenhang von einem Out-By-One-Fehler.
In der Literatur werden Fehler dieser Art auch plakativ als fence post error be-
zeichnet [154]. Der Name lehnt sich an die folgende Frage an: „Wie viele Zaunpfo-
sten im Abstand von 10 Metern werden benötigt, um eine 100 Meter lange Strecke
abzuzäunen?“. Von vielen Probanden wird die Frage vorschnell mit 10 beantwortet.
Die Antwort ist auch hier um eins zu gering, da am Ende der Strecke ein zusätzlicher
Zaunpfosten eingerechnet werden muss. Betrachten wir die abzuzäunende Strecke
als numerisches Intervall, so entspricht die Zaunpfostendarstellung exakt der Reprä-
sentation mit Hilfe symmetrischer Grenzen.
Ähnlich gelagert ist die folgende Frage: „Für wie viele Zahlen x gilt x ≥ 8 und
x ≤ 37?“. Dass sich das richtige Ergebnis irgendwo in der Nähe von 37 − 8 = 29
bewegt, liegt auf der Hand. Ob die exakte Antwort 28, 29 oder 30 lautet, ist dagegen
erst auf den zweiten Blick einsichtig. Zeit ist ein knappes Gut und so wird in der
täglichen Arbeit nicht selten auf diesen zweiten Blick verzichtet. Selbst erfahrene
Programmierer sind vor Out-By-One-Fehlern nicht gefeit und werden von diesen
ebenfalls in regelmäßigen Abständen kalt erwischt.
Durch die Verwendung asymmetrischer Grenzen lassen sich viele Out-By-One-
Fehler systematisch vermeiden. Hierzu wird die untere Intervallgrenze, wie bisher,
durch das kleinste Element innerhalb des Intervalls und die obere Grenze durch das
kleinste Element außerhalb des Intervalls beschrieben. Gegenüber symmetrischen
Grenzen besitzen asymmetrische Grenzen die folgenden Vorteile [154]:
I Die Größe eines Intervalls ist stets die Differenz zwischen der oberen und der
unteren Grenze.
I Ein Intervall ist genau dann leer, wenn die untere und die obere Grenze das glei-
che Element referenzieren.
Bei jedem Dateizugriff kommen wir mit asymmetrischen Grenzen in Kontakt, ohne
uns dessen explizit bewusst zu sein. Hier referenziert der Schreib/Lesezeiger einer
Datei stets das nächste zu lesende bzw. zu schreibende Element und folgt damit in
direkter Weise der Idee der asymmetrischen Grenzen.
Kommen wir zurück zu unserem Beispiel: Überzeugt von den Nachteilen sym-
metrischer Grenzen wollen wir die Originalimplementierung der binären Suche
auf asymmetrische Grenzen umstellen. Hierbei handelt es sich um eine typische
Refactoring-Aufgabe, da wir die internen Strukturen des Programms erneuern, oh-
ne dass die Änderungen nach außen sichtbar werden. Der geänderte Programmtext
ist in Abb. 7.18 dargestellt.
7.3 Ist die Software-Alterung unumgänglich? 403
[Link]
public static int binarySearch(int[] a, int key) { 1
int low = 0; 2
int high = [Link]; 3
4
while (low < high) { 5
int mid = (low + high) >> 1; 6
int midVal = a[mid]; 7
8
if (midVal < key) 9
low = mid + 1; 10
else if (midVal > key) 11
high = mid; 12
else 13
return mid; // key found 14
} 15
return -(low + 1); // key not found. 16
} 17
Im direkten Vergleich mit der Originalimplementierung aus Abb. 4.3 enthält das
Programm drei Änderungen. Die obere Intervallgrenze high wird neuerdings mit
[Link] initialisiert und nicht mehr länger mit [Link]-1. Da der Indexbe-
reich mit 0 beginnt, referenziert die Variable high jetzt das erste Element außer-
halb des Arrays. Als Zweites wurde in der While-Schleife die boolesche Bedingung
low<=high durch den Ausdruck low<high ersetzt. Die Änderung trägt der Tatsache
Rechnung, dass ein asymmetrisch repräsentiertes Intervall genau dann leer ist, wenn
die untere und die obere Grenzen denselben Wert besitzen. Die letzte Änderung be-
trifft die Neuberechnung des Indexes high innerhalb des ersten Else-Zweigs. In der
asymmetrischen Implementierung wird der Wert von mid unverändert übernommen,
während dieser in der symmetrischen Variante vor der Zuweisung um eins verringert
wird.
Analog zu Abb. 4.4 demonstriert Abb. 7.19 die binäre Suche am Beispiel der
Suche nach dem Element 80. Auch im Falle asymmetrischer Grenzen terminiert der
Algorithmus nach vier Iterationen ohne Erfolg. Trotzdem wollen wir an dieser Stelle
der Frage nachgehen, ob sich das Programmverhalten durch die interne Umstellung
nach außen wirklich nicht geändert hat. Funktional sind beide Programmvarianten
in der Tat identisch, d. h., der berechnete Rückgabewert ist unabhängig von der Sym-
metrie oder Asymmetrie der verwendeten Intervallgrenzen. Noch keine Gedanken
haben wir uns bisher über das Laufzeitverhalten gemacht. Misslingt, wie in unserem
Beispiel, die Suche nach einem Element, so benötigen beide Implementierungen
stets logarithmisch viele Iterationen. Ist das gesuchte Element dagegen vorhanden,
legen beide Programmvarianten ein völlig unterschiedliches Laufzeitverhalten an
den Tag.
404 7 Software-Lebenszyklus
binarySearch(..., 80)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
low mid high
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
Asymmetrische Grenzen
Abb. 7.19 Binäre Suche des Elements 80 mit Hilfe asymmetrischer Grenzen
Ein solches Szenario demonstriert Abb. 7.20. Wird das Element 60 gesucht, so
referenziert die Variable mid in der symmetrischen Variante unmittelbar den rich-
tigen Listenplatz, so dass der Algorithmus nach der ersten Iteration terminiert. In
der asymmetrischen Variante verfehlt die Variable mid das Element aufgrund der
geänderten Intervallgrenzen ganz knapp um eine Position. Bezeichnet n die Anzahl
der Array-Elemente, so werden jetzt log2 n − 1 weitere Schritte benötigt, um an
die richtige Elementposition zu gelangen.
Das Szenario wurde bewusst künstlich konstruiert und beweist an dieser Stelle
keinesfalls die Überlegenheit symmetrischer Grenzen. Genauso gut ließe sich ein
Beispiel entwerfen, in dem die asymmetrische Variante besser abschneidet. Fol-
gerichtig ist aus theoretischer Sicht nichts gegen unser Refactoring-Ergebnis ein-
zuwenden. Trotzdem kann sich das Phänomen in der Praxis zu einem handfesten
Problem ausweiten. Stellen Sie sich vor, es handelt sich bei unserem Programm
nicht um einen einfachen Suchalgorithmus, sondern um ein komplexes Produkt,
für das die Laufzeit einen entscheidenden Einfluss auf den Markterfolg besitzt. In
diesem Szenario könnte unser Beispiel-Array ein stark beachteter Benchmark sein,
an dem Kunden ihre Kaufentscheidung orientieren und sich die Konkurrenten die
Zähne ausbeißen. In diesem Umfeld werden Sie als Software-Entwickler kaum ei-
ne Chance haben, Ihr Refactoring-Vorhaben innerhalb des Unternehmens durch-
zusetzen. Randbedingungen wie diese sind insbesondere für langlebige Software-
Systeme typisch und weiterer Sand im Getriebe, der die Entwicklung zunehmend
schwerfälliger werden lässt.
Die diskutierte Implementierung der binären Suche enthält eine weitere Pro-
grammstelle, die eine besondere Beachtung verdient. In der Originalimplementie-
rung der Firma Sun wird die Intervallmitte mit Hilfe des folgenden Ausdrucks be-
stimmt:
7.3 Ist die Software-Alterung unumgänglich? 405
binarySearch(..., 60)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
low mid high
Symmetrische Grenzen
binarySearch(..., 60)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
low mid high
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
10 12 22 28 30 51 52 60 62 81 82 89 90 92 93 99
Asymmetrische Grenzen
I Divisionsoperation I Bit-Shift-Operationen
a= 1 1 1 1 1 ... 1 = -1 a = 1 1 1 1 1 ... 1 = -1
a/2= 0 0 0 0 0 ... 0 = 0 a >> 1 = 1 1 1 1 1 ... 1 = -1
a >>> 1 = 0 1 1 1 1 ... 1 0
Abb. 7.21 Vorsicht bei der Optimierung arithmetischer Operationen! Nicht in jedem Fall lässt sich
die Division durch 2 mit Hilfe einer Bit-Shift-Operation ausdrücken
Der Shift-Operator >> wird an dieser Stelle eingesetzt, um die Summe der Inter-
vallgrenzen low und high zu halbieren. Beide Variablen besitzen den Datentyp
int und speichern jede für sich eine vorzeichenbehaftete natürliche Zahl in 32-Bit-
Zweierkomplementdarstellung. Die Division durch einen Rechts-Shift zu ersetzen,
ist eine beliebte Optimierung, die von vielen Programmierern bedenkenlos einge-
setzt wird. In der Tat ergibt sich für das Beispiel der binären Suche eine Geschwin-
digkeitssteigerung von ca. 30 %.
406 7 Software-Lebenszyklus
Leider ist die Optimierung genauso beliebt, wie sie im Allgemeinen falsch ist.
Das Beispiel in Abb. 7.21 zeigt, dass die Division durch 2 und der Rechts-Shift
um eine Bitstelle zu durchaus anderen Ergebnissen führen können. Mit Hilfe des
Divisionsoperators ergibt die Division von −1 durch 2 den Wert 0 und entspricht
damit der Erwartung. Die Anwendung des Shift-Operators >> schiebt zunächst alle
Bits um eine Position nach rechts und füllt die freigewordene Bitstelle mit dem ur-
sprünglichen Vorzeichenbit wieder auf. In unserem Beispiel bleibt das ursprüngliche
Bitmuster dadurch unverändert, so dass der Rechts-Shift von −1 ebenfalls wieder
zu −1 evaluiert.
Java kennt einen weiteren Shift-Operator >>>, der im Gegensatz zu >> alle frei-
werdenden Bitstellen stets mit Nullen auffüllt – unabhängig vom Wert des Vorzei-
chenbits. Auch hier erhalten wir nicht das korrekte Ergebnis 0, sondern mit MAX_INT
die größte darstellbare positive Zahl. Als Fazit bleibt die Erkenntnis, dass sich die
Division durch 2 im Allgemeinen nicht durch den Shift-Operator ersetzen lässt.
Im Quelltext der binären Suche führt die Verwendung des Shift-Operators aller-
dings zu keinem Fehler, da die Summe der Intervallgrenzen – sehen wir von der
letzten Iteration ab – stets positiv ist. In diesem Fall liefern der Rechts-Shift und
die Division durch 2 das gleiche Ergebnis. Dass der Rechts-Shift nur im positiven
Zahlenbereich der Division durch 2 entspricht, ist vielen Programmierern nicht ge-
läufig. In Code-Reviews und Inspektionen lohnt sich daher ein zweiter Blick auf alle
arithmetischen Verwendungen von Shift-Operatoren – die Chance ist groß, dass Sie
hier den einen oder anderen verborgenen Fehler aufdecken werden.
Der Ausdruck (low+high)>>1 enthält eine zweite Besonderheit, die sich in der
Tat als handfester Programmierfehler entpuppt. Was geschieht, wenn die Summe
der linken und rechten Intervallgrenze einen Integer-Überlauf verursacht? Zugege-
benermaßen können wir diesen Effekt nur für sehr große Testfälle herbeiführen. Die
Anzahl der Listenelemente muss 231 Elemente übersteigen und ein Integer-Array
dieser Größe belegt mehr als 8 GiB Speicher.
Glücklicherweise lässt sich der Fehler auf einfache Weise beheben. Anstatt beide
Grenzen zunächst zu addieren und danach zu dividieren, wird zunächst der Abstand
zwischen den Intervallgrenzen berechnet. Wird anschließend die halbe Differenz auf
die untere Grenze addiert, so erhalten wir das gewünschte Ergebnis ohne die Gefahr
eines Überlaufs. Korrigiert lautet die entsprechende Programmzeile wie folgt:
int mid = low + ((high-low) >> 1);
7.3.2 Redesign
Für viele langlebige Software-Systeme kommt irgendwann der Punkt, an dem eine
Weiterentwicklung nicht mehr sinnvoll erscheint. Zwar lässt sich die fortschreiten-
de Degeneration durch qualitätssichernde Maßnahmen verzögern, jedoch niemals
völlig zum Stillstand bringen. Als einzige Alternative bleibt die partielle oder voll-
ständige Neuimplementierung der Software. In der Fachterminologie sprechen wir
in diesem Fall von einem Redesign.
7.3 Ist die Software-Alterung unumgänglich? 407
Im Gegensatz zu den Methoden des Refactorings, die auf eine nach außen un-
sichtbare Erneuerung der inneren Code-Strukturen ausgerichtet sind, werden im
Zuge des Redesigns auch die äußeren Schnittstellen auf den Prüfstand gestellt. Eine
Neuimplementierung setzt einen Schlussstrich unter bestehende Altlasten und geht
damit weit über die übliche Wartung und Pflege der Programmquellen hinaus.
Wie wir später sehen werden, liegen die hiermit verbundenen Chancen und Risi-
ken in der Praxis sehr nahe beieinander. Um die Auswirkungen eines Redesigns in
kalkulierbaren Grenzen zu halten, müssen vor dem Transitionsschritt die folgenden
Fragen deutlich beantwortet sein:
Refactoring t
Produkt A
Produkt-
transition
Produkt A'
Redesign
Produkt-
transition
Produkt A''
t
Aus der Warte der Projektleiter erscheint das Problem in einem anderen Licht.
Anders als die Entwickler verfügen sie in der Regel nur über eine sehr vage Ein-
schätzung der inneren Qualitätsparameter eines Software-Systems. In vielen Fällen
bekommen Projektleiter nicht eine einzige Zeile Code zu Gesicht und sind in die-
sem Fall vollständig auf die fachgerechte Einschätzung ihrer Entwickler angewie-
sen. Adäquat zu beurteilen, wie es um die inneren Werte eines Produkts wirklich
steht, ist auf den höheren Managementebenen faktisch kaum noch möglich. Auch
hier wirkt sich die Eigenschaft von Software, ein inhärent immaterielles Produkt zu
sein, abermals zu unserem Nachteil aus.
Eine erfolgreiche Produkttransition gelang der Firma Apple mit der Einführung
des Betriebssystems Mac OS X im Jahre 2001. Die Macintosh-Betriebssysteme der
ersten Generationen waren zu dieser Zeit weit von der Leistungsfähigkeit entfernt,
die wir heute als selbstverständlich erachten. Bis Mac OS 9 war das Betriebssystem
ein klassisches Single-User-System, das für die parallele Programmausführung ge-
nauso wenig ausgelegt war wie für den sicheren Betrieb. Insbesondere der fehlende
Speicherschutz führte zu einer dramatischen Beeinträchtigung der Systemstabilität,
da jede fehlerhafte Applikation im Extremfall das komplette Betriebssystem zum
Absturz bringen konnte. Mit dem Beginn des Internet-Zeitalters Anfang der Neun-
zigerjahre zeichnete sich in klareren Konturen ab, dass das bisherige Mac OS an
seine Grenzen stößt. Zwar war es gelungen, rudimentäre Multitasking- und Netz-
werkfähigkeiten in das Betriebssystem zu integrieren, die inneren Strukturen ließen
jedoch kaum noch Raum für zukünftige Weiterentwicklungen.
Apple entschied sich für einen drastischen Schritt und akquirierte 1996 unter
der Federführung von Gil Amelio die Firma NeXT. Das eingekaufte Betriebssys-
tem NeXTStep wurde weiterentwickelt und firmiert heute unter dem Namen Mac
7.3 Ist die Software-Alterung unumgänglich? 409
I Nachholeffekte
Die vielen guten Ideen, die aufgrund von Zeit- und Geldmangel in das erste Sys-
tem nicht einfließen konnten, werden aus der Schublade geholt. Nachdem das
Produkt seinen Erfolg bewiesen hat und das Team zeigen konnte, zu welcher
Leistung es fähig ist, scheint der richtige Zeitpunkt für den großen Wurf gekom-
men.
410 7 Software-Lebenszyklus
I Rückwärtskompatibilität
Das alte Produkt hat mittlerweile eine große Marktdurchdringung erreicht und
wird von zahlreichen Kunden im Feld eingesetzt. Um die nötige Kundenakzep-
tanz zu erhalten, muss das neue Produkt vollständig rückwärtskompatibel sein.
Stellt jede Forderung für sich alleine bereits eine große Herausforderung dar, so ist
deren Kombination dem Chaos geweiht. Der gleichzeitige Komplettumbau der al-
ten Strukturen unter Beibehaltung der vollständigen Rückwärtskompatibilität ist ein
Spagat, der in den wenigsten Fällen gelingt. Kommen zusätzliche Begehrlichkei-
ten in Form zahlloser neuer Funktionalitäten hinzu, so stößt ein Projekt schnell an
seine Grenzen. Hochmut kommt bekanntlich vor dem Fall und in der Tat erblicken
viele der auf diese Weise neu entwickelten Zweitsysteme entweder nie oder nur in
deutlich reduzierter Form das Licht der Öffentlichkeit.
Doch wodurch wird die Selbstüberschätzung an dieser Stelle eigentlich verur-
sacht? Gründe hierfür gibt es viele. Einen ganz entscheidenden haben wir bereits
in Kapitel 1 kennen gelernt: Unsere Neigung, linear zu denken. Im Taumel des Er-
folgs werden warnende Stimmen, ein doppelt so großes System nehme mehr als
doppelt so viel Zeit in Anspruch, gerne überhört. So kann das Vorliegen exakter
Zahlen über Budget und Entwicklungszeit gerade bei der Konzeption des Zweitsys-
tems zum Verhängnis werden – nämlich genau dann, wenn Zeit und Budget für die
Neuimplementierung, ganz der Intuition folgend, linear hochgerechnet werden.
Beispiele des Second-System-Effekts gibt es zuhauf und wir wollen an dieser
Stelle exemplarisch einen Blick auf das 1965 begonnene und nie vollendete Multics-
Projekt werfen (Multiplexed Information and Computing Service). Im Rahmen die-
ses Projekts sollte ein innovatives Betriebssystem entstehen, das seinen Vorgän-
ger CTSS (Compatible Time-Sharing System) weit in den Schatten stellen würde.
Vorangetrieben wurde das Projekt durch das Massachusetts Institute of Technolo-
gy (MIT), den Industriegiganten General Electrics (GE) und die Bell Laboratories
in Murray Hill. Neben vielen anderen innovativen Fähigkeiten sollte Multics ein
hierarchisches Dateisystem unterstützen, das über die gleiche Schnittstelle verfügt,
wie sie auch für die Kommunikation mit externen Geräten zum Einsatz kommt.
Das Konzept, Dateien und Geräte einheitlich anzusprechen, ist heute ein integraler
Bestandteil aller Unix-ähnlichen Betriebssysteme.
Multics erlitt das klassische Schicksal eines Zweitsystems. Die Ansprüche erwie-
sen sich in zunehmendem Maße als nicht erfüllbar und die veranschlagten Zeit- und
Geldbudgets als zu knapp bemessen. Im Jahre 1969 stiegen die Bell Laboratories als
erster Geldgeber aus der Multics-Entwicklung aus und zogen einen Schlussstrich
unter das Projekt [197]. Rückblickend beschreibt Dennis Ritchie die Situation wie
folgt:
“By 1969, Bell Labs management, and even the researchers came to be-
lieve that the promises of Multics could be fulfilled only too late and too
expensively.”
Dennis M. Ritchie, Bell Labs [221]
7.3 Ist die Software-Alterung unumgänglich? 411
“Look at the scenario from the customer’s standpoint. You bought pro-
grams X, Y and Z. You then upgraded to Windows XP. Your computer now
crashes randomly, and program Z doesn’t work at all. You’re going to tell
your friends, «Don’t upgrade to Windows XP. It crashes randomly, and it’s
not compatible with program Z.» Are you going to debug your system to de-
termine that program X is causing the crashes, and that program Z doesn’t
work because it is using undocumented window messages? Of course not.
You’re going to return the Windows XP box for a refund.”
Raymond Chen, Microsoft [41]
In der Konsequenz führt die strikte Einhaltung dieser Philosophie zu einer An-
sammlung von Altlasten, da jede zukünftige Windows-Version eine Obermen-
ge der Vorgängerversion darstellen muss. Das Entfernen alter Programmteile ist
kaum noch möglich und die Weiterentwicklung beschränkt sich auf das Hinzufü-
gen neuen Codes. Der in Abschnitt 7.2.5 skizzierte Zuwachs der Windows-Code-
Basis ist ein starkes Indiz für die Folgen dieser Entwicklung.
Nichtsdestotrotz besitzt dieses Transitionsszenario die geringsten Auswirkun-
gen auf den Anwender und hat damit die größte Chance, die notwendige Kun-
denakzeptanz für den angestrebten Produktwechsel zu erlangen. Auf der nega-
tiven Seite verzichtet die Firma ausdrücklich auf die seltene Möglichkeit, sich
selbst von Altlasten zu befreien. Bereits vorhandene Alterungssymptome wer-
den in diesem Fall von Anfang an in das neue Produkt vererbt.
I Kompatibilitätsumgebung
Ein anderes Transitionsszenario besteht in der Schaffung einer sogenannten
Kompatibilitätsumgebung. Apple beschritt diesen Weg unter anderem während
der Einführung von Mac OS X. Das Unternehmen verzichtete bewusst darauf,
sein neues Betriebssystem rückwärtskompatibel zu konzipieren. Stattdessen wur-
de mit der Classics-Umgebung eine Möglichkeit geschaffen, einen abgeschotte-
ten OS-9-Bereich unter Mac OS X zu simulieren. Aus der Sicht des Kunden ist
dieses Transitionsszenario mit einigen Unannehmlichkeiten verbunden. Durch
den zwangsweisen Umweg über die Classics-Umgebung ist die Ausführung ei-
ner alten OS-9-Applikation unter OS X weniger komfortabel als z. B. die Aus-
führung einer alten Windows-2000-Applikation unter Windows Vista. Aus der
Sicht des Herstellers bringt der Ansatz jedoch unschätzbare Vorteile mit sich,
7.3 Ist die Software-Alterung unumgänglich? 413
da das neue Betriebssystem keine Rücksicht auf Altlasten jeglicher Art nehmen
muss.
Mittlerweile sind die meisten OS-9-Applikationen vom Markt verschwunden,
so dass die Classics-Umgebung heute kaum noch eine Bedeutung besitzt. In ent-
sprechender Weise hat sich Apple im Zuge der PowerPC-Intel-Transition auch
dieser Altlast mittlerweile entledigt. In den Intel-Versionen von Mac OS X steht
die Classics-Umgebung nicht mehr zur Verfügung.
Insgesamt wird mit der Schaffung einer Kompatibilitätsumgebung ein Mittel-
weg zwischen der vollständigen Rückwärtskompatibilität und der kompromiss-
losen Entledigung von Altlasten beschritten. Ob die Akzeptanz der Kunden auf
diese Weise gewonnen werden kann, ist von Fall zu Fall verschieden. Eines ist
jedoch sicher: Die Qualität der Software ist auf jeden Fall der Gewinner.
Kapitel 8
Software-Infrastruktur
In den vorangegangenen Kapiteln haben wir uns ausführlich mit den verschiede-
nen Charakteristika großer, langlebiger Software-Systeme befasst und bereits an der
einen oder anderen Stelle die Bedeutung einer sowohl technisch als auch organi-
satorisch ausgereiften Vorgehensweise betont. In diesem Kapitel wollen wir diese
Betrachtung vertiefen und uns der Infrastruktur zuwenden, die für eine erfolgreiche
Durchführung mittlerer und großer Software-Projekte unabdingbar ist.
Hierzu wollen wir einen der in Abschnitt 7.1 eingeführten Aspekte langlebiger
Software-Systeme wieder aufgreifen, der einen zentralen Einfluss auf den industri-
ellen Software-Entwicklungsprozess hat: Die Rede ist von der Produktdiversifizie-
rung. In der industriellen Software-Entwicklung durchläuft ein langlebiges Produkt
im Laufe der Zeit mehrere Phasen, die in Abb. 8.1 grafisch zusammengefasst sind.
Insgesamt lassen sich vier verschiedene Entwicklungsstadien unterscheiden:
I Initialentwicklung
In den allermeisten Fällen wird in der initialen Phase zunächst ein einziges Sys-
tem erstellt, das für eine spezielle Hardware-Plattform und ein ausgewähltes Be-
triebssystem konzipiert ist. Auch wenn die Diversifizierung in diesem Stadium
noch nicht begonnen hat, werden bereits hier die Weichen für die erfolgreiche
Bewältigung der nachfolgenden Phasen gestellt. So trägt z. B. die frühe Beach-
tung der Regeln zur portablen Programmierung (vgl. Abschnitt 3.5) dazu bei,
viele der in späteren Phasen auftretenden Alterungserscheinungen abzumildern
oder sogar gänzlich zu vermeiden.
I Versionsdiversifizierung
Zeitgleich mit der Auslieferung der initialen Produktversion beginnt die erste
Stufe der Diversifizierung. Durch die permanente Weiterentwicklung der Soft-
ware gesellen sich in kurzen Abständen weitere Versionen hinzu. Die meisten
werden als Zwischenversion nie einem größeren Anwenderkreis zugeführt, an-
dere erreichen dagegen den Status eines offiziellen Release. Diese müssen zu-
sätzlich zu den allgemeinen Weiterentwicklungsaktivitäten gepflegt und gewartet
werden.
1. Initialentwicklung 2. Versionsdiversizierung
Produkt Versionen
3. Variantendiversizierung 4. Komponentendiversizierung
Varianten
Varianten
Varianten
Versionen Versionen
Varianten
Varianten
Versionen
Versionen Versionen
I Variantendiversifizierung
Kann ein Produkt längere Zeit erfolgreich am Markt platziert werden, so erhöht
sich in aller Regel auch die Anzahl der Varianten, in der es vertrieben wird. Die
meisten Produktvarianten gehen aus einem der folgenden Portabilitätsszenarien
hervor:
– Portierung auf andere Betriebssysteme (Photoshop für Windows, . . .)
I Komponentendiversifizierung
Die dritte Dimension der Diversifizierung entsteht immer dann, wenn ein Pro-
dukt als Komponente in anderen Software-Produkten eingesetzt wird. Der ap-
plikationsübergreifende XML-Parser, das in Abschnitt 7.2.1 im Detail vorge-
stellte Timing-Analyse-Modul oder der in allen Office-Anwendungen gleicher-
maßen eingebettete Formel-Editor sind drei Beispiele von vielen. Obwohl die-
se dritte Schiene der Diversifizierung die Herausforderungen an die Versions-
verwaltung abermals erhöht, kann die applikationsübergreifende Wiederverwen-
dung von Software-Komponenten beachtliche Synergieeffekte freilegen. Vie-
le Software-Experten sehen in der Identifikation der geeigneten Komponen-
ten eine der Schlüsselentscheidungen für die langfristig erfolgreiche Software-
Entwicklung.
Allzu großer Euphorie sei an dieser Stelle jedoch eine Absage erteilt. Die
Komponentenbildung wird zwar von vielen Seiten regelmäßig in die Position
des schon so oft beschworenen silver bullets hineingedrängt, die Praxiserfahrung
konnte die Erwartungen bisher jedoch nicht stützen. Insbesondere die Fallstu-
die in Abschnitt 7.2.1 konnte auf eindringliche Weise demonstrieren, dass die
Früchte der Synergie in der Praxis nicht ohne Reibungsverluste geerntet werden
können.
Die Arbeit in großen Teams sowie der Umgang mit der Produktdiversifizierung ma-
terialisieren sich in den folgenden drei Kernaktivitäten, die zusammen das techni-
sche Rückgrat der industriellen Software-Infrastruktur bilden:
I Versionsverwaltung
I Defektmanagement
Auf alle drei Aktivitäten werden wir in den folgenden Abschnitten genauer einge-
hen.
8.1 Versionsverwaltung
Ein Versionsverwaltungssystem verwahrt alle Artefakte eines Software-Systems in
einem zentralen Repository und implementiert die zentrale Infrastruktur für die ko-
ordinierte Arbeit mehrerer Entwickler an derselben Code-Basis (vgl. Abb. 8.2). Im
418 8 Software-Infrastruktur
Versionsverwaltung
Entwickler Primäre
oder Team Artefakte
Entwickler Sekundäre
oder Team Repository
Artefakte
Entwickler Tertiäre
oder Team Artefakte
I Primäre Artefakte
In diese Kategorie fallen alle Artefakte, aus denen das Software-System erzeugt
wird. Hierzu zählen neben dem Quellcode z. B. auch alle Steuerdateien wie Ma-
kefiles oder Shell-Skripte.
I Sekundäre Artefakte
Zu dieser Kategorie gehören alle Artefakte, die dem Software-System unmit-
telbar zugeordnet werden können, jedoch nicht direkt am Übersetzungsprozess
beteiligt sind. Beispiele dieser Kategorie sind die Testdatenbank, das Pflichten-
und Lastenheft sowie die Produktdokumentation.
I Tertiäre Artefakte
In diese Kategorie fallen alle Artefakte, die zum Erzeugen des Systems benötigt
werden. Hierunter fallen insbesondere der Compiler, der Linker und alle weiteren
Programme, die zur Übersetzung des Quellcodes benötigt werden.
Während die Entwicklung sehr großer Software-Projekte ohne eine ausgefeilte Ver-
sionsverwaltung de facto nicht mehr möglich ist, werden entsprechende Werkzeuge
in kleinen und mittelgroßen Projekten auch heute noch nicht flächendeckend ein-
gesetzt. Die Idee der Versionsverwaltung ist dabei keinesfalls eine der letzten Jah-
re. Die Grundzüge der eingesetzten Techniken gehen auf das Source Code Control
System, kurz SCCS, zurück, das bereits Anfang der Siebzigerjahre entwickelt wur-
de [232]. SCCS war Bestandteil vieler Unix-Derivate und fand dadurch eine weite
Verbreitung. Heute spielt SCCS selbst kaum noch eine Rolle – einzig das Datenfor-
mat überlebte und wird in moderneren Systemen wie TeamWare und BitKeeper in
8.1 Versionsverwaltung 419
abgewandelter Form weiterverwendet [25, 250]. TeamWare stammt von Sun Micro-
systems und wird unter anderem für die Entwicklung des hauseigenen Betriebssy-
stems Solaris eingesetzt [268, 73]. Das Versionsverwaltungssystem BitKeeper der
Firma BitMover wurde insbesondere durch den zeitweiligen Einsatz in der Linux-
Kernel-Entwicklung bekannt.
SCCS wurde Anfang der Achtzigerjahre von dem an der Purdue University, In-
diana, entwickelten Revision Control System, kurz RCS, abgelöst [255]. Genau wie
sein Vorgänger ist das Werkzeug für die Parallelentwicklung in großen Teams nicht
geeignet und verfügt aus heutiger Sicht nur über einen rudimentären Funktionsum-
fang. Der Nachfolger von RCS, das Concurrent Versions System, kurz CVS, besei-
tigte viele der ursprünglich vorhandenen Limitierungen und erfreut sich im Open-
Source-Bereich immer noch großer Beliebtheit. CVS unterstützt die Teamarbeit un-
ter anderem durch die Möglichkeit, über eine Netzwerkverbindung von beliebigen
Orten aus auf das Repository zuzugreifen. Nichtsdestotrotz schränken zahlreiche
Limitierungen den professionellen Einsatz deutlich ein. So bietet CVS, genau wie
sein Vorgänger, nur eine rudimentäre Unterstützung von Verzeichnisstrukturen. In
der Konsequenz ist z. B. die simple Umbenennung eines Verzeichnisses nur über
erhebliche Umwege möglich.
Anfang 2004 erschien das Open-Source-System Subversion in der Version 1.0
und wurde mit der Absicht entwickelt, die gröbsten Limitierungen von CVS zu be-
seitigen. Seither erfreut sich dieses System einer regen Aufmerksamkeit und einer
kontinuierlich steigenden Benutzergruppe. Landläufig wird Subversion als der di-
rekte Nachfolger von CVS angesehen, bei genauerer Betrachtung entpuppt sich die
Software jedoch als vollständige Neuentwicklung.
Zu den im industriellen Umfeld am häufigsten eingesetzten Versionsverwal-
tungssystemen gehört das Werkzeug ClearCase der Firma IBM. ClearCase bringt
zahlreiche Erweiterungen mit sich und unterstützt insbesondere die dezentrale
Software-Entwicklung an weltweit verteilten Standorten auf ausgeklügelte Weise.
Die in den folgenden Abschnitten präsentierten Praxisbeispiele werden sich vor-
wiegend auf das frei verfügbare Versionsverwaltungssystem Subversion, wie auch
auf das kommerziell vertriebene ClearCase beziehen.
I Transparenz
Um das Chaos zu vermeiden, das sich im Zusammenspiel vieler Entwickler fast
zwangsläufig einstellt, müssen alle Änderungen protokolliert erfolgen. Im Be-
sonderen müssen für jedes Artefakt die folgenden Fragen eindeutig beantwortet
werden können:
– Was wurde an dem Artefakt geändert?
420 8 Software-Infrastruktur
I Rekonstruktion
Zu jeder Zeit muss es möglich sein, ältere Versionen und Varianten der Softwa-
re wiederherzustellen. Zu den typischen Anwendungen gehört die Rückgewin-
nung alter Release-Versionen genauso wie die Rekonstruktion des Vortageszu-
stands. Darüber hinaus unterstützen moderne Versionsverwaltungssysteme semi-
automatische Rollback-Szenarien, in denen Änderungen nachträglich zurückge-
nommen werden können, die sich als falsch oder überflüssig herausgestellt ha-
ben.
I Simultaner Zugriff
Die Versionsverwaltung muss darauf ausgerichtet sein, mehreren Software-
Entwicklern die simultane Arbeit an verschiedenen Versionen und Varianten der
gleichen Code-Basis zu ermöglichen. Erreicht wird die kontrollierte Zusammen-
arbeit durch lokale Arbeitskopien in Zusammenspiel mit den Branching- und
Merging-Techniken, die weiter unten im Detail vorgestellt werden.
Aus den obigen Anforderungen geht eine zentrale konzeptuelle Eigenschaft eines
jeden Versionsverwaltungssystems hervor, nämlich die Aufzeichnung aller Datei-
inhalte über die Zeit. Anders als das klassische Dateisystem, das für jede Datei
ausschließlich den Zustand nach der letzten Änderung vorhält, speichert die Versi-
onsverwaltung die komplette Historie einer Datei ab.
Wie in Abb. 8.3 ersichtlich, erstreckt sich die Aufzeichnung auf Dateien und
Verzeichnisse gleichermaßen. Die Verwaltung der Datei- und Verzeichnishistorie
erweist sich aus technischer Sicht schwieriger, als es der erste Blick vermuten lässt.
So muss ein Versionsverwaltungssystem mit der Problematik umgehen, dass einige
Dateien erst später erzeugt und andere vorzeitig gelöscht werden. In der Konsequenz
können sich unter dem gleichen Namen zu verschiedenen Zeitpunkten verschiedene
Dateien verbergen.
Die Möglichkeit, parallel an ein und derselben Code-Basis zu arbeiten, wird in
modernen Versionsverwaltungssystemen durch eines der folgenden Arbeitsprinzi-
pien erreicht:
I Sandbox-Prinzip
Dem Prinzip der Sandbox folgend, erzeugt jeder Entwickler vor einer Programm-
änderung zunächst eine lokale Arbeitskopie des Repository-Inhalts. Das als
Sandbox bezeichnete Duplikat muss nicht das komplette Repository umfassen
und beschränkt sich in der Regel auf das bearbeitete Produkt oder Modul. Alle
Änderungen des Programmierers sind zunächst auf die lokale Kopie beschränkt
und beeinflussen weder den Repository-Inhalt noch die Arbeit anderer Entwick-
ler. Erst wenn alle Dateien bearbeitet und das Ergebnis ausführlichen Tests un-
terzogen wurde, wird die lokale Kopie in das Repository zurückgeschrieben. Die
8.1 Versionsverwaltung 421
Erzeugen
src/
src/[Link]
Erzeugen
src/[Link]
Erzeugen
doc/
doc/README
t
I Virtuelle Dateisysteme
Ein Nachteil des Sandbox-Prinzips ist die große Datenmenge, die auf dem Ar-
beitsrechner gespeichert werden muss. Das Prinzip des virtuellen Dateisystems
löst das Problem, indem die Arbeitskopie in Form eines Netzwerkverzeichnis-
ses auf dem Entwicklungsrechner eingeblendet wird und nur noch diejenigen
Dateien lokal vorgehalten werden, die durch den Programmierer selbst verän-
dert wurden. Alle anderen Dateien werden durch den Repository-Server über
das Netzwerk bereitgestellt.
Mit dem Multiversion File System, kurz MVFS, beschreitet ClearCase den
Weg der virtuellen Datenhaltung. Durch die Fähigkeit, beliebige Versionen eines
Artefakts dynamisch einzublenden, erreicht das Werkzeug auf der einen Seite
ein Leistungsspektrum, das mit Sandbox-basierten Werkzeugen kaum noch zu
vergleichen ist. Auf der anderen Seite geht die Flexibilität mit einer signifikan-
ten Erhöhung der Komplexität einher und schlägt sich nicht zuletzt in einem
beträchtlich gesteigerten Einarbeitungsaufwand nieder.
Die Konzeption eines typischen Versionsverwaltungssystems ist in Abb. 8.4 am Bei-
spiel des frei verfügbaren Werkzeugs Subversion zusammengefasst. Wie in der obe-
ren Hälfte der Abbildung skizziert, werden dem Anwender eine Reihe verschie-
dener Benutzungsschnittstellen angeboten. Insbesondere kann der Entwickler frei
zwischen einer Unix-typischen Kommandozeilenschnittstelle und mehreren grafi-
schen Front-Ends wählen. Für die Übertragung der Daten von und zu dem lokalen
Entwicklungsrechner kennt Subversion ebenfalls mehrere Möglichkeiten. Neben
dem lokalen Dateizugriff unterstützt das Werkzeug das proprietäre Svn-Protokoll
422 8 Software-Infrastruktur
Gra'sche
Kommandozeilen-
Benutzungs-
schnittstelle
ober(äche
Client-Schnittstelle
Repository-Zugriff
Internet
Apache svnserve
Repository-Schnittstelle
Repository
Checkout Repository
Update Repository
Checkin Repository
Repository
/ /
/prj1 /prj1
foo.c foo.c
checkout
Makele /prj1 Makele
/prj2 .svn
bar.c .svn
Makele Administrativer Bereich
8.1.2 Revisionen
Wird eine ausgecheckte Datei lokal geändert und wieder eingecheckt, so erzeugt
das Versionsverwaltungssystem eine neue Revision im Repository. Des Weiteren
wird eine individuelle Revisionsnummer generiert, über die sich die Datei zu jedem
späteren Zeitpunkt eindeutig referenzieren lässt. Die in modernen Versionsverwal-
tungssystemen verwendeten Versionierungsschemata lassen sich in zwei Gruppen
einteilen:
I Lokale Versionierung
Die Versionierung wird für jedes Artefakt getrennt durchgeführt, d. h., es wer-
den ausschließlich die Revisionsnummern derjenigen Dateien erhöht, die auch
1 Für die Durchführung eines Checkin bzw. Checkout haben sich heute die Begriffe des Ein-
checkens bzw. Auscheckens einer Datei unternehmensübergreifend etabliert. Die Begriffe sind in
der Software-Industrie so allgegenwärtig, dass wir uns der Verwendung dieser Anglizismen an
dieser Stelle nicht verweigern wollen.
424 8 Software-Infrastruktur
Repository Repository
checkin checkin
1 1 2 2 2 2
checkin checkin
1 2 2 3 3 3
update update
1 2 2 3 3 3
wirklich geändert wurden. Folgerichtig sind zwei Revisionen einer Datei genau
dann verschieden, wenn ihre Revisionsnummern unterschiedlich sind. Die Mehr-
zahl der sich heute im Einsatz befindlichen Versionsverwaltungssysteme arbeiten
nach diesem Prinzip, so z. B. auch CVS und ClearCase.
I Globale Versionierung
Die Versionierung wird für das gesamte Repository gemeinsam durchgeführt,
d. h., bei jedem Checkin einer Datei werden die Revisionsnummern aller Datei-
en erhöht. Folgerichtig können zwei Revisionen einer Datei durchaus gleich sein,
auch wenn deren Revisionsnummern verschieden sind. Diese Art der Versionie-
rung wird z. B. von Subversion umgesetzt und ist insbesondere beim Umstieg
von CVS zu beachten.
Abb. 8.7 demonstriert den Unterschied zwischen beiden Versionierungsschemata.
Das versionskontrollierte Beispielprojekt besteht aus insgesamt drei Dateien, die zu
Beginn allesamt die Revisionsnummer 1 tragen. Zwei Benutzer checken das Projekt
zunächst aus dem Repository aus und beginnen mit der lokalen Weiterentwicklung
einer einzigen Datei. Sobald der erste Nutzer seine Arbeitsergebnisse in das Repo-
sitory zurückschreibt, wird der Unterschied zwischen der lokalen und der globalen
Versionierung deutlich. In der lokalen Versionierung (links) wird ausschließlich die
Revisionsnummer der geänderten Datei erhöht, während die globale Versionierung
(rechts) alle anderen Versionsnummern gleichermaßen anpasst. Folgerichtig haben
8.1 Versionsverwaltung 425
release-1.0
src/
release-1.0
src/[Link]
release-1.0
src/[Link]
release-1.0
doc/
release-1.0
doc/README
t
Abb. 8.8 Eine Konfiguration entspricht einem vertikalen Schnitt durch das Repository
rechts nach dem zweiten Checkin alle Dateien bereits die Revisionsnummer 3 er-
reicht, während links die maximale Revisionsnummer nur auf 2 geklettert ist. Auch
das abschließend initiierte Update hat in beiden Schemata einen unterschiedlichen
Effekt. Die lokale Versionierung erneuert ausschließlich diejenigen Dateien, deren
Inhalt sich zwischenzeitlich geändert hat, während sich im globalen Versionierungs-
schema auch die Revisionsnummern von Dateien erhöhen, die überhaupt noch nicht
bearbeitet wurden.
An dieser Stelle kommen wir zu einem zentralen Begriff der Versionsverwal-
tung: Die Rede ist von dem Begriff der Konfiguration. Grob gesprochen beschreibt
eine Konfiguration eine beliebige Zusammenstellung verschiedener Revisionen der
Dateien eines Software-Systems. Eine wichtige Konfiguration haben wir bereits im
Rahmen des Checkouts kennen gelernt. Ein normaler Checkout erzeugt auf dem lo-
kalen Rechner des Software-Entwicklers in aller Regel diejenige Konfiguration, die
für jede Datei die letzte Revision, d. h. diejenige mit der höchsten Revisionsnum-
mer, enthält. Im Fachjargon wird diese spezielle Konfiguration auch als Latest- oder
Head-Konfiguration bezeichnet.
Der große Unterschied zu einem gewöhnlichen Dateisystem, das im Grunde ge-
nommen stets die Head-Konfiguration aller Dateien zeigt und auch nur diese spei-
chert, kann ein Versionsverwaltungssystem jede beliebige Dateizusammenstellung
erzeugen. Bildlich lässt sich eine Konfiguration, wie in Abb. 8.8 skizziert, als ein
vertikaler Schnitt durch das Repository auffassen. Für die Beschreibung eines sol-
chen Schnittes stehen dem Benutzer in der Regel mehrere Möglichkeiten zur Verfü-
gung:
I Zeitstempel
Die Dateirevisionen werden über eine konkrete Datums- und Zeitangabe ausge-
426 8 Software-Infrastruktur
wählt. Zeigt die aktuelle Version der Software ein plötzliches Fehlverhalten, das
vorher noch nicht zu beobachten war, kann das alte System durch Angabe eines
Zeitstempels rekonstruiert und parallel gegen das neue System getestet werden.
Das folgende Beispiel demonstriert die Angabe eines Zeitstempels in Subversion
in Kombination mit dem Checkout-Befehl:
svn checkout --revision {"2007-10-03 23:59"} [Link]
I Label
Nahezu alle Versionsverwaltungssysteme bieten die Möglichkeit, spezielle Re-
visionen einer Datei mit einem frei wählbaren Label zu versehen. Eine derart
definierte Konfiguration lässt sich zu jedem späteren Zeitpunkt über den gewähl-
ten Bezeichner wieder auffinden und automatisch rekonstruieren. Unter anderem
wird der Label-Mechanismus verwendet, um alle Dateien eines Release zu mar-
kieren. Beispielsweise checkt der folgende Aufruf sämtliche Dateien aus, die mit
dem Label release-1.0 markiert sind:
svn checkout [Link]
configspec
// ClearCase config spec 1
2
// Regel 1: 3
element * CHECKEDOUT 4
5
// Regel 2: 6
element /vobs/src/core/* XP_SP2 7
8
// Regel 3: 9
element /vobs/src/ie/* -time 15-Nov.12:00 10
11
// Regel 4: 12
element * /main/LATEST 13
weiter oben stehenden Regeln entzieht, die Revision LATEST und damit die bis dato
aktuellste Version ausgewählt wird. Da die Head-Revision für jedes nicht gelöschte
Element existiert, kann diese Regel als einzige niemals fehlschlagen. Als Switch-
Case-Konstrukt betrachtet entspricht sie dem Default-Zweig.
Die Regeln 2 und 3 sind Beispiele für die hohe Flexibilität von ClearCase. Zum
einen blendet der erzeugte View für alle nicht ausgecheckten Dateien im Verzeichnis
/vobs/src/core die Revisionen ein, die mit dem Label XP_SP2 markiert sind. Zum
anderen werden für alle Dateien im Verzeichnis /vobs/src/ie diejenigen Revisio-
nen ausgewählt, die am 15. November um 12:00 aktuell waren. Durch den regel-
basierten Aufbau der Config-Spec lassen sich beliebige Views erstellen und damit
auf elegante Weise jede erdenkliche Konfiguration erzeugen. Wie bereits erwähnt,
geht die Flexibilität mit einer deutlichen Steigerung des Einarbeitungsaufwands ein-
her. Erfahrungsgemäß schleichen sich bei der Erstellung der Regelbeschreibungen
schnell Fehler ein, so dass die Config-Specs in vielen großen Firmen durch eine ei-
genständige Integrationsabteilung und nicht durch die Software-Entwickler selbst
erstellt werden.
Schritt 1 Schritt 2
Lea Leo Lea Leo
checkout checkout
A A A A
Repository
A A
Repository
B C
Lea und Leo führen gleichzeitig einen Lea und Leo entwickeln die Datei
Checkout auf die gleiche Datei aus. unabhängig voneinander weiter.
Schritt 3 Schritt 4
Lea Leo Lea Leo
A A A A
Repository Repository
B C
B C B C
checkin checkin
Lea beendet die Arbeit zuerst und über- Leo überträgt seine Änderungen ebenfalls
trägt ihre Änderungen in das Repository. und überschreibt damit die Arbeit von Lea.
I Fall 4: Die Datei wurde sowohl lokal als auch im Repository geändert.
Ist die Datei sowohl im Repository als auch lokal unverändert, so bleiben sowohl
ein Checkin als auch ein Update ohne Auswirkung. Wurde die Datei lokal bearbei-
tet, nicht aber im Repository, so überträgt ein Checkin die lokalen Änderungen in
das Repository zurück. Ein Update bleibt hingegen ohne Auswirkung. Ändert sich
die Datei stattdessen im Repository, ist aber lokal noch unangetastet, so bleibt ein
Checkin ohne Auswirkung und ein Update überschreibt die lokale Datei mit der im
Repository gespeicherten Revision. Zum Konflikt kommt es, wenn die betrachte-
te Datei sowohl lokal als auch im Repository geändert wurde. Ein Checkin würde
die Änderungen des Repositories überschreiben, während ein Update alle lokalen
Änderungen löschen würde. Zur Lösung des Konflikts werden in der Praxis zwei
verschiedene Checkout-Modelle eingesetzt, die wir im Folgenden genauer betrach-
ten werden.
430 8 Software-Infrastruktur
Fall 1 Fall 2
Repository Repository
checkout checkout
A A A A
Fall 3 Fall 4
Repository Repository
checkout checkout
A A A A
C B C
[Link] Checkout-Modelle
Der oben beschriebene Checkin-Konflikt lässt sich auf vermeintlich einfache Weise
auflösen, indem die parallele Programmentwicklung auf der Dateiebene durch ein
serielles Arbeitsmodell ersetzt wird. Hierzu überwacht das Versionsverwaltungs-
system mit Hilfe eines Semaphors, unter wessen Kontrolle eine bestimmte Datei
steht und gewährt allen anderen Benutzern einen ausschließlich lesenden Zugriff.
Erst nachdem die Datei wieder eingecheckt wurde, kann ein anderer Anwender
die Schreibrechte erlangen. In der Terminologie der Versionsverwaltung wird ein
Semaphor-basierter Checkout als Reserved Checkout bezeichnet. Bildlich gespro-
chen ist jede Datei mit einer Art Schlüssel geschützt. Dieser muss zunächst einge-
holt werden, bevor auf das Artefakt schreibend zugegriffen werden darf. Erst beim
Einchecken wird der Schlüssel wieder frei und gegebenenfalls an einen anderen Be-
nutzer weitergegeben. Abb. 8.12 fasst das beschriebene Checkout-Modell grafisch
zusammen.
In der Praxis führen Reserved Checkouts fast immer zu massiven Problemen, da
jede Datei aufgrund der seriellen Arbeitsweise nur von einem einzigen Entwickler
modifiziert werden kann. Für den Rest der Arbeitsgruppe entstehen Totzeiten, die
sich mit zunehmender Teamgröße kontinuierlich vergrößern. Auch in anderen Be-
8.1 Versionsverwaltung 431
Schritt 1 Schritt 2
Lea Lea Leo
checkout
A A
lock lock
A A
Repository Repository
B
Lea reserviert zuerst die Datei und Der Versuch von Leo, die Datei ebenfalls
checkt sie damit erfolgreich aus. auszuchecken, schlägt jetzt fehl.
Schritt 3 Schritt 4
Lea Leo Leo
checkout
A B
Repository lock
B B
checkin Repository
B
unlock
Lea schreibt ihre Änderungen zurück Jetzt reserviert Leo die Datei und
und gibt die Datei wieder frei. checkt sie erfolgreich aus.
reichen verstärken sich die negativen Auswirkungen mit der Anzahl der Entwickler,
die um den Schlüssel einer bestimmten Datei konkurrieren. Viele Programmierer,
die beim Auschecken einer Datei nicht zum Zuge kommen, führen die Entwick-
lung mit Hilfe einer lokal angefertigten Kopie fort und unterwandern damit faktisch
den Kontrollmechanismus des Versionsverwaltungssytems. Des Weiteren führt die
Konkurrenzsituation dazu, dass viele Entwickler Dateien nur zögerlich wieder frei
geben, sobald sie den exklusiven Zugriff darauf erlangt haben – schließlich könnte
doch noch eine Änderung nötig werden und wer kann schon garantieren, dass die
Datei nicht für unabsehbare Zeit von einem anderen Entwickler beansprucht wird?
Die zweite Möglichkeit besteht in der Verwendung sogenannter Unreserved
Checkouts, die es beliebig vielen Entwicklern erlauben, dieselbe Datei auszu-
checken und die lokalen Kopien unabhängig voneinander weiterzuentwickeln. Die
Koordination erfolgt in diesem Fall erst dann, wenn die Änderungen in das Repo-
sitory zurückgeschrieben werden sollen. Hat sich der Dateiinhalt dort nicht geän-
dert, so funktioniert der Checkin wie gewöhnlich. Hat dagegen bereits ein anderer
Entwickler seine Arbeitsergebnisse eingepflegt, müssen diese vor einem Checkin
zunächst in die lokale Kopie übernommen werden. Dazu bestimmt das Versions-
verwaltungssystem zunächst die Differenz zwischen den Weiterentwicklungen und
432 8 Software-Infrastruktur
Schritt 1 Schritt 2
Lea Leo Lea Leo
checkout checkout
A A A A
Repository
A B
Repository
B C B C
checkin
Lea und Leo führen gleichzeitig einen Lea checkt ihre Änderungen ein.
Checkout auf die gleiche Datei aus. Leo wird benachrichtigt.
Schritt 3 Schritt 4
Lea Leo Lea Leo
A A A B
Repository Repository
B B+C
B B+C B B+C
merge checkin
Leo integriert die im Repository enthaltenen Leo checkt seine Änderungen ein. Das
Änderungen in seine lokale Datei. Repository enthält jetzt beide Änderungen.
int foo()
{
}
2*21;
return 42;
int main()
{
foo()+foo();
return 2*foo();
}
I Beispiel 2: Die Dateiverschmelzung führt zum Konflikt, der manuell aufgelöst werden muss.
int foo()
{
}
return 2*21;
7;
int main()
{
}
return
???
foo()+foo();
kreditieren. So verlockend der Einsatz von Reserved Checkouts an dieser Stelle auch
erscheinen mag, so wenig funktioniert der Ansatz in der Praxis. Erfahrungsgemäß
stößt das Verfahren schon ab einer Gruppengröße von 4 bis 5 Entwicklern an seine
Grenzen und ist damit für große Software-Projekte keine Alternative. Gleichzeitig
werden die Probleme der automatisierten Dateiverschmelzung häufig überschätzt.
Die eigene Praxiserfahrung zeigt an dieser Stelle, dass in der täglichen Arbeit we-
niger als 10 % der durchgeführten Checkins zu einem Merge-Problem führen und
90 % der Konflikte ohne größere Schwierigkeiten manuell korrigiert werden kön-
nen.
434 8 Software-Infrastruktur
// A.h //A.h
// A.h
const int const int
#define HZ 1000
HZ = 1000; HZ = 1000;
#include "A.h"
#include "A.h" #include "A.h"
int main() {
int main() { int main() {
const int
return HZ; return HZ;
*ret = &HZ;
} }
return *ret;
Repository
// A.h
checkin #define HZ 1000 checkin
#include "A.h"
int main() {
const int
*ret = &HZ;
return *ret;
projektbezogene Checkins gelöst werden. In diesem Fall wird jedem Entwickler ein
individuelles Zeitfenster zugeteilt, innerhalb dessen er den exklusiven Schreibzu-
griff auf das Repository besitzt. Bevor der nächste Entwickler seine eigenen Mo-
difikationen übertragen darf, müssen sämtliche Änderungen des Repositories mit
Hilfe des Update-Mechanismus in die lokale Arbeitskopie eingepflegt werden. Be-
troffen sind in diesem Fall alle geänderten Dateien und nicht nur diejenigen, die
der Entwickler selbst bearbeitet hat. Auf diese Weise werden etwaig vorhandene
Inkonsistenzen in der lokalen Konfiguration sichtbar und können vor dem Checkin
aufgelöst werden. Auf der negativen Seite ist dieser Ansatz mit zusätzlichen An-
strengungen verbunden. Der Mehraufwand wird von vielen Firmen als so groß er-
achtet, dass dieser Weg heute weitgehend gescheut und stattdessen das Risiko von
Inkonsistenzen bewusst oder unbewusst in Kauf genommen wird.
Ein solches nummernbasiertes Schema wird z. B. von CVS und Subversion verwen-
det. ClearCase zeigt sich an dieser Stelle großzügig und stellt es dem Entwickler
frei, Zweige mit beliebigen Namen zu versehen.
In großen Projekten gehören die Generierung neuer sowie die Verschmelzung
existierender Zweige zum Alltagsgeschäft. Die in der Praxis anzutreffenden Ein-
satzzwecke fallen in der Regel in eine von vier Kategorien, die jede für sich eine
charakteristische Zweigstruktur entstehen lässt (vgl. 8.17):
I Team-Koordination
Mit den in Abschnitt [Link] vorgestellten Checkout-Modellen können verschie-
dene Programmierer zur gleichen Zeit auf demselben Zweig und derselben Code-
Basis arbeiten, ohne dass Änderungen im Zuge der Parallelentwicklung verloren
gehen. Auf der negativen Seite steigt mit der Anzahl der gleichzeitig auf einem
Zweig tätigen Entwickler auch das Risiko, dass der im Repository gespeicherte
8.1 Versionsverwaltung 437
Team-Koordination
GUI-Team
Kernel-Team
IO-Team
Produkt-P-ege
Panther Tiger Leopard
Mac OS X
Leopard-Support
Tiger-Support
Panther-Support
Release-Erstellung
Main-Branch
Release-Branch
Release
Versuchsreihen
Main-Branch
Alternative 1
Verwerfen
Alternative 2
entstandenen Topologie wird der Hauptzweig selbst nicht mehr für die Weiter-
entwicklung eingesetzt. Stattdessen wird er nur noch verwendet, um in regelmä-
ßigen Zyklen die aktuellen Änderungen aller Seitenzweige einzusammeln und
erneut zu verteilen. Die eigentliche Entwicklung findet ausschließlich auf den
Seitenästen statt.
Auf der negativen Seite führt dieser Ansatz zu einer merklichen Verzögerung,
bis sich die Änderungen zweier Arbeitsgruppen gegenseitig erreichen. Die Arbeit
einer Gruppe bleibt solange für alle anderen unsichtbar, bis die Änderungen in
den Hauptzweig integriert und von dort aus auf die anderen Seitenäste verteilt
wurden. Je kürzer die Dauer des Verteilungszyklus gewählt wird, desto schneller
breiten sich die Änderungen nach und nach auf die anderen Entwicklungszweige
aus.
Trotzdem ist es in zweierlei Hinsicht nicht sinnvoll, die Intervalldauer auf
das mögliche Minimum zu reduzieren. Zum einen erfordert die Verteilung einen
nicht zu unterschätzenden Personalaufwand, da die Zusammenführung zweier
Äste mehrere Stunden oder gar Tage dauern kann. Zum anderen können zur
Wahrung der Konsistenz während der Sammel- und Verteilungsphase keine Än-
derungen in den Zweig eingecheckt werden. Kurzum: Die fortwährende Inte-
gration würde zu einer permanenten Arbeitsblockade auf Seiten des Software-
Entwicklers führen.
I Produktpflege
Die Wartung und Pflege verschiedener Release-Versionen haben wir bereits wei-
ter oben als eine der grundlegenden Anwendungsdomänen der Versionsverwal-
tung herausgearbeitet. Mit Hilfe des Verzweigungsprinzips kann die simultane
Arbeit an mehreren Produktversionen auf natürliche Art und Weise nachgebildet
werden. Im Zuge der Release-Erstellung wird zeitgleich ein Support-Branch er-
zeugt, auf dem alle zukünftigen Wartungs- und Pflegeaktivitäten vorgenommen
werden. Hiervon unberührt geht die Entwicklung des nächsten Release auf dem
Hauptzweig weiter.
Das Beispiel in Abb. 8.17 demonstriert die mögliche Produktpflege des Be-
triebssystems Mac OS X. Für jeden Release (Cheetah, Puma, Jaguar, Panther,
Tiger, Leopard, . . .) existiert ein individueller Zweig, auf dem Fehlerkorrektu-
ren und kleine Produktänderungen vorgenommen werden. Die Entwicklung der
nächsten Betriebssystemausgabe wird dagegen auf dem Hauptzweig vorangetrie-
ben.
Obwohl nicht zwingend erforderlich, werden die Änderungen auf dem
Support-Branch typischerweise in regelmäßigen Abständen zurückintegriert.
Dieses Vorgehen ist insbesondere für Fehlerbereinigungen sinnvoll, da die meis-
ten auf den Seitenästen korrigierten Fehler auch auf dem Hauptzweig noch vor-
handen sind und damit im Zuge der Integration automatisch mitkorrigiert wer-
den.
I Release-Erstellung
In großen Projekten ist die Release-Erstellung ein komplexer Prozess. Um ein
Software-System in einen Release-fähigen Zustand zu bringen, müssen zahlrei-
8.1 Versionsverwaltung 439
che kleine und große Fehler korrigiert werden. Bis alle Abnahmetests fehlerfrei
absolviert werden, durchläuft ein typischer Release-Kandidat eine Konvergenz-
phase, die durch eine stetige Abnahme der Änderungsrate charakterisiert ist. Da
jeder Programmeingriff ein potenzielles Risiko für neue Fehler darstellt, werden
kurz vor dem Release-Termin nur noch solche Code-Korrekturen zugelassen, die
schwerwiegende Software-Fehler revidieren.
Da die Software auf dem Hauptzweig ständig weiterentwickelt wird, käme die
dortige Release-Erstellung einem Schuss auf ein bewegliches Ziel gleich. Um
die benötigte Stabilität zu gewährleisten, wird rechtzeitig ein spezieller Release-
Zweig abgespalten, der nur für die Zeit der Konvergenzphase existiert. Alle Än-
derungen, die auf diesem Zweig vorgenommen werden, stehen unter der Kontrol-
le des Release-Teams, so dass willkürlich durchgeführte Code-Modifikationen
kurz vor Auslieferungstermin unterbunden werden können. Die vorübergehende
Isolation des Release-Codes ermöglicht, die Entwicklung auf dem Hauptzweig
ungebremst voranzutreiben.
Ist die Konvergenzphase erfolgreich abgeschlossen, erfolgt die eigentliche
Release-Erstellung auf Tastendruck. Hierzu werden die Head-Revisionen aller
Dateien mit einem speziellen Release-Label markiert, über den die so fixierte
Konfiguration zu jedem späteren Zeitpunkt wiederhergestellt werden kann. Da-
mit alle Fehlerkorrekturen, die während der Konvergenzphase in den Programm-
code eingepflegt wurden, nicht verloren gehen, wird der Release-Branch vor dem
Löschen auf den Hauptzweig zurückintegriert.
I Versuchsreihen
Die Verzweigungstechnik gestattet es auf einfache Weise, verschiedene Imple-
mentierungsmöglichkeiten gegeneinander zu vergleichen. Hierzu wird für jede
Alternativimplementierung ein eigener Zweig abgespalten und das Ergebnis be-
wertet. Nur der Seitenast mit der favorisierten Lösung wird anschließend auf den
Hauptzweig zurückintegriert – alle anderen werden gelöscht. Die Verzweigungs-
technik ermöglicht damit die parallele Evaluation verschiedener Alternativen,
ohne die bestehende Software auf dem Hauptzweig zu modifizieren. Änderun-
gen, die als ungeeignet identifiziert wurden, lassen sich postwendend verwerfen.
Die geschilderten Szenarien lassen sich in der Praxis nicht voneinander trennen, so
dass die in Abb. 8.17 dargestellten Zweigstrukturen ineinander geschachtelt auftre-
ten. Das entstehende Zweiggeflecht kann in seiner Größe massiv anwachsen und
wird typischerweise von eigens gegründeten Integrations-Teams verwaltet und ge-
pflegt. Die Komplexität, die sich schon in den vorherigen Kapiteln als der größte
Feind des Software-Entwicklers erwiesen hat, offenbart sich hier erneut als allge-
genwärtig.
[Link] Delta-Technik
Mit jedem neuen Zweig und jeder neuen Revision steigt neben der strukturellen
Komplexität des Repositorys auch dessen Platzbedarf immer weiter an. Um die zu
440 8 Software-Infrastruktur
Repository-Struktur
2.1.1
2.1 2.2
Wurzel-
revision
1 2 3 4
2.1.1 2.1.1
I Vorwärtsdeltas
Ein Vorwärtsdelta ist eine Berechnungsvorschrift, die aus der Vorgängerrevision
die Nachfolgerevision berechnet. In diesem Fall muss ausschließlich die Wurzel-
revision vollständig gespeichert werden, so dass sich die Verwendung von Vor-
wärtsdeltas als besonders platzsparend erweist. Nachteilig schlägt der Aufwand
zu Buche, der für die vollständige Rekonstruktion der Head-Revision benötigt
wird. In diesem Fall müssen alle Deltas nacheinander auf die Wurzelrevision
angewendet werden. Mit den permanent sinkenden Massenspeicherpreisen ver-
lieren Vorwärtsdeltas zusehends an Bedeutung.
I Rückwärtsdeltas
Ein Rückwärtsdelta ist eine Berechnungsvorschrift, die aus der Nachfolgerevisi-
on die Vorgängerrevision berechnet. In diesem Fall werden die Head-Revisionen
aller Zweige vollständig im Repository gespeichert und aus diesen die entspre-
chenden Vorgängerrevisionen rekonstruiert. Sind viele Zweige vorhanden, müs-
8.1 Versionsverwaltung 441
sen deutlich mehr Dateien vollständig vorgehalten werden, als es die Verwen-
dung von Vorwärtsdeltas erfordert. Rückwärtsdeltas erweisen sich in der Praxis
allerdings als die deutlich effizientere Technik, da auf die Head-Revision un-
gleich häufiger zugegriffen wird, als auf alle anderen Revisionen.
Kunde aquiriert
Kunde A
Kunde B
Gemeinsame
Vorgängerrevision
Merge-Revision
V'
Die saubere Lösung des Problems liegt für unseren Automobilzulieferer näher,
als von vielen vielleicht erwartet. Anstelle die verschiedenen Produktvarianten auf
individuelle Seitenäste abzubilden, werden diese auf separate Unterverzeichnisse
verteilt und alle zusammen in einem einzigen Zweig gespeichert. Für die genaue
Verzeichnisstruktur gibt es keine allgemeingültigen Regeln. Beispielsweise könn-
te der Automobilzulieferer alle kundenspezifischen Dateien in einem eigenen Ver-
zeichnis zusammenfassen und von den kundenunabhängigen Programmteilen tren-
nen. Eine solche Zweiteilung haben wir bereits in Abb. 3.27 im Zusammenhang
mit der Verzeichnisstruktur des Linux-Kernels kennen gelernt – eine Verzeichnis-
struktur, von der sich auch der hier zitierte Automobilzulieferer inspirieren lassen
könnte.
[Link] Dateiverschmelzung
Die Zusammenführung von Änderungen, die von mehreren Benutzern gleichzeitig
an ein und derselben Datei durchgeführt wurden, ist das technische Fundament der
Parallelentwicklung. Um die Betrachtungen an dieser Stelle so einfach wie möglich
zu gestalten, setzen wir ein Szenario mit nur zwei parallelen Entwicklungssträngen
voraus.
Für die folgenden Überlegungen nehmen wir an, dass eine gemeinsame Vorgän-
gerdatei V von zwei unabhängig agierenden Programmierern zu den Revisionen V1
und V2 weiterentwickelt wurde. Die Aufgabe der Dateiverschmelzung (file merge)
besteht in der Konstruktion eines gemeinsamen Nachfolgers V , der sowohl die Än-
derungen von V1 als auch die Änderungen von V2 in sich vereint (vgl. Abb. 8.20).
Für die Berechnung der Merge-Revision greifen die meisten Versionsverwal-
tungssysteme auf die Berechnung der längsten gemeinsamen Teilfolge, kurz LGT,
444 8 Software-Infrastruktur
T heisst eine längste gemeinsame Teilfolge (LGT), wenn es keine andere Teilfolge
gibt, die mehr Symbole enthält. Die beiden Beispiele in Abb. 8.21 veranschaulichen
die Konstruktionsidee.
Obwohl die LGT-Berechnung für kleine Zeichenketten fast trivial erscheint,
steigt die algorithmische Komplexität quadratisch mit der Länge der untersuchten
Zeichenketten an [57]. Das rechte Beispiel in Abb. 8.21 zeigt zudem, dass die läng-
ste gemeinsame Teilfolge nicht in jedem Fall eindeutig ist. Folgerichtig werden wir
im Rest dieses Abschnittes meist von einer und nicht von der längsten gemeinsamen
Teilfolge sprechen.
Die Berechnung der Merge-Revision beginnt mit der Konstruktion einer längs-
ten gemeinsamen Teilfolge der gemeinsamen Vorgängerrevision V und den beiden
lokalen Weiterentwicklungen V1 und V2 . Die Berechnung wird in der Praxis auf Zei-
lenebene durchgeführt, d. h., jedes der Symbole sxy in den Gleichungen (8.1) bis
(8.4) entspricht einer separaten Code-Zeile der Quelldatei.
Wie das Beispiel in Abb. 8.22 verdeutlicht, werden die Dateien V , V1 und
V2 durch die LGT-Berechnung in verschiedene Bereiche partitioniert. Alle Code-
Zeilen, die der längsten gemeinsamen Teilfolge angehören, sind grau eingefärbt.
Zwischen den LGT-Fragmenten verbleiben mehrere Differenzbereiche, in denen die
drei Dateien nicht alle den gleichen Inhalt aufweisen.
Sind die Differenzbereiche bestimmt, werden diese nacheinander einer Konflikt-
behandlung unterzogen. Abb. 8.23 fasst alle zu unterscheidenden Fälle grafisch zu-
sammen. Um die Darstellung zu vereinfachen, sind alle Szenarien, in denen ein
Abschnitt in den Folgerevisionen V1 oder V2 vollständig gelöscht wurde bzw. in der
Ursprungsrevision V noch nicht vorhanden war, als eigenständige Fälle aufgeführt
und die leeren Programmabschnitte mit dem Symbol der leeren Menge markiert (0). /
8.1 Versionsverwaltung 445
Beispiel 1 Beispiel 2
S1: a b c d e f g S1: a b c d e f
S 2: h a i b c j f g S2: d e f a b c
S3: h a h b c f g
a
b c d e f g
a b c d e f
h a i b c j fg
d e f a b c
h a h b c f g
LGT1: a b c
LGT: a b c f g
a b c d e f
d e f a b c
LGT2: d e f
Gemeinsame
Erste Weiter- Vorgänger- Zweite Weiter-
entwicklung revision entwicklung
V1 V V2
/* Dave */ /* Mike */
int foobar() {
return 3;
}
In 6 der insgesamt 10 möglichen Merge-Szenarien ändert sich nur der Inhalt einer
einzigen Datei, so dass die Verschmelzung konfliktfrei und vor allem vollautoma-
tisiert durchgeführt werden kann. In den restlichen 4 Szenarien entstehen Merge-
Konflikte, die durch den Software-Entwickler manuell beseitigt werden müssen. Im
446 8 Software-Infrastruktur
V1 V V2 ?
X Y Z
Gemeinsame
Erste Weiter- Vorgänger- Zweite Weiter- X 㱵 㱵 X
entwicklung revision entwicklung
㱵 X 㱵 㱵
A A A
V1 V2 㱵 㱵 X X
V
B B B 㱵 X Y
X 㱵 Y
A X Y 㱵
? X X Y Y
B
Merge- X Y Y X
Revision
X Y X X
Ausgabe von
diff3 V1 V V2
====
1:1,2c Zeilen 1 bis 2 in Datei 1
Bereiche, in /* Dave */
denen sich
2:0a Leerer Block in Datei 2 ab Zeile 0
alle drei Dateien
3:1,2c Zeilen 1 bis 2 in Datei 3
unterscheiden
/* Mike */
====1
1:7,10c Zeilen 7 bis 10 in Datei 1
Bereiche, in int foobar() {
denen sich nur die return 3;
erste Datei von }
den anderen
unterscheidet. 2:4a Leerer Block in Datei 2 ab Zeile 4
3:6a Leerer Block in Datei 3 ab Zeile 6
====3
Bereiche, in 1:12c Zeile 12 in Datei 1
denen sich nur die 2:6c und Zeile 6 in Datei 2
dritte Datei von return 2;
den anderen 3:8c Zeile 8 in Datei 3
unterscheidet. return 3;
Die für unser Beispielprogramm generierte Ausgabe von Diff3 ist in Abb. 8.24 dar-
gestellt und entspricht exakt dem Ergebnis, das wir weiter oben mit Hilfe der längs-
ten gemeinsamen Teilfolge berechnet haben.
[Link] Delta-Berechnung
Wie in Abschnitt [Link] beschrieben, legt ein typisches Versionsverwaltungssys-
tem die meisten Programmdateien nicht im Volltext ab. Stattdessen werden ledig-
lich die Änderungen zwischen zwei Revisionen in Form eines Deltas im Repository
gespeichert. Ein Delta ist damit nichts anderes als eine kompakte Berechnungsvor-
schrift, mit der sich die Nachfolgerevision (Vorwärtsdelta) bzw. die Vorgängerrevi-
sion (Rückwärtsdelta) einer bestimmten Datei verlustfrei rekonstruieren lässt.
Wie in Abb. 8.25 gezeigt, kann die längste gemeinsame Teilfolge nicht nur
zur Dateiverschmelzung, sondern auch zur Delta-Berechnung eingesetzt werden.
448 8 Software-Infrastruktur
S2
Alle LGT-Anteile von S2 werden mit Hilfe von Referenzen auf S1 aufgebaut
Beispiel
1 2 3 4 5 1 2 3 4 5
S1 : a b c a b S1 : a b a b c
S2 : a b a b c S2 : a b c a b
Beispiel 1 Beispiel 2
S1 : a b c a b S1 : a b a b c
c a b c a b
5
a b c 5 b a c
4
b c a c a b 4
3 a b 3 b c
b 2 c 2
1 1
S2 : a b a b c S2 : a b c a b
I Für jedes Suffix Si =< si , si+1 , . . . , sn , $ > von S enthält der Baum ein Blatt Bi .
Das Blatt ist so angeordnet, dass Si den aneinandergereihten Kantenmarkierun-
gen auf dem Pfad von der Wurzel zu Bi entspricht.
I Mehrere von einem Knoten ausgehende Kanten sind stets verschieden markiert.
Die Verwendung des speziellen Endezeichens $ stellt sicher, dass kein Suffix der
Beginn eines anderen Suffixes ist. Die Anzahl der Suffixe entspricht somit exakt
der Anzahl der Blätter des Baums. In der praktischen Anwendung kann auf die
explizite Repräsentation des Endezeichens verzichtet werden, wenn innere Knoten
als spezielle Suffix-Knoten zugelassen und als solche markiert werden.
Als Beispiel sind in Abb. 8.26 die Suffix-Bäume für die Zeichenketten „abcab“
und „ababc“ dargestellt. Auf die explizite Darstellung des Endezeichens wurde hier
bereits verzichtet und die entsprechenden Suffix-Knoten stattdessen farblich her-
vorgehoben. Die zusätzlich annotierte Zahl entspricht der Startposition des Suffixes
innerhalb der Zeichenkette S.
Den Ausgangspunkt für die Delta-Berechnung bilden auch hier wieder die Datei
S1 und seine Nachfolgerevision S2 . Für die Berechnung eines Vorwärtsdeltas wird
zunächst der Suffix-Baum von S1 berechnet und anschließend von links nach rechts
über die Symbole von S2 iteriert. In jedem Schritt wird der konstruierte Suffix-Baum
450 8 Software-Infrastruktur
8.2 Build-Automatisierung
Der Begriff der Build-Automatisierung fasst alle Methoden und Techniken zusam-
men, die sich mit dem selbsttätigen Zusammenfügen einzelner Programmkompo-
nenten zu einem vollständigen Software-System beschäftigen. In diesem Zusam-
menhang werden wir uns in Abschnitt 8.2.1 zunächst dem Prinzip der inkrementel-
len Compilierung widmen und anschließend in Abschnitt 8.2.2 die verschiedenen
Möglichkeiten der verteilten Datenverarbeitung diskutieren.
c a b c a b
5
a b c 5 b a c
4
b c a c a b 4
3 a b 3 b c
b 2 c 2
1 1
Reduktion Reduktion
cab ab b c ab b
4 5
3 cab cab 5 c abc abc c
1 2 3 1 2 4
foo.h bar.h
extern int foo(); 1 extern int bar(); 1
foo.c bar.c
#include "foo.h" 1 #include "bar.h" 1
#include "bar.h" 2 2
3 3
int foo() { 4 int bar() { 4
return 2 + bar(); 5 return 20; 5
} 6 } 6
main.c
#include "foo.h" 1
2
int main() { 3
return foo() + foo(); 4
} 5
Alle Teilkomponenten lassen sich durch die folgenden Aufrufe des C-Compilers
nacheinander in Objektdateien übersetzen:
> gcc -c foo.c
> gcc -c bar.c
> gcc -c main.c
> gcc bar.o foo.o main.o -o main
Der letzte Aufruf des C-Compilers startet den Linker und bindet die drei erzeugten
Objektdateien zu einem ausführbaren Programm mit dem Namen main zusammen.
Die inkrementelle Compilierung kommt immer dann zum Einsatz, wenn im Zuge
der Weiterentwicklung nur wenige Quelldateien geändert wurden. In diesem Fall
erfolgt die Übersetzung in drei Schritten:
I Abhängigkeitsanalyse
Im ersten Schritt werden die Abhängigkeiten zwischen den einzelnen Modulen
auf Dateiebene untersucht. Das Ergebnis der Analyse ist ein Abhängigkeitsgraph,
wie er in Abb. 8.29 (links) exemplarisch für das weiter oben eingeführte Beispiel-
projekt skizziert ist. Konstruktionsbedingt entspricht jedes Blatt einer Programm-
quelle, während die inneren Knoten den zum Bau des Software-Systems benötig-
ten Zwischenerzeugnissen entsprechen. In der Terminologie der inkrementellen
Compilierung wird jeder Vaterknoten als Ziel (target) und jeder Kindknoten als
Abhängigkeit (dependency) bezeichnet.
Legende
main
Ziel
(Target)
Abhängigkeit
(Dependeny)
main.c foo.h foo.c bar.h bar.c
Hierzu reicht es aus, die Zeitstempel der Quelldateien mit denen der zuvor er-
zeugten Objektdateien abzugleichen. Vorsicht ist bei der netzwerkübergreifen-
den Entwicklung geboten: Wird der Build-Prozess auf mehrere Rechner verteilt,
muss im Vorfeld für eine exakte Synchronisation der Uhren gesorgt werden.
I Compilierung
Im dritten Schritt wird der Abhängigkeitsgraph von unten nach oben, von den
Blättern zur Wurzel, durchlaufen (Bottom-Up-Traversierung). In jedem inneren
Knoten wird das Ziel genau dann neu erzeugt, wenn es selbst geändert wurde
oder mindestens eine der Abhängigkeiten neu generiert werden musste.
Wie der Vergleich der beiden Beispiele in Abb. 8.30 demonstriert, haben die seman-
tischen Abhängigkeiten einen großen Einfluss auf die zu compilierenden Dateien.
Wird z. B. die Datei foo.h modifiziert, so müssen zunächst die Objektdateien foo.o
und main.o frisch erzeugt werden. Anschließend wird aus diesen die Programm-
datei main neu gebildet. Zwischen der Objektdatei bar.o und der Header-Datei
foo.h besteht dagegen keine Abhängigkeit, so dass die erneute Compilierung von
bar.o entfällt. Wird dagegen die Datei bar.h geändert, so müssen die Objektdatei-
en foo.o und bar.o und die Programmdatei main neu erzeugt werden. In diesem
Fall entfällt die Neuübersetzung der Datei main.c.
In der Praxis wird die inkrementelle Compilierung werkzeuggestützt durchge-
führt. Klassische Vertreter sind die Applikationen Make ([174], [103]) und Ant
([125], [6]), die auch von vielen IDEs im Hintergrund aufgerufen werden. Die
Grundprinzipien beider Werkzeuge werden in den nächsten beiden Unterabschnit-
ten genauer untersucht.
[Link] Make
Make ist ein flexibel ausgelegtes Werkzeug, mit dessen Hilfe beliebige Sequenzen
von Shell-Skript-Kommandos bedingungsgesteuert ausgeführt werden können. Ent-
wickelt wurde die Applikation Ende der Siebzigerjahre von Stuart Feldman an den
454 8 Software-Infrastruktur
Beispiel 1 Beispiel 2
main main
main.c foo.h foo.c bar.h bar.c main.c foo.h foo.c bar.h bar.c
Wird die Datei foo.h geändert, Wird die Datei bar.h geändert,
muss das Ziel bar.o nicht muss das Ziel main.o nicht
neu erzeugt werden neu erzeugt werden
Abb. 8.30 Die semantischen Abhängigkeiten entscheiden, welche Ziele neu zu erzeugen sind
Bell Laboratories in Murray Hill und gehört heute immer noch zu den am häufigsten
eingesetzten Werkzeugen im Bereich der inkrementellen Compilierung. Der große
Verbreitungsgrad geht nicht zuletzt auf die frühe Integration in die verschiedenen
Unix-Betriebssystemlinien zurück – hier ist Make immer noch ein fester Bestandteil
der Standardwerkzeugpalette. Im Laufe der Zeit wurde die Applikation in verschie-
dene Richtungen weiterentwickelt. Hierunter fallen die BSD- und GNU-Varianten
Bsdmake und Gmake, wie auch die Windows-spezifische Fortentwicklung Nmake.
Ein Warnung vorweg: Obwohl die Werkzeuge über die gleiche Grundfunktionalität
verfügen, bestehen zwischen den diversen Varianten subtile Unterschiede, die in der
praktischen Arbeit regelmäßig zu Inkompatibilitäten führen.
Vor der ersten Verwendung von Make wird zunächst eine Kontrolldatei erzeugt,
die in der Grundeinstellung den Namen Makefile trägt und eine textuelle Beschrei-
bung des Abhängigkeitsgraphen enthält. Abb. 8.31 zeigt eine entsprechende Kon-
trolldatei für die inkrementelle Compilierung des weiter oben eingeführten Beispiel-
projekts. Ein typisches Makefile ist aus den folgenden Bestandteilen aufgebaut, die
sich in angepasster Form auch in der betrachteten Beispieldatei wiederfinden lassen.
I Deklarationen (Declarations)
Der optionale Deklarationsteil steht am Anfang eines Makefiles und wird typi-
scherweise zur Definition symbolischer Bezeichner verwendet. Die Beispieldatei
in Abb. 8.31 definiert zwei Makros OBJ und CC, die in sämtlichen nachfolgenden
Abschnitten referenziert werden können. Jedes spätere Vorkommen der Symbole
${OBJ} und ${CC} wird durch die im Deklarationsteil zugewiesenen Zeichen-
ketten substituiert. Anstelle der geschweiften Makroseparatoren { und } gestattet
Make gleichermaßen die Verwendung runder Klammern ($(OBJ) bzw. $(CC)).
I Ziele (Targets)
Ziele sind nichts anderes als die Namen derjenigen Dateien, die es im Laufe des
8.2 Build-Automatisierung 455
I Abhängigkeiten (Dependencies)
Abhängigkeiten beschreiben die semantischen Beziehungen zwischen einem Ziel
und seinen Unterzielen. Grob besprochen sind in der Abhängigkeitsliste alle Da-
teien aufgelistet, die für den Bau des spezifizierten Ziels erforderlich sind. So
werden für die Erzeugung der Applikationsdatei main die drei Objektdateien
foo.o, bar.o und main.o benötigt. Für den Bau des Unterziels bar.o sind mit
bar.h und bar.c dagegen zwei Dateien ausreichend. Hinter den Zielen und Ab-
hängigkeiten eines Makefiles verbirgt sich damit nichts anderes als eine textuelle
Beschreibung des am Anfang dieses Abschnitts eingeführten Abhängigkeitsgra-
phen.
I Aktionen (Tasks)
Aktionen sind Shell-Kommandos, die das assoziierte Ziel aus den jeweiligen Ab-
hängigkeiten erzeugen. Die Aktionen des in Abb. 8.31 dargestellten Makefiles
umfassen die Compiler- und Linker-Aufrufe, die zur Übersetzung aller Objekt-
module sowie der finalen Applikationsdatei ausgeführt werden müssen.
Die Reihenfolge, in der die Ziele innerhalb eines Makefiles angeordnet sind, spielt
eine entscheidende Rolle. Ohne Angabe von Kommandozeilenparametern versucht
Make ausschließlich das erstgenannte Ziel zu erzeugen – in unseren Beispiel die
Applikationsdatei main. Anhand der vorgefundenen Abhängigkeiten und der Ak-
tualität der Zieldateien wird entschieden, welche Unterziele zusätzlich bearbeitet
werden müssen. Über die Analyse der Abhängigkeiten traversiert Make den Abhän-
gigkeitsgraphen zunächst in Richtung der Blätter und führt im Anschluss daran die
456 8 Software-Infrastruktur
Makefile
# Software-Qualität (Dirk W. Hoffmann) 1
2
all: [Link] [Link] 3
4
# Sachwortverzeichnis 5
[Link]: [Link] 6
makeindex -s [Link] [Link] 7
8
# Bibliographie und Webliographie 9
[Link]: [Link] [Link] 10
bibtex swquality 11
12
clean: 13
-rm *.ind *.nnd *.bbl *.blg *.aux chapters/*.aux 14
Abb. 8.32 Auszug aus dem Makefile zur Erzeugung dieses Buchs
benötigten Aktionen von unten nach oben aus. Das erstgenannte Ziel des Makefiles
bildet die Wurzel des Abhängigkeitsgraphen, so dass die dort verankerten Aktionen
stets zuletzt ausgeführt werden.
An dieser Stelle soll eine wesentliche Eigenschaft des forcierten Arbeitsprinzips
nicht unerwähnt bleiben: Die spezifizierten Aktionen werden von Make nicht selbst
interpretiert, sondern lediglich an eine Shell weitergereicht. Hierdurch entsteht eine
Flexibilität, die das Anwendungsspektrum weit über die inkrementelle Compilie-
rung hinaus erweitert. Als Beispiel ist in Abb. 8.32 ein Ausschnitt des Makefiles
dargestellt, das im Rahmen der Erstellung dieses Buchs verwendet wurde. Unter
anderem erzeugt der Aufruf von Make aus der Abhängigkeit [Link] bei
Bedarf das Sachwortverzeichnis [Link]. Der zu diesem Zweck aufgerufe-
ne Befehl makeindex ist Bestandteil aller gängigen Distributionen des Textsatzsys-
tems LATEX.
Auch der Funktionsumfang von Make geht weit über die in Abb. 8.31 gezeigten
Befehle hinaus. Insbesondere ist der Makro-Operator $ ein weit leistungsfähigeres
Instrument als es unser kleines Einführungsbeispiel an dieser Stelle vermuten lässt.
Neben individuell definierten Platzhaltern lassen sich drei weitere Makro-Typen un-
terscheiden:
I Vordefinierte Makros
Make stellt eine Reihe spezieller Makros zur Verfügung, die zum einen häufig
verwendete Compiler- und Linker-Befehle vordefinieren und zum anderen In-
formationen über Umgebungsparameter wie z. B. den aktuellen Pfad oder den
Login-Namen des angemeldeten Benutzers vorhalten. Einige ausgewählte Ma-
kros dieser Kategorie sind in Tabelle 8.1 zusammengefasst. Eine detaillierte Liste
der vordefinierten Werte wird durch den Aufruf von Make mit der Kommando-
zeilenoption -p auf der Konsole ausgegeben.
8.2 Build-Automatisierung 457
Makefile
FILES = main foo bar 1
OBJ = $(addsuffix .o, $(FILES)) 2
CC = gcc 3
4
main: $(OBJ) 5
$(CC) $^ -o $@ 6
7
%.o: %.c 8
$(CC) -c $< -o $@ 9
I Dynamische Makros
Einige Makros ändern ihren Inhalt in Abhängigkeit des bearbeiteten Ziels bzw.
der vorgefundenen Abhängigkeiten. Wie die Auswahl in Tabelle 8.1 zeigt, er-
möglichen dynamische Makros verschiedene Zugriffe auf das aktuelle Ziel und
dessen Abhängigkeiten, ohne die entsprechenden Dateinamen explizit zu benen-
nen. Wie wir später sehen werden, lassen sich gleichartige Ziele hierdurch auf ein
einziges, generisches Ziel reduzieren und die Länge des Makefiles in der Regel
drastisch verringern.
I Modifizierende Makros
Makros dieser Kategorie erlauben die systematische Manipulation von Varia-
bleninhalten. So lassen sich Datei- und Verzeichnisanteile aus Pfadangaben ex-
trahieren oder eine Liste von Dateinamen mit einem speziellen Suffix versehen.
Auch hier gibt Tabelle 8.1 einen Eindruck über die zur Verfügung stehenden
Möglichkeiten. Eine detaillierte Beschreibung der Makros findet sich in [174].
Mit Hilfe der vorgestellten Makro-Typen lässt sich das Beispiel-Makefile aus
Abb. 8.31 deutlich eleganter und vor allem wartungsfreundlicher formulieren. Wie
in Abb. 8.33 gezeigt, kann insbesondere die Anzahl der Ziele von vier auf zwei
reduziert werden. Neben dynamischen Makros setzt das Makefile zusätzlich das
Platzhaltersymbol (%) ein. Hierdurch wird das Ziel zu einer generischen Regel, die
Make auf alle Dateien anwendet, die mit einem .o-Suffix enden.
Ergänzend zu den applikationsspezifischen Zielen enthalten die meisten Makefi-
les zusätzliche Unterziele für die Erledigung von Standardaufgaben. Ein typischer
Vertreter dieser Kategorie ist das Standardziel clean, das in kaum einem Makefile
fehlt. Unter diesem Ziel werden alle Aktionen zusammengefasst, die der Wieder-
herstellung eines sauberen Projektzustands dienen.
Konkret bedeutet dies, dass der Aufruf make clean für gewöhnlich alle zwi-
schenzeitlich erzeugten Software-Artefakte löscht und nur die eigentlichen Pro-
grammquellen im Projektverzeichnis belässt. Ein anschließender Aufruf von Make
erzeugt das Hauptziel sowie sämtliche Unterziele von Grund auf neu. In der Termi-
nologie der inkrementellen Compilierung sprechen wir in diesem Zusammenhang
von einem Clean build.
458 8 Software-Infrastruktur
Abb. 8.34 zeigt, wie das oben eingeführte Makefile um ein entsprechendes
clean-Target erweitert werden kann. Ein geschärfter Blick auf die spezifizierten
8.2 Build-Automatisierung 459
Makefile
FILES = main foo bar 1
OBJ = $(addsuffix .o, $(FILES)) 2
CC = gcc 3
4
.PHONY: clean 5
6
main: $(OBJ) 7
$(CC) $^ -o $@ 8
9
%.o: %.c 10
$(CC) -c $< -o $@ 11
12
clean: 13
@echo ”Cleaning up...” 14
@-rm *.o 15
@-rm main 16
Aktionen bringt zwei Besonderheiten zum Vorschein. Zum einen ist clean als
Phony target deklariert. Ziele dieser Art werden selbst dann bearbeitet, wenn alle
Abhängigkeiten ein älteres Änderungsdatum aufweisen als das Ziel selbst. Fehlt die
Phony-Deklaration, so versagt der Aufruf make clean aufgrund der leeren Abhän-
gigkeitsliste in den (zugegebenermaßen seltenen) Fällen, in denen sich eine Datei
mit dem Namen clean in demselben Verzeichnis wie das Makefile befindet.
Des Weiteren wurde beiden rm-Befehlen das von Make speziell interpretierte
Präfix @- vorangestellt. Während @ die Klartextausgabe des ausgeführten Befehls
auf der Konsole unterbindet, sorgt das Minuszeichen dafür, dass die Befehlsabarbei-
tung auch dann fortgesetzt wird, falls die Ausführung eines Befehls missglückt und
mit einem Fehlercode beendet wird. Im Falle des rm-Befehls ist dies z. B. immer
dann der Fall, wenn keine Dateien gefunden werden, die den angegebenen Such-
mustern entsprechen.
460 8 Software-Infrastruktur
Neben dem Clean-Target verfügen die meisten Makefiles über weitere Standard-
ziele, von denen die wichtigsten in Tabelle 8.2 zusammengefasst sind. An dieser
Stelle gilt es zu beachten, dass es sich bei den abgebildeten Namen um pure Konven-
tionen handelt, die sich im Laufe der Zeit als De-facto-Standard etablieren konnten.
Da die Bezeichner in keiner Weise von Make interpretiert werden, können sie durch
beliebige andere Namen substituiert werden, ohne die prinzipielle Funktionsweise
zu beeinträchtigen.
Ein noch nicht angesprochenes, aber nicht minder wichtiges Standardziel ist das
Dependency target dep. Viele Makefiles verwenden ein solches Ziel, um die Ab-
hängigkeiten zwischen den Programmkomponenten eines Software-Systems auto-
matisch zu ermitteln. Insbesondere in den Programmiersprachen C und C++ lassen
sich die Querbeziehungen zwischen den Quelldateien durch eine Untersuchung der
Include-Direktiven auf syntaktischer Ebene erkennen. Für die automatische Analy-
se der Include-Beziehungen stehen dem Software-Entwickler zwei standardisierte
Methoden zur Verfügung:
I gcc -MM
Viele C-Compiler verfügen über einen Kompatibilitätsmodus, der speziell für
die Zusammenarbeit mit Make ausgelegt ist. Wird z. B. der GNU-C-Compiler
mit der Option -MM aufgerufen, so werden die Dateiabhängigkeiten durch den
Präprozessor analysiert und in der Syntax von Make ausgegeben. Der Compi-
ler und Linker wird in diesem Fall nicht aktiviert. Make selbst verfügt über eine
Include-Direktive, mit deren Hilfe sich die generierten Abhängigkeiten dyna-
misch einbinden lassen. Abb. 8.35 zeigt das auf diese Weise erweiterte Makefile
unseres Beispielprojekts.
Von nun an wird die Applikationsdatei in einem zweistufigen Prozess erzeugt.
Der erste Aufruf (make dep) sorgt zunächst für die Erstellung der Dependency-
Datei depend. Im Anschluss daran wird das Projekt durch einen wiederholten
Start von Make unter der Berücksichtigung der vorher ermittelten Abhängigkei-
ten neu übersetzt.
I makedepend
Eine alternative Möglichkeit gibt uns das externe Unix-Werkzeug Makedepend
an die Hand. Ähnlich wie im Falle des GNU-C-Compilers extrahiert die Appli-
kation die Querbeziehungen zwischen den Programmquellen durch eine Analyse
der Include-Anweisungen. Im Gegensatz zum ersten Ansatz werden die Abhän-
gigkeiten jedoch nicht mit Hilfe der Make-eigenen Include-Direktive eingelesen,
sondern direkt in das Makefile integriert. Abb. 8.36 zeigt das durch Makedepend
modifizierte Makefile unseres Beispielprojekts.
[Link] Ant
Make ist nicht das einzige Werkzeug für die inkrementelle Compilierung. Neben ei-
nigen IDE-spezifischen Lösungen konnte sich im Java-Umfeld vor allem das XML-
basierte Werkzeug Ant etablieren, das ein ähnliches Arbeitsprinzip wie Make auf-
weist. Vor der Verwendung von Ant wird die Konfigurationsdatei [Link] er-
8.2 Build-Automatisierung 461
Makefile (Variante 1)
FILES = main foo bar 1
SRC = $(addsuffix .c, $(FILES)) 2
OBJ = $(addsuffix .o, $(FILES)) 3
CC = gcc 4
5
main: $(OBJ) 6
$(CC) $^ -o $@ 7
8
%.o: %.c 9
$(CC) -c $< -o $@ 10
11
dep: $(SRC) 12
$(CC) -MM $(SRC) > dep 13
14
-include ./dep 15
Makefile (Variante 2)
FILES = main foo bar 1
SRC = $(addsuffix .c, $(FILES)) 2
OBJ = $(addsuffix .o, $(FILES)) 3
CC = gcc 4
5
main: $(OBJ) 6
$(CC) $^ -o $@ 7
8
%.o: %.c 9
$(CC) -c $< -o $@ 10
11
dep: 12
makedepend -- $(CFLAGS) -- $(SRC) 13
14
#DO NOT DELETE 15
main.o: main.c foo.h 16
foo.o: foo.c foo.h bar.h 17
bar.o: bar.c bar.h 18
zeugt, die aus funktionaler Sicht einem Makefile gleicht. Im Gegensatz zu dem
proprietären Format von Make verwendet Ant das standardisierte XML-Format.
Entsprechend unterschiedlich präsentiert sich das Erscheinungsbild beider Dateien.
Abb. 8.37 stellt den generellen Aufbau beider Formate gegenüber.
Eine vollständige Ant-Konfigurationsdatei ist in Abb. 8.38 abgebildet. Insgesamt
werden die drei Ziele init (Projekt initialisieren, Verzeichnisse erzeugen), compile
(Compiler aufrufen) und distribute (Java-Archiv erzeugen) definiert. Das Ziel
462 8 Software-Infrastruktur
I Make I Ant
Makefile [Link]
target ... : depends... 1 <project default ="..."> 1
command 2 <target name = "..." 2
3 depends = "..."> 3
4 <command> 4
5 </target> 5
6 </project> 6
[Link]
<?xml version="1.0" encoding="ISO-8859-1"?> 1
<project default="compile"> 2
<property name="src" location="src"/> 3
<property name="build" location="build"/> 4
<property name="dist" location="dist"/> 5
<target name="init"> 6
<mkdir dir ="${build}"/> 7
</target> 8
<target name="compile" depends="init"> 9
<javac src dir="${src}" destdir="${build}"/> 10
</target> 11
<target name="distribute" depends="compile"> 12
<mkdir dir="S{dist}/lib"/> 13
<jar jarfile="${dist}/lib/Dist-${DSTAMP}.jar"> 14
</target> 15
</project> 16
Makefile
SUBDIRS = src lib 1
2
all : 3
for d in $(SUBDIRS); do \ 4
(cd $d; $(MAKE) all "CFLAGS=$(DEBUGFLAGS)") \ 5
done 6
I Compile-Zeit
Die vollständige Compilierung eines komplexen Software-Systems beträgt meh-
rere Stunden bis hin zu Tagen. Um die Übersetzungszeit in akzeptablen Grenzen
zu halten, werden umfangreiche Software-Systeme auf großen Rechnerfarmen
übersetzt (cluster computing). Unterstützt eine Applikation mehrere Betriebssys-
teme und Hardware-Plattformen, so kommt das Prinzip der verteilten Compilie-
rung zwangsläufig zum Einsatz – unabhängig von der Größe der Programmquel-
len.
Um die Modularität komplexer Software-Systeme nicht durch den Einsatz einer ein-
zigen, monolithisch aufgebauten Steuerungsdatei zu zerstören, halten viele Projekte
lokale Makefiles auf Modul- oder Paketebene vor. Der gesamte Build-Prozess wird
durch ein globales Makefile koordiniert, das den Make-Prozess für alle Teilmodule
und Pakete rekursiv aktiviert. Wie ein solches Makefile strukturiert sein kann, zeigt
Abb. 8.39. Hier wird in einer For-Schleife nacheinander in die entsprechenden Un-
terverzeichnisse gewechselt und die Make-Applikation lokal gestartet. Der Aufruf
weist zwei typische Merkmale auf. Zum einen wird das vordefinierte MAKE-Makro
eingesetzt, so dass in jedem Unterverzeichnis dieselbe Applikationsvariante akti-
viert wird, die auch für die Verarbeitung der globalen Steuerungsdatei ausgewählt
wurde. Zum anderen ermöglicht Make, Makrodefinitionen über die Kommandozeile
weiterzureichen – hier demonstriert am Beispiel der Variablen CFLAGS.
464 8 Software-Infrastruktur
Makefile
OBJECT = main.o foo.o bar.o 1
CC = gcc 2
3
main: ${OBJECT} 4
@echo "Linking files..." 5
@${CC} ${OBJECT} -o main 6
@echo "main created" 7
8
main.o: foo.h bar.h 9
@echo "Compiling main.c ..." 10
@${CC} -c main.c -o main.o 11
@echo "main.o created" 12
13
foo.o: foo.h bar.h foo.c 14
@echo "Compiling foo.c ..." 15
@${CC} -c foo.c -o foo.o 16
@echo "foo.o created" 17
18
bar.o: bar.h bar.c 19
@echo "Compiling bar.c ..." 20
@${CC} -c bar.c -o bar.o 21
@echo "bar.o created" 22
Wie die Ausgabe zeigt, werden alle Ziele nacheinander abgearbeitet. Insbesondere
wird mit dem Bau des nächsten Ziels erst dann begonnen, wenn der zuvor initi-
ierte Übersetzungsvorgang vollständig abgeschlossen ist. Rufen wir Make dagegen
8.2 Build-Automatisierung 465
mit der Option -j 4 auf, so werden mehrere Instanzen des C-Compilers simultan
gestartet:
> make -j 4
Compiling main.c ...
Compiling bar.c ...
Compiling foo.c ...
main.o created
bar.o created
foo.o created
Linking files...
main created
Der Grad der Parallelisierung hängt maßgeblich von den semantischen Abhängig-
keiten der bearbeiteten Ziele ab. Da der Linker in unserem Beispiel erst dann ak-
tiviert werden kann, nachdem alle Objektdateien vollständig erzeugt wurden, lässt
sich das letzte Ziel nicht parallelisieren. Insgesamt werden in dem gezeigten Bei-
spiel damit nur 3 und nicht, wie der Kommandozeilenaufruf an dieser Stelle sugge-
riert, 4 Prozesse parallel abgearbeitet.
Die vorgestellte Parallelisierung von Make besitzt eine entscheidende Einschrän-
kung: Alle Prozesse werden zentral auf ein und demselben Rechner gestartet. Damit
lassen sich zwar die verschiedenen Recheneinheiten eines Mehrprozessorsystems
optimal nutzen, im Falle sehr großer Software-Systeme sind die Systemressourcen
jedoch vergleichsweise schnell erschöpft.
Zur Überwindung der Kapazitätsgrenze ist der Übersetzungsprozess in großen
Software-Projekten dezentral organisiert (vgl. Abb. 8.41). Im Kern der Hardware-
Infrastruktur steht mit dem sogenannten Cluster eine spezielle Rechnerfarm, die
durch die folgenden Eigenschaften charakterisiert ist:
I Kopplungsgrad
Zwischen den zahlreichen Recheneinheiten (Knoten) eines Clusters besteht eine
starke Kopplung. Alle Einheiten sind in diesem Fall an den gleichen Massenspei-
cher angebunden, so dass jeder Knoten auf die gespeicherten Daten der anderen
zugreifen kann. Die gemeinsame Anbindung wird über ein verteiltes Dateisystem
(distributed file system) erreicht. Beispiele sind das Unix-Netzwerkprotokoll
NFS (Network File System [240]) oder das Dateisystem CIFS (Common Inter-
net File System), das unter anderem im Windows-Umfeld beheimatet ist [121].
I Prozessmanagement
Die einzelnen Knoten des Clusters arbeiten im Batch-Betrieb, d. h., sie werden
nicht direkt über den Arbeitsplatzrechner des Software-Entwicklers angespro-
chen. Hierzu werden die einzelnen Übersetzungsaufträge zunächst an eine zen-
trale Recheninstanz gesandt (batch submission) und von dort auf die einzelnen
Einheiten weitergeleitet (batch distribution). Durch das zentrale Prozessmanage-
ment können die Aufträge im Rahmen einer aktiven Lastenverteilung (workload
management) gleichmäßig auf die einzelnen Cluster-Knoten verteilt werden. Auf
diese Weise wird eine optimale Ausnutzung der bereitstehenden Ressourcen er-
reicht.
466 8 Software-Infrastruktur
Rechnerfarm (Cluster)
Arbeitsplatz- Workload- .c
rechner Manager .c
.c .c
.c
Software- .c
Entwickler
.o
.c
Netzwerkdateisystem
I Verfügbarkeit
Eine typische Rechnerfarm wird von vielen Entwicklern gleichzeitig genutzt. Ei-
nige Firmen gehen sogar soweit, die zentrale Software-Infrastruktur in einem
einzigen, unternehmensweit genutzten Cluster zusammenzufassen. Da der Aus-
fall der Rechnerfarm tief greifende Folgen für die gesamte Entwicklungsarbeit
nach sich zieht, gelten besondere Anforderungen an die Verfügbarkeit und Feh-
lersicherheit. Unter dem Stichwort der Hochverfügbarkeit wurden Systeme die-
ser Art in der Vergangenheit ausführlich untersucht [210, 168, 234, 32].
Um die hohen Verfügbarkeitsanforderungen zu gewährleisten, müssen fehler-
hafte Komponenten isoliert und repariert werden können, ohne den laufenden
Betrieb zu gefährden. Ähnliche Anforderungen gelten für die Massenspeicher,
die mit technischen Maßnahmen gegen einen drohenden Datenverlust abgesi-
chert werden müssen. Typischerweise kommen für diesen Zweck RAID-Systeme
(Redundant Array of Independent Disks) zum Einsatz, die Daten redundant spei-
chern und den Austausch fehlerhafter Komponenten zur Laufzeit erlauben (hot
plugging, hot swapping) [200].
jobfile
#BSUB-q Q3 # Job queue 1
#BSUB-J test7 # Job name 2
#BSUB-o [Link] # Output file 3
#BSUB-c 5 # Time limit (5 minutes) 4
5
# Change directory 6
cd src 7
8
# Create object file 9
gcc -Wall -c foo.c 10
11
# Copy file to destination directory 12
cp foo.o ../modules 13
Abb. 8.42 Job-Datei für die Verarbeitung mit LSF (Load Sharing Facility)
ist in Abb. 8.42 eine Konfigurationsdatei abgebildet, die einen einfachen Compiler-
Aufruf initiiert. Die Job-Parameter werden innerhalb des Kommentarblocks am An-
fang der Datei festgelegt und mit dem speziellen Schlüsselwort BSUB als solche
gekennzeichnet. Die Beispieldatei legt fest, dass der Arbeitsauftrag zunächst in die
Warteschlange (queue) Q3 eingefügt werden soll und von dort aus zu einem späte-
ren Zeitpunkt auf einen der Rechnerknoten des LSF-Pools verteilt wird. Der zweite
und dritte Konfigurationsparameter legen den Namen des Jobs sowie die Ausgabe-
datei fest. Zu guter Letzt wird mit BSUB-c 5 ein fünfminütiges Zeitlimit definiert.
Länger laufende Prozesse werden von LSF automatisch terminiert, so dass die In-
tegrität des Clusters nicht durch abgestürzte oder fehlerhafte Prozesse längerfristig
beeinträchtigt werden kann.
Auf der Konsole kann ein Batch-Prozess mit Hilfe des bsub-Kommandos gestar-
tet werden. Alle abgesendeten Prozesse werden von LSF selbstständig auf die freien
Cluster-Knoten verteilt und für den Benutzer unsichtbar im Hintergrund ausgeführt.
Statusinformationen über die wartenden und die aktuell durch LSF bearbeiteten Pro-
zesse lassen sich mit dem bjobs-Kommando auf der Konsole ausgeben:
> bsub < jobfile
Job <1155> is submitted to queue Q3
8.3 Testautomatisierung
Neben der Übersetzung der Programmquellen wird in großen Software-Projekten
auch der Software-Test so weit wie möglich automatisiert durchgeführt. Der Grad
der erreichbaren Automatisierung hängt neben der Prüfebene vor allem auch von
den Schnittstellen ab, die ein Software-System nach außen zur Verfügung stellt. In
der Praxis spielt insbesondere die automatisierte Durchführung von Regressions-
tests eine hervorgehobene Rolle. Im nächsten Abschnitt werden wir die zur Verfü-
gung stehenden Automatisierungsmöglichkeiten im Detail kennen lernen und uns in
Abschnitt 8.3.2 anschließend mit den Herausforderungen beschäftigen, vor die uns
der automatisierte Test graphischer Benutzungsoberflächen stellt.
8.3.1 Regressionstests
Wie bereits in Abschnitt 4.3.7 ausführlich diskutiert, wird ein Regressionstest immer
dann durchgeführt, wenn die bestehende Version eines Software-Systems verändert
wurde. Die Testserie soll sicherstellen, dass sich im Zuge einer Weiterentwicklung
oder Fehlerkorrektur keine neuen Defekte unbemerkt in die Code-Basis einschlei-
chen. Die praktische Durchführung von Regressionstests wird insbesondere durch
die folgenden beiden Randbedingungen beeinflusst:
I Effizienz
Die schiere Größe realer Regressionsdatenbanken macht eine Automatisierung
schon aus Gründen der Effizienz zwingend notwendig. Die erzielte Zeiter-
sparnis wiegt den zusätzlich benötigten Entwicklungsaufwand bemerkenswert
schnell auf. So setzen viele erfahrene Software-Entwickler bereits in Ein-Mann-
Projekten auf eine automatisierte Testdurchführung [15, 159]. Eine Warnung vor-
weg: Trotz des erheblichen Effizienzgewinns darf der verbleibende Personalein-
satz in großen Software-Projekten selbst bei einer hundertprozentigen Automa-
tisierung nicht unterschätzt werden. Zum einen erfordert die komplexe Testin-
frastruktur einen hohen administrativen Aufwand, zum anderen müssen im Zuge
der Weiterentwicklung auch die bestehenden Testtreiber regelmäßig überarbeitet
werden [177].
I Reproduzierbarkeit
Durch die automatisierte Durchführung von Regressionstests lässt sich der au-
genblickliche Zustand eines Software-Systems reproduzierbar erfassen. In vie-
len Firmen werden die Testergebnisse statistisch ausgewertet und als Grundlage
für die weitere Projektplanung verwendet. Mitunter enthalten die Regressionsda-
tenbanken verschiedene Testkategorien, die unterschiedliche Aspekte eines neu
übersetzten Systems überprüfen. Typischerweise werden direkt im Anschluss an
die Build-Phase eine kleine Gruppe elementarer Tests durchgeführt, die lediglich
die Basisfunktionalität der erzeugten Programmdateien überprüfen (smoke tests).
Grobe Fehler, die z. B. während der Startphase bereits zu einem Systemabsturz
führen, werden auf diese Weise zeitnah erkannt. Hierdurch wird die sinnlose Ab-
arbeitung der vollständigen Regressionsdatenbank verhindert.
8.3 Testautomatisierung 469
Rechnerfarm (Cluster)
Regressions- Workload-
datenbank Manager
Testtreiber
Fehlerberichte, Statistik
=?
Referenzausgabe Ausgabe
(golden log) (log &le) Netzwerkdateisystem
Von der technischen Seite betrachtet unterscheidet sich der automatisierte Software-
Test kaum von der dezentralen Compilierung. Aufgrund ihrer großen Anzahl wer-
den die Testfälle im Batch-Betrieb auf die verschiedenen Knoten einer Rechnerfarm
verteilt und dezentral bearbeitet (vgl. Abb. 8.43). Anders als im Falle der verteilten
Compilierung wird ein einzelner Testfall jedoch stets lokal auf einem singulären
Knoten ausgeführt. Im Falle von Systemtests wird eine Verteilung der Testfälle auf
diejenigen Rechnerknoten angestrebt, die der kundenseitig eingesetzten Hardware
möglichst nahe kommen.
Wie in Abb. 8.43 ebenfalls angedeutet, werden die Ergebnisse der Regressions-
tests gesammelt und gemeinsam ausgewertet. Typischerweise wird der Ausgang ei-
nes einzelnen Tests einer von drei Kategorien zugeordnet:
I Erfolg (Success)
Der Software-Test wurde erfolgreich durchgeführt und das ermittelte Ist-
Ergebnis stimmt mit dem festgelegten Soll-Ergebnis überein.
I Fehler (Error)
Der Software-Test konnte durchgeführt werden, das ermittelte Ist-Ergebnis und
das vorgegebene Soll-Ergebnis weichen jedoch voneinander ab. Ein derartiger
Fehler liegt z. B. dann vor, wenn ein Funktionsaufruf einen abweichenden Wert
berechnet.
I Fehlschlag (Failure)
Während der Testdurchführung wurde ein Systemversagen festgestellt. Ein Fehl-
schlag liegt z. B. dann vor, wenn das zu testende System einen Programmabsturz
verursacht und hierdurch erst gar kein Ist-Ergebnis ermittelt werden konnte.
470 8 Software-Infrastruktur
[Link]
Test
<< interface >>
run(TestResult)
TestCase TestSuite
run(TestResult) run(TestResult)
runTest() addTest(Test)
setUp()
tearDown()
runTest()
[Link]
import [Link].*; 1
import static [Link].*; 2
import [Link].*; 3
4
public class BinarySearchTest { 5
6
private int[] a = new int[17]; 7
8
9
@Before 10
public void setUp() { 11
a[0] = 10; a[1] = 12; a[2] = 22; a[3] = 28; 12
a[4] = 30; a[5] = 51; a[6] = 52; a[7] = 60; 13
a[8] = 62; a[9] = 81; a[10]= 82; a[11]= 89; 14
a[12]= 90; a[13]= 92; a[14]= 92; a[15]= 93; 15
a[16]= 99; 16
17
} 18
19
@Test 20
public void binarySearch1() { 21
assertTrue([Link](a, 89) == 11); 22
} 23
24
@Test 25
public void binarySearch2() { 26
assertTrue([Link](a, 28) == 4); 27
} 28
29
@Test 30
public void binarySearch3() { 31
assertTrue([Link](a, 100) < 0); 32
} 33
34
public static void main(String args[]) { 35
[Link]("BinarySearchTest"); 36
} 37
} 38
Abb. 8.45 Unit-Test der Methode binarySearch auf Basis des JUnit-Frameworks
8.3.2 Oberflächentests
Der automatisierte Oberflächentest überprüft die funktionalen oder temporalen Ei-
genschaften eines Software-Systems auf der Ebene der grafischen Benutzungsober-
fläche (graphical user interface, kurz GUI) [160].
Diese Spielart des Software-Tests vereint zwei entscheidende Vorteile. Zum
einen wird die Applikation durch die Testumgebung wie durch einen realen Be-
nutzer angesteuert und damit die höchstmögliche Ebene der Systemarchitektur an-
8.3 Testautomatisierung 473
I Zusicherungen
Zusicherung Geprüfte Eigenschaft
assertTrue (expr) Der Übergabewert ist logisch wahr
assertFalse (expr) Der Übergabewert ist logisch falsch
assertNull (expr) Der Übergabewert ist die Nullreferenz
assertNotNull (expr) Der Übergabewert ist nicht die Nullreferenz
assertSame (obj1, obj2) Indentität: Beide Objektreferenzen sind gleich
assertNotSame (obj1, obj2) Beide Objektreferenzen sind ungleich
assertEquals (obj1, obj2) Äquivalenz: Beide Objekte sind gleich
gesprochen. Zum anderen sind GUI-Tests die einzige Möglichkeit, funktionale Feh-
ler in der grafischen Benutzungsschnittstelle aufzudecken. Bei der Durchführung
eines automatisierten GUI-Tests gilt es zu beachten, dass stets das zugrunde liegen-
de Software-System und niemals die Benutzungsschnittstelle selbst im Mittelpunkt
steht. Wichtige Eigenschaften, wie die intuitive Benutzerführung oder die Einhal-
tung von Style-Guides, werden im Rahmen von Ergonomietests geprüft. Diese las-
sen sich aus technischer Sicht nur sehr schwer oder gar nicht automatisieren und
werden daher in aller Regel manuell durchgeführt.
In der Praxis werden automatisierte Oberflächentests insbesondere durch die fol-
genden Rahmenbedingungen erschwert:
I Auswahl
Moderne grafische Benutzungsoberflächen stellen dem Benutzer eine Vielzahl
von Eingabemöglichkeiten und Optionen zur Verfügung. Werden die primitiven
Operationen zudem miteinander kombiniert, entsteht eine unüberschaubare An-
zahl möglicher Testfälle. Die gezielte Reduktion der Freiheitsgrade unter gleich-
zeitiger Erhaltung einer hohen Testabdeckung ist damit auch im Falle des GUI-
Tests eine der Hauptherausforderungen [175]. Erschwerend kommt hinzu, dass
die Ablaufgeschwindigkeit durch den interaktiven Charakter nicht beliebig be-
schleunigt werden kann. Im schlimmsten Fall muss der Test mit der gleichen
Geschwindigkeit durchgeführt werden, mit der ein menschlicher Anwender die
Applikation bedient. Insgesamt kann der Gesamtaufwand eines GUI-Tests hier-
durch beträchtlich steigen.
474 8 Software-Infrastruktur
I Komplexität
Die Durchführung eines ausführlichen Oberflächentests erfordert zahlreiche In-
teraktionen mit der Benutzungsschnittstelle. Hierfür ist ein ausgeklügelter Me-
chanismus notwendig, der die getestete Applikation effizient und reproduzier-
bar in einen bestimmten Zustand versetzt und gleichzeitig eine hohe Robust-
heit gegen äußere Einflussfaktoren aufweist. Typische Testsysteme erlauben hier-
für z. B. das Einfügen künstlicher Wartezeiten, um die Auswirkungen unvor-
hergesehener Verzögerungszeiten abzufedern. Eine ähnliche Robustheitsproble-
matik entsteht im Zusammenhang mit unvorhersehbar auftretenden Dialogen.
Zu den klassischen Beispielen zählen Fenster, die auf eine mögliche Software-
Aktualisierung hinweisen, genauso wie Meldungen, die den Benutzer in unre-
8.3 Testautomatisierung 475
Checkpoints Fehlerberichte,
Statistik
gelmäßigen Abständen daran erinnern, nicht mehr verwendete Symbole auf dem
Desktop zu besitzen.
I Wartung
Nicht selten erfahren grafische Oberflächen im Zuge eines Versionswechsels si-
gnifikante Änderungen. Entsprechend häufig müssen Oberflächentests überarbei-
tet und an die neue Benutzungsschnittstelle angepasst werden. Im direkten Ver-
gleich mit Testfällen auf der Unit-Ebene sind GUI-Testtreiber in der Regel deut-
lich komplexer aufgebaut und müssen bei einer Änderung der Bedienerführung
in großen Teilen überarbeitet werden. Damit gehört der Oberflächentest insge-
samt zu den wartungsintensivsten Varianten des Software-Tests.
I Program
Aus den gesammelten Daten wird ein Skript-ähnlicher Ablaufplan erzeugt, mit
dem sich die aufgezeichnete Benutzeraktion originalgetreu reproduzieren lässt.
Die gängigen GUI-Testsysteme verwenden hierzu textbasierte Formate, wie z. B.
476 8 Software-Infrastruktur
Winrunner
# Select the "Start" menu at the lower left corner 1
set_window ("Shell_TrayWnd", 3); 2
button_press ("Start"); 3
4
# Launch Internet Explorer 5
set_window ("Start Menu_1", 6); 6
list_deselect_item ("SysListView32_0", "Internet"); 7
8
# Go to [Link] and search for "Software Qualität" 9
set_window ("Google - Microsoft Internet Explorer", 3); 10
edit_set ("Edit", "[Link]"); 11
obj_type ("Edit", "<kReturn>"); 12
obj_type ("Internet Explorer_Server", 13
"Software Qualität<kReturn>"); 14
I Replay
Mit Hilfe der im zweiten Schritt generierten Skript-Datei wird die aufgezeich-
nete Benutzerinteraktion reproduziert. An jedem Checkpoint wird überprüft, ob
bestimmte Ereignisse ausgelöst, festgelegte Zustände erreicht oder die initiier-
ten Aktionen mit einem Fehler beendet wurden. In der Regel greift der Replay-
Generator direkt in das Message-Passing-System der grafischen Benutzungsober-
fläche ein und generiert intern die exakt gleichen Nachrichten, die auch das Be-
triebssystem bei der nativen Verwendung von Maus und Tastatur erzeugt. Da
sich die simulierten Benutzerinteraktionen faktisch nicht mehr von real getätig-
ten Eingaben unterscheiden, erreicht der Capture-Replay-Test ein Höchstmaß an
Realitätsnähe.
Abb. 8.48 vermittelt einen greifbaren Eindruck über die im Rahmen eines GUI-
Tests erzeugten Replay-Skripte. Die abgebildete Datei wurde mit dem Werk-
zeug WinRunner erzeugt, das auf die automatisierte Überprüfung komplexer GUI-
Interaktionen ausgelegt ist [176]. WinRunner verwendet eine proprietäre Eingabe-
sprache namens TSL (Test Script Language), die in Aufbau und Funktion einem
klassischen Shell-Skript ähnelt. Die abgebildete Beispieldatei besteht aus drei Tei-
len und führt eine einfache Google-Suche mit Hilfe des Internet Explorers durch.
8.4 Defektmanagement 477
8.4 Defektmanagement
Die Begriffe Defekt- bzw. Fehlermanagement subsumieren alle Techniken und Me-
thoden, die einen strukturierten Umgang mit Software-Anomalien innerhalb des Ent-
wicklungsprozesses gewährleisten. Hierunter fallen alle Auffälligkeiten des beob-
achteten Systems, die von einer oder mehreren Personen als potenzieller Software-
Fehler interpretiert werden.
Der abstrakt gehaltene Begriff der Anomalie wird in diesem Zusammenhang aus
zweierlei Gründen verwendet. Zum einen erweist sich nicht jede Auffälligkeit nach-
träglich als wirklicher Defekt – der systematische Nachweis, dass tatsächlich ein
Fehlverhalten vorliegt, ist bereits Teil des Defektmanagements. Zum anderen ist die
Bezeichnung deutlich neutraler als der negativ belegte Begriff des Fehlers2 . Die
vorzeitige Bezeichnung einer Anomalie als Fehler kann im komplizierten Geflecht
zwischenmenschlicher Verhaltensmuster schnell zu Missstimmungen führen – nicht
zuletzt steckt hinter jeder Zeile Code auch ein Software-Entwickler aus Fleisch und
Blut.
8.4.1 Fehlerdatenbanken
In nahezu allen größeren Software-Projekten werden bekannte und neu gefunde-
ne Anomalien in Form von Fehlerberichten in einer Datenbank gespeichert (vgl.
Abb. 8.49). Typischerweise haben die folgenden Nutzergruppen Zugriff auf die ein-
gestellten Datensätze:
I Software-Tester
Wird im Rahmen der Produktverifikation bzw. -validation eine Anomalie kon-
statiert, erzeugt der Software-Tester einen Eintrag in der Fehlerdatenbank. Die-
ser wird anschließend zur weiteren Bearbeitung an die Entwicklungsabteilung
übergeben.
I Software-Entwickler
Über die Testdatenbank hat der Software-Entwickler die Möglichkeit, eine ge-
naue Fehlerbeschreibung einzusehen. Sobald der Defekt korrigiert wurde, wird
die Lösung in der Fehlerdatenbank dokumentiert.
I Projektleitung (Management)
Eine gut gepflegte Fehlerdatenbank dient nicht nur der Kommunikation zwischen
2 Im englischen Sprachgebrauch wird der Begriff error ebenfalls weitgehend vermieden. Stattdes-
sen wird hier bewusst auf die Begriffe incident oder issue ausgewichen.
478 8 Software-Infrastruktur
Management
Abnahme
Bericht,
Software- Statistik
Tester
Meldung Übernahme
Meldung Korrektur
Fehler-
Software-
datenbank
Entwickler
Feedback
Kunde
I Kunden
In vereinzelten Fällen erhalten auch die Kunden selbst Zugriff auf die Fehlerda-
tenbank. So besitzen bestimmte Anwender die Möglichkeit, selbst Fehlerberichte
einzustellen bzw. den aktuellen Stand der Bearbeitung abzufragen. Die Fehler-
meldung durch den Kunden kann manuell oder automatisiert erfolgen (vgl. Ab-
schnitt 8.4.2).
Für die Unterhaltung einer Fehlerdatenbank existieren heute zahlreiche leistungs-
fähige Bug-Tracking-Systeme. Zu den bekanntesten Werkzeugen aus dem Open-
Source-Bereich zählen unter anderem die Systeme Bugzilla und Mantis [276].
[Link] Fehlermerkmale
Typische Bug-Tracking-Systeme verwenden zur Ablage eines Fehlerberichts vor-
definierte Template-Strukturen, die sich zwischen den verschiedenen Werkzeugen
meist nur geringfügig unterscheiden. Die geforderten Angaben lassen sich in Iden-
tifikationsmerkmale, Klassifikationsmerkmale und Beschreibungsmerkmale unter-
gliedern (vgl. Tabelle 8.4):
I Identifikationsmerkmale
Hierzu gehören eine eindeutig vergebene Identifikationsnummer, der Name und
die Version des getesteten Software-Artefakts, die Hardware- und Software-
Umgebung, in der die Anomalie beobachtet wurde, sowie das Datum der Ein-
tragserstellung.
8.4 Defektmanagement 479
I Klassifikationsmerkmale
Für jeden in der Datenbank gespeicherten Eintrag wird die Schwere der beob-
achteten Anomalie ermittelt und die Priorität für dessen Bearbeitung festgelegt.
Andere Attribute umfassen eine Bestätigung der Reproduzierbarkeit sowie den
aktuellen Bearbeitungszustand des Fehlers.
I Beschreibungsmerkmale
Zu dieser Klasse gehören alle Merkmale, die zur genaueren Beschreibung des
Fehlverhaltens und der vermuteten Fehlerursache dienen. Die meisten Bug-
Tracking-Werkzeuge erlauben, ganze Dateien in die Testdatenbank einzustellen.
Diese können weiterführende Dokumente und vor allem auch konkrete Testfälle
zur Fehlerreproduktion umfassen.
Identifikationsmerkmale
ID : Eindeutige Identifikationsnummer (automatisch vergeben)
Category : Auf welches Produkt oder Projekt bezieht sich der Fehler?
Reporter : Von wem wurde der Eintrag erstellt?
Date submitted : Wann wurde der Eintrag erstellt?
Last update : Wann wurde der Eintrag zuletzt geändert?
Assigned To : Welchem Mitarbeiter wurde der Fehler zugeteilt?
Klassifikationsmerkmale
Severity : Wie schwerwiegend ist der Fehler?
Priority : Wie dringlich ist die Korrektur?
View status : Für wen ist der Fehlereintrag sichtbar?
Resolution : Auf welche Weise wurde der Fehler korrigiert?
Status : In welchem Bearbeitungszustand befindet sich der Fehler?
Relationship : Steht der Fehlereintrag in Bezug zu anderen Einträgen?
Beschreibungsmerkmale
Summary : Kurzbeschreibung / Titel des Fehlers
Description : Ausführliche Beschreibung des Fehlverhaltens
Attachments : Zusätzlich abgelegte Dateien (z. B. Testfälle)
Bugzilla
Mantis
Bugzilla
!!! Weniger wäre mehr:
Mit jedem neuen Freiheitsgrad wird die Ein-
ordnung eines Fehlers zunehmend schwieriger
P5 P4 P3 P2 P1
Mantis
Abb. 8.51 Defekt- und Prioritätsklassen der Bug-Tracking-Systeme Bugzilla und Mantis
[Link] Zustandsmodelle
Von der Entdeckung bis zur Korrektur durchläuft ein Software-Fehler in größeren
Projekten zahlreiche Bearbeitungsstufen. Der aktuelle Bearbeitungszustand eines
Eintrags gehört zu den wichtigsten Informationen, die in der Fehlerdatenbank ge-
speichert werden. Das Zustandsübergangsmodell (state model) legt fest, in welcher
Reihenfolge die Zustände durchlaufen werden und unter welchen Bedingungen ein
Zustandswechsel erfolgt.
Abb. 8.52 fasst das Zustandsmodell des Bug-Tracking-Systems Bugzilla grafisch
zusammen. Insgesamt werden die folgenden 7 Bearbeitungszustände unterschieden:
I Zustand Unconfirmed
Gegenwärtig ist noch ungeklärt, ob die beschriebene Anomalie tatsächlich auf
einen Software-Fehler zurückzuführen ist oder ganz einfach einer Fehlinterpre-
tation des Berichterstatters entspringt. Der Zustand Unconfirmed ist der Initial-
zustand für jeden neu in die Datenbank eingestellten Bericht.
482 8 Software-Infrastruktur
Bug wird
wiedereröffnet
reopened veri$ed
closed
I Zustand New
Es liegt eine Bestätigung vor, dass es sich bei der gemeldeten Anomalie tatsäch-
lich um einen Software-Fehler handelt, der weiter verfolgt werden muss. Neben
der Legitimation durch einen mit entsprechenden Privilegien ausgestatteten Be-
nutzer implementiert Bugzilla ein spezielles Voting-System. Erhält ein Fehlerbe-
richt genug Stimmen, wird er automatisch in den Zustand New versetzt, auch
wenn keiner der abstimmenden Nutzer über die entsprechenden Privilegien ver-
fügt.
I Zustand Assigned
Der Fehler ist einem Software-Entwickler zugewiesen und wird von diesem aktiv
bearbeitet. Typischerweise wird die Verantwortlichkeit manuell durch den Pro-
jektleiter festgelegt oder von einem Entwickler in Eigenregie übernommen. In
einigen Firmen ist der Entwicklungsprozess so aufgesetzt, dass Fehler automa-
tisch an die zuvor ausgewählten produkt- oder modulverantwortlichen Mitarbei-
ter verteilt werden.
I Zustand Resolved
Der zuständige Software-Entwickler hat die Bearbeitung der Programmquellen
8.4 Defektmanagement 483
I Zustand Reopened
Die durchgeführte Korrektur wurde als unzureichend befunden und muss ver-
worfen werden. In der Regel wird der Fehler in diesem Fall zur Nachbearbeitung
erneut an einen Software-Entwickler übergeben.
I Zustand Verified
Die Korrektur wurde überprüft und akzeptiert. Der Bearbeitungsvorgang ist da-
mit beendet und der Fehlerbericht bereit, um geschlossen zu werden.
I Zustand Closed
Der Fehlerbericht wurde geschlossen, bleibt aber zum Zweck der Nachverfol-
gung und der Aufbereitung statistischer Informationen weiterhin in der Daten-
bank gespeichert. Erweist sich die durchgeführte Korrektur im Nachhinein als
unzureichend, so kann der Fehlerbericht wiedereröffnet werden.
Das Zustandsmodell des Bug-Tracking-Systems Mantis kennt ebenfalls 7 Zustän-
de, die in Abb. 8.53 zusammengefasst sind. Genau wie Bugzilla kennt auch Mantis
den Zustand New, weist diesem jedoch eine andere Bedeutung zu. Semantisch ent-
spricht der Mantis-Zustand New dem Bugzilla-Zustand Unconfirmed, während der
Bugzilla-Zustand New dem Mantis-Zustand Acknowledged bzw. dem Zustand Con-
firmed entspricht. Für den Fall, dass zur weiteren Bearbeitung des Fehlerberichts
zusätzliche Informationen erforderlich sind, stellt Mantis den Zustand Feedback be-
reit, der vor dem Übergang nach Acknowledged eingenommen wird. Die Zustände
Assigned, Resolved und Closed besitzen in beiden Systemen die gleiche Bedeutung.
Auf die zusätzliche, in Bugzilla fest verankerte Abnahmestufe Verified wird in Man-
tis verzichtet.
Im Gegensatz zu Bugzilla lassen sich in Mantis alle Zustandsübergänge frei kon-
figurieren. Für jeden Zustand werden die möglichen Folgezustände in der sogenann-
ten Workflow Transition Matrix global festgelegt. Ausgestattet mit den entsprechen-
den Administrationsrechten lässt sich die Matrix flexibel konfigurieren und auf diese
Weise z. B. auch das Bugzilla-Zustandsübergangsmodell weitgehend nachbilden. In
der Standardeinstellung sind beliebige Zustandstransitionen erlaubt.
[Link] Defektkategorien
Spätestens beim Schließen eines Fehlerberichts wird in der Datenbank festgehalten,
in welche Defektkategorie der bearbeitete Fehler fällt. Für diesen Zweck bieten die
gängigen Bug-Tracking-Systeme mehrere vordefinierte Kategorien an, aus denen
der Software-Entwickler frei wählen kann. In Tabelle 8.5 sind die Defektkategori-
en der Bug-Tracking-Systeme Bugzilla und Mantis exemplarisch gegenübergestellt.
484 8 Software-Infrastruktur
Anomalie wird
gemeldet
new
Sammeln zusätzlicher Work'ow Transition Matrix
Informationen
acknowledge
feedback
con&rmed
feedback
assigned
resolved
Der Fehler wird
closed
bestätigt
new
acknowledged
new
con&rmed feedback
Zuweisung an acknowledge
den Entwickler
con&rmed
assigned
assigned
Korrektur durch
den Entwickler resolved
resolved
closed
Bug wird
geschlossen
closed
Wie die Tabelle zeigt, ist die Fehlerkategorisierung in den verschiedenen Systemen
uneinheitlich gelöst.
Die Definition einer vergleichsweise großen Anzahl von Defektlassen macht an
dieser Stelle durchaus Sinn, schließlich trägt die zugewiesene Kategorie – anders als
im Falle des Schweregrads und der Priorität – wichtige semantische Informationen
in sich.
[Link] Rollen
In den obigen Ausführungen haben wir an mehreren Stellen mit dem Software-
Entwickler und dem Software-Tester bereits zwei wichtige Rollen ins Spiel ge-
bracht. Im Groben entsprechen diese verschiedenen Tätigkeitsbeschreibungen und
sind nicht an individuelle Mitarbeiter gebunden – insbesondere kann ein und der-
selbe Mitarbeiter mehrere Rollen gleichzeitig begleiten. Neben der operativen Tä-
tigkeit ist eine Rolle stets auch mit bestimmten Privilegien verbunden. So sind viele
Entwicklungsprozesse so aufgesetzt, dass nur der Software-Tester mit den nötigen
Rechten versehen ist, um einen Fehlerbericht zu schließen.
Der Rollengedanke wird von den meisten Werkzeugen weitreichend unterstützt.
So stellt z. B. das Bug-Tracking-System Mantis in der Standardeinstellung 6 ver-
8.4 Defektmanagement 485
Lösungskategorien in Bugzilla
Fixed Der Fehler ist beseitigt und getestet.
Invalid Das geschilderte Problem ist kein Fehler.
Won’t fix Der Fehler wird nicht korrigiert.
Duplicate Der Fehler ist ein Duplikat eines anderen Fehlers.
Worksforme Das Fehlverhalten kann nicht nachvollzogen werden.
Moved Der Fehler wurde in eine andere Datenbank übertragen.
Lösungskategorien in Mantis
Open Es existiert noch keine Lösung.
Fixed Der Fehler ist beseitigt und getestet.
Reopened Der Fehlerbericht wurde erneut geöffnet.
Unable to reproduce Das Fehlverhalten kann nicht nachvollzogen werden.
Not fixable Der Fehler lässt sich nicht beheben.
Duplicate Der Fehler ist ein Duplikat eines anderen Fehlers.
No change required Es sind keine Änderungen an der Software nötig.
Suspended Die Bearbeitung des Bugs wurde zurückgestellt.
Won’t fix Der Fehler wird nicht korrigiert.
Administrator
Developer
Developer
Manager
Manager
Reporter
Reporter
Updater
Updater
Viewer
Viewer
View Assign
Report Move
Monitor Delete
Update Reopen
Handle Update readonly
schiedene Rollen zur Verfügung, die dem jeweiligen Inhaber unterschiedliche Rech-
te gewähren. Tabelle 8.6 enthält einen Auszug aus dem umfassenden Konfigurati-
onsmenü und gibt einen ersten Eindruck über die in der Standardeinstellung verteil-
ten Privilegien. Mantis erlaubt die freie Konfiguration der Rechte, so dass einzelnen
Rollen neue Privilegien zugewiesen oder bestehende Privilegien aberkannt werden
können.
Fehler, die erst während des Betriebs durch den Kunden bemerkt werden, unter-
scheiden sich in zwei wesentlichen Punkten:
I Von externer Seite gemeldete Anomalien müssen mit höchster Priorität behan-
delt werden, insbesondere wenn hierdurch ein schwerwiegendes Fehlverhalten
verursacht wird. In vielen Fällen können nicht behobene Software-Fehler hohe
Vertragsstrafen nach sich ziehen oder gar die Sicherheit von Mensch und Ma-
schine gefährden.
I Manuelle Berichterstellung
In vielen Fällen haben Anwender einen direkten Zugriff auf die Fehlerdaten-
bank – wenn auch mit deutlich eingeschränkten Rechten. Insbesondere in Open-
Source-Projekten wird dieser Ansatz zur Fehlerverwaltung und -korrektur mit
Nachdruck verfolgt.
In vielen industriellen Projekten stellt der Software-Hersteller dem Kunden
spezielle Applikations-Ingenieure an die Seite, die eine Schnittstelle zwischen
Auftraggeber und Entwickler bilden und beiden Seiten beratend zur Seite stehen.
In diesem Fall erfolgt die Erstellung eines Fehlerberichts nicht mehr durch den
Kunden selbst, sondern vor Ort durch den verantwortlichen Ansprechpartner.
I Automatisierte Berichterstellung
Der Fehlerbericht wird selbstständig erstellt und automatisiert an den Hersteller
übermittelt. Da der Fehlerbericht sensible Daten über die Hard- und Software-
konfiguration des Anwenders beinhaltet, erfolgt die Datenübertragung in der Re-
gel erst nach der expliziten Zustimmung des Benutzers. Im direkten Vergleich
mit der manuellen Berichterstellung ist die automatisierte Meldung weit weniger
flexibel. Insbesondere ist die Technik auf bestimmte Fehlerklassen beschränkt,
verlangt dafür aber keinerlei Expertise des Anwenders.
Die selbstständige Aufbereitung von Crash reports ist ein Spezialfall der automati-
sierten Berichterstellung. Viele moderne Betriebssysteme setzen diese Technik ein,
um die während eines Systemabsturzes protokollierten Daten in geregelter Art und
Weise an den Hersteller zu übermitteln.
Mit dem Erscheinen von Windows XP hat Microsoft die WER-Technologie (Win-
dows Error Reporting) ins Leben gerufen, deren Grundstruktur in Abb. 8.54 darge-
stellt ist. WER arbeitet als globaler Exception-Handler und wird aktiviert, sobald die
8.4 Defektmanagement 487
System-
Anwender
absturz
Fehler- WER-Datenbank
Bericht
Daten-
aufbereitung
Web-
Hersteller
interface
aktuell laufende Applikation einen Systemabsturz verursacht. Wird kein eigener, lo-
kal verankerter Exception-Handler verwendet, so übergibt Windows die Kontrolle
an das Dienstprogramm [Link]. Das Programm sammelt Informationen über
die Absturzursache und präsentiert dem Benutzer einen interaktiven Dialog, den je-
der von uns aus schmerzlicher Erfahrung kennt. Entscheidet sich der Benutzer für
das Senden eines Fehlerberichts, so werden die aufbereiteten Daten über eine ver-
schlüsselte HTTP-Verbindung an den WER-Server von Microsoft übermittelt.
Dort angekommen wird der Fehlerbericht mit den bereits gespeicherten Berich-
ten abgeglichen. Hierzu werden die gesammelten Daten verdichtet und der empfan-
gene Bericht einem bestimmten Bucket zugeordnet. Neben den Produktnamen und
Revisionsnummern werden für die Zuordnung neu eingegangener Fehlerberichte
auch die Offset-Adressen der fehlerauslösenden Befehle ausgewertet und auf die-
se Weise Duplikate mit hoher Sicherheit erkannt. Jeder Bucket verfügt über einen
Referenzzähler, der die Anzahl der Doppelmeldungen protokolliert. Die Erkennung
von Duplikaten ist ein Kernelement der WER-Technik und ermöglicht, die gespei-
cherten Fehlerberichte im Laufe der Zeit automatisch zu priorisieren.
488 8 Software-Infrastruktur
Command: TeXShop
Path: /Applications/[Link]/Contents/MacOS/TeXShop
Parent: WindowServer [53]
PID: 241
Thread: 0
Thread 0 Crashed:
0 [Link] 0x90a564c7 objc_msgSend + 23
Die gesammelten Datenbestände stehen nicht nur Microsoft, sondern auch dem
Urheber der fehlerverursachenden Software zur Verfügung. Über ein spezielles
Web-Interface kann jeder registrierte Hersteller die aufbereiteten Fehlerberichte der
eigenen Software einsehen und auf die gesammelten Debug-Informationen frei zu-
greifen.
Die statistische Aufbereitung der eingehenden Berichte und die damit zusam-
menhängende Priorisierung ist eine unbestrittene Stärke des Crash reportings. Die
ungleiche Häufigkeitsverteilung der Systemabstürze wurde von Microsoft vor al-
lem in den Beta-Phasen der Betriebssysteme XP und Vista ausgenutzt. Durch die
statistische Analyse der Datenbankbestände konnte die Arbeit stets auf diejenigen
Systemabstürze konzentriert werden, die sich im Feld am häufigsten manifestieren.
Auf der 2003 in Los Angeles abgehaltenen Microsoft Professional Developers Con-
ference beschreibt Bill Gates die Vorteile des WER-Systems im Zusammenhang mit
der Identifikation instabiler Gerätetreiber wie folgt:
8.4 Defektmanagement 489
“One thing that’s been amazing at Microsoft is the impact that our mo-
nitoring data has had on how we prioritize our software work. I’m sure
you’ve all seen in Windows XP that whenever an application or the sys-
tem malfunctions, you get the ability to send a report back to Microsoft. We
get a lot of those reports, and we’ve created very good data-management
systems to go in and look at those things, and therefore understand what
drivers aren’t reliable.”
Bill Gates, PDC 2003
Obwohl die richtige Verwertung der statistischen Daten zu einer nennenswerten
Steigerung der Software-Qualität führen kann, wird das Vorgehen nicht von allen
Seiten befürwortet. Einige Kritiker sehen in der statistischen Auswertung der WER-
Datenbestände die endgültige Verlagerung der Fehlersuche auf den Rücken des An-
wenders. Die Zukunft wird zeigen, ob sich der Kunde auf Dauer mit der Rolle des
Beta-Testers abfinden wird – Wahlalternativen vorausgesetzt.
Die automatische Übermittlung von Crash reports ist mittlerweile in vielen Be-
triebssystemen und Anwendungen fest verankert. In der freien GNOME-Plattform
verrichtet ein Werkzeug namens Bug Buddy seine Dienste und die Mozilla-
Corporation setzt einen Quality feedback agent namens Talkback ein.
In Apples Betriebssystem Mac OS X wird die Funktion durch den CrashReporter
übernommen (Abb. 8.55), der betriebssystemseitig als Hintergrundprozess gestartet
wird. Sobald ein Programm einen Systemabsturz verursacht, wird ein Crash report
erzeugt und ein entsprechender Eintrag in die systeminterne Log-Datei geschrieben.
Über den angezeigten Dialog hat der Benutzer die Möglichkeit, den im Hintergrund
aufbereiteten Fehlerbericht zu übertragen.
Kapitel 9
Managementprozesse
Eingangs- Ausgangs-
größen Aktivität größen
Aktivität
Aktivität
Aktivität
Aktivität Aktivität
Ressourcen
Abb. 9.1 Prozesse beschreiben sich stets wiederholende Arbeitsabläufe eines Unternehmens
I Reifegradmodelle
Reifegradmodelle verfolgen das Ziel, die Arbeitsabläufe eines Unternehmens zu
analysieren und zu optimieren. Hierzu werden die Prozesse einer Organisation
anhand einer definierten Systematik auf Projekt- und Unternehmensebene be-
wertet und die gewonnenen Ergebnisse anschließend in Maßnahmen zur Pro-
zessverbesserung umgesetzt.
Neben der Schaffung produktiver Entwicklungsprozesse zielen Reifegradmo-
delle vor allem auf die Optimierung der Organisationsstrukturen ab. Hierzu zäh-
len unter anderem die eindeutige Verteilung von Rollen und Verantwortlichkei-
ten, die Einrichtung transparenter Kommunikationswege und die Definition klar
geregelter Eskalationsmechanismen. Damit unterscheidet sich die Zielsetzung ei-
nes Reifegradmodells grundsätzlich von der eines Vorgehensmodells. Während
Vorgehensmodelle Eckpunkte und Maßnahmen für die konkrete Projektdurch-
führung definieren, liefern Reifegradmodelle die quantitative Basis, um steuernd
in die Arbeitsabläufe und Organisationsstrukturen einzugreifen.
Im Folgenden werden wir diejenigen Vorgehens- und Reifegradmodelle genauer
beleuchten, die in der industriellen Software-Entwicklung heute eine bedeutende
Rolle spielen. Wir beginnen unseren Streifzug in Abschnitt 9.1 mit der Vorstellung
der gängigen Vorgehensmodelle. Neben einem Rückblick auf die historischen Wur-
zeln werden wir das V-Modell, den Rational Unified Process (RUP) sowie das ver-
gleichsweise neue Modell des Extreme Programmings (XP) im Detail betrachten.
Im Anschluss daran werden wir in Abschnitt 9.2 mit dem CMM, CMMI und SPI-
CE drei der am weitesten verbreiteten Reifegradmodelle genauer unter die Lupe
nehmen.
9.1 Vorgehensmodelle 493
Operational Plan
Program Speci!cation
Coding speci!cations
Coding
Shakedown
Design
Abb. 9.2 Ein Stück Geschichte: Das Stagewise model aus dem Jahre 1956
9.1 Vorgehensmodelle
9.1.1 Wasserfallmodell
Die steigende Komplexität moderner Software-Systeme macht es heute in immer
stärkerem Maße erforderlich, die Entwicklung zu systematisieren und in Form von
bewährten Methodiken (best practices) durchzuführen. Die Wurzeln moderner Vor-
gehensmodelle lassen sich bis in die frühen Tage der Software-Technik zurückver-
folgen. Der erste Grundstein wurde von Herbert D. Benington im Jahre 1956 und
damit zeitgleich mit der Entwicklung der ersten Hochsprachen gelegt. Mit dem Sta-
gewise model schlug er eine phasenorientierte Vorgehensweise für die Software-
Entwicklung vor, die bereits viele Elemente moderner Vorgehensmodelle in sich
trägt (vgl. Abb. 9.2) [20].
Winston W. Royce entwickelte die Ideen von Benington weiter und veröffent-
lichte im Jahre 1970 ein Vorgehensmodell, das heute vor allem unter dem Namen
Wasserfallmodell bekannt ist [224]. Royce selbst bezeichnete die von ihm postulier-
te Vorgehensweise schlicht als “Implementation steps to develop a large program
for delivery to a customer”. Der heute geläufige Name Wasserfallmodell wurde erst
11 Jahre später durch den amerikanischen Software-Ingenieur Barry W. Boehm ge-
prägt [30].
Abb. 9.3 zeigt den Aufbau des Wasserfallmodells in der ursprünglich von Roy-
ce publizierten Form. Das Modell besteht aus insgesamt 7 Phasen, die kontinuier-
494 9 Managementprozesse
Systemanforderungen
Software-Anforderungen
Analyse
Entwurf
Implementierung
Test
Betrieb
lich durchlaufen werden. Jedes Projekt beginnt mit der Analyse der Anforderun-
gen. Royce sieht hierfür zwei Stufen vor, in denen die System- und die Software-
Anforderungen in zeitlich getrennter Reihenfolge erstellt werden. Anschließend
werden die Analyse-, Entwurfs- und Implementierungsphase durchlaufen. Sind die-
se vollständig abgearbeitet, so liegt als Ergebnis das fertige Software-System vor,
das in den beiden letzten Phasen getestet und an den Kunden übergeben wird.
In einem idealisierten Projektverlauf werden die verschiedenen Phasen des Was-
serfallmodells streng sequenziell durchlaufen. Wie in Abb. 9.3 bereits eingezeich-
net, sieht Royce zusätzliche Rückkopplungsschleifen vor, die ein gewisses Vor und
Zurück zwischen den Phasen zulässt. Um die serielle Natur des Modells nicht auf-
zubrechen, ist ein Rücksprung jedoch auf die jeweils direkt vorangehende Phase
beschränkt. Wurde eine Phase erfolgreich abgeschlossen, so wird der Projektfort-
schritt in einem Dokument fixiert. Royce verwirklichte damit bereits sehr früh zwei
Schlüsselkonzepte, die in den meisten modernen Vorgehensmodellen in ganz ähnli-
cher Form vorhanden sind: Dokumente und Meilensteine.
I Dokumente
Das Wasserfallmodell ist dokumentengetrieben und unterstreicht hierdurch die
Tatsache, dass Software aus mehr als nur den Quelltexten besteht. Wie ein Blick
in die Originalarbeit zeigt, nimmt die Dokumentation für Royce eine zentrale
Rolle in der gesamten Software-Entwicklung ein:
“At this point it is appropriate to raise the issue of – «how much docu-
mentation?» My own view is «quite a lot;» certainly more than most pro-
grammers, analysts, or program designers are willing to do if left to their
own devices. The first rule of managing software development is ruthless
enforcement of documentation requirements. [...] Management of software
is simply impossible without a very high degree of documentation. As an
example, let me offer the following estimates for comparison. In order to
9.1 Vorgehensmodelle 495
I Meilensteine
Die systematische Erstellung von Dokumenten trägt dazu bei, dass die einzelnen
Projektphasen durch klar definierte Meilensteine voneinander getrennt werden.
Diese machen den Projektfortschritt messbar und stellen für Royce gleichzeitig
eine Art Rückfallposition dar. Stagniert der Projektfortschritt in einer Phase, so
kann ein Projekt jederzeit auf einem klar definierten Zwischenstand wieder auf-
setzen:
“At any point in the design process after the requirements analysis is com-
pleted there exists a firm and closeup, moving baseline to which to return
in the event of unforeseen design difficulties. What we have is an effecti-
ve fallback position that tends to maximize the extent of early work that is
salvageable and preserved.”
Winston W. Royce [224]
9.1.2 V-Modell
Das V-Modell ist eine Erweiterung des Wasserfallmodells, das zahlreiche Aspekte
der Software-Qualitätssicherung zusätzlich in das Modell integriert. Wie in Abb. 9.4
dargestellt, bleiben die Kernphasen des Wasserfallmodells erhalten, werden jedoch
durch einen zusätzlich hinzugefügten Ast komplettiert. Jede vertikale Ebene spie-
gelt eine bestimmte Entwicklungsstufe wider, deren Abstraktionsgrad sich von oben
nach unten kontinuierlich verringert. Der linke Zweig des Modells repräsentiert die
einzelnen Entwicklungsschritte und der rechte Zweig die hieraus abgeleiteten Qua-
litätsmaßnahmen der gleichen Abstraktionsebene.
Wie die Achsenbeschriftung in Abb. 9.4 zeigt, führt das V-Modell eine Unter-
scheidung der Begriffe Verifikation und Validation ein:
I Validation
Im Rahmen der Validation wird die Spezifikation eines Produkts im Hinblick
auf den geplanten Einsatzzweck überprüft. Ob die Spezifikation im Rahmen der
9.1 Vorgehensmodelle 497
Anforderungs- Use-Cases
Abnahmetest
analyse
on
ati
lid
Va
Testfälle, Use-Cases
Systementwurf Systemtest
Software-
S f Testfälle
Integrationstest
Architektur
En
n
tio
tw
ka
Test-
ick
r i$
lun
fälle
Ve
Spezi$kation Unit-Test
g
Implementierung
I Verifikation
Im Zuge der Verifikation wird überprüft, ob das entwickelte Produkt die Spe-
zifikation erfüllt. Kurzum: Die Implementierung wird gegen die Spezifikation
abgeglichen. Die Spezifikation selbst wird nicht in Frage gestellt und als korrekt
vorausgesetzt. Der klassische Software-Test fällt vollständig in den Bereich der
Verifikation – hier wird das gemessene Ist-Ergebnis mit dem aus der Spezifikati-
on abgeleiteten Soll-Ergebnis abgeglichen.
Insgesamt forciert das V-Modell, wie auch das Wasserfallmodell, den klassischen
Top-Down-Entwurf . Die einzelnen Phasen werden im Idealfall konsekutiv durch-
laufen, wobei zwischen zwei benachbarten Phasen Übergänge in beide Richtun-
gen möglich sind. Zwischen den Entwicklungsphasen des linken Zweigs und den
Verifikations- und Validationsphasen des rechten Zweigs besteht ein unmittelbarer
Zusammenhang, der in Abb. 9.4 durch die waagerechten Pfeile symbolisiert wird.
Es werden die Ergebnisse der verschiedenen Entwicklungsphasen unmittelbar für
die Ableitung der Testfälle verwendet, die in den späteren Verifikations- und Vali-
dationsphasen zur Überprüfung der Programm- und Systemkomponenten eingesetzt
werden. Die Ableitung der Testfälle erfolgt bereits während der Entwicklung, so
498 9 Managementprozesse
Produktgruppe Aktivitätsgruppe
dass zwischen den beiden Zweigen eine engere Bindung besteht, als die klassischen
Visualisierungen des Modells auf den ersten Blick suggerieren.
[Link] V-Modell XT
Das in Abb. 9.4 skizzierte V-Modell wurde in den Folgejahren in verschiedene Rich-
tungen weiterentwickelt. Eine dieser Weiterentwicklungen ist das V-Modell XT (XT
= eXtreme Tailoring), das im Jahre 2005 in der Version 1.0 veröffentlicht wurde und
heute für die Planung und Durchführung von IT-Projekten des Bundes verbindlich
vorgeschrieben ist [70, 219]. Jeweils zum 1. Februar und 1. August eines Jahres sieht
das Modell eine Aktualisierungsmöglichkeit vor. Die bis dato letzte Aktualisierung
wurde am 1. Februar 2006 mit dem Erscheinen der Version 1.2 vollzogen [71].
Mit dem V-Modell XT wurde ein umfangreiches Vorgehensmodell geschaffen,
das die V-förmige Vorgehensweise aus Abb. 9.4 in sich integriert, darüber hinaus
jedoch nur wenig mit dem ursprünglichen Modell gemeinsam hat.
Das Fundament des V-Modells XT wird durch verschiedene Vorgehensbaustei-
nen gebildet (vgl. Abb. 9.5). Für die gängigen Aufgabenstellungen eines Projekts
hält das Modell eine Reihe vordefinierter Bausteine vor, die das Zusammenspiel von
Produkten, Aktivitäten und Rollen festlegen. Die Produkte eines Bausteins entspre-
chen den Arbeitsergebnissen, die durch die festgelegten Aktivitäten erzeugt werden.
Innerhalb eines Vorgehensbausteins lässt die Definition von Teilaktivitäten und un-
tergeordneten Produkten (Themen) eine hierarchische Gliederung entstehen. Über
definierte Rollen legt jeder Vorgehensbaustein die Verantwortlichkeiten für die ver-
schiedenen Produkte und Aktivitäten fest. Die Vorgehensbausteine sind in sich ab-
geschlossen, interagieren jedoch miteinander über den Austausch der Arbeitsergeb-
nisse. Die Definition entsprechender Abhängigkeiten und Querverbindungen ist ex-
pliziter Bestandteil des Modells.
Das V-Modell XT trägt der Tatsache Rechnung, dass verschiedene Projekte in der
Praxis unterschiedlich ablaufen und einer individuellen Anpassung der Arbeitsab-
läufe bedürfen. Um ein möglichst breites Spektrum abzudecken, wird jedes Projekt
anhand gewisser charakteristischer Eigenschaften klassifiziert und einem hieraus
9.1 Vorgehensmodelle 499
abgeleiteten Projekttypus zugeordnet. Der Typ eines Projekts wird zum einen durch
den Projektgegenstand und zum anderen durch die Projektrolle bestimmt:
I Projektgegenstand
Mit dem Begriff des Projektgegenstands wird im V-Modell XT das Ergebnis be-
zeichnet, das im Rahmen eines Projekts erarbeitet wird. In den meisten Fällen ist
der Projektgegenstand ein zu erstellendes System, das einer der folgenden fünf
Kategorien zugeordnet werden kann:
– Hardware-System
– Software-System
– Komplexes System
– Eingebettetes System
– Systemintegration
Alternativ kann sich der Projektgegenstand auf die Einführung und Pflege ei-
nes organisationsspezifischen Vorgehensmodells beziehen. Dieser Projektgegen-
stand nimmt im V-Modell XT eine besondere Stellung ein.
I Projektrolle
Das V-Modell XT definiert 6 verschiedene Projektrollen, die primär das
Auftraggeber- und Auftragnehmerverhältnis charakterisieren. Von außen be-
trachtet kann ein Projekt als Auftraggeber (AG), als Auftragnehmer (AN) oder
in beiden Rollen zusammen (AG/AN) agieren. Werden zusätzlich die verschie-
denen Möglichkeiten der Unterauftragsvergabe berücksichtigt, ergeben sich die
folgenden 6 Projektrollen des V-Modells XT:
– Auftraggeber mit einem Auftragnehmer
Anhand des Projektgegenstands und der Projektrolle wird im V-Modell XT der Typ
eines Projekts abgeleitet. Jeder Projekttyp wird durch eine bestimmte Projektdurch-
führungsstrategie beschrieben. Diese definiert eine Menge verpflichtender sowie ei-
ne Menge optionaler Vorgehensbausteine, die zur Projektdurchführung zwingend
500 9 Managementprozesse
12 2
1 Projektmanagement 13 Vertragsabschluss (AN)
34
12 2
2 Qualitätssicherung 14 Systemerstellung
34 3
12 2
3 K
Kon?gurationsmanagement 15 Hardware-Entwicklung
34 3
Problem- und 12 2
4 16 Software-Entwicklung
Änderungsmanagement 34 3
12 2
5 Lieferung und Abnahme (AG)
Li 17 Logistikkonzeption
3 3
12 2
6 Vertragsabschluss (AG) 18 Benutzbarkeit und Ergonomie
Be
3 3
1 Weiterentwicklung und 2
7 Anforderungsfestlegung 19
3 Migration von Altsystemen 3
12 1
9 Systemsicherheit 21 Multi-Projektmanagement
3
15 16
17 17
18 1 2 3 4 6 18 13 14
1 2 3 4 6 13 14
7 12
8 11
9 10
17
1 2 18 14
1 2 4 5 6 18 13 14
7 12
8 11
9 10
19 20 21
Abb. 9.7 Ablaufrahmen und Entscheidungspunkte der vier Projekttypen des V-Modells XT
I Systementwicklungsprojekt (AN)
In Projekten dieses Typs agiert das betrachtete Unternehmen als Auftragnehmer.
Im Rahmen des Projekts müssen zunächst Angebote erstellt und im Falle einer
Auftragserteilung entsprechende Verträge geschlossen werden. Nach einem er-
folgreichen Vertragsabschluss wird das vereinbarte System entwickelt, getestet
und anschließend an den Auftraggeber ausgeliefert.
I Systementwicklungsprojekt (AG/AN)
Auftraggeber und Auftragnehmer sind nicht in jedem Fall getrennte Unterneh-
men. In vielen Projekten agiert ein Unternehmen in beiden Rollen zugleich. Dies
ist immer dann der Fall, wenn ein Projekt vollständig innerhalb einer Organisa-
tion durchgeführt wird – in diesem Fall werden der Auftraggeber und der Auf-
tragnehmer häufig durch verschiedene Fachabteilungen repräsentiert. Ebenfalls
in diese Kategorie fallen Projekte, in denen zwei oder mehrere Unternehmen im
Rahmen einer speziellen Kooperation sehr eng zusammenarbeiten. Im Gegensatz
zu Projekten, in denen das Unternehmen entweder als Auftraggeber oder als Auf-
tragnehmer auftritt, erübrigt sich bei dieser Konstellation die Ausschreibungs-
und Vertragsphase. Zusätzlich entfällt die im Falle zweier unabhängig operieren-
den Unternehmen unvermeidliche Doppelorganisation.
die durch den Kauf von Rational im Jahre 2003 auch das Vorgehensmodell über-
nahm.
Das Fundament des Rational Unified Process besteht aus insgesamt 6 best prac-
tices, die allgemein anerkannte und in der Vergangenheit bewährte Arbeitsabläufe
und Vorgehensweisen in sich vereinen:
I Iterative Software-Entwicklung
Der Rational Unified Process trägt der Tatsache Rechnung, dass komplexe
Software-Systeme in der Praxis nicht in Form eines streng sequenziell durchlau-
fenen Modells entwickelt werden können. RUP forciert stattdessen ein iteratives
Vorgehen, das ein Produkt schrittweise wachsen lässt. Am Ende jeder Iteration
steht ein funktionsfähiger Software-Prototyp.
Die kontinuierliche Erstellung von Zwischenprodukten besitzt mehrere ent-
scheidende Vorteile. Zum einen lässt sich hierdurch der aktuelle Projektfort-
schritt auf objektive Art und Weise ermitteln. Zum anderen wird die Kommu-
nikation mit der Kundenseite deutlich erleichtert und Abweichungen zwischen
dem real entwickelten und dem gewünschten Produkt in einer frühen Projekt-
phase erkannt. Die Entwicklung wird dabei stets durch ein aktives Risikomana-
gement begleitet. Hierzu werden die Risiken in jeder Projektphase bewertet und
in die Planung der nächsten Arbeitsschritte einbezogen.
I Anforderungsmanagement
Der Rational Unified Process beschreibt ausführlich die verschiedenen Bereiche
des Anforderungsmanagements. Hierzu gehören die Herausarbeitung, die Orga-
nisation und die Dokumentation der Anforderungen eines Software-Systems. Für
die Anforderungsbeschreibung greift RUP extensiv auf den Formalismus der Use
cases zurück, die wir bereits in Abschnitt 4.3.4 im Zusammenhang mit der Kon-
struktion von Black-Box-Tests kennen gelernt haben.
I Komponentenbasierte Architekturen
Durch die iterative Vorgehensweise entsteht das erste funktionsfähige Software-
System zu einem vergleichsweise frühen Projektzeitpunkt. Um eine schrittweise
Weiterentwicklung in verschiedene Richtungen zu ermöglichen, wird eine ro-
buste und flexible Software-Architektur angestrebt. RUP verfolgt hierzu einen
komponentenbasierten Ansatz. Eine Komponente im Sinne des Rational Uni-
fied Process ist ein nichttriviales Software-Modul, das eine semantisch klar um-
rissene Aufgabe vollzieht. RUP beschreibt ein systematisches Vorgehen für die
Erstellung einer adäquaten Software-Architektur. Die Wiederverwendbarkeit be-
stehender Komponenten wird hierbei explizit angestrebt.
I Visuelle Software-Modellierung
Für die Verhaltens- und Strukturbeschreibung eines Software-Systems greift der
Rational Unified Process auf visuelle Modelle zurück. Der Einsatz einer grafi-
schen Repräsentation erlaubt es, von den technischen Details eines Software-
Systems zu abstrahieren und ist mit der Hoffnung verbunden, die Systemkom-
plexität auf ein handhabbares Maß zu reduzieren. Die Grundlage für die visuelle
504 9 Managementprozesse
Darstellung von RUP bildet die Unified Modeling Language (UML). Dass in
diesem Modell ausgerechnet die UML als Modellierungssprache zum Einsatz
kommt ist bei weitem kein Zufall. Schließlich war es das Unternehmen Ratio-
nal selbst, das die UML in den Neunzigerjahren als Modellierungssprache ent-
wickelte.
I Software-Qualitätskontrolle
Die Software-Qualitätssicherung ist ein weiteres Kernelement des Rational Uni-
fied Processes. Das Vorgehensmodell unterstützt den Software-Entwickler bei
der Planung, dem Entwurf, der Implementierung und der Ausführung speziel-
ler Tests, die der Sicherstellung der verschiedenen Qualitätsparameter dienen.
Hierzu gehören insbesondere die Zuverlässigkeit, die Funktionalität und die
Applikations- und System-Performance der erstellten Software-Artefakte. RUP
integriert zahlreiche Assessment-Maßnahmen in den Prozess, mit deren Hilfe die
verschiedenen Qualitätsparameter sowie der Projektfortschritt einer möglichst
objektiven Messung unterzogen werden sollen.
I Software-Änderungskontrolle
Durch ein definiertes Change-Management trägt der Rational Unified Process
der Tatsache Rechnung, dass Software ständigen Änderungen unterliegt. Das ite-
rative Vorgehen von RUP fordert, dass Modifikationen kontrolliert eingepflegt
werden, ohne die Stabilität des erreichten Projektzustands zu gefährden. Das Vor-
gehensmodell definiert hierzu mehrere Arbeitsabläufe für die Verwaltung und die
geregelte Durchführung von Änderungen. Eine wichtige Rolle spielt in diesem
Zusammenhang die Versionskontrolle, die zum einen alle Artefakte transparent
verwaltet und durch die Schaffung individuell abgeschotteter Arbeitsumgebun-
gen die Voraussetzung für die Team-Arbeit innerhalb eines Projekts schafft.
Project Management
Environment
Business Modeling
Requirements
Core Engineering
Work&ows
Implementation
Test
Deployment
I Environment workflow
üben eine unterstützende Funktion innerhalb eines Software-Projekts aus. Unter an-
derem sind dort alle Aktivitäten enthalten, die der Bereitstellung der technischen
Infrastruktur dienen und die Projektdurchführung in organisatorischer Hinsicht un-
terstützen. Im Gegensatz hierzu decken die Workflows
I Business modeling workflow,
I Requirements workflow,
I Implementation workflow,
I Test workflow,
I Deployment workflow
alle Aktivitäten ab, die sich in direkter Weise mit der Erstellung des Software-
Systems beschäftigen. Sie werden in der RUP-Terminologie auch als Core enginee-
ring workflows bezeichnet. Obwohl die Namen der Workflows große Gemeinsam-
keiten mit den Phasenbezeichnungen des Wasserfallmodells aufweisen, unterschei-
den sich beide Vorgehensmodelle in einem zentralen Punkt. Während das Wasser-
fallmodell eine streng konsekutive Abarbeitung der verschiedenen Phasen verfolgt,
sind diese im Rational Unified Process ineinander verschränkt. Wie die Verlaufs-
kurven in Abb. 9.8 andeuten, werden die einzelnen Workflows zeitgleich bearbeitet,
wenn auch mit unterschiedlicher Intensität. Die zeitliche Überlagerung ist eine we-
sentliche Stärke des Rational Unified Processes und bildet die Realität der Software-
Entwicklung weit besser ab, als alle streng sequenziell konzipierten Vorgehensmo-
delle.
I Einfachheit (Simplicity)
Entwickler sind durch den XP-Prozess angehalten, sich stets auf die einfachste
Lösung zu konzentrieren. Diese Vorgehensweise ist mit der Einsicht verbunden,
dass die Schwierigkeit, ein Software-System fehlerfrei zu entwickeln, mit zu-
nehmender Systemkomplexität überproportional zunimmt. Wird an den richti-
gen Stellen auf unnötige Komplexität verzichtet, kann zum einen die Anzahl der
Fehler reduziert und zum anderen das gleiche Produkt in deutlich geringerer Zeit
entwickelt werden.
I Rückmeldung (Feedback)
Die Aktivitäten und Prozesse von XP sind darauf ausgelegt, effizient arbeiten-
de Rückmeldemechanismen zu schaffen, die zu jeder Zeit über den aktuellen
Zustand des Projekts bzw. des entwickelten Systems Auskunft geben. Entspre-
chende Feedback-Kanäle werden auf verschiedenen Ebenen eingerichtet:
– System-Feedback
XP forciert die Erstellung umfassender Unit-Tests während der gesamten
Software-Entwicklung. Die automatisierte Ausführung einer Test-Suite gibt
nach kurzer Zeit Auskunft über den aktuellen Zustand der Software, ohne
508 9 Managementprozesse
– Kunden-Feedback
Im Rahmen eines XP-Projekts werden in regelmäßigen Abständen Akzep-
tanztests erstellt. Bei der Ausarbeitung der Testfälle ist sowohl die Kundensei-
te als auch die Herstellerseite in Person der Software-Tester beteiligt. Durch
dieses Vorgehen erhält der Auftraggeber in regelmäßigen Abständen Feed-
back über den aktuellen Zustand des bestellten Systems. Zusätzlich werden
die Anforderungen und Wünsche des Kunden ohne Umwege an das Projekt-
team kommuniziert.
– Team-Feedback
Reicht der Auftraggeber einen Änderungswunsch (change request) in Form
einer Story card ein, wird diese durch eine sich unmittelbar anschließende
Aufwandsschätzung quittiert. Auf diese Weise erhält der Kunde eine unmittel-
bare Rückmeldung über die Machbarkeit, den Aufwand und die entstehenden
Kosten der nachgefragten Änderung.
I Mut (Courage)
XP appelliert wie kaum ein anderes Vorgehensmodell an den Mut der Entwickler.
So werden grundlegende Änderungen an der Systemarchitektur akzeptiert, wenn
sie aus Software-technischer Sicht gerechtfertigt sind. Das Festhalten an subopti-
malen Architekturen oder Implementierungen wird selbst dann abgelehnt, wenn
die Änderungen einen erheblichen Aufwand verursachen. Hinter diesem Para-
digma verbirgt sich die Sichtweise, dass die Fokussierung auf die Produktquali-
tät eine essentielle Rolle für den Projekterfolg spielt. Wird diese Vorgehensweise
konsequent verfolgt, so übersteigen die Ersparnisse, die durch eine kontinuierlich
optimierte Software-Struktur langfristig entstehen, den kurzfristig zu erbringen-
den Refactoring-Aufwand um ein Vielfaches.
I Respekt (Respect)
Die ursprüngliche Beschreibung des XP-Modells leitete die postulierte Vorge-
hensweise aus den bisher vorgestellten Werten Kommunikation, Einfachheit,
Rückmeldung und Mut ab [14]. Mit dem Erscheinen der zweiten Ausgabe von
Extreme Programming Explained wurden diese um den fünften Wert Respekt er-
gänzt [17]. Die Erweiterung bringt zum Ausdruck, dass das Gelingen eines Pro-
jekts nicht zuletzt von einer gut funktionierenden Team-Psychologie abhängt.
Faktoren wie Motivation und Teamgeist sind Erfolgsbausteine, die in anderen
Vorgehensmodellen häufig stiefmütterlich behandelt oder gar vollständig igno-
riert werden. In XP nehmen diese einen prominenten Platz ein.
Die fünf Werte des Extreme Programmings charakterisieren die Philosophie des
Vorgehensmodells, legen jedoch selbst noch keine konkreten Arbeitsabläufe und
Handlungen fest. Stattdessen dienen sie als Nährboden, aus denen die verschie-
denen XP-Praktiken erwachsen. Die meisten Praktiken sind auch hier sogenannte
9.1 Vorgehensmodelle 509
Planungsspiel
Vierzigstundenwoche
Metapher
Einfaches Design
Refactoring
Programmier-
standards
Gemeinsame Verantwortlichkeit Fortlaufende Integration
Best practices und damit Vorgehensweisen, die ihren Nutzen in der täglichen Arbeit
vielfach unter Beweis stellen konnten und im Zusammenspiel ihre volle Wirkung
entfalten. Die aus den fünf Werten abgeleiteten Praktiken lassen sich in insgesamt
12 Hauptverfahrensbereiche einteilen (vgl. Abb. 9.9):
I Metapher (Metaphor)
Im literarischen Sinne ist eine Metapher die bildliche Umschreibung eines kon-
kreten Sachverhalts. Der XP-Gedanke legt die Verwendung solcher bildlicher
Beschreibungen für die Spezifikation eines Software-Systems auf oberster Ebe-
ne nahe. Die Vorgehensweise ist mit der Hoffnung verbunden, eine teamüber-
greifende Grundidee für die zu erstellende Software zu generieren, ohne diese
in umfangreichen Dokumenten umständlich niederlegen zu müssen. So gibt die
Metapher Rechenblatt für eine Software zur Gehaltsberechnung bereits eine ers-
te Idee, wie die zu erstellende Applikation später aussehen wird. Die gewählte
Metapher ist dabei nicht in jeder Hinsicht wörtlich zu nehmen – sie definiert
lediglich eine Richtung, die in einzelnen Aspekten von dem gewählten Begriff
abweichen darf. Sie beschreibt wesentliche Aspekte eine Systems, die durch
die Software-Architektur im Detail festgelegt werden. Damit unterstützen Me-
taphern die Beschreibung der Software-Architektur, können diese aufgrund ihrer
abstrakten Natur aber nicht ersetzen.
heutigen Belange wird von XP bewusst angestrebt und steht im Gegensatz zur
klassischen Lehrmeinung, die einen eher weitsichtigen Entwurf forciert. Obwohl
dieses Vorgehen berechtigte Fragen aufwirft, bietet es einige beachtliche Vor-
teile. Im Besonderen wird der ausufernden Komplexität eines Software-Systems
aktiv entgegengewirkt.
I Test (Testing)
Das Rückgrat von XP wird nicht zuletzt durch die auf allen Ebenen angestrebte
Testautomatisierung gebildet. Testfälle sind für XP das Mittel der Wahl, um den
aktuellen Zustand eines Software-Systems an den Entwickler oder den Kunden
zurückzumelden. Die Automatisierung derselben ist nicht minder wichtig. Zum
einen werden die Testergebnisse aufgrund ihrer Reproduzierbarkeit auf eine ob-
jektive Basis gestellt, zum anderen reduziert die automatisierte Durchführung
den zu erbringenden Zusatzaufwand auf ein Minimum. Der Software-Test ist in
diesem Vorgehensmodell so tief verankert, dass für jede Programmeigenschaft
explizit die Erstellung eines Testfalls gefordert wird. Typische XP-Projekte be-
ginnen daher in der Regel mit der Erstellung von Unit-Tests und nicht mit der
Implementierung des eigentlichen Codes.
I Refactoring
XP ermutigt seine Entwickler, stets die beste Software-Architektur und
-Implementierung für das aktuell zu lösende Problem zu wählen. Wartungsar-
beiten an der inneren Code-Struktur gehören hierdurch zur täglichen Arbeit und
werden durch das Vorgehensmodell explizit eingefordert. In diesem Punkt un-
terscheidet sich XP von der landläufigen Vorgehensweise in IT-Projekten. Re-
factorings, d. h. die Durchführung von Code-Änderungen bei gleichzeitiger Be-
wahrung der alten Funktionalität, werden in der Praxis vergleichsweise selten
durchgeführt und durch das Management in der Regel nicht gefördert (vgl. Ab-
schnitt 7.3.1). Der Hauptgrund liegt zum einen in einer fehlenden Weitsicht – die
Bedeutung einer sauberen Architektur und Implementierung wird für den lang-
fristigen Erfolg eines Produkts häufig unterschätzt. Zum anderen wird an dieser
Stelle Arbeit investiert, die nach außen hin nicht sichtbar ist. Wird der vorhan-
dene Termindruck in einem Projekt entsprechend groß, so wird das Refactoring
oft hinten angestellt und die zur Verfügung stehende Arbeitskraft auf Tätigkeiten
verlagert, die einen scheinbaren Projektfortschritt nach außen hin dokumentie-
ren.
ße, so besteht die Gefahr, dass die gemeinsame Verantwortung schwindet und
das Projekt in den initialen, ungeregelten Zustand zurückfällt. An dieser Stelle
gilt es zu bedenken, dass XP vor allem für Projekte kleinerer und mittlerer Größe
konzipiert ist. Hier erweist sich das Prinzip der gemeinsamen Verantwortlichkeit
durchaus als eine vielversprechende Vorgehensweise.
9.2 Reifegradmodelle
Während die im letzten Abschnitt vorgestellten Vorgehensmodelle einen idealisier-
ten Projektverlauf beschreiben, dienen Reifegradmodelle zur Bewertung und Opti-
mierung der Prozesse eines Unternehmens. In der Vergangenheit haben sich ver-
schiedene Modelle etabliert, die mehrheitlich durch die folgenden Eigenschaften
charakterisiert sind:
I Stufen
Die Reife einer Organisation wird mit Hilfe eines Stufenmodells beschrieben.
Hierzu wird das Unternehmen entweder als Ganzes klassifiziert oder die Be-
wertung für inhaltlich getrennte Schlüsselbereiche individuell durchgeführt. Die
verschiedenen Stufen werden in der Regel konsekutiv durchlaufen, so dass kein
Reifegrad übersprungen werden kann.
I Assessments
Die Anforderungen einer Reifestufe sind so angelegt, dass sie einer objektiven
Prüfung unterzogen werden können. Die Bewertung der Reife wird in Form ei-
nes Assessments durchgeführt. Die vorgefundenen Arbeitsabläufe und Strukturen
werden hierzu systematisch mit den Anforderungen des Reifegradmodells abge-
glichen und die erreichte Reifestufe festgelegt.
Assessments sind ein wesentlicher Kern aller Reifegradmodelle und verfol-
gen zwei maßgebliche Ziele: Zum einen wird durch die Reifegradbestimmung
die Leistungsfähigkeit verschiedener Unternehmen messbar und untereinander
vergleichbar. Zum anderen werden im Rahmen eines Assessments Hinweise und
Vorschläge erarbeitet, wie die Prozess- und Arbeitsstrukturen verbessert werden
können. Je nachdem, von wem und in welcher Form die Bewertung durchgeführt
wird, fällt ein Assessment in eine von mehreren Kategorien:
– Selbst- versus Fremd-Assessments
Selbst-Assessments dienen zur Bewertung der unternehmenseigenen Reife.
Im Rahmen eines Fremd-Assessments wird dagegen die Leistungsfähigkeit
eines anderen Unternehmens evaluiert. Eingesetzt wird die Fremdbewertung
insbesondere in der Auswahl von Subunternehmern und Zulieferern.
In den folgenden Abschnitten werden wir wichtigsten der in der Vergangenheit vor-
gestellten Reifegradmodelle einer genaueren Betrachtung unterziehen. Nach einem
kurzen Abstecher in die historischen Anfänge werden wir mit dem CMM, CMMI
und ISO 15504 (SPICE) die heute bedeutendsten Vertreter im Bereich der Software-
Entwicklung kennen lernen.
..
Stu ng. ,
fe.. ägu ung
Se
.
u s p r
n tfalt
lbs A nte
(Se tverw ale
lf-a ir itä t, T laube cht
,
ctu klichu id ual ik, G l d , Ma
aliz iv th
atio ng Ind st, E d, G
e
We
r
n) Ku
n
l s tan ie
(Es tsch oh arch
tee ätz ,W r
m n ung t a tus e, Hie de,
eed
s)
S rier
a r F r eun sorge
(Be Z u K t, r
lon gehö haf , Fü
gin rigk e rsc ation
Sch gn r t n ik tz, ung
utz eed eit Pa mun pla r
und s) o m r b eits siche
K , A A b
(Sa Sich e
tätt ung
,
Kö f ety e r hns Ordn
rpe nee heit o
W ht, t
(Ph rliche ds) c litä
ysi Re g , xua
olo Bedü u n S e
gic r ahr laf,
al n fnisse u f t, N , Sch
eed L rm e
s) Wä
wenn die Bedürfnisse einer Stufe vollständig erfüllt sind, werden die Bedürfnisse
der nächsthöheren für den Menschen relevant.
Ein anderes populäres Reifegradmodell ist das Quality Management Maturity
Grid (QMMG), das in den Siebzigerjahren von dem Amerikaner Philip Crosby po-
stuliert wurde [63]. Mit einfachen Sätzen beschreibt das Gitter, wie sich Organi-
sationen unterschiedlicher Reife in Bezug auf verschiedene Qualitätsaspekte ver-
halten. Mit dem Reifegradgitter verfolgte Crosby zwei Ziele. Zum einen versetzt
die eigenschaftsbasierte Beschreibung ein Unternehmen in die Lage, eine schnelle
und präzise Kategorisierung der eigenen Organisation vorzunehmen. Zum anderen
geben die Charakteristika der noch nicht erreichten Reifegrade Leitlinien für die
weitere Entwicklung der Organisationsstrukturen vor. Wie die Bedürfnispyramide
von Maslow unterscheidet auch das Reifegradmodell von Crosby fünf verschiedene
Stadien, die evolutionär durchlaufen werden. Inhaltlich werden die Stadien durch
die folgenden Eigenschaften charakterisiert:
I Stadium 1: Unsicherheit (uncertainty)
Die Unternehmensführung begreift den Faktor Qualität nicht als Managementin-
strument. Präventive Maßnahmen zur Fehlervermeidung werden nicht durchge-
führt, d. h., Probleme werden erst dann gelöst, wenn sie auftreten. Ein Verständ-
nis für die geordnete Durchführung von Aktivitäten besteht in diesem Stadium
genauso wenig wie die Fähigkeit, die Ursachen der unternehmenseigenen Quali-
tätsprobleme systematisch zu ergründen.
Unsicherheit
Verständnis
Stadium 1:
Stadium 2:
Stadium 3:
Stadium 4:
Stadium 5:
Erkenntnis
Erwachen
Sicherheit
Ein Qualitätsmanager ist
in die Geschäftsleitung
integriert.
Die Handlungspriorität
liegt auf der
Qualitätsverständnis Fehlerprävention.
Das Denken orientiert
sich an der Qualität.
Qualitätsorganisation
20
15
10
0
Stadium 1 Stadium 2 Stadium 3 Stadium 4 Stadium 5
Unsicherheit Erwachen Erkenntnis Verständnis Sicherheit
Zusätzlich erfasst Crosby die Reife einer Organisation quantitativ, indem er die Qua-
litätskosten in Bezug zum Umsatz eines Unternehmens stellt. Wie in Abb. 9.12
gezeigt, unterscheidet Crosby zwei Messgrößen. Die erste Kurve beschreibt die
berichteten Qualitätskosten, d. h. diejenigen Kosten eines Unternehmens, die nach
eigenen Angaben entstanden sind. Dieser Kurve stehen die tatsächlichen Qualitäts-
kosten gegenüber. Wenngleich die absoluten Zahlen schon aufgrund ihres Alters mit
Vorsicht zu genießen sind und als allgemein gehaltenes Modell die Besonderheiten
der Software-Entwicklung außer Betracht gelassen werden, treten zwei wesentliche
Eigenschaften der Prozessreife deutlich hervor:
I Gesenkte Gesamtkosten
Die Investition in Qualität führt mittel- und langfristig zu einer Senkung der Ge-
samtkosten. Im Modell von Crosby sinken die Qualitätskosten von 20 % des
Umsatzes auf nur noch 2.5 % [63]. Auf der anderen Seite ist die Investition in
Qualität mit erheblichen Initialkosten verbunden, vor denen viele Unternehmen
im Stadium der Unsicherheit zurückschrecken.
I Präzise Vorhersagen
Je weiter sich eine Organisation entwickelt, desto präziser lassen sich projekt-
und unternehmensbezogene Parameter schätzen. Die von Crosby gemessenen
9.2 Reifegradmodelle 519
Das Quality Management Maturity Grid gilt als ein Meilenstein im Bereich der
Reifegradmodelle und wurde nach seinem Erscheinen in mehrere Richtungen fort-
entwickelt. Crosby selbst entwickelte seinen Ansatz zum Quality Management Pro-
cess Maturity Grid weiter [64]. Von vielen Experten wird dieses Modell als der
Vorgänger des Capability Maturity Models erachtet, das wir im nächsten Abschnitt,
zusammen mit seinen Vor- und Nachteilen, in ein helleres Licht rücken werden.
9.2.2 CMM
In den späten Sechzigerjahren wurde der Begriff der Software-Krise (software cri-
sis) geprägt. Heute wie damals steht er stellvertretend für die Probleme und Symp-
tome ins Wanken geratener Software-Projekte – Budget- und Zeitüberschreitungen,
Qualitätsdefizite, nicht oder unzureichend erfüllte Anforderungen sowie Probleme
im Projektmanagement sind wenige Beispiele von vielen. Bekannt wurde der Aus-
druck vor allem durch die Dankesrede des niederländischen Computerwissenschaft-
ler Edsger W. Dijkstra, die er während der Verleihung des ACM Turing Awards im
Jahre 1972 hielt:
“As the power of available machines grew by a factor of more than a thou-
sand, society’s ambition to apply these machines grew in proportion, and it
was the poor programmer who found his job in this exploded field of tension
between ends and means. The increased power of hardware, together with
the perhaps even more dramatic increase in its reliability, made solutions
feasible that the programmer had not dared about them and, even worse,
he had to transform such dreams into reality! Is it a wonder that we found
ourselves in a software crisis? No, certainly not, and as you may guess, it
was even predicted well in advance; [...]”
Edsger W. Dijkstra [74]
SEI
1.0 1.1 ISO/IEC
SE-CMM
1.02 1.03
SA-CMM
0.0 0.2 1.0 1.1 1.02 1.1 1.2
SW-
CMMI
CMM
1.0 2.0
P-CMM
0.98
IPD-CMM
TR Teil 1 Teil 5
SPICE
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
Abb. 9.13 Zeitliche Entwicklung der verschiedenen Reifegradmodelle in der Übersicht
Pittsburgh angesiedelt wurde. Gegründet wurde das SEI mit dem Ziel, die Fähig-
keiten der US-amerikanischen IT-Industrie im Bereich des Software-Engineerings
zu verbessern. Im Jahre 2007 unterhielt das SEI fünf verschiedene Bereiche, von
denen insbesondere das Software Engineering Process Management Program inter-
nationale Bedeutung erlangen konnte. Das SEI fasst die Aufgaben des Programms
– zu denen auch die Fortentwicklung des Capability Maturity Models gehört – im
Rahmen einer Selbstdarstellung wie folgt zusammen:
Reifegrade
(maturity levels)
zeigen an enthalten
Prozessbefähigung Schlüsselbereiche
(process capabilitiy) (key process areas)
erreichen enthalten
betreffen enthalten
beschreiben
Aktivitäten
oder Infrastruktur
muss, spiegeln die evolutionäre Entwicklung eines Unternehmens wider. Die ein-
zelnen Stufen sind aufeinander aufbauend ausgelegt und spannen den Bogen von
Unternehmen ohne erkennbare Planungsaktivitäten (Level 1) bis hin zu Organi-
sationsstrukturen auf Basis selbstoptimierender Prozesse (Level 5).
I Ziele (goals)
Ob eine Organisation die Maßgaben der verschiedenen Schlüsselbereiche er-
reicht, wird über die Erfüllung oder Nichterfüllung der Ziele gemessen. Durch
522 9 Managementprozesse
die Formulierung klarer Ziele treten der Geltungsbereich und die Intention eines
Schlüsselbereiches hervor. Durch den direkten Vergleich der assoziierten Ziele
wird insbesondere die gegenseitige Abgrenzung zweier Schlüsselbereiche deut-
lich vereinfacht.
5: Optimizing
Continous Process Change Management
process Technology Change Management
improvement Defect Prevention
4: Managed
Process Software Quality Management
control Quantitative Process Management Change
manage-
ment
Peer Reviews
Intergroup Coordination
3: Dened
Project
management
Durch den zusätzlich verursachten Aufwand kann sich die mittlere Bearbeitung
eines Projekts im direkten Vergleich mit einer Stufe-1-Organisation sogar verlän-
gern. Verringert wird dagegen der Streuungsgrad (Varianz), so dass sich die Para-
meter Umfang, Zeitbedarf und Kosten zwischen ähnlichen Projekten weniger stark
unterscheiden. Die Vorteile einer Organisation dieser Stufe liegen damit vor allem in
einer besseren Planbarkeit und Termintreue. Dieser Reifegrad besitzt die folgenden
Schlüsselbereiche:
I Requirements Management
Die Anforderungen eines Software-Systems müssen in geordneter Art und Wei-
se erfasst, verwaltet und im Rahmen eines definierten Änderungsmanagements
systematisch aktualisiert werden. Die Organisation muss in der Lage sein, ein
524 9 Managementprozesse
I Training Program
Die Weiterbildung des Personalstabs wird als ein Schlüsselelement für den lang-
fristigen Erfolg einer Organisation angesehen. Hierzu werden Trainingsprogram-
me als fester Bestandteil in die Unternehmensprozesse integriert. Typische Maß-
nahmen umfassen in diesem Zusammenhang die Erfassung des Weiterbildungs-
bedarfs sowie die Durchführung von Schulungen auf Entwickler- und Manage-
mentebene.
I Intergroup Coordination
Dieser Schlüsselbereich beschäftigt sich mit den organisatorischen Strukturen
und Maßnahmen, die eine produktive Zusammenarbeit zwischen den Mitarbei-
tern verschiedener Arbeitsgruppen sicherstellen. Ein besonderer Fokus liegt auf
der effektiven Kommunikation zwischen Software-Entwicklern und Ingenieu-
ren anderer Fachrichtungen. Die fachübergreifende Zusammenarbeit wird als ein
Schlüsselelement für die erfolgreiche Produktentwicklung auf Systemebene an-
gesehen.
I Peer Reviews
Peer Reviews sind ein leistungsfähiges Instrument der Software-
Qualitätssicherung. In Organisationen dieses Reifegrads ist die Durchführung
entsprechender Reviews institutionalisiert und damit ein fester Bestandteil des
Entwicklungsprozesses. Neben der Verbesserung der Produktqualität wird das
Wissen eines Software-Entwicklers zugleich an eine größere Entwicklergrup-
pe weitergegeben. Damit helfen Reviews, zwei weitere Ziele des CMM zu
erreichen. Sie entpuppen sich als ein Instrument der Mitarbeiterfortbildung
und tragen gleichzeitig dazu bei, den Unternehmenserfolg aktiv von einzelnen
Individuen abzukoppeln.
ebenfalls in einem definierten Prozess und bezieht alle Phasen eines Projekts
mit ein. Durch die permanente Analyse der Messdaten werden Abweichungen
erkannt und frühzeitig mit definierten Maßnahmen beantwortet. Transparente
Messverfahren sowie der Rückgriff auf historische Daten versetzen die Orga-
nisation in die Lage, die Leistungsfähigkeit der unternehmenseigenen Prozesse
selbst zu messen und in objektiven Maßzahlen auszudrücken.
[Link] Zertifizierung
Hat ein Unternehmen die Maßnahmen zur Erreichung eines Reifegrads vollständig
umgesetzt, so kann die Prozessreife innerhalb eines Assessments festgestellt wer-
den. Ein CMM-Assessment wird in Form eines formalen Audits durchgeführt. Um
die Objektivität der Zertifizierung zu gewährleisten, wird das Audit durch eine ex-
terne Zertifizierungsorganisation geleitet. Verläuft das Assessment erfolgreich, so
wird der Reifegrad der Organisation durch ein Zertifikat formal bescheinigt. Ob-
wohl eine vollständig durchgeführte Zertifizierung erhebliche Kosten verursacht, ist
der Erhalt eines Prozessreifezertifikats für ein Unternehmen in zweierlei Hinsicht
von Bedeutung:
I Innenwirkung
Die Zertifizierung unterstreicht das unternehmenseigene Qualitätsverständnis.
Durch das Erlangen eines Zertifikats bekennt sich eine Organisation zu den Zie-
len des attestierten Reifegrads und signalisiert dem Personalstab, die definierten
Arbeitsabläufe und Maßnahmen mit hoher Priorität zu befolgen. Des Weiteren
wird die Prozessreife der Organisation von einer neutralen Instanz bestätigt, so
dass die formale Zertifizierung das Vertrauen eines Unternehmens in die eige-
nen Prozesse deutlich erhöhen kann. Zusätzlich werden im Rahmen eines Audits
Verbesserungsvorschläge ausgearbeitet, die für die zukünftige Entwicklung der
Organisationsstrukturen gewinnbringend eingesetzt werden können.
I Außenwirkung
Viele Unternehmen erreichen durch ein Prozesszertifikat eine nicht zu unter-
schätzende Außenwirkung. Zum einen trägt die Zertifizierung weite Teile der
Unternehmensphilosophie nach außen, zum anderen kann ein erfolgreich erwor-
benes Zertifikat helfen, das kundenseitige Vertrauen in die Leistungsfähigkeit
des Unternehmens zu stärken. In einigen Bereichen setzten Auftraggeber bei der
Auswahl ihrer Unterauftragnehmer eine Zertifizierung sogar zwingend voraus.
Üblich sind solche Vorgehensweisen insbesondere in sicherheitskritischen Be-
reichen, wie z. B. in der Automobil- und Luftfahrtindustrie. In diesen Fällen er-
übrigt sich die Frage nach dem Sinn einer Zertifizierung gänzlich – sie ist hier
eine Grundvoraussetzung für den Erhalt eines Auftrags.
9.2 Reifegradmodelle 529
Auftragserteilung
Phase 3
Phase 2
Phase 1
Management-
Ablaufplanung Final ,ndings
interviews
Wie in Abb. 9.16 skizziert, erfolgt ein formales Reifegradaudit in drei Phasen, die
nacheinander durchlaufen werden.
I Phase 1
Nach der Auftragserteilung erfolgen zunächst mehrere vorbereitende Gespräche
zwischen der gewählten Zertifizierungsorganisation und dem Auftraggeber. Es
wird ein Audit-Team gebildet, das sich aus einem externen Auditleiter sowie
mehreren Co-Auditoren zusammensetzt. Anschließend werden im Rahmen ei-
nes Dokumenten-Reviews die Strukturen und Abläufe der Organisation erfasst.
Die Analyse wird durch vorgefertigte Fragelisten unterstützt. Aufgrund der ge-
wonnenen Ergebnisse erstellt der Auditleiter eine Vorbeurteilung, die einen ers-
ten Rückschluss auf die Erfolgsaussichten der Zertifizierung ermöglicht. Fällt die
Beurteilung positiv aus, werden die Inhalte der restlichen Phasen geplant und der
zeitliche Ablauf festgelegt.
I Phase 2
Die zweite Phase des Reifegradaudits wird vollständig vor Ort durchgeführt und
erstreckt sich in Abhängigkeit der Projekt- und Organisationsgröße über einen
Zeitraum von 1 bis 2 Wochen. Die Prozessreife der Organisation wird im Rah-
men mehrerer Interviews überprüft, die mit den verschiedenen Personengruppen
des Unternehmens getrennt geführt werden. Entwickler-Interviews werden meist
in Form von Gruppenbefragungen durchgeführt und haben zum Ziel, die Ar-
beitsabläufe mit dem festgelegten Vorgehensmodell abzugleichen. Projektleiter-
und Management-Interviews werden zumeist in Einzelgesprächen abgehalten
und dienen zur Evaluierung der technischen und organisatorischen Arbeitsab-
530 9 Managementprozesse
läufe auf Projekt- und Unternehmensebene. Nach jedem Interview werden die
gesammelten Stellungnahmen bewertet und mit den Anforderungen und Zielen
der angestrebten Reifestufe abgeglichen. Zusätzlich wird die Konsistenz der ge-
tätigten Aussagen gruppenübergreifend geprüft.
I Phase 3
Die in der Interview-Phase gesammelten Informationen werden zusammengetra-
gen und der Unternehmensleitung durch das Audit-Team in Form eines vorläu-
figen Untersuchungsergebnisses präsentiert (draft findings). Im Anschluss daran
werden verschiedene Gruppen-Interviews durchgeführt, in denen vorher getätig-
te Aussagen präzisiert oder etwaig entstandene Missverständnisse ausgeräumt
werden können. Die Ergebnisse der Gruppen-Interviews werden mit den Draft
findings abgeglichen und der endgültige Untersuchungsbericht erstellt (final fin-
dings). Die Ergebnisse der Final-Findings entscheiden über die Zertifikatertei-
lung und enthalten zusätzliche Vorschläge und Empfehlungen für die Prozessop-
timierung und -weiterentwicklung. Zu guter Letzt wird das Ergebnis der Zer-
tifizierung für statistische Zwecke an das Software Engineering Institute (SEI)
gemeldet und das Audit hierdurch offiziell abgeschlossen.
9.2.3 CMMI
Das in Abschnitt 9.2.2 im Detail vorgestellte Capability Maturity Model wurde
vom SEI speziell für den Bereich der Software-Entwicklung konzipiert. Um diesen
Zusammenhang deutlich zu machen, wird das Modell mitunter auch als Software-
CMM, kurz SW-CMM, bezeichnet. Mit der steigenden Popularität des Capability
Maturity Models entstanden in den Neunzigerjahren vergleichbare CMMs für an-
dere Anwendungsdomänen. Hierzu zählen die Bereiche System engineering (SE-
CMM, [4]), Software acquisition (SA-CMM, [53]), Integrated product development
(IPD-CMM, [5]) und Human ressources (P-CMM, [65]).
Die anhaltende Diversifizierung blieb nicht ohne Folgen. Unternehmen, die sich
der Reifegradphilosophie des SEI ernsthaft verschrieben, waren mehr und mehr
gezwungen, verschiedene CMM-Modelle parallel zu implementieren. Durch die
hiermit verbundenen Doppelarbeiten sowie ab und an auftretenden Inkonsistenzen
wuchs das Risiko, den Nutzen eines solchen Reifegradmodells ad absurdum zu füh-
ren.
Dem fortschreitenden Wirrwarr machte das SEI mit dem CMMI-
Reifegradmodell (CMM Integration) ein Ende [2, 44, 152]. Die bereits begonnenen
Arbeiten an der Version 2.0 des Software-CMMs wurden eingestellt und stattdessen
mit dem CMMI ein universelles Modell konzipiert, das mehrere Disziplinen in
sich vereint. Die bereits geleisteten Arbeiten an der Version 2.0 des SW-CMM
waren jedoch nicht umsonst – zahlreiche Elemente der Draft-Version wurden
ohne Änderungen in das CMMI-Modell übernommen. Wie schon sein Vorgänger
wurde auch CMMI zunächst als Entwurf veröffentlicht und in einer zweijährigen
Erprobungsphase optimiert. Im Jahre 2000 wurde das CMMI in der Version 1.02
veröffentlicht und ein Jahr später durch die Version 1.1 ersetzt [246, 245]. Im
9.2 Reifegradmodelle 531
August 2006 erschien mit der Version 1.2 die bis dato letzte Überarbeitung des
Standards [247].
Der konzeptuelle Entwurf von CMMI wurde durch drei Ziele maßgeblich beein-
flusst:
I Universalität
Anders als das CMM ist das CMMI nicht speziell für den Bereich der Software-
Entwicklung konzipiert. Stattdessen stellt das CMMI ein universelles Modell-
gerüst zur Verfügung, das die erfolgreichsten Elemente der diversen CMM-
Vorgängermodelle in sich vereint und bereichsübergreifend eingesetzt werden
kann. Durch die individuelle Erweiterung dieses Grundgerüsts werden andere
Disziplinen mit einbezogen. Den Kern des Modells bildet das CMMI-SE/SW, das
die Bereiche System Engineering (SE) und Software Engineering (SW) abdeckt.
Das Reifegradmodell CMMI-SE/SW/IPPD/SS erweitert den Kern um die Berei-
che Integrated Product and Process Development (IPPD) und Supplier Sourcing
(SS).
I Flexibilität
Das strenge Stufenmodell des CMM empfanden vielen Unternehmen als zu starr,
da im Rahmen einer Zertifizierung die entsprechende Reife in ausnahmslos allen
Schlüsselbereichen nachgewiesen werden musste. Unternehmen waren dadurch
gezwungen, Schlüsselbereiche der nächsten Stufe zurückzustellen, auch wenn
diese für das Erreichen der Geschäftsziele hätten vorrangig behandelt werden
müssen.
Mit dem CMMI wurde aus diesem Grund ein kontinuierliches Bewertungs-
schema eingeführt (continous representation), das das starre Korsett der Reife-
stufen beseitigt. Durch die erhöhte Flexibilität können die Prozesse besser den
Geschäftszielen angepasst werden und stehen seltener mit diesen im Konflikt.
I Kompatibilität
Die Transition von CMM zu CMMI sollte so einfach wie möglich gestaltet wer-
den, um die getätigten Investitionen bereits zertifizierter Unternehmen weitge-
hend zu erhalten. Gelöst wurde dieses Problem durch die Einführung paralleler
Bewertungsschemata. Neben der bereits erwähnten kontinuierlichen Bewertung
unterstützt CMMI weiterhin ein Stufenmodell (staged representation), das sich
stark an den Inhalten des ursprünglichen CMM orientiert. Die gestufte Bewer-
tung zeigt die altbekannten Flexibilitätsdefizite, erlaubt es CMM-zertifizierten
Firmen jedoch, mit vergleichsweise geringem Aufwand in die CMMI-Welt zu
migrieren.
Obwohl sich das kontinuierliche und das gestufte Bewertungsschema nach außen
hin erheblich unterscheiden, decken sie inhaltlich dieselben Bereiche ab. Wie in
Abb. 9.17 gezeigt, bestehen beide Schemata aus insgesamt 22 Schlüsselbereichen,
die matrixartig angeordnet sind. Die vertikale Anordnung folgt dem Stufenmodell
des CMM. Wie im Fall des ursprünglichen Capability Maturity Models entspricht
die Stufe 1 keinem Reifegrad im eigentlichen Sinne und ist dementsprechend mit
keinen Schlüsselbereichen assoziiert.
532 9 Managementprozesse
Process Project
Engineering Support
Management Management
Organizational
Level 5
Causal Analysis
CAR
OID
Organizational Quantitative
Level 4
QPM
OPP
Process Project
Performance Management
Organizational Decision
OPD
Risk Product
DAR
RM
PI
Management Integration
De,nition Resolution
Integrated
Organizational Requirements
IPM
RD
OT
Project
Training Development
Management
Level 3
Organizational Technical
OPF
TS
Veri,cation
VAL
Validation
Staged
Process and
REQM
PPQA
Requirements
PP
Supplier
Level 2
SAM
Con,guration
CM
Agreement
Management
Management
Project
PMC
Measurement
MA
Monitoring and
and Analysis
Control
Abb. 9.17 CMMI unterstützt sowohl die gestufte (vertikal) als auch die kontinuierliche Bewertung
(horizontal) der Prozessreife
Quantitatively Quantitatively
Level 4 Managed
Managed Managed
Prozessreife
Level 0 Incomplete
Abb. 9.18 Zusammenhang zwischen den Maturity levels und den Capability levels von CMM und
CMMI
Tabelle 9.1 Der Equivalent-Staging-Mechanismus des CMMI erlaubt, Reifeprofile des kontinu-
ierlichen Modells auf die Reifegrade des Stufenmodells abzubilden
KPAs KPAs KPAs KPAs
der Stufe 2 der Stufe 3 der Stufe 4 der Stufe 5
Reifestufe 1 ist erfüllt, wenn... – – – –
Reifestufe 2 ist erfüllt, wenn... ≥2 – – –
Reifestufe 3 ist erfüllt, wenn... ≥3 ≥3 – –
Reifestufe 4 ist erfüllt, wenn... ≥3 ≥3 ≥3 –
Reifestufe 5 ist erfüllt, wenn... ≥3 ≥3 ≥3 ≥3
verschiedene Capability levels, die in Abb. 9.18 den Reifegraden des CMM- bzw.
CMMI-Stufenmodells gegenübergestellt sind.
Aufgrund der separaten Bewertung der einzelnen Schlüsselbereiche kann die
Reife eines Unternehmens nicht mehr länger durch eine einzige Stufe charakteri-
siert werden. Stattdessen wird der aktuelle Prozesszustand einer Organisation in
Form eines Reifeprofils gemessen, das jedem Schlüsselbereich einen Capability le-
vel zwischen 0 (incomplete) und 5 (optimizing) zuordnet (vgl. Abb. 9.19). In der
Terminologie des CMMI wird das entstehende Ergebnis auch als Capability level
profile bezeichnet.
Ein solches Reifeprofil spiegelt das kontinuierliche Bewertungsschema des CM-
MI wieder, lässt sich jedoch auf direkte Weise auf das gestufte Schema abbilden.
Hierzu definiert das CMMI für jeden Schlüsselbereich und jeden Reifegrad eine
Mindestanforderung, die für die Erreichung der entsprechenden Stufe erfüllt sein
muss. Die Zuordnung wird als Equivalent staging bezeichnet und ist in Tabelle 9.1
detailliert aufgeschlüsselt. Um beispielsweise die Reifestufe 3 zu erlangen, müssen
534 9 Managementprozesse
Process Project
Engineering Support
Management Management
Level 5
CAR
OID
0 0
Level 4
QPM
capability level capability level
OPP
0 0
capability level
OPD
DAR
RM
PI
3 2 4 3
RD
OT
3 3 4
Level 3
TS
3 3
capability level
VER
Im Schlüsselbereich RM
wird der Capability-Level 3 3
nicht erreicht, so dass das
abgebildete Reifepro1l der
capability level
VAL
Reifestufe 2 entspricht.
3
REQM
PPQA
3 3 3
Level 2
SAM
Level 2 3 3
PMC
3 3
Abb. 9.19 Im kontinuierlichen Bewertungsmodell des CMMI wird die Prozessreife einer Orga-
nisation in einem Reifeprofil festgehalten, das alle Schlüsselbereiche unabhängig voneinander be-
wertet. Reifeprofile lassen sich auf Reifegrade abbilden
die Schlüsselbereiche der Stufen 2 und 3 eine Bewertung von mindestens 3 auf-
weisen. Die Schlüsselbereiche der Stufen 4 und 5 spielen dagegen keine Rolle. Ein
Blick auf das Reifeprofil in Abb. 9.19 zeigt, dass die Erfordernisse hier nicht erfüllt
sind, da der Schlüsselbereich Risk Management (RM) nur einen Capability level
von 2 aufweist. Insgesamt entspricht das abgebildete Profil damit der Reifestufe 2 –
9.2 Reifegradmodelle 535
diese ist nach Tabelle 9.1 erreicht, sobald alle Schlüsselbereiche der Stufen 2 und 3
mit mindestens 2 bewertet sind.
Um eine objektive Bewertung der Prozessreife zu gewährleisten, müssen die ver-
schiedenen, für die Begutachtung eingesetzten Methoden vergleichbare und konsi-
stente Ergebnisse liefert. Zu diesem Zweck wurden durch das Software Enginee-
ring Institute die Appraisal Requirements for CMM (ARC) entwickelt, die ein Un-
ternehmen in die Lage versetzen sollen, bestehende Methoden zu bewerten bzw.
eine entsprechende Assessment-Methode nach vorgegebenen Richtlinien selbst zu
entwickeln [248]. Das SEI führt mit ARC drei Assessment-Typen ein, die sich be-
züglich des Ressourcen-Verbrauchs erheblich unterscheiden. Class-B- und Class-C-
Assessments finden in kleinen Gruppen von 2 bis 7 Personen statt und werden un-
ternehmensintern ohne Beteiligung eines Lead-Assessors durchgeführt. Ein Lead-
Assessor wird nur für Class-A-Assessments benötigt. Diese Assessment-Variante ist
die formalste der drei und gleichzeitig die Voraussetzung für die offizielle Bewer-
tung einer Organisation in Form eines Reifeprofils (kontinuierliches Bewertungs-
schema) oder eines Reifegrads (gestuftes Bewertungsschema).
Eine in der Praxis häufig eingesetzte Bewertungsmethode ist die Standard CMMI
Assessment Method for Process Improvement. Die kurz als SCAMPI bezeichnete
Bewertungsmethode wurde durch das Software Engineering Institute entwickelt und
erfüllt vollständig die Anforderungen eines Class-A-Assessments [249, 1].
Prozess
bewertet
führt führt
zur zur
definieren, postuliert die ISO-Norm 15504 eine Reihe von Anforderungen, die ein
Modell bzw. eine Methode erfüllen muss. Insgesamt ist mit der ISO 15504 ein Mo-
dell entstanden, dessen Vorgaben von CMM und CMMI sowohl inhaltlich als auch
methodisch weitgehend erfüllt werden. Die Kompatibilität kommt an dieser Stelle
nicht von ungefähr – das SEI war sowohl im Rahmen von Managementaufgaben
als auch in der Rolle eines technischen Zulieferers (technical contributor) an der
Entwicklung der Norm beteiligt.
Inhaltlich erstreckt sich SPICE über drei Aufgabenbereiche (vgl. Abb. 9.20). Im
Kern des Standards steht das Prozess-Assessment, das auch hier zwei Ziele ver-
folgt: Mit Hilfe eines Assessments werden die Reife von Unternehmensprozessen
strukturiert bewertet und zum anderen Verbesserungsvorschläge für deren Optimie-
rung erarbeitet. SPICE legt den Fokus erstrangig auf die Selbstbewertung einer
Organisation und nur zweitrangig auf die Zertifizierung. Die Durchführung eines
Prozess-Assessments ist durch das SPICE-Referenzmodell festgelegt, das sich aus
zwei Dimensionen zusammensetzt: Der Prozess- und der Reifegraddimension (vgl.
Abb. 9.21).
SPICE
Process categories
Capability Levels
& groups
Prozessdimension Reifegraddimension
Grundlegende Aktivitäten
Prozesse werden in sechs
für die Erreichung der
Reifegradstufen eingeteilt
Projektziele
I Support
In dieser Kategorie sind alle Prozesse zusammengefasst, die eine unterstützende
Funktion für andere Prozesse besitzen. Beispiele sind die Dokumentation, das
Konfigurationsmanagement, die Verifikation und Validation sowie die Qualitäts-
sicherung.
Jeder Einzelprozess ist mit einem oder mehreren fest definierten Zielen assoziiert.
Um ein Unternehmen in die Lage zu versetzen, die Ziele zu erreichen, wird in SPI-
CE jeder Prozess durch eine Menge von Top-Level-Aktivitäten – den Base practi-
ces – beschrieben. Exemplarisch sind in Abb. 9.23 die Kernelemente des SPICE-
9.2 Reifegradmodelle 539
The purpose of the process is to establish and maintain the integrity of the work products/items
of a process or project and make them available to concerned parties.
Outcomes
I Work products/items generated by the process or project are identified, defined and baseli-
ned.
I The status of the work products/items and modifications are recorded and reported.
Base practices
Configuration Management
I SG 1: Establish Baselines
I SG 3: Establish Integrity
Practices by Goal
Mit der Einführung des Capability Maturity Models erlebten die Reifegradmo-
delle Ende der Achtzigerjahre einen regelrechten Boom, der bis heute in ungebro-
chener Form anhält. Trotzdem sollte die Einführung eines Prozessmodells stets mit
Bedacht und Sorgfalt geschehen. Neben den vielen unbestrittenen Stärken, die in
der Vergangenheit mehrfach unter Beweis gestellt wurden, ist die Einführung eines
schwergewichtigen Prozessmodells auch mit Nachteilen und Risiken verbunden. Ei-
nige der am häufigsten geäußerten Kritikpunkte sind im Folgenden zusammenge-
fasst:
I Die Qualitätssicherung in der Zwickmühle
In nahezu allen Reifegradmodellen spielt die Software-Qualitätssicherung eine
zwiespältige Doppelrolle. Auf der einen Seite ist sie mit dem Auftrag ausgestat-
tet, eine beratende Funktion innerhalb der Projekte auszuüben. Auf der anderen
Seite ist sie ein zentrales Steuerungsinstrument des Managements. Diese Rolle
wird insbesondere dadurch untermauert, dass die Software-Qualitätssicherung in
Form einer eigenständigen und unabhängig operierenden Abteilung organisiert
542 9 Managementprozesse
PA 5.2
PA 5.1
Continous Process
Optimizing
improvement change
Level 4
PA 4.1
PA 4.2
Process
Predictable Measurement
control
Level 3
PA 3.1
PA 3.2
Process Process
Established
de2nition ressource
PA 2.1
PA 2.2
Performance Work product
Managed
management management
Level 1
PA 1.1
Process
Performed
performance
Level 0
Incomplete
ist und direkt an das Management berichtet. In der Praxis ist diese Doppelrolle
kaum in angemessener Form zu erfüllen. Aufgrund ihrer kontrollierenden Funk-
tion wird die QS-Abteilung nicht selten als Fremdkörper oder gar als Feind emp-
funden – als der „Big Brother“ oder die „dunkle Seite der Macht“. Die Idee einer
beratenden und unterstützenden Institution wird hierdurch faktisch ad absurdum
geführt. Nichtsdestotrotz ist das Interesse des Managements, durch die Etablie-
rung entsprechender Strukturen mehr Transparenz in die Projekte zu bringen, be-
rechtigt und in vielen Fällen unvermeidlich. Vergessen wird an dieser Stelle nur
allzu oft, dass die Messung, welcher Projektparameter auch immer, einen Eingriff
in ein zum Teil fragiles System menschlicher Individuen nach sich zieht. Wird
die Kontrolle innerhalb der Projekte als zu stark empfunden, werden die Projekt-
beteiligten in einer natürlichen Reaktion versuchen, sich dagegen abzuschotten.
Physiker unter den Lesern mögen sich an Schrödingers Katze erinnert fühlen
– diese stirbt ebenfalls mit der Messung (vgl. Abb. 9.25). Die gängigen Reife-
gradmodelle werden der Problematik an dieser Stelle nur unzureichend gerecht
und suggerieren mit der großflächigen Installation schwergewichtiger Prozesse
eine einfache Lösung für ein Problemspektrum, das mit einfachen Mitteln in der
Praxis kaum zu bewältigen ist.
9.2 Reifegradmodelle 543
Erstellung von Software einen herstellenden Vorgang und nicht selten wird die
Entwicklung in diesen Unternehmen auch als Software-Produktion bezeichnet.
Auf den ersten Blick scheint die Frage rein philosophischer Natur und von gerin-
ger praktischer Relevanz zu sein. Auf den zweiten Blick wird deutlich, dass die
unterschiedlichen Sichtweisen weitreichende Auswirkungen auf den gesamten
Software-Entwicklungsprozess besitzen. So basiert das gesamte Fundament der
Reifegradmodelle auf einer produktionstechnischen Sichtweise – insbesondere
Zielparameter wie die Planbarkeit und Wiederholbarkeit sind mit einer künstle-
rischen Tätigkeit nur bedingt vereinbar.
Aber lässt sich qualitativ hochwertige Software tatsächlich „produzieren“
oder werden in Wirklichkeit Prozesse benötigt, die den Bereich der Software-
Entwicklung als eine Art künstlerisches Schaffen begreifen? Die folgende Um-
schreibung der Programmierertätigkeit stammt aus dem Buchklassiker The My-
thical Man Month von Frederick Brooks und wirft erste Zweifel an einer rein
produktionsorientierten Sicht auf:
“The programmer, like the poet, works only slightly removed from pure
thought-stuff. He builds his castles in the air, from air, creating by exertion
of the imagination. Few media of creation are so flexible, so easy to polish
and rework, so readily capable of realizing grand conceptual structures.”
Frederick Brooks [90]
1. Ahern, D.M., Clouse, A., Armstrong, J.: CMMI Scampi Distilled. Addison-Wesley, Amster-
dam (2005)
2. Ahern, D.M., Clouse, A., Turner, R.: CMMI Distilled. Addison-Wesley, Amsterdam (2003)
3. Aho, A.V., Sethi, R., Ullman, J.D.: Compilers, Principles, Techniques, and Tools. Addison-
Wesley, Boston (1986)
4. et al., R.B.: A systems engineering capability maturity model version 1.1. Tech. Rep.
CMU/SEI-95-MM-003, Software Engineering Institute (SEI) (1995)
5. et al., R.B.: An integrated product development capability maturity model. Tech. Rep.
CMU/SEI-97-MM-001, Software Engineering Institute (SEI) (1997)
6. [Link]
7. [Link]
8. Balzert, H.: Lehrbuch der Software-Technik, Band 2. Spektrum Akademischer Verlag, Hei-
delberg (1998)
9. Balzert, H.: Lehrbuch der Software-Technik, Band 1. Spektrum Akademischer Verlag, Hei-
delberg (2000)
10. Balzert, H.: Lehrbuch der Objektmodellierung. Analyse und Entwurf. Spektrum Akademi-
scher Verlag (2004)
11. Balzert, H.: UML2 in 5 Tagen. W3l Verlag (2005)
12. Basili, V.R., Briand, L.C., Melo, W.L.: A validation of object-oriented design metrics as
quality indicators. IEEE Transactions on Software Engineering 22(10), 751–761 (1996)
13. Battelle, J.: The 70 percent solution. Business 2.0 6(11) (2005)
14. Beck, K.: Extreme Programming – Das Manifest. Addison-Wesley, München (2000)
15. Beck, K.: Test-Driven Development : By Example. Addison-Wesley, Boston (2002)
16. Beck, K.: JUnit kurz und gut. O’Reilly and Associates, Sebastopol (2005)
17. Beck, K., Andres, C.: Extreme Programming Explained. Addison-Wesley, Upper Saddle
River, NJ (2005)
18. Beck, K., Gamma, E.: Contributing to Eclipse. Principles, Patterns, and Plugins. Addison-
Wesley Longman, Amsterdam (2004)
19. Beizer, B.: Software Testing Techniques. Van Nostrand Reinhold, New York (1983)
20. Benington, H.D.: Production of large computer programs. In: Proceedings of the ONR Sym-
posium on Advanced Computer Programs for Digital Computers, pp. 350–361. Washington,
D.C., Office of Naval Research (1956). Nachgedruckt in [21]
21. Benington, H.D.: Production of large computer programs. In: ICSE ’87: Proceedings of the
9th international conference on Software Engineering, pp. 299–310. IEEE Computer Society
Press, Los Alamitos, CA (1987)
22. Berge, C.: Graphs and Hypergraphs. North-Holland Publishing, Amsterdam (1979)
23. Berge, C.: Graphs. North-Holland Publishing, Amsterdam (1989)
24. Bird, R.: Introduction to Functional Programming Using Haskell. Prentice Hall Europe,
London (1998)
25. [Link]
26. Blahut, R.E.: Algebraic Codes for Data Transmission. Cambridge University Press, Cam-
bridge (2002)
27. Blair, M., Obenski, S., Bridickas, P.: Patriot missile defense: Software problem led to system
failure at dhahran, saudi arabia. Report GAO/IMTEC-92-26, Information Management and
Technology Division, United States General Accounting Office, Washington, D.C. (1992)
28. Boehm, B.: A spiral model of software development and enhancement. SIGSOFT Softw.
Eng. Notes 11(4), 14–24 (1986)
29. Boehm, B.: A spiral model of software development and enhancement. IEEE Computer
21(5), 61–72 (1988)
30. Boehm, B.W.: Software Engineering Economics. Prentice Hall, Englewood Cliffs, NJ (1981)
31. Boehm, B.W.: Improving software productivity. IEEE Computer 20(9), 43–57 (1987)
32. Bookman, C.: Linux Clustering : Building and Maintaining Linux Clusters. New Riders,
Boston (2003)
33. Bornat, R.: Understanding and Writing Compilers : A Do-it-Yourself Guide. Macmillan,
London (1979)
34. Bourne, K.C.: Testing Client/Server Systems. McGraw-Hill, New York (1997)
35. Bryant, R.E.: Graph-based algorithms for boolean function manipulation. IEEE Transactions
on Computers C-35(8), 677–691 (1986)
36. Burch, J.R., Clarke, E.M., McMillan, K.L., Dill, D.L., Hwang, L.J.: Symbolic model
checking: 1020 states and beyond. In: Proceedings of the Fifth Annual IEEE Symposium
on Logic in Computer Science, pp. 1–33. IEEE Computer Society Press, Washington, D.C.
(1990)
37. Burgess, A.: Phone outages blamed on switching software. IEEE Software 8(5), 100–101
(1991)
38. Campbell, S., Chancelier, J.P., Nikoukhah, R.: Modeling and Simulation in Scilab/Scicos.
Springer-Verlag, Berlin, Heidelberg, New York (2005)
39. Card, D.N., Glass, R.L.: Measuring Software Design Quality. Prentice Hall, Englewood
Cliffs, NJ (1980)
40. Cardelli, L.: Type systems. ACM Computing Surveys 28(1), 263–264 (1996)
41. Chen, R.: The Old New Thing. Practical Development Throughout the Evolution of Win-
dows. Addison-Wesley, Amsterdam (2006)
42. Chidamber, S.R., Kemerer, C.F.: Towards a metrics suite for object oriented design. In:
OOPSLA ’91: Conference proceedings on Object-oriented programming systems, langua-
ges, and applications, pp. 197–211. ACM Press, New York (1991)
43. Chidamber, S.R., Kemerer, C.F.: A metrics suite for object oriented design. IEEE Transacti-
ons on Software Engineering 20(6), 476–493 (1994)
44. Chrissis, M.B., Konrad, M., Shrum, S.: CMMI : guidelines for process integration and pro-
duct improvement, 2nd edition edn. Addison-Wesley, Amsterdam (2006)
45. Church, A.: Review of turing 1936. Journal of Symbolic Logic 2(1), 42–43 (1937)
46. Clarke, C.: Program invariants as fixpoints. Computing 21(4), 273–294 (1979)
47. Clarke, E.M., Emerson, E.A., Sistla, A.P.: Automatic verification of finite-state concurrent
systems using temporal logic specifications. ACM Transactions on Programming Languages
and Systems 8, 244–263 (1986)
48. Clarke, L.A., Podgpurski, A., Richardson, D.J., Zeil, S.J.: A comparison of data flow path
selection criteria. In: Proceedings of the 8th International Conference on Software Enginee-
ring, pp. 244–251. IEEE Computer Society Press, London, England (1985)
49. Cocke, J., Sweeney, D.W.: High speed arithmetic in a parallel device (1957). Technical
Report, IBM
50. Colbourn, C.J., Dinitz, J.H.: CRC Handbook of Combinatorial Designs. CRC Press, Boca
Raton, FL (1996)
51. Committee, A.S.: Rationale for the ANSI C Programming Language. Silicon Press, Summit,
NJ (1990)
Literaturverzeichnis 549
52. Cook, W.R.: A proposal for making eiffel type-safe. The Computer Journal 32(4), 305–310
(1989)
53. Cooper, J., Fisher, M.: Software acquisition capability maturity model (sa-cmm), version
1.03. Tech. Rep. CMU/SEI-2002-TR-010, Software Engineering Institute (SEI) (2002)
54. Coorporation, M.: Design guidelines for class library developers. URL
[Link]
55. Copeland, L.: A Practitioner’s Guide to Software Test Design. Artech House Publishers,
Norwood, MA (2004)
56. Coppick, J.C., Cheatham, T.J.: Software metrics for object-oriented systems. In: ACM annual
conference on Communications, pp. 317–322. ACM Press, New York (1992)
57. Cormen, T., Leserson, C.E., Rivest, R., Stein, C.: Introduction to Algorithms, 2nd editon.
MIT Press (2001)
58. Cormen, T.H., Leiserson, C.E., Rivest, R., Stein, C.: Algorithmen – Eine Einführung. Olden-
bourg Wissenschaftsverlag, München (2007)
59. Corporation, I.: Statistical analysis of floating point flaw in the pentium. White paper (1994)
60. Covey, S.R.: The seven Habits of Highly Effective People. Simon & Schuster, London, UK
(2004)
61. Cowart, R., Knittel, B.: Using Microsoft Windows XP Home. Que Publishing, Indianapolis,
Ind (2004)
62. Craig, R.D., Jaskiel, S.P.: Systematic Software Testing. Artech House Publishers (2002)
63. Crosby, P.B.: Quality is Free. McGraw-Hill, New York, NY (1979)
64. Crosby, P.B.: Quality is Still Free. McGraw-Hill, New York, NY (1996)
65. Curtis, B., Hefley, B., Miller, S.: People capability maturity model (p-cmm) version 2.0.
Tech. Rep. CMU/SEI-2001-MM-001, Software Engineering Institute (SEI) (2001)
66. Dabney, J.B., Harman, T.L.: Mastering Simulink. Pearson / Prentice Hall, Upper Saddle
River, NJ (2004)
67. DeMarco, T.: Controlling Software Projects: Management, Measurement, and Estimates.
Prentice Hall PTR, Upper Saddle River, NJ (1986)
68. DeMarco, T., Lister, T.: Peopleware. Dorset House Publishing Co., New York (1987)
69. Demillo, R.A., Lipton, R.J., Sayward, F.G.: Hints on test data selection: Help for the practi-
cing programmer. Computer 11(4), 34–41 (1978)
70. Deutschland, B.: V-modell xt 1.0 (2005). URL [Link]
71. Deutschland, B.: V-modell xt 1.2 (2006). URL [Link]
72. Diestel, R.: Graphentheorie. Springer-Verlag, Berlin, Heidelberg, New York (2006)
73. Dietze, R., Heuser, T., Schilling, J.: OpenSolaris für Anwender, Administratoren und Re-
chenzentren: Von den ersten Schritten bis zum produktiven Betrieb auf Sparc, PC und Po-
werPC basierten Plattformen. Springer-Verlag, Berlin, Heidelberg, New York (2006)
74. Dijkstra, E.W.: The humble programmer. Communications of the ACM 15(10), 859–866
(1972)
75. Diller, A.: Z : An Introduction to Formal Methods. John Wiley and Sons, Chichester (1994)
76. Dvorak, J.: What’s going on at microsoft? PC Magazine (1999)
77. Eaton, J.W.: GNU Octave. Network Theory, Bristol (2005)
78. Echtle, K.: Fehlertoleranzverfahren. Springer-Verlag, Berlin, Heidelberg, New York (1990)
79. [Link]
80. Edelman, A.: The mathematics of the pentium devision bug. SIAM Review 39(39), 54–67
(1997)
81. [Link]
82. Elmer-Dewitt, P.: Ghost in the machine. Times Magazine pp. 58–59 (1990)
83. Elmore, E.: The transient response of damped linear networks with particular regard to wi-
deband amplifiers. Journal of Applied Physics 19(1), 55–63 (1948)
84. Elshoff, J.L.: An investigation into the effects of the counting method used on software
science measurements. ACM SIGPLAN Notices 13(2), 30–45 (1978)
85. Emerson, E., Clarke, E.: Using branching time temporal logic to synthesize synchronization
skeletons. Science of Computer Programming 2, 241–266 (1982)
550 Literaturverzeichnis
86. Erickson, J.: Hacking. The Art of Exploitation. No Starch Press, San Francisco (2003)
87. Evans, D.: Static detection of dynamic memory errors. In: SIGPLAN Conference on Pro-
gramming Language Design and Implementation (PLDI 96), pp. 21–24. Philadelphia, PA
(1996)
88. Evans, D., Guttag, J., Horning, J., Tan, Y.M.: Lclint: A tool for using specifications to check
code. In: Proceedings of the ACM SIGSOFT Symposium on the Foundations of Software
Engineering, pp. 87–96. New Orleans (1994)
89. Evans, D., Larochelle, D.: Improving security using extensible lightweight static analysis.
IEEE Software 19(1), 41–51 (2002)
90. F. P. Brooks, J.: The Mythical Man-Month. Addison-Wesley, Reading, MA (1995)
91. Fagan, M.E.: Design and code inspections to reduce errors in program development. IBM
Systems Journal 15(3), 258–287 (1976)
92. Fagan, M.E.: Advances in software inspections. IEEE Transactions on Software Engineering
12(7), 744–751 (1986)
93. Fenton, N.E., Ohlsson, N.: Quantitative analysis of faults and failures in a complex software
system. IEEE Transactions on Software Engineering 26(7), 653–661 (2000)
94. Fenton, N.E., Pfleeger, S.L.: Software Metrics: A Rigorous & Practical Approach. Interna-
tional Thomson Computer Press (1997)
95. Fitzgerald, J., Larsen, P.G., Mukherjee, P., Plat, N., Verhoef, M.: Validated Designs for
Object-oriented Systems. Springer-Verlag, Berlin, Heidelberg, New York (2005)
96. Floyd, R.: Assigning meaning to programs. In: Proceedings of Symposia on Applied Mathe-
matics, pp. 19–32. American Mathematical Society, Providence (1967)
97. Foster, J.C.: Buffer Overflows. Mitp-Verlag, Bonn (2005)
98. Fowler, M.: Refactoring – Improving the Design of Existing Code. Addison-Wesley, Rea-
ding, MA (1999)
99. Gannon, C.: Error detection using path testing and static analysis. Computer 12(8), 26–31
(1979)
100. Gates, B.: Der unsichtbare Computer. Brand Eins 10, 132–133 (2002)
101. Gilb, T., Graham, D.: Software Inspection. Addison-Wesley (1993)
102. Girgis, M.R., Woodward, M.R.: An experimental comparison of the error exposing ability of
program testing criteria. In: Proceedings of the Workshop on Software Testing, pp. 64–73.
Banff (1986)
103. [Link]
104. Gödel, K.: Über formal unentscheidbare Sätze der Principia Mathematica und verwandter
Systeme. Monatshefte für Mathematik und Physik 38, 173–198 (1931)
105. [Link]
106. Gordon, M.J.C., Melham, T.F.: Introduction to HOL: A theorem proving environment for
higher-order logic. Cambridge University Press, Cambridge (1993)
107. Gumbel, M.: Java Standard Libraries: Java 2 Collection Framework und Generic Collection
Library for Java. Addison-Wesley, München [u.a.] (2000)
108. Gusfield, D.: Algorithms on Strings, Trees, and Sequences. Cambridge University Press,
Cambridge (1997)
109. Güting, R.H., Erwig, M.: Übersetzerbau. Techniken, Werkzeuge, Anwendungen. Springer-
Verlag, Berlin, Heidelberg, New York (1999)
110. Halstead, M.H.: Natural laws controlling algorithm structure? ACM SIGPLAN Notices 7(2),
19–26 (1972)
111. Halstead, M.H.: Elements of Software Science. Elsevier (1977)
112. Hamer, P.G., Frewin, G.D.: M. h. halstead’s software science – a critical examination. In:
Proceedings of the ACM SIGSOFT-SIGPLAN/IEEE Computer Society International Confe-
rence on Software Engineering (ICSE), pp. 197–206. IEEE Computer Society Press, Tokyo,
Japan (1967)
113. Hamlet, R.G.: Testing programs with the aid of a compiler. IEEE Transactions on Software
Engineering SE-3(4), 279–289 (1977)
114. Hamming, R.W.: Error-detecting and error-correcting codes. Bell System Technical Journal
2(26), 147–160 (1950)
Literaturverzeichnis 551
115. Hamming, R.W.: Coding and Information Theory. Prentice Hall, Englewood Cliffs, NJ
(1980)
116. Hanselman, D.C., Littlefield, B.L.: Mastering Matlab 7. Pearson / Prentice Hall, Upper Sadd-
le River, NJ (2004)
117. Hedayat, A.S., Sloane, N.J.A., Stufken, J.: Orthogonal Arrays: Theory and Applications.
Springer-Verlag, Berlin, Heidelberg, New York (1999)
118. Henry, S.M., Kafura, D.: Software structure metrics based on information flow. IEEE Tran-
sactions on Software Engineering 7(5), 510–518 (1981)
119. Henry, S.M., Selig, C.: Predicting source-code complexity at the design stage. IEEE Software
7(2), 36–44 (1990)
120. Herold, H.: Lex und Yacc : Die Profitools zur lexikalischen und syntaktischen Textanalyse.
Addison-Wesley, München (2003)
121. Hertel, C.R.: Implementing CIFS. The Common Internet File System. Prentice Hall, Engle-
wood Cliffs, NJ (2003)
122. Hitz, M., Montazeri, B.: Chidamber and kemerer’s metrics suite: A measurement theory
perspective. IEEE Transactions on Software Engineering 22(4), 267–271 (1996)
123. Hoare, C.A.R.: An axiomatic basis for computer programming. Communications of the
ACM 12(10), 576–585 (1969)
124. Hoglund, G., McGraw, G.: Exploiting Software: How to Break Code. Addison-Wesley Long-
man, Amsterdam (2004)
125. Holzner, S.: Ant. O’Reilly and Associates, Sebastopol (2005)
126. Howden, W.E.: Methodology for the generation of program test data. IEEE Transactions on
Computers C-24, 554–560 (1904)
127. Howden, W.E.: An evaluation of the effectiveness of symbolic testing. Practice and Experi-
ence 8, 381–397 (1978)
128. Howden, W.E.: Theoretical and empirical studies of program testing. IEEE Transactions on
Software Engineering SE-4(4), 293–298 (1978)
129. Howden, W.E.: Theoretical and empirical studies of program testing. In: Proceedings of the
3rd International Conference on Software Engineering, pp. 235–243. Atlanta (1978)
130. Howden, W.E.: Weak mutation testing and completeness of test sets. IEEE Transactions on
Software Engineering SE-8(4), 371–379 (1982)
131. Huisman, M.: Reasoning about java programs in higher order logic with pvs and isabelle.
Ph.D. thesis, IPA Dissertation Series 2001-03 (2001)
132. Humphrey, W.S.: Characterizing the software process: A maturity framework. Tech. Rep.
CMU/SEI-87-TR-11, Software Engineering Institute (SEI) (1987)
133. Humphrey, W.S.: Managing the Software Process. Addison-Wesley, Reading, MA (1989)
134. Hunt, A., Thomas, D.: Pragmatic Unit Testing: In Java with JUnit. The Pragmatic Bookshelf,
Raleigh, NC (2004)
135. Hunt, A., Thomas, D.: Pragmatic Unit Testing in C# with NUnit. The Pragmatic Bookshelf,
Raleigh, NC (2006)
136. Hutton, G.: Introduction to Functional Programming Using Haskell. Cambridge University
Press, Cambridge (1998)
137. Institute of Electrical and Electronics Engineers, 345 East 47th Street, New York, NY 10017,
USA: IEEE Standard Glossary of Software Engineering Terminology, std 610.12-1990 edn.
(1990)
138. Jacky, J.: The Way of Z: Practical Programming with Formal Methods. Cambridge University
Press, Cambridge (1996)
139. [Link]
140. [Link]
141. Johnson, S.C.: Lint, a program checker. In [173] (1979)
142. Johnson, S.C.: A tour through the portable c compiler. In [173] (1979)
143. Johnson, S.C.: Yacc meets c++. Computing Systems 1(2), 159–167 (1988)
144. Johnson, S.C.: Yacc yet another compiler compiler. Report GAO/IMTEC-92-26, Information
Management and Technology Division, United States General Accounting Office, Washing-
ton, D.C. (1992)
552 Literaturverzeichnis
145. Johnson, S.C.: Objecting to objects. In: Technical Conference Proceedings. San Francisco,
CA (1994)
146. Jones, C.B.: Systematic Software Development using VDM. Prentice Hall, Upper Saddle
River, NJ (1990)
147. Juran, J.M., Godfrey, A.B.: Juran’s Quality Handbook, 5th edition edn. McGraw-Hill, New
York (2000)
148. Kecher, C.: UML 2.0. Das umfassende Handbuch. Galileo Press (2006)
149. Kernighan, B.W., Pike, R.P.: The UNIX Programming Environment. Prentice Hall, Engle-
wood Cliffs, NJ (1984)
150. Kernighan, B.W., Ritchie, D.M.: The C Programming Language, 1st edition edn. Prentice
Hall, Englewood Cliffs, NJ (1978)
151. Klein, T.: Buffer Overflows und Format-String-Schwachstellen. Funktionsweisen, Exploits
und Gegenmaßnahmen. [Link], Heidelberg (2003)
152. Kneuper, R.: CMMI. Verbesserung von Softwareprozessen mit Capability Maturity Model
Integration. [Link], Heidelberg (2006)
153. Koch, E.: Das 80/20-Prinzip. Mehr Erfolg mit weniger Aufwand. Campus Verlag (2004)
154. Koenig, A.: C Traps and Pitfalls. Addison-Wesley, Reading, MA (1989)
155. Koziol, J., Litchfield, D., Aitel, D.: The Shellcoder’s Handbook: Discovering and Exploiting
Security Holes. John Wiley and Sons, New York (2004)
156. Kropf, T.: Introduction to Formal Hardware Verification. Springer-Verlag, Berlin, Heidel-
berg, New York (1999)
157. Larochelle, D., Evans, D.: Statically detecting likely buffer overflow vulnerabilities. In: Pro-
ceedings of the 10th USENIX Security Symposium, pp. 177–190. USENIX, Washington
D.C. (2001)
158. Levine, J.R., Mason, T., Brown, D.: Lex and Yacc. UNIX Programming Tools. O’Reilly and
Associates, Sebastopol (1992)
159. Li, K., Wu, M.: Effective Software Test Automation. Sybex, Alameda, CA (2004)
160. Li, K., Wu, M.: Effective GUI Test Automation. Sybex, Alameda, CA (2005)
161. Li, W., Henry, S.: Object-oriented metrics that predict maintainability. Journal of Systems
and Software 23(2), 111–122 (1993)
162. Liberty, J.: Programmieren mit C#. O’Reilly and Associates, Köln (2005)
163. Liggesmeyer, P.: Software-Qualität. Spektrum Akademischer Verlag (2002)
164. van der Linden, P.: Expert C Programming – Deep C Secrets. SunSoft Press, Prentice Hall,
Englewood Cliffs, NJ (1994)
165. Link, J.: Softwaretests mit JUnit. [Link], Heidelberg (2005)
166. Linzmayer, O.W.: Apple Confidential 2.0: The Definitive History of the World’s Most Co-
lorful Company. No Starch Press, San Francisco, CA (2004)
167. Lorenz, M., Kidd, J.: Object-Oriented Software Metrics – A Practical Guide. Prentice Hall,
Englewood Cliffs, NJ (1994)
168. Marcus, E., Stern, H.: Blueprints for High Availability. John Wiley and Sons, New York
(2003)
169. Marshall, E.: Fatal error: How patriot overlooked a scud. Times Magazine 13 (1992)
170. Maslow, A.H.: A theory of human motivation. Psychological Review 50, 370–396 (1943)
171. McCabe, T.J.: A complexity measure. IEEE Transactions on Software Engineering SE-2(4),
308–320 (1976)
172. McCabe, T.J.: Structured Testing. IEEE Computer Society Press (1983)
173. McIlroy, M.D., Kernighan, B.W.: Unix Programmer’s Manual, 7th Edition, vol. 2B. AT & T
Bell Laboratories, Murray Hill, NJ (1979)
174. Mecklenburg, R.: GNU make. O’Reilly and Associates, Sebastopol (2005)
175. Memon, A.M., Pollack, M.E., Soffa, M.L.: Using a goal-driven approach to generate test
cases for guis. In: ICSE ’99: Proceedings of the 21st international conference on Software
engineering, pp. 257–266. IEEE Computer Society Press, Los Alamitos, CA (1999)
176. [Link]
177. Meszaros, G.: Xunit Test Patterns: Refactoring Test Code. Addison-Wesley, Upper Saddle
River, NJ (2007)
Literaturverzeichnis 553
178. Meyer, B.: Object-Oriented Software Construction. Prentice Hall, Upper Saddle River, NJ
(1988)
179. Meyer, B.: Objektorientierte Software-Entwicklung. Hanser Fachbuchverlag, München
(1992)
180. Microsystems, S.: Code conventions for the java programming language. URL
[Link]
181. (MISRA), T.M.I.S.R.A.: Guidelines for the Use of the C Language in Vehicle Based Softwa-
re. MISRA, Ltd., Nuneaton, Warwickshire (1998)
182. (MISRA), T.M.I.S.R.A.: MISRA-C: 2004, Guidelines for the Use of the C Language in Cri-
tical Systems. MISRA, Ltd., Nuneaton, Warwickshire (2004)
183. Mitchell, J.C.: Foundations for Programming Languages. MIT Press, Cambridge (1996)
184. Möller, K.H., Paulish, D.J.: An empirical investigation of software fault distribution. In:
Proceedings of IEEE First International Software Metrics Symposium, pp. 82–90. Baltimore,
MD (1993)
185. Morris, M.F., Roth, P.F.: Computer Performance Evaluation: Tools and Techniques for Ef-
fective Analysis. Van Nostrand Reinhold, New York (1982)
186. Myers, G.J.: Composite Structured Design. Van Nostrand Reinhold, Wokingham, UK (1989)
187. Myers, G.J.: The Art of Software Testing. John Wiley and Sons, New York (2004)
188. Naftalin, M., Wadler, P.: Java Generics and Collections. O’Reilly and Associates, Sebastopol
(2006)
189. Nagel, E., Newman, J.R.: Der Gödel’sche Beweis. Oldenbourg Wissenschaftsverlag, Mün-
chen (2006)
190. [Link]
191. Norrish, M.: C formalised in hol. Ph.D. thesis, University of Cambridge, UK (1998)
192. Ntafos, S.C.: On testing with required elements. In: Proceedings of the Computer Software
and Applications Conference (COMPSAC), pp. 132–139. Edinburgh University Press, Chi-
cago, IL (1981)
193. Ntafos, S.C.: On required element testing. IEEE Transactions on Software Engineering SE-
10(6), 795–803 (1984)
194. Offutt, A.J.: How strong is weak mutation? In: Proceedings of the 4th Symposium on Softwa-
re Testing, Analysis, and Verification, pp. 200–213. IEEE Computer Society Press, Victoria,
British Columbia, CA (1991)
195. Offutt, A.J.: Investigations of the software testing coupling effect. ACM Transactions on
Software Engineering and Methodology (TOSEM) 1(1), 5–20 (1992)
196. [Link]
197. Organick, E.I.: The Multics System: An Examination of its Structure. MIT Press, Cambridge,
MA (1975)
198. Ostrand, T., Weyuker, E.: The distribution of faults in a large industrial software system. In:
Proceedings of the 2002 ACM SIGSOFT International Symposium on Software Testing and
Analysis (ISSTA), pp. 55–64. ACM Press, Roma, Italy (2002)
199. Park, D.: Fixpoint induction and proofs of program properties. Machine Intelligence 5, 59–78
(1969)
200. Patterson, D.A., Gibson, G., Katz, R.H.: A case for redundant arrays of inexpensive disks
(raid). In: SIGMOD ’88: Proceedings of the 1988 ACM SIGMOD international conference
on Management of data, pp. 109–116. ACM Press, New York (1988)
201. Paulk, M.C., Curtis, B., Averill, E., Bamberger, J., Kasse, T., Konrad, M., Perdue, J., Weber,
C.V., Withey, J.: Capability maturity model for software. Tech. Rep. CMU/SEI-91-TR-24
ADA240603, Software Engineering Institute (SEI) (1991)
202. Paulk, M.C., Curtis, B., Chrissis, M.B., Weber, C.V.: Capability maturity model, version 1.1.
IEEE Software 10(4), 18–27 (1993)
203. Paulk, M.C., Weber, C.V., Curtis, B., Chrissis, M.B. (eds.): The Capability Maturity Model:
Guidelines for Improving the Software Process. Addison-Wesley, Reading, MA (1995)
204. Paulk, M.C., Weber, C.V., Garcia, S.M., Chrissis, M.B., Bush, M.: Key practices of the ca-
pability maturity model, version 1.1. Report CMU/SEI-93-TR-25, Software Engineering
Institute, Carnegie Mellon University, Pittsburgh, PA (1993)
554 Literaturverzeichnis
205. Pepper, P.: Funktionales Programmieren in OPAL, ML, HASKELL und GOFER. Springer-
Verlag, Berlin, Heidelberg, New York (2003)
206. Peterson, W.W., Brown, D.T.: Cyclic codes for error detection. Proceedings of the IRE 49,
228–235 (1961)
207. Peterson, W.W., Weldon, E.J.: Error Correcting Codes. MIT Press, Cambridge, MA (1972)
208. Petzold, C.: Programming Windows 3.1. Microsoft Press, Redmond, WA (1992)
209. Petzold, C.: Programming Windows 95. Microsoft Press, Redmond, WA (1996)
210. Pfister, G.F.: In search of clusters : The Coming Battle in Lowly Parallel Computing. Prentice
Hall, Upper Saddle River, NJ (1995)
211. Pfleeger, S.L., Fitzgerald, J.C., Rippy, D.A.: Using multiple metrics for analysis of improve-
ment. Software Quality Journal 1, 27–36 (1992)
212. Phadke, M.S.: Quality Engineering Using Robust Design. Prentice Hall (1989)
213. Pierce, B.C.: Types and Programming Languages. MIT Press, Cambridge (2002)
214. Piwowarski, P.: A nesting level complexity measure. ACM SIGPLAN Notices 17(9), 44–50
(1982)
215. [Link]
216. [Link]
217. Pradham, D., Reddy, S.: A fault-tolerant communication architecture for distributed systems.
Digest of Papers FTCS 11, 214–220 (1981)
218. Pradhan, D.K.: Fault-Tolerant Computer System Design. Prentice Hall, Englewood Cliffs,
NJ (1996)
219. Rausch, A., Broy, M., Bergner, K.: Das V-Modell XT. Grundlagen, Methodik und Anwen-
dungen. Springer-Verlag, Berlin, Heidelberg, New York (2007)
220. Riedemann, E.H.: Testmethoden für sequentielle und nebenläufige Systeme. Teubner Verlag,
Stuttgart (1997)
221. Ritchie, D.M.: The development of the c language. Tech. rep., Bell Labs/Lucent Technolo-
gies, Murray Hill, NJ 07974 USA (2003)
222. Roberts, D., Johnson, R.: A refactoring tool for smalltalk. Theory and Practice of Object
Systems 3, 253–263 (1997)
223. Robertson, J.E.: A new class of digital division methods. IRE Transactions Electronic Com-
puters 7(7), 218–222 (1958)
224. Royce, W.W.: Managing the development of large software systems: Concepts and techni-
ques. TRW Software Series SS-70-01, 1–9 (1970). Nachgedruckt in [225]
225. Royce, W.W.: Managing the development of large software systems: Concepts and techni-
ques. In: ICSE ’87: Proceedings of the 9th International Conference on Software Enginee-
ring, pp. 328–338. IEEE Computer Society Press, Los Alamitos, CA (1987)
226. Salt, N.F.: Defining software science counting strategies. ACM SIGPLAN Notices 17(3),
58–67 (1982)
227. Schildt, H.: The Annotated ANSI C Standard: American National Standard for Programming
Languages-C : ANSI/ISO 9899-1990. McGraw-Hill, Berkeley, CA (1993)
228. Sedgewick, R.: Algorithmen. Pearson Studium, München (2002)
229. Sedgewick, R.: Algorithmen in Java. Grudlagen, Datenstrukturen, Sortieren, Suchen. Teil
1–4. Pearson Studium, München (2003)
230. [Link]
231. Sharble, R.C., Cohen, S.S.: The object-oriented brewery: a comparison of two object-
oriented development methods. SIGSOFT Software Engineering Notes 18(2), 60–73 (1993)
232. Silverberg, I.: Source File Management with SCCS. Prentice Hall, Upper Saddle River, NJ
(1991)
233. Singh, A.: Mac OS X Internals. Addison-Wesley, Upper Saddle River, NJ (2006)
234. Soltau, M.: Unix/Linux Hochverfügbarkeit. Mitp-Verlag, Bonn (2002)
235. Spivey, J.M.: The Z Notation : A Reference Manual. Prentice Hall, New York (1992)
236. [Link]
237. Spolsky, J.: How Microsoft lost the API war. Apress (2004)
Literaturverzeichnis 555
269. Weinberg, G.M.: The Psychology of Computer Programming. Dorset House Publishing
(1998)
270. Welker, K.D., Oman, P.W.: Software maintainability metrics models in practics. Crosstalk,
Journal of Defense Software Engineering 8(11), 19–23 (1995)
271. Winkelhofer, G.A., Kessler, H.: Projektmanagement: Leitfaden zur Steuerung und Führung
von Projekten. Springer-Verlag, Berlin, Heidelberg, New York (2004)
272. [Link]
273. Wolfram, S.: The Mathematica Book. Wolfram Media Inc., Champaign, IL (2004)
274. Wordsworth, J.B.: Software Development with Z. Addison-Wesley, Wokingham, England
(1992)
275. Yourdon, E., Constantine, L.L.: Structured Design. Prentice Hall, Englewood Cliffs, NJ
(1979)
276. Zeller, A., Krinke, J.: Essential Open Source Toolset. John Wiley and Sons, New York (2005)
277. Zukowski, J.: Java Collections. Springer-Verlag, Berlin, Heidelberg, New York (2001)
Sachverzeichnis
557
558 Sachverzeichnis
Fallthrough-Mechanismus 295 G
False negative 292, 310, 312, 338
False positive 338 Gödel, Kurt 342
Fan-In 265 Gamma, Erich 470
Fan-Out 265 Garbage collector 388
Fat binary 133, 134 Gates, Bill 2, 489
Feature 385 GCC siehe GNU Compiler Collection
Fehler Gemeinsame Teilfolge 444
Generics 82, 83
-bericht 477
Generische Schnittstelle 90
-bewertung 61
Geschäftsprozess 187
-blindheit 323 ggT siehe Gröster gemeinsamer Teiler
-datenbank 477 Glass, Robert L. 266
-induktion 238 GNU
-management 477, 527 -Notationsstil 72
-merkmal 478 -Projekt 278
-transformation 238 Compiler Collection 233
arithmetischer 238 Golden log 470
Konstanten- 238 Gröster gemeinsamer Teiler 252
lexikalischer 27 Grafische Benutzungsoberfläche 472
Logik- 238 Grammatik 27, 276
numerischer 44 Graph
Offset- 238 stark zusammenhängender 216
semantischer 36 Graphical user interface 472
syntaktischer 27 Gray-Box-Test 174
Greedy-Methode 28
Variablen- 238, 319
Grenze
Verknüpfungs- 238
asymmetrische 402
Zuweisungs- 238 symmetrische 401
Fehlertoleranz 21, 98 Grenzinduktion 183
Feldman, Stuart 453 Grenzwertbetrachtung 180
Feldtest 169 GT siehe Gemeinsame Teilfolge
Fence post error 402 GUI siehe Graphical user interface
Fenton, Norman E. 269
FileMerge (Applikation) 434 H
Final findings 530
Finalzustand 186 Höherwertige Logik 146, 342
Fisher, Ronald A. 10 Halstead, Maurice H. 251
Fixpunktiteration 357, 362 Halstead-Metrik 251
Flawfinder 307 Halteproblem 314, 348
FlexRay-Bus 3 Hardware-Software-Co-Design 374
Floyd, Robert W. 361 Head-Konfiguration 425, 426
Follow-Up-Sitzung 331 Heap 302
Formale Synthese 105 Henry, Sally M. 266
Hidden feature 331
Format-String 285
Hoare
Fowler, Martin 396
-Kalkül 342
Frame pointer siehe Base pointer -Tripel 343
Free Software Foundation 278 Hoare, Charles A. R. 342
Funktionalität (Kriterium) 7 Hochverfügbarkeit 466
Funktions Hot plugging 466
-Epilog 305 Hot swapping 466
-Prolog 304 Human ressources 530
Funktionstest 170 Humphrey, Watts S. 519
Sachverzeichnis 561
ILP32-Datenmodell 113 K
ILP64-Datenmodell 114
Include-Beziehung 189 k-dr-Sequenz 228
Individualisierung 491 K&R-Notationsstil 71
Induktionstheorem 342 Kafura, Dennis G. 266
Initialentwicklung 415 Kalkül 338
Injektionsvektor 306 Kernel space 140
Input/Output Control 90 Kernighan, Brian W. 70, 292
Inside-Out-Integration 165 Key process area 521
Inspektion 23, 313, 322, 327 Kiviat-Diagramm 270
Inspektionsbericht 331 Klasse 77
Installationstest 172 Klasseninvariante 95
Integrated product development 530 Koenig, Andrew 28
Integration Kohäsion 265
anwendungsgetriebene 166 Kommunikation
Big-Bang- 163 dezentrale 3
Bottom-Up- 164 Kompatibilitätstest 171
funktionsorientierte 166 Kompatibilitätsumgebung 412
Inside-Out- 165 Komplexität 13
Outside-In- 164 algorithmische 128, 172
risikogetriebene 166 Daten- 264
strukturorientierte 163 Methoden- 264
System- 499 gewichtete 264
termingetriebene 166 zyklomatische 18, 259
testgetriebene 166 Komplexitätsklasse 172
Top-Down- 164 Komplexitätstest 172
Integrationsabteilung 428 Komponentenmetrik 263
Integrationstest 159, 163 Komponententest 159
Intervallschachtelung 161 Konfiguration 192, 425
Intervallsuche 161 Head- 425, 426
Invariante 95, 343 Latest- 425, 426
Klassen- 95 Konfigurationsmanagement 25
Schleifen- 95 Konformitätsanalyse 23, 271
ISO Konformitätsregel 271
9126 6 Konklusion 343
12207 535 Konstantenfehler 238
13568 146 Konstruktive Qualitätssicherung 21, 65
15288 535 Kontrollflussanomalie 313
15504 515, 535 Kontrollflussgraph 202
C90 284 expandierter 204
kantenmarkierter 203
J knotenmarkierter 204
kollabierter 205
Jaskiel, Stefan P. 157 teilkollabierter 204
Java 78, 81, 132, 388, 406, 471 Kontrollflussmodellierung 202
Coding Style 70 Kopenhagener Deutung 543
Jeffries, Ron 507 Kopplungs
JIT siehe Just-in-Time -analyse 265
Job specification 466 -effekt 241
562 Sachverzeichnis
Multiple-Metrics-Graph 270 P
Multiversion File System 421
Mutant 238 p-use 221
Mutationstest 23, 200, 238 P64-Datenmodell 112
prädizierender 240 Paarweises Testen 192
schwacher 242 Pad 125
starker 239 Parallelität
vergleichender 240 als Fehlerquelle 41
Mutationstransformation 238 Parallelstruktur 126, 390
MVFS siehe Multiversion File System Parameterextraktion 279
Myers, Glenford J. 245 Pareto
-Diagramm 267
-Prinzip 268
N
Pareto, Vilfredo F. 268
Park, David 361
Nachbedingung 93, 338, 343 Parser 274
NCSS-Metrik 249 Partition 175
Network byte order 121, 122 Pascal-Case-Notationsstil 66
Network file system 465 Pathfinder 41
Netzdiagramm 270 Paulish, Daniel J. 269
NFS siehe Network file system Peano, Giuseppe 342
Nicely, Thomas R. 58 Peano-Axiome 342
Notationskonvention 65, 66 Peer review 525, 526
Notationsstil 66 Petzold, Charles 67
Allman- 72 Pfad 222
BSD- 72 definitionsfreier 222
Camel-Case- 66 Pfadüberdeckung 201, 210
GNU- 72 strukturierte 212
K&R- 71 Phase 504
Lowercase- 67 Phony target 459
Pascal-Case- 66 Pike, Robert C. 292
Uppercase- 66 Pilotprojekt 527
Whitesmith- 72 PL/I 105
Ntafos, Simeon C. 227 Planungsspiel 509
Plattformunabhängigkeit 107
O Portabilität 21, 46, 107
auf Sprachebene 131
auf Systemebene 134
Oberflächentest 472 Portable C Compiler 283
Objectory Process 502 Portierbarkeit (Kriterium) 9
Objektorientierte Metrik 262 Portierung
Offline review 327, 328 Architektur- 107
Offsetfehler 238 Betriebssystem- 107
Ohlsson, Niclas 269 Sprach- 108
On-the-fly-Compilierung 132 System- 108
Operand 251 Prädikat 341
Operator 251 atomares 215
Optimierung zusammengesetztes 215
inkrementelle 147 Prädikatenlogik 341
Orthogonales Feld 193 Prädizierender Mutationstest 241
Ostrand, Thomas J. 269 Prämisse 343
Out-By-One-Fehler 402 Predicative use 221
Outside-In-Integration 164 Prioritäteninversion 41, 43
Outsourcing 524 Prioritätenvererbung 43
564 Sachverzeichnis