Fehlerbehebung bei WebAssembly mit modernen Tools

Ingvar Stepanyan
Ingvar Stepanyan

Der Weg bisher

Vor einem Jahr wurde in Chrome die anfängliche Unterstützung für das native WebAssembly-Debugging in den Chrome-Entwicklertools angekündigt.

Wir haben die Unterstützung für grundlegende Stufen demonstriert und über die Möglichkeiten gesprochen, die sich durch die Verwendung von DWARF-Informationen anstelle von Quellkarten in Zukunft ergeben:

  • Variablennamen auflösen
  • Typen für die Quelltextformatierung
  • Ausdrücke in Quellsprachen auswerten
  • ...und vieles mehr!

Heute können wir die versprochenen Funktionen präsentieren und zeigen, welche Fortschritte die Emscripten- und Chrome DevTools-Teams in diesem Jahr insbesondere bei C- und C++-Apps gemacht haben.

Bitte beachten Sie vor Beginn, dass dies eine Betaversion der neuen Version ist. Sie müssen die neueste Version aller Tools auf eigenes Risiko verwenden. Sollten Probleme auftreten, melden Sie sie bitte unter https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350.

Beginnen wir mit demselben einfachen C-Beispiel wie beim letzten Mal:

#include <stdlib.h>

void assert_less(int x, int y) {
  if (x >= y) {
    abort();
  }
}

int main() {
  assert_less(10, 20);
  assert_less(30, 20);
}

Zum Kompilieren verwenden wir die aktuelle Emscripten-Version und übergeben ein -g-Flag, genau wie im ursprünglichen Beitrag, um Informationen zur Fehlerbehebung anzugeben:

emcc -g temp.c -o temp.html

Jetzt können wir die generierte Seite über einen localhost-HTTP-Server bereitstellen (z. B. mit serve) und in der neuesten Version von Chrome Canary öffnen.

Dieses Mal benötigen wir außerdem eine Hilfserweiterung, die sich in die Chrome-Entwicklertools einbinden lässt und die in der WebAssembly-Datei codierte Informationen zur Fehlerbehebung leichter auswertet. Sie können sie unter goo.gle/wasm-debugging-extension installieren.

Außerdem müssen Sie das WebAssembly-Debugging in den DevTools unter Experiments aktivieren. Öffnen Sie die Chrome-Entwicklertools, klicken Sie rechts oben im Entwicklertools-Bereich auf das Zahnradsymbol (), rufen Sie den Bereich Experiments (Experimente) auf und klicken Sie auf das Kästchen WebAssembly Debugging: Enable DWARF support (WebAssembly-Debugging: DWARF-Unterstützung aktivieren).

Bereich „Tests“ in den Entwicklertools-Einstellungen

Wenn Sie die Einstellungen schließen, werden Sie von den Entwicklertools aufgefordert, sie neu zu laden, um die Einstellungen anzuwenden. Tun wir das. Das war's mit der einmaligen Einrichtung.

Jetzt können wir zum Bereich Quellen zurückkehren, Bei Ausnahmen pausieren (Symbol ⏸) aktivieren, dann Bei erkannten Ausnahmen pausieren auswählen und die Seite neu laden. Die DevTools sollten bei einer Ausnahme pausiert sein:

Screenshot des Bereichs „Quellen“ mit der Aktivierung von „Bei erkannten Ausnahmen anhalten“

Standardmäßig wird bei einem Emscripten-generierten Glue-Code angehalten. Rechts sehen Sie jedoch eine Aufrufabfolge, die den Stacktrace des Fehlers darstellt. Sie können auch zur ursprünglichen C-Zeile wechseln, die abort aufgerufen hat:

In den DevTools wurde die Ausführung in der Funktion „assert_less“ angehalten und in der Ansicht „Scope“ werden die Werte „x“ und „y“ angezeigt

Wenn Sie jetzt in der Ansicht Scope (Umfang) nachsehen, sehen Sie die ursprünglichen Namen und Werte der Variablen im C/C++-Code. Sie müssen also nicht mehr herausfinden, was unleserliche Namen wie $localN bedeuten und in welcher Beziehung sie zum von Ihnen geschriebenen Quellcode stehen.

Dies gilt nicht nur für primitive Werte wie Ganzzahlen, sondern auch für zusammengesetzte Typen wie Strukturen, Klassen, Arrays usw.

Rich-Type-Unterstützung

Sehen wir uns ein etwas komplizierteres Beispiel an. Dieses Mal zeichnen wir mit dem folgenden C++-Code ein Mandelbrot-Fraktal:

#include <SDL2/SDL.h>
#include <complex>

int main() {
  // Init SDL.
  int width = 600, height = 600;
  SDL_Init(SDL_INIT_VIDEO);
  SDL_Window* window;
  SDL_Renderer* renderer;
  SDL_CreateWindowAndRenderer(width, height, SDL_WINDOW_OPENGL, &window,
                              &renderer);

  // Generate a palette with random colors.
  enum { MAX_ITER_COUNT = 256 };
  SDL_Color palette[MAX_ITER_COUNT];
  srand(time(0));
  for (int i = 0; i < MAX_ITER_COUNT; ++i) {
    palette[i] = {
        .r = (uint8_t)rand(),
        .g = (uint8_t)rand(),
        .b = (uint8_t)rand(),
        .a = 255,
    };
  }

  // Calculate and draw the Mandelbrot set.
  std::complex<double> center(0.5, 0.5);
  double scale = 4.0;
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      std::complex<double> point((double)x / width, (double)y / height);
      std::complex<double> c = (point - center) * scale;
      std::complex<double> z(0, 0);
      int i = 0;
      for (; i < MAX_ITER_COUNT - 1; i++) {
        z = z * z + c;
        if (abs(z) > 2.0)
          break;
      }
      SDL_Color color = palette[i];
      SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
      SDL_RenderDrawPoint(renderer, x, y);
    }
  }

  // Render everything we've drawn to the canvas.
  SDL_RenderPresent(renderer);

  // SDL_Quit();
}

Wie Sie sehen, ist diese Anwendung noch ziemlich klein – es ist eine einzelne Datei mit 50 Codezeilen. Dieses Mal verwende ich jedoch auch einige externe APIs, z. B. die SDL-Bibliothek für Grafiken sowie komplexe Zahlen aus der C++-Standardbibliothek.

Ich werde es mit demselben -g-Flag wie oben kompilieren, um Debug-Informationen einzubeziehen. Außerdem werde ich Emscripten bitten, die SDL2-Bibliothek bereitzustellen und einen beliebigen Arbeitsspeicher zuzulassen:

emcc -g mandelbrot.cc -o mandelbrot.html \
     -s USE_SDL=2 \
     -s ALLOW_MEMORY_GROWTH=1

Wenn ich die generierte Seite im Browser aufrufe, sehe ich die wunderschöne fraktale Form in einigen zufälligen Farben:

Demoseite

Wenn ich die Entwicklertools öffne, sehe ich wieder die ursprüngliche C++-Datei. Dieses Mal haben wir jedoch keinen Fehler im Code (Puh!), also setzen wir stattdessen einen Haltepunkt am Anfang des Codes.

Wenn wir die Seite noch einmal neu laden, wird der Debugger direkt in unserer C++-Quelldatei angehalten:

Entwicklertools beim Aufruf „SDL_Init“ pausiert

Auf der rechten Seite sind bereits alle Variablen zu sehen. Im Moment sind aber nur width und height initialisiert. Es gibt also nicht viel zu prüfen.

Legen wir einen weiteren Haltepunkt in unserer Hauptschleife für den Mandelbrot-Algorithmus fest und fahren wir fort, um ein wenig vorzuspringen.

DevTools in den verschachtelten Schleifen pausiert

Unser palette ist jetzt mit einigen zufälligen Farben gefüllt. Wir können sowohl das Array selbst als auch die einzelnen SDL_Color-Strukturen maximieren und ihre Komponenten prüfen, um sicherzustellen, dass alles in Ordnung ist (z. B., dass der Alphakanal immer auf volle Deckkraft gesetzt ist). In ähnlicher Weise können wir die reellen und fiktiven Teile der komplexen Zahl, die in der Variablen center gespeichert ist, erweitern und prüfen.

Wenn Sie auf eine tief verschachtelte Property zugreifen möchten, die über die Ansicht Umfang sonst nur schwer zu finden ist, können Sie auch die Console verwenden. Komplexere C++-Ausdrücke werden jedoch noch nicht unterstützt.

Konsolenbereich mit dem Ergebnis von „palette[10].r“

Führen wir die Ausführung noch ein paar Mal fort, um zu sehen, wie sich auch die innere x ändert. Dazu können wir entweder noch einmal in der Ansicht Scope nachsehen, den Variablennamen der Beobachtungsliste hinzufügen, ihn in der Konsole auswerten oder den Mauszeiger im Quellcode auf die Variable bewegen:

Kurzinfo zur Variablen „x“ in der Quelle mit ihrem Wert „3“

Hier können wir C++-Anweisungen Schritt für Schritt ausführen und beobachten, wie sich auch andere Variablen ändern:

Kurzinfos und Bereichsansicht mit Werten für „color“, „point“ und andere Variablen

Okay, das funktioniert also hervorragend, wenn Debug-Informationen verfügbar sind. Was aber, wenn wir Code debuggen möchten, der nicht mit den Debugging-Optionen erstellt wurde?

Rohes WebAssembly-Debugging

Wir haben Emscripten beispielsweise gebeten, eine vorkonfigurierte SDL-Bibliothek für uns bereitzustellen, anstatt sie selbst aus der Quelle zu kompilieren. Daher kann der Debugger zumindest derzeit keine zugehörigen Quellen finden. Sehen wir uns noch einmal an, wie Sie die SDL_RenderDrawColor aufrufen:

DevTools mit der Ansicht „Disassembly“ von „mandelbrot.wasm“

Wir sind wieder beim reinen WebAssembly-Debugging.

Das sieht zwar etwas beängstigend aus und ist etwas, mit dem die meisten Webentwickler nie zu tun haben werden, aber gelegentlich möchten Sie vielleicht eine Bibliothek debuggen, die ohne Debug-Informationen erstellt wurde. Das kann daran liegen, dass es sich um eine Drittanbieterbibliothek handelt, über die Sie keine Kontrolle haben, oder dass Sie auf einen dieser Fehler stoßen, die nur in der Produktion auftreten.

Um Ihnen in diesen Fällen zu helfen, haben wir auch einige Verbesserungen am grundlegenden Debugging vorgenommen.

Wenn Sie zuvor das reine WebAssembly-Debugging verwendet haben, stellen Sie möglicherweise fest, dass die gesamte Zerlegung jetzt in einer einzigen Datei angezeigt wird. Sie müssen nicht mehr erraten, welcher Funktion ein Sources-Eintrag wasm-53834e3e/ wasm-53834e3e-7 entspricht.

Neues Schema zur Namensgenerierung

Außerdem wurden die Namen in der Demontageansicht verbessert. Bisher wurden nur numerische Indizes oder bei Funktionen gar kein Name angezeigt.

Jetzt generieren wir Namen ähnlich wie bei anderen Deaktivierungstools. Dazu verwenden wir Hinweise aus dem WebAssembly-Namensabschnitt, Import-/Exportpfade und, wenn alles andere fehlschlägt, generieren wir sie basierend auf dem Typ und dem Index des Elements wie $func123. Wie Sie im Screenshot oben sehen, führt dies bereits zu etwas besser lesbaren Stacktraces und Disassemblierungen.

Wenn keine Typinformationen verfügbar sind, kann es schwierig sein, Werte außer den primitiven zu prüfen. Beispielsweise werden Verweise als reguläre Ganzzahlen angezeigt, ohne dass man weiß, was im Speicher dahinter gespeichert ist.

Arbeitsspeicherprüfung

Bisher konnten Sie das WebAssembly-Speicherobjekt, das in der Ansicht Umfang durch env.memory dargestellt wird, nur maximieren, um einzelne Bytes abzurufen. Das funktionierte in einigen einfachen Szenarien, war aber nicht besonders praktisch zu erweitern und erlaubte nicht, Daten in anderen Formaten als Bytewerten neu zu interpretieren. Wir haben auch hierfür eine neue Funktion hinzugefügt: ein lineares Speicherprüftool.

Wenn Sie mit der rechten Maustaste auf das env.memory klicken, sollte jetzt die neue Option Arbeitsspeicher prüfen angezeigt werden:

Kontextmenü für „env.memory“ im Bereich „Scope“ (Umfang) mit dem Menüpunkt „Inspect Memory“ (Arbeitsspeicher prüfen)

Daraufhin wird der Speicher-Inspektor geöffnet, in dem Sie den WebAssembly-Speicher in Hexadezimal- und ASCII-Ansicht prüfen, bestimmte Adressen aufrufen und die Daten in verschiedenen Formaten interpretieren können:

Bereich „Memory Inspector“ in den DevTools mit Hexadezimal- und ASCII-Ansicht des Arbeitsspeichers

Erweiterte Szenarien und Vorbehalte

Profilerstellung für WebAssembly-Code

Wenn Sie die Entwicklertools öffnen, wird WebAssembly-Code in eine nicht optimierte Version heruntergestuft, um das Debuggen zu ermöglichen. Diese Version ist deutlich langsamer. Sie können sich also nicht auf console.time, performance.now und andere Methoden zur Messung der Geschwindigkeit Ihres Codes verlassen, während die Entwicklertools geöffnet sind, da die angezeigten Zahlen nicht die tatsächliche Leistung widerspiegeln.

Stattdessen sollten Sie den Bereich „Leistung“ in den DevTools verwenden. Dort wird der Code mit voller Geschwindigkeit ausgeführt und Sie erhalten eine detaillierte Aufschlüsselung der Zeit, die in den verschiedenen Funktionen verbracht wurde:

Profilbereich mit verschiedenen Wasm-Funktionen

Alternativ kannst du deine Anwendung mit geschlossenen Entwicklertools ausführen und sie öffnen, wenn du fertig bist, um die Console zu prüfen.

Wir werden die Profiling-Szenarien in Zukunft verbessern, aber derzeit ist dies ein wichtiger Hinweis. Weitere Informationen zu WebAssembly-Tiering-Szenarien finden Sie in unserer Dokumentation zur WebAssembly-Kompilierungspipeline.

Builds und Debugging auf verschiedenen Maschinen (einschließlich Docker / Host)

Wenn Sie in einer Docker-Umgebung, auf einer virtuellen Maschine oder auf einem Remote-Build-Server erstellen, kommt es wahrscheinlich vor, dass die Pfade zu den Quelldateien, die während des Builds verwendet werden, nicht mit den Pfaden in Ihrem eigenen Dateisystem übereinstimmen, in dem die Chrome DevTools ausgeführt werden. In diesem Fall werden Dateien im Bereich Quellen angezeigt, aber nicht geladen.

Um dieses Problem zu beheben, haben wir in den C/C++-Erweiterungsoptionen eine Pfadzuordnungsfunktion implementiert. Sie können damit beliebige Pfade neu zuordnen und die Entwicklertools dabei unterstützen, Quellen zu finden.

Wenn sich das Projekt auf Ihrem Hostcomputer beispielsweise unter dem Pfad C:\src\my_project befindet, aber in einem Docker-Container erstellt wurde, in dem dieser Pfad als /mnt/c/src/my_project dargestellt wurde, können Sie ihn beim Debuggen neu zuordnen, indem Sie diese Pfade als Präfixe angeben:

Optionsseite der C/C++-Debugging-Erweiterung

Das erste übereinstimmende Präfix „gewinnt“. Wenn Sie mit anderen C++-Debuggern vertraut sind, entspricht diese Option dem Befehl set substitute-path in GDB oder einer target.source-map-Einstellung in LLDB.

Optimierte Builds beheben

Wie bei allen anderen Sprachen funktioniert die Fehlerbehebung am besten, wenn Optimierungen deaktiviert sind. Dabei können Sie Funktionen inline einfügen, Code neu anordnen oder Teile des Codes entfernen. All dies kann den Debugger und folglich Sie als Nutzer verwirren.

Wenn Sie mit einer eingeschränkten Fehlerbehebung einverstanden sind und trotzdem einen optimierten Build debuggen möchten, funktionieren die meisten Optimierungen wie erwartet, mit Ausnahme der Funktions-Inline-Optimierung. Wir planen, die verbleibenden Probleme in Zukunft anzugehen. Bis dahin können Sie die Funktion mit -fno-inline deaktivieren, wenn Sie mit Optimierungen auf -O-Ebene kompilieren, z. B.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline

Informationen zur Fehlerbehebung trennen

In den Debug-Informationen werden viele Details zu Ihrem Code, zu definierten Typen, Variablen, Funktionen, Bereichen und Speicherorten gespeichert – alles, was für den Debugger nützlich sein könnte. Daher kann sie oft größer sein als der Code selbst.

Um das Laden und Kompilieren des WebAssembly-Moduls zu beschleunigen, können Sie diese Debug-Informationen in eine separate WebAssembly-Datei aufteilen. Geben Sie dazu in Emscripten ein -gseparate-dwarf=…-Flag mit dem gewünschten Dateinamen an:

emcc -g temp.c -o temp.html \
     -gseparate-dwarf=temp.debug.wasm

In diesem Fall speichert die Hauptanwendung nur einen Dateinamen temp.debug.wasm. Die Hilfserweiterung kann ihn finden und laden, wenn Sie die DevTools öffnen.

In Kombination mit den oben beschriebenen Optimierungen können Sie mit dieser Funktion sogar fast optimierte Produktions-Builds Ihrer Anwendung bereitstellen und sie später mit einer lokalen Datei beheben. In diesem Fall müssen wir zusätzlich die gespeicherte URL überschreiben, damit die Erweiterung die Sidefile finden kann, z. B.:

emcc -g temp.c -o temp.html \
     -O3 -fno-inline \
     -gseparate-dwarf=temp.debug.wasm \
     -s SEPARATE_DWARF_URL=file://[local path to temp.debug.wasm]

Wird fortgesetzt...

Puh, das waren eine Menge neuer Funktionen!

Mit all diesen neuen Integrationen werden die Chrome-Entwicklertools zu einem leistungsstarken Debugger, der nicht nur für JavaScript, sondern auch für C- und C++-Anwendungen geeignet ist. So können Apps, die mit einer Vielzahl von Technologien erstellt wurden, einfacher denn je in ein gemeinsames, plattformübergreifendes Web gebracht werden.

Wir sind jedoch noch nicht am Ende. Hier einige der Dinge, an denen wir in Zukunft arbeiten werden:

  • Die Fehlerbehebung wurde optimiert.
  • Unterstützung für Formatierer benutzerdefinierter Typen hinzugefügt.
  • Wir arbeiten an Verbesserungen des Profilings für WebAssembly-Apps.
  • Unterstützung für die Codeabdeckung hinzugefügt, um ungenutzten Code leichter zu finden.
  • Verbesserte Unterstützung von Ausdrücken bei der Konsolenerstellung.
  • Unterstützung für weitere Sprachen
  • und weitere!

In der Zwischenzeit können Sie die aktuelle Betaversion mit Ihrem eigenen Code testen und alle gefundenen Probleme unter https://issues.chromium.org/issues/new?noWizard=true&template=0&component=1456350 melden.

Vorschaukanäle herunterladen

Verwenden Sie als Standard-Entwicklungsbrowser Chrome Canary, Chrome Dev oder Chrome Beta. Über diese Vorschaukanäle erhältst du Zugriff auf die neuesten Entwicklertools, kannst hochmoderne Webplattform-APIs testen und Probleme auf deiner Website erkennen, bevor deine Nutzer dies tun.

Chrome-Entwicklertools-Team kontaktieren

Mit den folgenden Optionen können Sie über neue Funktionen, Updates oder andere Themen im Zusammenhang mit den DevTools sprechen.