c systems notebook

Statische Analysewerkzeuge

Statische Analysewerkzeuge prüfen den Quellcode, ohne ihn auszuführen. Sie erkennen Speicherfehler, Typfehler, ineffiziente Strukturen und Verstöße gegen Programmierregeln. So lassen sich Probleme frühzeitig erkennen, bevor sie zur Laufzeit Fehler verursachen.

Clang-Tidy

Clang-Tidy ist ein Analysewerkzeug für C und C++, das auf der LLVM/Clang-Infrastruktur basiert. LLVM liefert das Compiler-Backend und Clang den Frontend-Parser. Da Clang-Tidy dieselbe Parser- und Typanalyse wie Clang verwendet, kennt Clang-Tidy den Code genau, auch Makros, Templates und Headerabhängigkeiten.

Weiterhin enthält Clang-Tidy eigene statische Prüfungen, die nicht Teil der normalen Compilerwarnungen sind. Es erkennt Stilabweichungen, Verstöße gegen Coding-Guidelines (z. B. C++ Core Guidelines oder Google Style) und ineffiziente Datenstrukturen. Damit deckt Clang-Tidy mehr Fehlerquellen ab als der Compiler selbst.

Zunächst muss Clang-Tidy installiert werden:

# Debian/Ubuntu
$ sudo apt update
$ sudo apt install clang-tidy

# Fedora
$ sudo dnf install dnf-plugins-core
$ sudo dnf config-manager --add-repo https://rpm.llvm.org/fedora/llvm.repo
$ sudo dnf install clang clang-tools-extra

# Arch Linux
$ sudo pacman -S clang-tidy

# openSUSE
$ sudo zypper install clang-tools

# Alpine Linux
$ sudo apk add clang-extra-tools

Unser Beispiel zur Analyse:

#include <stdint.h>

struct S {
    uint8_t  a;  // 1 Byte
    uint32_t b;  // 4 Bytes
    uint16_t c;  // 2 Bytes
};

struct S s;

int main() {}

Die Ausgabe (mit zusätzlichen Leerzeilen zur besseren Lesbarkeit) ist folgende:

$ clang-tidy main.c --checks='*' -- -std=c11

817 warnings generated.
…/main.c:3:8: warning: accessing fields in struct 'S' is inefficient due ↩
to padding; only needs 7 bytes but is using 12 bytes [altera-struct-pack-align]
struct S {
       ^

…/main.c:3:8: note: use "__attribute__((packed))" to reduce the amount of ↩
padding applied to struct 'S'

…/main.c:3:8: warning: accessing fields in struct 'S' is inefficient due ↩
to poor alignment; currently aligned to 4 bytes, but recommended alignment ↩
is 8 bytes [altera-struct-pack-align]
struct S {
       ^

…/main.c:3:8: note: use "__attribute__((aligned(8)))" to align struct 'S' ↩
to 8 bytes

…/main.c:9:10: warning: variable 's' is non-const and globally accessible, ↩
consider making it const [cppcoreguidelines-avoid-non-const-global-variables]
struct S s;
         ^

…/main.c:9:10: warning: variable name 's' is too short, expected at least ↩
3 characters [readability-identifier-length]

Suppressed 813 warnings (813 in non-user code).
Use -header-filter=.* to display errors from all non-system headers. ↩
Use -system-headers to display errors from system headers as well.

Clang-Tidy erkennt in diesem Beispiel zwei Arten von Problemen: Einerseits weist es auf eine ineffiziente Speicheranordnung der Struktur S hin, weil durch die Reihenfolge der Mitglieder unnötige Padding-Bytes entstehen. Es schlägt vor, die Struktur entweder mit __attribute__((packed)) kompakter zu machen oder ihre Ausrichtung mit __attribute__((aligned(8))) anzupassen. Andererseits meldet es, dass die globale Variable s besser als const deklariert werden sollte und dass ihr Name zu kurz ist. Diese letzten Hinweise stehen nicht im Zusammenhang mit dem Speicherlayout, sondern erscheinen, weil in diesem Beispiel alle verfügbaren Prüfungen aktiviert wurden.

Alternativen

Andere Werkzeuge verfolgen ähnliche Ansätze, unterscheiden sich aber im Umfang und in der Lizenz:

  • Cppcheck ist Open Source (GPL) und arbeitet mit einer eigenen Engine, die ohne Compiler auskommt. Das Werkzeug konzentriert sich auf Laufzeitfehler, Speicherprobleme und undefiniertes Verhalten. Es lässt sich über sudo dnf install cppcheck installieren und ist unter https://cppcheck.sourceforge.io/ verfügbar.
  • PC-lint Plus ist kommerziell, bietet eine umfangreiche, anpassbare Regelbasis und wird häufig in Embedded-Umgebungen eingesetzt. Die Website ist https://pclintplus.com/.

Clang-Tidy integriert sich gut in moderne Build-Systeme wie CMake oder Ninja und ist daher oft die erste Wahl in offenen Projekten. Cppcheck ist nützlich, wenn keine Compiler-Infrastruktur vorhanden ist oder ein schneller Scan großer Codebestände nötig ist. PC-lint Plus eignet sich für professionelle Umgebungen mit langen Produktlebenszyklen.

Struktur-Layout mit Pahole visualisieren

Die Option -g erzeugt Debug-Informationen im DWARF-Format mit Details zu Datentypen und Speicherlayout. Externe Werkzeuge nutzen diese Informationen zur Analyse.

Das Werkzeug Pahole aus dem Linux-Projekt Dwarves nutzt diese Debug-Daten, um Strukturen zu analysieren und Löcher (engl: holes) im Speicherlayout sichtbar zu machen. Der Name Pahole leitet sich von ›Poke-a-hole‹ ab, was auf das Aufspüren von Löchern im Speicherlayout anspielt, aber auch ›Print All Holes‹ ist eine gute Merkhilfe.

Üblicherweise muss Pahole zuerst installiert werden (build-essential/base-devel werden u. U. für Abhängigkeiten benötigt).

# Debian/Ubuntu
$ sudo apt update
$ sudo apt install build-essential dwarves

# Fedora
$ sudo dnf install dwarves

# Arch Linux
$ sudo pacman -S base-devel dwarves

# openSUSE
$ sudo zypper install dwarves

# Alpine Linux
$ sudo apk add build-base dwarves

Falls das Paket in der jeweiligen Version fehlt, lässt sich Pahole auch aus dem Quellcode bauen.

Kommen wir zurück zu unserem bekannten Beispiel:

#include <stdint.h>

struct S {
    uint8_t  a;
    uint32_t b;
    uint16_t c;
};

// Globale Variable, um den Compiler daran zu hindern,
// die Struktur wegzuoptimieren.
struct S s;

int main() { }

Im ersten Schritt wird das Programm mit der Option -g übersetzt, damit die Debug-Informationen (DWARF-Daten) in die Objektdatei gesetzt werden:

$ gcc -g main.c -o main

Ausgabe von pahole:

$ pahole main
struct S {
    uint8_t                    a;                    /*     0     1 */

    /* XXX 3 bytes hole, try to pack */

    uint32_t                   b;                    /*     4     4 */
    uint16_t                   c;                    /*     8     2 */

    /* size: 12, cachelines: 1, members: 3 */
    /* sum members: 7, holes: 1, sum holes: 3 */
    /* padding: 2 */
    /* last cacheline: 12 bytes */
};

Pahole hat Blockkommentare eingefügt, um Strukturinformationen und Lücken im Speicherlayout hervorzuheben. Diese Kommentare enthalten Kennzeichnungen wie Offsets, Mitgliedergrößen und Hinweise auf ungenutzte Bytes. Im Detail:

Spalten (pro Mitgliedzeile)

  • Erste Spalte: Offset in Bytes. Beispiel aus /* 0 1 */, der erste Wert 0 ist der Offset.
  • Zweite Spalte: Größe in Bytes. Im Beispiel oben ist der zweite Wert 1 die Mitgliedsgröße.
  • Diese beiden Werte stehen als Blockkommentar am Zeilenende jedes Mitglieds.

Zeilenhinweise zwischen Mitgliedern

  • /* XXX N bytes hole, try to pack */: ungenutzte Bytes (Alignment‑Lücke) zwischen zwei Mitgliedern.

Zusammenfassung am Ende

  • size: Gesamtgröße der Struktur in Bytes.
  • cachelines: Anzahl der betroffenen Cache‑Lines (typisch 64 Byte).
  • members: Anzahl der Mitglieder.
  • sum members: Summe der Mitgliedergrößen.
  • holes: Anzahl der Lücken.
  • sum holes: Summe der Füllbytes.
  • padding: Füllbytes am Ende der Struktur.
  • last cacheline: belegte Bytes in der letzten Cache‑Line.

Pahole kann Strukturen auch reorganisieren, also die Mitglieder innerhalb einer Struktur neu anordnen, um Speicherlöcher zu verringern. Dazu dient die Option --reorganize oder kurz -R. Zusammen mit --show_reorg_steps (-S) werden die einzelnen Schritte dieser Umordnung angezeigt:

$ pahole -C S -R -S main
/* Moving 'c' from after 'b' to after 'a' */
struct S {
        uint8_t                    a;                    /*     0     1 */

        /* XXX 1 byte hole, try to pack */

        uint16_t                   c;                    /*     2     2 */
        uint32_t                   b;                    /*     4     4 */

        /* size: 8, cachelines: 1, members: 3 */
        /* sum members: 7, holes: 1, sum holes: 1 */
        …
}

Mit -C <Strukturname> lässt sich gezielt eine bestimmte Struktur untersuchen, hier S. Nach der Umordnung hat Pahole das Mitglied c direkt nach a verschoben, um das große Loch zwischen a und b zu verkleinern. Das Ergebnis zeigt eine kompaktere Struktur mit nur noch einem Byte ungenutztem Speicher statt drei.

Ein weiteres Feature von Pahole ist die Cacheline-Analyse. (Quelle: Eine Cache-Line ist ein Speicherblock, typischerweise 64 Bytes auf x86-Systemen, den die CPU als kleinste Einheit zwischen Cache und RAM überträgt.) Mit --cacheline_size=<n> (z. B. --cacheline_size=64) kann Pahole prüfen, ob eine Struktur in eine Cache-Line passt. Wenn eine Struktur über eine Cache-Line-Grenze hinausgeht, kann das zu zusätzlichen Ladezyklen führen. Die Option hilft also, solche Überschreitungen zu erkennen und Performanceprobleme zu vermeiden. Pahole kann keine Daten automatisch in 64-Byte-Blöcke zusammenfassen, sondern nur bewerten, wie die Struktur im Verhältnis zu diesen Blöcken liegt. Und Pahole reorganisiert Mitglieder nur innerhalb einer Struktur, nicht über mehrere Strukturen oder Blöcke hinweg.

Strukturlayout mit clang anzeigen

Pahole liest die Debug-Informationen (DWARF) aus kompilierten Binärdateien und zeigt, wie der Compiler eine Struktur tatsächlich im Speicher angeordnet hat, inklusive Offsets, Größen und eingefügtem Padding. Es arbeitet also nach dem Build.

Eine Alternative bietet der Clang-Compiler selbst. Er kann das Speicherlayout einer Struktur direkt beim Analysieren des Quelltexts berechnen und ausgeben. Dabei wertet er die Typinformationen so, wie sie im Übersetzungsvorgang entstehen würden, führt aber keine eigentliche Übersetzung oder Linkphase aus.

Bleiben wir bei unserem Beispiel:

struct S {
    uint8_t  a;
    uint32_t b;
    uint16_t c;
};

Aufruf und Anzeige:

$ clang -Xclang -fdump-record-layouts -c main.c

*** Dumping AST Record Layout
         0 | struct S
         0 |   uint8_t a
         4 |   uint32_t b
         8 |   uint16_t c
           | [sizeof=12, align=4]

*** Dumping IRgen Record Layout
Record: RecordDecl 0x8e6b40 <main.c:3:1, line:7:1> line:3:8 struct S definition
|-FieldDecl 0x8e6c28 <line:4:5, col:14> col:14 a 'uint8_t':'unsigned char'
|-FieldDecl 0x8e6cb8 <line:5:5, col:14> col:14 b 'uint32_t':'unsigned int'
`-FieldDecl 0x8e6d48 <line:6:5, col:14> col:14 c 'uint16_t':'unsigned short'

Layout: <CGRecordLayout
  LLVMType:%struct.S = type { i8, i32, i16 }
  IsZeroInitializable:1
  BitFields:[
]>

Der Dump zeigt die Offsets der Mitglieder und die Gesamtgröße inklusive Alignment, kennzeichnet das Padding aber nicht direkt. Es lässt sich aus den Offsets ableiten:

  • a bei Offset 0, Größe 1 → für das nächste Mitglied ist eine 4-Byte-Ausrichtung nötig → 3 Bytes internes Padding (Offsets 1–3).
  • b bei Offset 4, Größe 4 → endet bei 8.
  • c bei Offset 8, Größe 2 → endet bei 10.
  • Gesamtgröße 12 → 2 Bytes Tail-Padding (Offsets 10–11).

Insgesamt gibt es 5 Bytes Padding. Clang zeigt damit dasselbe Speicherlayout, das Pahole später aus den Debug-Daten liest, nur früher im Übersetzungsprozess. Wer Padding-Stellen direkt erkennen möchte, muss wiederum -Wpadded setzen, wie wir das schon gesehen haben. Dann meldet der Compiler jedes automatisch eingefügte Padding als eigene Warnung.

Clang gibt mit -fdump-record-layouts das Layout aller Strukturen aus, die in der Übersetzungseinheit vorkommen. Das kann schnell sehr unübersichtlich werden. Eine gezielte Auswahl wie bei Pahole gibt es nicht. Für reale, größere Projekte bleibt Pahole praktischer, weil es auf eine konkrete Struktur in einer kompilierten Binärdatei fokussiert werden kann.

Strukturlayout mit MSVC anzeigen

Wer mit dem Microsoft-Compiler (MSVC) arbeitet, kann pahole nicht einsetzen. Der Grund ist, dass MSVC kein DWARF-Debuggingformat erzeugt, sondern eigene Debug-Informationen im sogenannten CodeView/PDB-Format ablegt. pahole wertet dagegen DWARF-Daten in ELF-Objekten aus, wie sie typische Unix-Compiler (GCC, Clang) erzeugen. Damit ist das Werkzeug für MSVC-Builds ungeeignet.

Eine grob vergleichbare Möglichkeit bietet MSVC über eine undokumentierte Option des Compilers: /d1reportSingleClassLayout<Name>. Damit lässt sich das vom Compiler angenommene Speicherlayout einer bestimmten Klasse oder Struktur ausgeben. Beispiel:

cl main.cpp /c /d1reportSingleClassLayoutMyStruct

Die Ausgabe zeigt ebenso die Größe, die Offsets der Mitglieder und eventuelle Auffüllbytes. Da der Schalter nicht offiziell unterstützt wird, kann er sich zwischen Compiler-Versionen ändern oder auch entfernt werden. (Quelle: https://devblogs.microsoft.com/cppblog/diagnosing-hidden-odr-violations-in-visual-c-and-fixing-lnk2022/)