Code-Coverage undercover: Nutzen und Tücken Trace-basierter Codeabdeckung

Seite: 2/3

Firmen zum Thema

Coverage messen, Programmcode instrumentieren

Beim Code-Coverage handelt es sich um ein kontrollflussbasiertes Verfahren. Dafür muss zuerst das zu messende Programm oder Modul mit den entsprechenden Stimuli ausgeführt oder zumindest simuliert werden. Anschließend wird je nach Coverage-Stufe die Ausführung von Statements, Verzweigungen oder Einzelbedingungen bewertet.

Eine Möglichkeit, die notwendigen Daten zu gewinnen, ist die Instrumentierung des originalen Programmcodes. Die Konsequenz daraus ist aber, dass der eingefügte Testcode nicht nur das Laufzeitverhalten und die Codegröße, sondern gegebenenfalls sogar das Speicherlayout, beeinflusst. Bei sicherheitskritischen Anwendungen oder Systemen mit hohen Echtzeitanforderungen ist dies durchaus ein kritischer Punkt.

Trace als Alternative, um Statement-Coverage abzuleiten

Will man auf die Instrumentierung verzichten und ist zudem auf ein exaktes Zeitverhalten der zu testeten Applikation angewiesen, kommt man eigentlich um Trace nicht herum. Dafür ist aber eine geeignete Trace-Hardware inklusive einer Trace-Schnittstelle zum Coverage-Tool durch das Embedded-System bereitzustellen. Zumindest für den betrachteten Automotive-Sektor kann man inzwischen auf entsprechend ausgestaltete Controller zurückgreifen. Mittlerweile ist die Verfügbarkeit von Trace sogar ein K.-o.-Kriterium bei der Plattformentscheidung.

Aus den gewonnen Trace-Daten – typischerweise sind das die Adressen der ausgeführten Maschinenbefehle oder zumindest der Verzweigungsoperationen – lässt sich mit Hilfe entsprechend geeigneter Werkzeuge sehr leicht ein Statement-Coverage ableiten. Kniffeliger wird es hingegen bei den höheren Coverage-Stufen. Hier sind auch Informationen zur Programmstruktur, also dem Kontrollflussgraphen notwendig (Bild 2).

Unter anderem benötigt das Coverage-Tool diesen, um anhand der aufgezeichneten Adressen die Richtung zu ermitteln, welche die Programmausführung nach einer bedingten Verzweigung eingeschlagen hat und natürlich auch um festzustellen, welche Programmteile überhaupt nicht ausgeführt wurden. Letzte sind ja im Trace schlichtweg nicht vorhanden. Das Wissen darüber muss also aus einer anderen Quelle kommen. Aus nichtoptimierten Code lässt sich der Kontrollfluss relativ einfach aus dem verfügbaren Quellcode und den Debug-Informationen im ELF-File herleiten. Bei optimiertem Code wird es hingegen schnell kompliziert.

Bildergalerie
Bildergalerie mit 6 Bildern

Optimierter Code – anders als man denkt

Jeder Entwickler weiß, dass eine direkte Übersetzung des Quellcodes in ausführbare Maschineninstruktionen weder hinsichtlich der Laufzeit noch des Speicherbedarfes optimal ist. Compiler bieten daher verschiedene Optimierungsmöglichkeiten, die teilweise recht aggressiv die Struktur des Maschinencodes verändern. So geht oftmals die eindeutige Zuordnung von Basisblöcken des Quellcodes – also den ausschließlich sequenziellen Codepartien – zu denen des Maschinencodes verloren.

Basisblöcke können unter anderem vervielfältigt, zusammengefasst oder eliminiert werden. Solche Programmtransformationen treten beispielsweise bei Schleifentransformationen (Schleifenspaltung, -vereinigung oder -expansion), „if-then-else“-Transformationen (z.B. Eliminierung eines Anweisungszweiges) oder bei der Umwandlung von Basisblöcken in Tabellenzugriffe auf. Letzteres stellt vor allem für das Branch-Coverage eine Herausforderung dar.

Betrachten wir folgendes Beispiel: Für eine normale switch-Anweisung in C/C++ (Listing 1) erzeugt der Compiler zuerst einen Maschinencode, der den Basisblöcken der switch-Anweisung noch zugeordnet werden kann (Listing 2). Zu Beginn jedes Blockes wird überprüft, ob der Wert im Kopf der switch-Anweisung mit der case-Konstante übereinstimmt. Falls nicht, wird das nächste Label angesprungen, wo dann die Prüfung mit nächsten case-Konstanten stattfindet. Existiert ein default-Block, wird dieser ausgeführt, wenn alle anderen Vergleiche fehlschlugen.

Der Compiler hat hier verschiedene Möglichkeiten der Optimierung. Bei kleinen Wertebereichen der case-Konstanten kann diese beispielsweise als Index in eine Sprungtabelle verwendet werden, welche die Adressen der Label enthält (Listing 3). Damit fallen bereits die Basisblöcke der Vergleiche weg. Für Werte außerhalb des Wertebereiches wird einfach zum default-Label gesprungen.

Artikelfiles und Artikellinks

(ID:43140991)