Fehlerbehebung bei WebAssembly mit modernen Tools

Ingvar Stepanyan
Ingvar Stepanyan

Der Weg bis jetzt

Vor einem Jahr hat Chrome die erste Unterstützung für das native WebAssembly-Debugging in den Chrome-Entwicklertools angekündigt.

Wir haben grundlegende Unterstützung bei den einzelnen Schritten demonstriert und darüber gesprochen, wie sich die Nutzung von DWARF-Informationen anstelle von Quellzuordnungen in Zukunft für uns öffnen lässt:

  • Variablennamen auflösen
  • Quelltextformatierungstypen
  • Ausdrücke in Ausgangssprachen auswerten
  • ...und vieles mehr!

Heute möchten wir euch die versprochenen Funktionen und die Fortschritte vorstellen, die die Emscripten- und Chrome-Entwicklertools in diesem Jahr erzielt haben, insbesondere bei C- und C++-Apps.

Bevor wir beginnen, beachten Sie bitte, dass dies immer noch eine Betaversion der neuen Version ist. Sie müssen die neueste Version aller Tools auf eigenes Risiko verwenden. Sollten Probleme auftreten, melden Sie diese bitte unter https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

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 neueste Emscripten und übergeben wie im ursprünglichen Beitrag ein -g-Flag, um Informationen zur Fehlerbehebung einzuschließen:

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 die aktuelle Version von Chrome Canary öffnen.

Dieses Mal benötigen wir auch eine Hilfserweiterung, die in die Chrome-Entwicklertools integriert werden kann und dabei hilft, alle in der WebAssembly-Datei codierten Debugging-Informationen zu verstehen. Rufen Sie zur Installation die Erweiterung goo.gle/wasm-debugging-extension auf

Außerdem sollten Sie das WebAssembly-Debugging in den Tests der Entwicklertools aktivieren. Öffnen Sie die Chrome-Entwicklertools, klicken Sie oben rechts im Entwicklertools-Bereich auf das Zahnradsymbol (), rufen Sie den Bereich Experimente auf und aktivieren Sie WebAssembly-Debugging: DWARF-Unterstützung aktivieren.

Bereich „Tests“ in den Einstellungen der Entwicklertools

Wenn du die Einstellungen schließt, schlagen die Entwicklertools vor, sie neu zu laden, um die Einstellungen anzuwenden. Also los! Das war die einmalige Einrichtung.

Jetzt können wir zum Bereich Quellen zurückkehren, Pause bei Ausnahmen (⏸-Symbol) aktivieren, Bei abgefangenen Ausnahmen anhalten aktivieren und die Seite neu laden. Die Entwicklertools sollten für eine Ausnahme pausiert sein:

Screenshot des Bereichs „Quellen“, der zeigt, wie „Pause bei erkannten Ausnahmen“ aktiviert wird

Standardmäßig stoppt er bei einem von Emscripten generierten Glue Code. Auf der rechten Seite sehen Sie jedoch eine Aufrufstack-Ansicht, die den Stacktrace des Fehlers darstellt. Sie können zur ursprünglichen C-Zeile wechseln, die abort aufgerufen hat:

Entwicklertools wurden in der Funktion „assert_less“ pausiert und es werden die Werte „x“ und „y“ in der Ansicht „Scope“ angezeigt

In der Ansicht Bereich sehen Sie nun die ursprünglichen Namen und Werte der Variablen im C-/C++-Code. Sie müssen nicht mehr herausfinden, was nicht mehr benötigte Namen wie $localN bedeuten und wie sie sich auf den von Ihnen geschriebenen Quellcode beziehen.

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

Umfassende Unterstützung

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

#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 immer noch recht klein. Sie besteht aus einer einzelnen Datei mit 50 Codezeilen. Dieses Mal verwende ich aber auch einige externe APIs wie die SDL-Bibliothek für Grafiken sowie komplexe Zahlen aus der C++-Standardbibliothek.

Ich kompiliere sie mit demselben -g-Flag wie oben, um Informationen zur Fehlerbehebung einzubeziehen. Außerdem werde ich Emscripten bitten, die SDL2-Bibliothek bereitzustellen und Arbeitsspeicher beliebiger Größe 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 fraktale Form mit einigen zufälligen Farben:

Demoseite

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

Wenn wir die Seite erneut aktualisieren, pausiert der Debugger direkt in der C++-Quelle:

Entwicklertools beim Aufruf „SDL_Init“ pausiert

Rechts sind bereits alle Variablen zu sehen, aber derzeit werden nur width und height initialisiert, sodass nicht viel untersucht werden muss.

Setzen wir einen weiteren Haltepunkt in der Mandelbrot-Hauptschleife und setzen wir die Ausführung fort, um etwas nach vorne zu springen.

Entwicklertools in verschachtelten Schleifen pausiert

Jetzt wurde unser palette mit einigen zufälligen Farben gefüllt. Wir können sowohl das Array selbst als auch die einzelnen SDL_Color-Strukturen erweitern und ihre Komponenten überprüfen, um sicherzustellen, dass alles in Ordnung ist (z. B. ist der Alphakanal immer auf volle Deckkraft eingestellt). In ähnlicher Weise können wir die Real- und Imaginärteile der komplexen Zahl erweitern und prüfen, die in der Variablen center gespeichert ist.

Wenn Sie auf eine tief verschachtelte Property zugreifen möchten, die sonst in der Ansicht Umfang nur schwer zugänglich ist, können Sie die Console ebenfalls nutzen. Komplexere C++-Ausdrücke werden jedoch noch nicht unterstützt.

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

Setzen Sie die Ausführung einige Male fort und sehen Sie, wie sich auch die innere x ändert. Dazu sehen Sie entweder noch einmal in der Ansicht Scope (Bereich) nach, fügen den Variablennamen zur Überwachungsliste hinzu, werten ihn in der Konsole aus oder bewegen den Mauszeiger auf die Variable im Quellcode:

Kurzinfo für die Variable „x“ in der Quelle mit dem Wert „3“

Von hier aus können wir C++-Anweisungen schrittweise ausführen und beobachten, wie sich auch andere Variablen ändern:

Kurzinfos und die Umfangsansicht mit Werten für „Farbe“, „Punkt“ und andere Variablen

Okay, das funktioniert alles großartig, wenn Informationen zur Fehlerbehebung verfügbar sind. Aber was ist, wenn wir einen Code debuggen möchten, der nicht mit diesen Optionen erstellt wurde?

Raw WebAssembly-Debugging

Beispielsweise haben wir Emscripten gebeten, eine vorgefertigte SDL-Bibliothek für uns bereitzustellen, anstatt sie selbst aus der Quelle zu kompilieren. Daher gibt es derzeit zumindest keine Möglichkeit, dass der Debugger verknüpfte Quellen findet. Steigen wir wieder ein, um SDL_RenderDrawColor zu erreichen:

Entwicklertools, die die Auseinanderbauansicht von „mandelbrot.wasm“ zeigen

Wir kehren zurück zur einfachen Fehlerbehebung bei WebAssembly.

Es sieht jetzt etwas beängstigend aus und ist für die meisten Webentwickler nicht nötig. Gelegentlich möchten Sie aber möglicherweise eine Bibliothek debuggen, die ohne Debug-Informationen erstellt wurde. Das kann der Fall sein, weil es sich um eine Bibliothek eines Drittanbieters handelt, über die Sie keine Kontrolle haben, oder weil ein Fehler auftritt, der nur in der Produktion auftritt.

Um Ihnen dabei zu helfen, haben wir auch einige Verbesserungen an der grundlegenden Fehlerbehebung vorgenommen.

Wenn Sie bereits die unbearbeitete WebAssembly-Fehlerbehebung verwendet haben, werden Sie möglicherweise feststellen, dass die gesamte Disassemblierung jetzt in einer einzigen Datei angezeigt wird. Sie müssen nicht mehr raten, welcher Funktion ein Sources-Eintrag wasm-53834e3e/ wasm-53834e3e-7 möglicherweise entspricht.

Neues Schema zur Namensgenerierung

Wir haben auch die Namen in der Ansicht zum Auseinanderbauen verbessert. Bisher wurden nur numerische Indizes oder im Fall von Funktionen überhaupt kein Name angezeigt.

Jetzt generieren wir Namen ähnlich wie bei anderen Auseinanderbautools mit Hinweisen aus dem Abschnitt „WebAssembly-Name“, Import-/Exportpfaden und schließlich, wenn alles andere fehlschlägt, die Namen basierend auf dem Typ und Index des Elements wie $func123 generieren. Im Screenshot oben sehen Sie, dass dies bereits hilft, die Stacktraces und das Auseinandernehmen etwas besser lesbar zu machen.

Wenn keine Typinformationen verfügbar sind, kann es schwierig sein, neben den Primitiven alle Werte zu überprüfen. Zeiger werden beispielsweise als reguläre Ganzzahlen angezeigt, ohne zu wissen, was dahinter im Arbeitsspeicher gespeichert ist.

Arbeitsspeicherprüfung

Bisher konnten Sie nur das WebAssembly-Speicherobjekt erweitern, das in der Ansicht Scope durch env.memory dargestellt wird, um einzelne Bytes nachzuschlagen. Dies funktionierte in einigen trivialen Szenarien, war aber nicht besonders praktisch zu erweitern und ließ Daten nicht in anderen Formaten als Bytewerten neu interpretieren. Dafür haben wir eine neue Funktion hinzugefügt: den linearen Speicherinspektor.

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

Kontextmenü für „env.memory“ im Bereich „Bereich“ mit einem Element „Arbeitsspeicher prüfen“

Wenn Sie darauf klicken, wird ein Memory Inspector angezeigt, in dem Sie den WebAssembly-Speicher in Hexadezimal- und ASCII-Ansichten überprüfen, zu bestimmten Adressen wechseln und die Daten in verschiedenen Formaten interpretieren können:

Bereich „Memory Inspector“ in den Entwicklertools mit Hex- und ASCII-Ansicht der Arbeitsspeicher

Erweiterte Szenarien und Vorbehalte

Profilerstellung für WebAssembly-Code

Wenn Sie die Entwicklertools öffnen, wird der WebAssembly-Code auf eine nicht optimierte Version herabgestuft, um eine Fehlerbehebung zu ermöglichen. Diese Version ist viel langsamer. Daher können Sie sich nicht auf console.time, performance.now und andere Methoden zum Messen der Geschwindigkeit Ihres Codes verlassen, während die Entwicklertools geöffnet sind, da die angezeigten Zahlen nicht die tatsächliche Leistung widerspiegeln.

Verwenden Sie stattdessen den Bereich „Leistung“ der Entwicklertools. Dort wird der Code mit voller Geschwindigkeit ausgeführt und Sie erhalten eine detaillierte Aufschlüsselung der Zeit, die für die verschiedenen Funktionen aufgewendet wurde:

Profilerstellungsbereich mit verschiedenen Wasm-Funktionen

Alternativ können Sie Ihre Anwendung bei geschlossener Entwicklertools ausführen und sie anschließend öffnen, um die Console zu prüfen.

Wir werden Profilerstellungsszenarien in Zukunft verbessern, aber fürs Erste ist das eine Vorsichtsmaßnahme. Wenn Sie mehr über WebAssembly-Tiering-Szenarien erfahren möchten, lesen Sie unsere Dokumentation zur WebAssembly-Kompilierungspipeline.

Erstellung und Fehlerbehebung auf verschiedenen Maschinen (einschließlich Docker / Host)

Wenn Sie Builds in einem Docker, einer virtuellen Maschine oder einem Remote-Build-Server erstellen, müssen die Pfade zu den während des Builds verwendeten Quelldateien nicht mit den Pfaden in Ihrem eigenen Dateisystem übereinstimmen, in dem die Chrome-Entwicklertools ausgeführt werden. In diesem Fall werden die Dateien zwar im Bereich Quellen angezeigt, aber nicht geladen.

Um dieses Problem zu beheben, haben wir eine Pfadzuordnungsfunktion in den C/C++-Erweiterungsoptionen implementiert. Damit lassen sich beliebige Pfade neu zuordnen und die Entwicklertools finden Quellen leichter.

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 es während der Fehlerbehebung neu zuordnen, indem Sie diese Pfade als Präfixe angeben:

Seite „Optionen“ der C/C++-Debugging-Erweiterung

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

Fehler in optimierten Builds beheben

Wie bei allen anderen Sprachen funktioniert das Debugging am besten, wenn die Optimierungen deaktiviert sind. Durch Optimierungen können Inline-Funktionen in eine andere integriert, Code neu angeordnet oder Teile des Codes vollständig entfernt werden – und all dies kann den Debugger verwirren und folglich auch Sie als Nutzer verwirren.

Wenn Ihnen die Fehlerbehebung in einem optimierten Build nichts ausmacht und Sie trotzdem Fehler in einem optimierten Build beheben möchten, funktionieren die meisten Optimierungen wie erwartet, mit Ausnahme des Funktions-Inline-Objekts. Wir planen, die verbleibenden Probleme in Zukunft zu beheben, aber verwenden Sie vorerst -fno-inline, um sie beim Kompilieren mit Optimierungen auf -O-Ebene zu deaktivieren. Beispiele:

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

Debug-Informationen trennen

Mit Informationen zur Fehlerbehebung erhalten Sie viele Details zu Ihrem Code, definierten Typen, Variablen, Funktionen, Bereichen und Speicherorten sowie alles, was für den Debugger nützlich sein könnte. Daher kann er häufig größer als der Code selbst sein.

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

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

In diesem Fall speichert die Hauptanwendung nur den Dateinamen temp.debug.wasm und die Hilfserweiterung kann ihn beim Öffnen der Entwicklertools finden und laden.

In Kombination mit den oben beschriebenen Optimierungen können Sie mit dieser Funktion sogar fast optimierte Produktions-Builds Ihrer Anwendung ausliefern und diese später mit einer lokalen Nebendatei debuggen. In diesem Fall muss zusätzlich die gespeicherte URL überschrieben werden, damit die Erweiterung die Nebendatei finden kann. Beispiel:

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 ganz schön viele neue Funktionen!

Mit all diesen neuen Integrationen werden die Chrome-Entwicklertools zu einem brauchbaren, leistungsstarken Debugger nicht nur für JavaScript, sondern auch für C- und C++-Anwendungen. Dadurch ist es einfacher denn je, in einer Vielzahl von Technologien erstellte Apps in ein gemeinsames, plattformübergreifendes Web zu übertragen.

Aber unser Weg ist noch nicht vorbei. Hier einige der Themen, an denen wir weiter arbeiten werden:

  • Probleme in der Debugging-Umgebung beseitigen
  • Unterstützung für benutzerdefinierte Typformatierer.
  • Wir arbeiten an Verbesserungen der Profilerstellung für WebAssembly-Anwendungen.
  • Unterstützung für die Codeabdeckung, um nicht verwendeten Code leichter zu finden
  • Verbesserte Unterstützung für Ausdrücke bei der Konsolenauswertung.
  • Unterstützung weiterer Sprachen hinzugefügt.
  • und weitere!

Probieren Sie in der Zwischenzeit bitte die aktuelle Betaversion mit Ihrem eigenen Code aus und melden Sie etwaige Probleme unter https://bugs.chromium.org/p/chromium/issues/entry?template=DevTools+issue.

Vorschaukanäle herunterladen

Sie können Chrome Canary, Dev oder Beta als Standardbrowser für die Entwicklung verwenden. Über diese Vorschaukanäle erhältst du Zugriff auf die neuesten Entwicklertools-Funktionen, kannst neue Webplattform-APIs testen und Probleme auf deiner Website erkennen, bevor deine Nutzer es tun.

Chrome-Entwicklertools-Team kontaktieren

Verwende die folgenden Optionen, um die neuen Funktionen und Änderungen im Beitrag oder andere Themen im Zusammenhang mit den Entwicklertools zu besprechen.

  • Sende uns über crbug.com Vorschläge oder Feedback.
  • Wenn du ein Problem mit den Entwicklertools melden möchtest, klicke in den Entwicklertools auf Weitere Optionen   Mehr   > Hilfe > Probleme mit den Entwicklertools melden.
  • Senden Sie einen Tweet an @ChromeDevTools.
  • Hinterlasse Kommentare zu den Neuheiten in den Entwicklertools YouTube-Videos oder YouTube-Videos in den Entwicklertools-Tipps.