Model Checking: Automatisch Fehler finden in C Programmen 

In der Softwareentwicklung wird oft ein berproportional groer Anteil der Ressourcen fr die Verifikation aufgewandt. Dennoch werden Anwender tagtglich mit fehlerhafter Software konfrontiert. Um dies zu verhindern, wird meist versucht, die Korrektheit von Programmen durch aufwndiges, extensives Testen sicherzustellen. Ein hheres Ma an Zuverlssigkeit und Automatisierung wre jedoch wnschenswert. Ein Ansatz der Abhilfe verspricht ist Model Checking.

Model Checking wurde vor nun beinahe drei Jahrzehnten als Methode zur automatischen formalen Verifikation von Modellen mit endlich vielen Zustnden ersonnen. Die Vter dieser Methode, Edmund Clarke, Allen Emerson und Joseph Sifakis, wurden 2007 mit dem angesehenen, alljhrlich von der Association for Computing Machinery (ACM) verliehenen Turing-Preis ausgezeichnet. Die zugrundeliegende Idee ist die systematische und vollstndige Untersuchung aller erreichbaren Programmzustnde, wobei es sich bei einem Zustand um eine Belegung der Variablen handelt. Dieses Ziel ist aufgrund der berwltigenden Gre des Zustandsraumes realistischer Programme (allein eine 32-Bit-Variable kann mehrere Milliarden verschiedener Werte annehmen) nur 
durch den Einsatz intelligenter und effizienter Algorithmen erreichbar. Dementsprechend ist die Forschung in diesem Gebiet nach wie vor sehr rege, und auf den einschlgigen Konferenzen werden jhrlich neue und verbesserte Anstze prsentiert. Trotz des eher formalen Hintergrundes sind die dort vorgestellten Methoden sehr praxisorientiert: Model Checking ist lngst den Kinderschuhen entwachsen und bei der Verifikation komplexer Systeme wie Prozessoren und Busprotokollen nicht mehr wegzudenken, da die verkrzten Entwicklungszyklen und die erhhte Qualitt ein entscheidender Wettbewerbsvorteil sind. Dies schlgt sich in der Verfgbarkeit einer Vielzahl ausgereifter Verifikationswerkzeuge nieder. 

Das primre Einsatzgebiet von Model Checking ist die Verifikation von integrierten Schaltungen, jedoch wird die Methode zunehmend auch fr die Verifikation von Software verwendet. Das Modell liegt in diesem Fall als Software-Quelltext vor. Der Erfolg von Microsofts Model Checking-basiertem Static Driver Verifier hat dem Ansatz zu beachtlicher Popularitt verholfen. Dieses Werkzeug dient der automatischen berprfung der korrekten Verwendung der Programmierschnittstellen des Kernels in Windows Gertetreibern. Mittlerweile ist eine Reihe von Software Model Checkern verfgbar, einige darunter auch als freie Software unter der Apache- oder BSD-Lizenz (siehe Kasten).

Die algorithmischen Aspekte derartiger Werkzeuge wurden bereits in einem frheren iX-Artikel beleuchtet [1]. Im aktuellen Artikel erklren wir den praktischen Einsatz von automatischen 
Verifikationswerkzeugen fr Software. Die zugrunde liegenden Konzepte sind bei allen Werkzeugen hnlich. Wir erklren deren Anwendung exemplarisch anhand des C Bounded Model Checkers (CBMC) [3], welcher zum Zwecke der Verifikation von ANSI-C Programmen entwickelt wurde.

Ein Programm wird als korrekt erachtet, wenn es seiner Spezifikation entspricht. Anders als bei klassischen formalen Methoden (wie zum Beispiel der Vienna Development Method [2]), in denen blicherweise von einer vollstndigen, mathematisch rigorosen Spezifikation  ausgegangen wird, zielen Model Checker darauf ab, die Gltigkeit partieller Spezifikationen zu berprfen. Eine partielle Spezifikation beschreibt einen Teilaspekt des zu berprfenden Programms, wie zum Beispiel die Korrektheit eines Speicherzugriffes auf ein Datenfeld (in Hinsicht auf Pufferberlufe oder ungltige Zeiger). Whrend mit Hilfe von temporalen Logiken (wie der Linear Temporal Logic LTL) beraus komplexe Spezifikationen formuliert werden knnen, beschrnken sich viele Softwareverifikationswerkzeuge auf die berprfung
einfacher Behauptungen (Assertions). Diese werden mit Hilfe des assert Befehls, welcher Teil der ANSI-C Funktionenbibliothek ist, direkt im Programm angegeben. Dies hat den Vorteil, dass  der Programmierer keinen neuen Formalismus erlernen muss, sondern auf bereits vertraute und bewhrte Spezifikationsmechanismen zurckgreifen kann. Wie wir im Laufe des Artikels erklren werden, ist es mit Hilfe von entsprechenden API-Modellen dennoch mglich, ausreichend komplexe Eigenschaften zu beschreiben. Zustzlich zu den Eigenschaften, die vom Programmierer mit Hilfe der Funktionen assert (und __CPROVER_assert) spezifiziert werden, kann CBMC noch automatisch Verifikationsbedingungen fr arithmetischen berlauf, Division durch Null, Validitt von Zeigern und Pufferberlufe generieren.

Eine der Eigenheiten, die Software grundlegend von Hardwaremodellen unterscheidet, ist die Mglichkeit, zur Laufzeit Speicher zu allozieren. Dies erschwert die automatische Verifikation von Programmen und macht sie in manchen Fllen sogar unmglich. Daher ist der vorwiegende Einsatzzweck von Model Checkern das Auffinden von Fehlern. Konsequenterweise versucht CBMC erst gar nicht, die Korrektheit von Programmen vollstndig zu beweisen, sondern beschrnkt sich darauf, alle ausfhrbaren Programmpfade bis zu einer gewissen Tiefe von Instruktionsschritten auf Fehler zu untersuchen. Diese Tiefe muss vom Anwender beim Aufruf von CBMC festgelegt werden.

Wenn ein Model Checker eine Verletzung einer spezifizierten Eigenschaft feststellt, so liefert es ein entsprechendes Gegenbeispiel. Ein Gegenbeispiel ist, hnlich einem fehlgeschlagenen Testfall, ein ausfhrbarer Pfad des Programms (also eine Sequenz von Instruktionen), der zu einer Verletzung der Spezifikation fhrt. Im Gegensatz zum Testen mssen jedoch keine expliziten Testflle vorgegeben werden. Model Checker berprfen selbstttig smtliche Kombinationen (im Rahmen der vom Anwender vorgegebenen Einschrnkungen) von mglichen Eingabewerten. Manche Model Checker, so auch CBMC, sind in der Lage, fr jeden
Instruktionsschritt des Gegenbeispiels eine entsprechende Belegung der Programmvariablen zu liefern. Diese Fhigkeit erleichtert das Verstndnis des vorliegenden Fehlers ungemein.

Software Model Checking ist auch sonst eng mit Testen verwandt. So muss vom Anwender genau definiert werden, welche Teile des Programms berprft werden sollen. Der zu berprfende Quelltext muss klar gegen die Programmumgebung und die verwendeten Funktionsbibliotheken abgegrenzt werden. Warum dies so wichtig ist, ist am Beispiel eines Gertetreibers einfach ersichtlich: Fehlen dementsprechende Vorgaben, so ist der Model Checker gezwungen, das vollstndige Betriebssystem zu verifizieren. Dies sprengt nicht nur den Rahmen des praktisch
Machbaren (automatische Softwareverifikation skaliert nur sehr eingeschrnkt), es ist auch insofern problematisch, als viele Bibliotheken und API-Funktionen nicht im Quelltext vorliegen und daher nicht vom Model Checker verarbeitet werden knnen.

Das Verhalten der Programmumgebung wird mit Hilfe einer Testfunktion, welche die Funktionen des Programms im Rahmen von Testszenarien aufruft, festgelegt. Im einfachsten Fall handelt es sich hierbei um die Hauptfunktion des Programms. Anders als beim Testen mssen jedoch nicht alle Eingabewerte explizit definiert sein. Model Checker erlauben es, Variablen nicht-deterministisch zu initialisieren. Der Model Checker zieht dann alle mglichen Werte in Betracht. In CBMC wird dies durch die Verwendung von eigens dafr vorgesehenen Funktionen erreicht. Trifft CBMC auf eine Funktion, deren Name mit dem Prfix nondet_ beginnt, so nimmt der Model Checker an, dass diese Funktion einen beliebigen, nur durch den Typen der Funktion eingeschrnkten Rckgabewert liefern kann. Dieser kann weitergehend durch zustzliche Annahmen (Assumptions) eingeschrnkt werden, welche in CBMC mit Hilfe der Funktion
__CPROVER_assume eingefhrt werden. Dieses Vorgehen wird in den Zeilen 13 bis 15 des Quelltextes 1 demonstriert, welcher dazu dient, zu verifizieren, dass die Funktion
GroessterGemeinsamerTeiler tatschlich einen Teiler der bergebenen Parameter berechnet. Der zu berprfende Wertebereich der Parameter wird durch die Annahme in Zeile 15 auf Null bis Zehn eingeschrnkt. Fr eine vollstndige Verifikation dieser Funktion durch klassisches
Testen wren 121 Testflle notwendig. 

CBMC generiert fr dieses Beispielprogramm vier Verifikationsbedingungen (Claims), die mit Hilfe des Kommandozeilenparameters --show-claims angezeigt werden knnen (siehe Abbildung 1). Diese Bedingungen sind als zustzliche Assertions zu verstehen. Drei dieser Bedingungen beziehen sich auf eine potenzielle Division durch Null in den Zeilen  6 und 17, whrend die letzte der Bedingungen mit der Assertion in Zeile 17 korrespondiert. Wir knnen nun versuchen, mit Hilfe von CBMC das Programm bis zu einer (willkrlich gewhlten) Tiefe von drei Schleifeniterationen zu verifizieren:

  cbmc --unwind 3 gcd.c

Als Resultat dieses Aufrufes meldet CBMC, dass die ``unwinding assertion'' fr die Schleife in Zeile 5 verletzt wurde. Derartige Assertions werden von CBMC automatisch generiert, um zu berprfen, ob fr die betroffenen Schleifen eine ausreichende Iterationstiefe erreicht wurde. Dieses Verhalten lsst sich zwar deaktivieren (--no-unwinding-assertions), ist jedoch im Allgemeinen empfehlenswert, da der Anwender gewarnt wird, falls die festgelegte
Iterationstiefe nicht ausreicht, um das Programm vollstndig zu verifizieren.

In unserem Fall ntigt uns CBMC mit dieser Meldung, die maximale Iterationstiefe zu erhhen. Ein kurzer Blick auf das zu berprfende Programm verrt uns, dass 10 Iterationen fr eine vollstndige Verifikation hinreichend sind, da der Wert von b in jeder Iteration 
der Schleife verringert wird (tatschlich wren bereits 6 Iterationen ausreichend). Ein erneuter Versuch mit einer auf 10 erhhten Iterationsanzahl fhrt dazu, dass CBMC ein Gegenbeispiel prsentiert, welches eine Division durch Null enthlt (Gegenbeispiel 1). 
Tatschlich wurde im Programm (Quelltext 1) vergessen, dass die Parameter der Funktion GroessterGemeinsamerTeiler nicht Null sein drfen. Wir knnen dies erzwingen, indem wir der Bedingung in Zeile 15 (x > 0) && (y > 0) hinzufgen. Der Versuch das entsprechend modifizierte Programm zu verifizieren gelingt schlussendlich; CBMC meldet eine erfolgreiche Verifikation.

Das einfache, eben vorgestellte Programm verwendet keinerlei Systemschnittstellen. Fr komplexere Programme wie Gertetreiber ist es, wie bereits erwhnt, notwendig, die Funktionen des Betriebssystems zu modellieren, um den zu verifizierende  Quelltext des Gertetreibers gegen das Betriebssystem abzugrenzen. Eine hinreichend akkurate Modellierung der Kernelfunktionen ist beraus aufwndig, daher werden Verifikationstools wie Microsofts  Static Driver Verifier mit einem umfassenden Modell der Kernelfunktionen und einer Vielzahl vordefinierter Schnittstellenspezifikationen ausgeliefert. Wir bedienen uns in unserem Beispiel des Linux-Kernelmodells des frei verfgbaren Verifikations-Frameworks DDVerify [4]. Eine entsprechend vereinfachte Version dieses Modells steht als gepacktes Archiv unter (ix-Downloadlink??) zur Verfgung. Das enthaltene Beispiel lsst sich unter Linux und Mac OS X Systemen, auf denen der GNU C Compiler installiert ist, ausfhren.

In diesem Modell ist bereits eine Reihe von partiellen Spezifikationen integriert. Quelltext 2 zeigt die entsprechenden Modelle dreier Funktionen des Linux Kernels. Die Funktion request_region erlaubt dem Treiber, einen Bereich von Systemports fr den exlusiven Zugriff
zu reservieren. Im Modell dieser Kernelfunktion wird der angeforderte Bereich vermerkt (Zeilen 15 bis 16), um bei einem spteren Zugriff durch die Funktionen inb und outb berprfen zu knnen, ob der Treiber die entsprechenden Anschlsse auch reserviert hat (Zeilen 23 und 29). Weder in der Funktion inb, noch in der Funktion outb, findet ein tatschlicher Zugriff auf die Hardware statt. Die Funktion inb liefert bei Aufruf einen nicht-deterministischen Rckgabewert, was lediglich einer Annherung an die Wirklichkeit entspricht. Diese Ungenauigkeit im Modell kann zu ungerechtfertigten Gegenbeispielen fhren (die in diesem Fall durch eine Verfeinerung des Modelles vom Anwender explizit ausgeschlossen werden mssten), ist jedoch fr unsere Zwecke ausreichend. Wie genau das Systemmodell
sein muss, hngt stark von der zu verifizierenden Eigenschaft ab.

Da ein Gertetreiber mehr als einen Eintrittspunkt hat, enthlt unser Beispiel eine Hauptfunktion, die den Treiber initialisiert und die angebotenen Funktionen in nicht-deterministischer Reihenfolge ausfhrt. Die Verifikation beschrnkt sich auf ein sequenzielles Modell. Es wird lediglich ein Treiber ohne nebenlufige Prozesse simuliert. 

Die im Archiv enthaltene Datei machzwd.c enthlt den (unmodifizierten) Quelltext des MachZ ZF-Logic Watchdog Timer Gertetreibers des Linux Kernels 2.6.19. Die beigelegte
Batch-Datei ddv_cbmc ruft CBMC mit den Quelldateien des Betriebssystemmodells als Parameter auf. Ausserdem ist die Iterationstiefe global auf drei beschrnkt, mit Ausnahme einer Schleife in der von DDVerify generierten Initialisierungsfunktion, welche ein Datenfeld initialisert und daher von dieser Schranke ausgenommen ist. Derartige Ausnahmen knnen mit dem Parameter --unwindset definiert werden, mit dessen Hilfe Iterationsschranken
fr einzelne Schleifen festgelegt werden knnen. Die Indizierung, die CBMC fr Schleifen verwendet, kann mit dem Parameter --show-loops abgefragt werden.

Der Aufruf

 ddv_cbmc --show-claims

liefert 33 Verifikationsbedingungen (Claims), wovon die dritte und vierte auf die Behauptungen in Zeile 23 und 29 in Quelltext 2 zurckzufhren sind. Quelltext 3 zeigt eine (zwecks Verstndlichkeit) stark vereinfachte Version der Initialisierungsfunktion des machzwd Treibers. Die vierte Verifikationsbedingung fordert, dass der in Zeile 4 verwendete Anschluss 0x218 vom Treiber vor Verwendung reserviert wurde. Mit Hilfe von CBMC knnen wir feststellen, dass dies nicht der Fall ist: Der Aufruf 

 ddv_cbmc --claim 4

liefert ein detailliertes Gegenbeispiel, in dem diese Bedingung der Linux Systemschnittstellen verletzt wird. Dieser Fehler kann Auswirkungen auf die Systemstabilitt haben, wenn mehrere Treiber auf den entsprechenden Anschluss zugreifen. Ein derartiges Problem ist durch Testen nur schwer zu entdecken. Mit Hilfe eines Model Checkers sind solche (und auch komplexere) Fehler einfach aufzuspren.

Dennoch sind die Mglichkeiten von Model Checkern zur Zeit noch noch relativ eingeschrnkt. Die Grsse der Programme, die von automatischen Verifikationswerkzeugen erfolgreich verarbeitet werden knnen, liegt, je nach Komplexitt des jeweiligen Programms, zwischen einigen hundert und mehreren tausend Quelltextzeilen. Parallele Programme und komplizierte, dynamisch allozierte Datenstrukturen stellen die meisten Model Checker noch vor unlsbare Probleme. Die automatische Verifikation vollstndiger Systeme ist daher noch ausser Reichweite. Ein aktueller Trend ist modellbasierte Entwicklung, Model Checking und Testen zu kombinieren, um die Vorteile dieser Anstze zu vereinen. Als Beispiel sei das europische Forschungsprojekt MOGENTES genannt, dessen Ziel es unter anderem ist, Testflle aus Simulink-Modellen zu generieren. Fr Kompenententests ist Model Checking jedoch bereits heute uneingeschrnkt empfehlenswert. Der Aufwand, Modelle der Systemschnittstellen erstellen zu mssen, macht sich durch die hhere Zuverlssigkeit der entwickelten Module und dem daraus resultierenden verringerten Verifikationsaufwand bezahlt.

Dr. Daniel Krning ist Fakulttsmitglied der Universitt Oxford
und leitet dort eine Forschungsgruppe mit dem Themenschwerpunkt Verifikation.

Georg Weienbacher ist Doktorand an der Universitt Oxford und
wissenschaftlicher Mitarbeiter der ETH Zrich. Seine Forschung
wird durch ein Stipendium von Microsoft Research Cambridge untersttzt.

[1] Georg Weienbacher, Abstrakte Kunst: Fehler finden mit Model Checkern, iX 5/2004, Seite 116

[2] Georg Weienbacher, Ohne Beweis: VDM++: Lightweight Formal Methods, iX 3/2001, Seite 157

[3] Edmund Clarke, Daniel Krning, Flavio Lerda, A Tool for Checking ANSI-C Programs, in Tools and Algorithms for the Construction and Analysis of Systems (TACAS), 2004

[4] Thomas Witkowski, Nicolas Blanc, Daniel Krning, Georg Weienbacher, Model Checking Concurrent Linux Device Drivers, in Automated Software Engineering (ASE), 2007

[5] Vijay D'Silva, Daniel Krning, Georg Weienbacher, A Survey of Automated Techniques for Formal Software Verification, IEEE Transactions on CAD of Integrated Circuits and Systems 27(7), 2008 