Einer der Hauptvorteile des Debuggings von plattformübergreifendem Emscripten-Code ist, dass derselbe plattformübergreifende Quellcode entweder auf der nativen Plattform oder mithilfe des zunehmend leistungsstarken Toolsets des Webbrowsers – einschließlich Debugger, Profiler und anderer Tools – debuggt werden kann.
Emscripten bietet viele Funktionen und Tools zur Unterstützung des Debuggings
Compiler-Debug-Informations-Flags, die es Ihnen ermöglichen, Debug-Informationen im kompilierten Code zu speichern und sogar Quell-Maps zu erstellen, sodass Sie den nativen C++-Quellcode beim Debuggen im Browser durchgehen können.
Debug-Modus, der Debug-Protokolle ausgibt und temporäre Build-Dateien zur Analyse speichert.
Compiler-Einstellungen zur Aktivierung der Laufzeitprüfung von Speicherzugriffen und häufigen Zuweisungsfehlern.
Manuelles Print-Debugging von Emscripten-generiertem Code wird ebenfalls unterstützt, was in gewisser Weise sogar besser ist als auf nativen Plattformen.
AutoDebugger, der LLVM-Bitcode automatisch instrumentiert, um jeden Speichervorgang auszugeben.
Dieser Artikel beschreibt die wichtigsten von Emscripten bereitgestellten Tools und Einstellungen für das Debugging, zusammen mit einem Abschnitt, der erklärt, wie eine Reihe von Emscripten-spezifischen Problemen debuggt werden können.
Emcc kann Debug-Informationen in zwei Formaten ausgeben: entweder als DWARF-Symbole oder als Source-Maps. Beide ermöglichen es Ihnen, den C/C++-Quellcode in einem Browser-Debugger anzuzeigen und zu debuggen. DWARF bietet die präziseste und detaillierteste Debugging-Erfahrung und wird experimentell in Chrome 88 mit einer Erweiterung <https://goo.gle/wasm-debugging-extension> unterstützt. Eine detaillierte14 Nutzungsanleitung finden Sie hier <https://developer.chrome.com/blog/wasm-debugging-2020/>. Source-Maps werden in Firefox, Chrome und Safari breiter unterstützt, können aber im Gegensatz zu DWARF beispielsweise nicht zur Überprüfung von Variablen verwendet werden.
Emcc entfernt standardmäßig die meisten Debug-Informationen aus optimierten Builds. DWARF kann mit dem emcc -g Flag und Source-Maps mit der Option -gsource-map ausgegeben werden. Beachten Sie, dass Optimierungsstufen -O1 und höher zunehmend LLVM-Debug-Informationen entfernen und auch Laufzeit-ASSERTIONS-Prüfungen deaktivieren. Das Übergeben eines -g Flags beeinflusst auch den generierten JavaScript-Code und behält Leerzeichen, Funktionsnamen und Variablennamen bei.
Tipp
Selbst für mittelgroße Projekte können DWARF-Debug-Informationen beträchtlich groß sein und die Seitenleistung, insbesondere das Kompilieren und Laden des Moduls, negativ beeinflussen. Debug-Informationen können stattdessen auch in einer separaten Datei mit der Option -gseparate-dwarf ausgegeben werden! Die Größe der Debug-Informationen beeinflusst auch die Link-Zeit, da die Debug-Informationen in allen Objektdateien ebenfalls verknüpft werden müssen. Das Übergeben der Option -gsplit-dwarf kann hier helfen, wodurch Clang Debug-Informationen über die Objektdateien verstreut lässt. Diese Debug-Informationen müssen dann mit dem Tool emdwp in eine DWARF-Paketdatei (.dwp) verknüpft werden, was jedoch parallel zum Verknüpfen der kompilierten Ausgabe geschehen kann! Wenn es nach dem Linken ausgeführt wird, ist es so einfach wie emdwp -e foo.wasm -o foo.wasm.dwp, oder emdwp -e foo.debug.wasm -o foo.debug.wasm.dwp, wenn es zusammen mit -gseparate-dwarf verwendet wird (die dwp-Datei sollte den gleichen Dateinamen wie die Hauptsymbol-Datei mit einer zusätzlichen .dwp-Erweiterung haben).
Das Flag -g kann auch mit Ganzzahl-Stufen angegeben werden: -g0, -g1, -g2 (Standard bei -gsource-map) und -g3 (Standard bei -g). Jede Stufe baut auf der letzten auf, um progressiv mehr Debug-Informationen in der kompilierten Ausgabe bereitzustellen.
Hinweis
Da die Binaryen-Optimierung die Qualität der DWARF-Informationen weiter verschlechtert, überspringt -O1 -g die Ausführung des Binaryen-Optimizers (wasm-opt) vollständig, es sei denn, dies ist durch andere Optionen erforderlich. Sie können auch die Option -sERROR_ON_WASM_CHANGES_AFTER_LINK hinzufügen, wenn Sie sicherstellen möchten, dass die Debug-Informationen erhalten bleiben. Weitere Details finden Sie unter Skipping Binaryen.
Hinweis
Einige Optimierungen können deaktiviert werden, wenn sie in Verbindung mit den Debug-Flags sowohl im Binaryen-Optimizer (auch wenn er läuft) als auch im JavaScript-Optimizer verwendet werden. Wenn Sie beispielsweise mit -O3 -g kompilieren, überspringt der Binaryen-Optimizer einige der Optimierungsdurchläufe, die keine gültigen DWARF-Informationen erzeugen, und auch einige der normalen JavaScript-Optimierungen werden deaktiviert, um die angeforderten Debugging-Informationen besser bereitzustellen.
Die Umgebungsvariable EMCC_DEBUG kann gesetzt werden, um den Debug-Modus von Emscripten zu aktivieren
# Linux or macOS
EMCC_DEBUG=1 emcc test/hello_world.cpp -o hello.html
# Windows
set EMCC_DEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_DEBUG=0
Mit gesetztem EMCC_DEBUG=1 gibt emcc Debug-Ausgaben aus und generiert Zwischenergebnisse für die verschiedenen Compiler-Phasen. EMCC_DEBUG=2 generiert zusätzlich Zwischenergebnisse für jeden JavaScript-Optimizer-Durchlauf.
Die Debug-Protokolle und Zwischenergebnisse werden in TEMP_DIR/emscripten_temp ausgegeben, wobei TEMP_DIR das standardmäßige temporäre Verzeichnis des Betriebssystems ist (z.B. /tmp unter UNIX).
Die Debug-Logs können analysiert werden, um die in jedem Schritt vorgenommenen Änderungen zu profilieren und zu überprüfen.
Hinweis
Der begrenztere Umfang an Debug-Informationen kann auch durch Angabe des Compiler-Flags verbose output (emcc -v) aktiviert werden.
Emscripten verfügt über eine Reihe von Compiler-Einstellungen, die für das Debugging nützlich sein können. Diese werden mit der Option emcc -s gesetzt und überschreiben alle Optimierungs-Flags. Zum Beispiel
emcc -O1 -sASSERTIONS test/hello_world
Einige wichtige Einstellungen sind
ASSERTIONS=1wird verwendet, um Laufzeitprüfungen auf häufige Speicherallokationsfehler (z.B. Schreiben von mehr Speicher als zugewiesen) zu aktivieren. Es definiert auch, wie Emscripten Fehler im Programmablauf behandeln soll. Der Wert kann aufASSERTIONS=2gesetzt werden, um zusätzliche Tests durchzuführen.
ASSERTIONS=1ist standardmäßig aktiviert. Assertions werden für optimierten Code (-O1 und höher) deaktiviert.
SAFE_HEAP=1fügt zusätzliche Speicherzugriffsprüfungen hinzu und liefert klare Fehlermeldungen bei Problemen wie dem Dereferenzieren von 0 und Problemen mit der Speicherausrichtung.Sie können auch
SAFE_HEAP_LOGsetzen, umSAFE_HEAP-Operationen zu protokollieren.Das Übergaben des Linker-Flags
STACK_OVERFLOW_CHECK=1fügt einen Laufzeit-Magic-Token-Wert am Ende des Stacks hinzu, der an bestimmten Stellen überprüft wird, um zu verifizieren, dass der Benutzercode nicht versehentlich über das Ende des Stacks hinaus schreibt. Während ein Überlauf des Emscripten-Stacks kein Sicherheitsproblem für JavaScript darstellt (das davon unberührt bleibt), führt das Schreiben über den Stack hinaus zu Speicherbeschädigungen in globalen Daten und dynamisch zugewiesenen Speicherbereichen im Emscripten HEAP, was dazu führt, dass die Anwendung unerwartet fehlschlägt. Der WertSTACK_OVERFLOW_CHECK=2aktiviert leicht detailliertere Stack-Guard-Prüfungen, die auf Kosten einer gewissen Leistung einen präziseren Callstack liefern können. Der Standardwert ist 1, wennASSERTIONS=1gesetzt ist, andernfalls ist er deaktiviert.
Eine Reihe weiterer nützlicher Debug-Einstellungen sind in src/settings.js definiert. Weitere Informationen finden Sie in dieser Datei nach den Stichwörtern „check“ und „debug“.
Emscripten unterstützt auch einige der Clang-Sanitizer, wie den Undefined Behaviour Sanitizer und den Address Sanitizer.
Das Kompilieren mit emcc -v bewirkt, dass Emscripten den ausgeführten Unterbefehl ausgibt und -v an Clang übergibt.
Sie können den Quellcode auch manuell mit printf()-Anweisungen instrumentieren, dann den Code kompilieren und ausführen, um Probleme zu untersuchen. Beachten Sie, dass printf() zeilenweise gepuffert wird. Stellen Sie sicher, dass Sie \n hinzufügen, um die Ausgabe in der Konsole zu sehen.
Wenn Sie eine gute Vorstellung von der Problemzeile haben, können Sie print(new Error().stack) zu JavaScript hinzufügen, um an dieser Stelle einen Stack-Trace zu erhalten.
Debug-Ausgaben können sogar beliebigen JavaScript-Code ausführen. Zum Beispiel
function _addAndPrint($left, $right) {
$left = $left | 0;
$right = $right | 0;
//---
if ($left < $right) console.log('l<r at ' + stackTrace());
//---
_printAnInteger($left + $right | 0);
}
Chrome DevTools unterstützen das Source-Level-Debugging von WebAssembly-Dateien mit DWARF-Informationen. Um dies zu nutzen, benötigen Sie das Wasm-Debugging-Erweiterungs-Plugin hier: https://goo.gle/wasm-debugging-extension
Details finden Sie unter Debugging WebAssembly mit modernen Tools.
Die Emscripten-Speicherrepräsentation ist mit C und C++ kompatibel. Bei undefiniertem Verhalten können jedoch Unterschiede zu nativen Architekturen und auch Unterschiede zwischen Emscriptens Ausgabe für asm.js und WebAssembly auftreten.
In asm.js müssen Lese- und Schreibvorgänge ausgerichtet sein, und ein normaler Lese- oder Schreibvorgang auf einer nicht ausgerichteten Adresse kann stillschweigend fehlschlagen (auf die falsche Adresse zugreifen). Wenn der Compiler weiß, dass ein Lese- oder Schreibvorgang nicht ausgerichtet ist, kann er ihn auf eine Weise emulieren, die funktioniert, aber langsam ist.
In WebAssembly funktionieren nicht ausgerichtete Lese- und Schreibvorgänge. Jeder ist mit seiner erwarteten Ausrichtung versehen. Wenn die tatsächliche Ausrichtung nicht übereinstimmt, funktioniert es trotzdem, kann aber auf einigen CPU-Architekturen langsam sein.
Tipp
SAFE_HEAP kann verwendet werden, um Probleme mit der Speicherausrichtung aufzudecken.
Im Allgemeinen ist es am besten, nicht ausgerichtete Lese- und Schreibvorgänge zu vermeiden — oft treten sie, wie oben erwähnt, als Ergebnis von undefiniertem Verhalten auf. In einigen Fällen sind sie jedoch unvermeidlich — zum Beispiel, wenn der zu portierende Code einen int aus einer gepackten Struktur in einem bereits vorhandenen Datenformat liest. In diesem Fall, um die Dinge in asm.js richtig funktionieren zu lassen und in WebAssembly schnell zu sein, müssen Sie sicherstellen, dass der Compiler weiß, dass der Lese- oder Schreibvorgang nicht ausgerichtet ist. Dazu können Sie
Einzelne Bytes manuell lesen und den vollständigen Wert rekonstruieren
Verwenden Sie die Typedefs emscripten_align*, die nicht ausgerichtete Versionen der Basistypen (short, int, float, double) definieren. Alle Operationen auf diesen Typen sind nicht vollständig ausgerichtet (verwenden Sie in den meisten Fällen die 1-Varianten, die keinerlei Ausrichtung bedeuten).
Wenn Sie einen abort() von einem Funktionszeigeraufruf an nullFunc oder b0 oder b1 erhalten (möglicherweise mit einer Fehlermeldung „incorrect function pointer“), liegt das Problem darin, dass der Funktionszeiger beim Aufruf nicht in der erwarteten Funktionszeigertabelle gefunden wurde.
Hinweis
nullFunc ist die Funktion, die verwendet wird, um leere Indexeinträge in den Funktionszeigertabellen zu füllen (b0 und b1 sind kürzere Namen, die für nullFunc in optimierteren Builds verwendet werden). Ein Funktionszeiger auf einen ungültigen Index ruft diese Funktion auf, die einfach abort() aufruft.
Es gibt mehrere mögliche Ursachen
Ihr Code ruft einen Funktionszeiger auf, der von einem anderen Typ umgewandelt wurde (dies ist undefiniertes Verhalten, kommt aber in realem Code vor). In der optimierten Emscripten-Ausgabe wird jeder Funktionszeigertyp in einer separaten Tabelle basierend auf seiner ursprünglichen Signatur gespeichert, daher müssen Sie einen Funktionszeiger mit derselben Signatur aufrufen, um das richtige Verhalten zu erhalten (siehe Funktionszeiger-Probleme im Abschnitt zur Codewartbarkeit für weitere Informationen).
Ihr Code ruft eine Methode auf einem NULL-Zeiger auf oder dereferenziert 0. Diese Art von Fehler kann durch jede Art von Codierungsfehler verursacht werden, manifestiert sich aber als Funktionszeigerfehler, weil die Funktion zur Laufzeit nicht in der erwarteten Tabelle gefunden werden kann.
Um diese Art von Problemen zu debuggen
Kompilieren mit -Werror. Dies wandelt Warnungen in Fehler um, was nützlich sein kann, da einige Fälle von undefiniertem Verhalten sonst Warnungen anzeigen würden.
Verwenden Sie -sASSERTIONS=2, um nützliche Informationen über den aufgerufenen Funktionszeiger und seinen Typ zu erhalten.
Sehen Sie sich den Browser-Stack-Trace an, um zu sehen, wo der Fehler auftritt und welche Funktion hätte aufgerufen werden sollen.
Aktivieren Sie Clang-Warnungen bei gefährlichen Funktionszeiger-Umwandlungen mit -Wcast-function-type.
Erstellen Sie mit SAFE_HEAP=1.
Debugging mit Sanitizern kann hier helfen, insbesondere UBSan.
Ein weiteres Funktionszeigerproblem ist, wenn die falsche Funktion aufgerufen wird. SAFE_HEAP=1 kann hier helfen, da es einige mögliche Fehler bei Funktionszeigertabellen-Zugriffen erkennt.
Endlosschleifen führen dazu, dass Ihre Seite hängt. Nach einer gewissen Zeit benachrichtigt der Browser den Benutzer, dass die Seite hängt und bietet an, sie anzuhalten oder zu schließen.
Wenn Ihr Code in eine Endlosschleife gerät, ist eine einfache Möglichkeit, den Problemcode zu finden, die Verwendung eines JavaScript-Profilers. Im Firefox-Profiler sehen Sie, wenn der Code in eine Endlosschleife gerät, am Ende des Profils einen Codeblock, der wiederholt dasselbe tut.
Hinweis
Die Browser-Hauptschleife muss möglicherweise neu codiert werden, wenn Ihre Anwendung eine unendliche Hauptschleife verwendet.
Um Ihren Code auf Geschwindigkeit zu profilieren, erstellen Sie ihn mit Profiling-Informationen und führen Sie den Code dann im DevTools-Profiler des Browsers aus. Sie sollten dann sehen können, in welchen Funktionen die meiste Zeit verbracht wird.
Die Speicherprofiling-Tools des Browsers verstehen im Allgemeinen nur Allokationen auf JavaScript-Ebene. Aus dieser Perspektive ist der gesamte lineare Speicher, den die Emscripten-kompilierte Anwendung verwendet, eine einzige große Allokation (eines WebAssembly.Memory). Die DevTools zeigen keine Informationen über die Nutzung innerhalb dieses Objekts an, daher benötigen Sie dafür andere Tools, die wir nun beschreiben werden.
Emscripten unterstützt mallinfo(), mit dem Sie Informationen von dlmalloc über aktuelle Allokationen erhalten können. Ein Beispiel für die Verwendung finden Sie im Test.
Emscripten verfügt auch über eine Option --memoryprofiler, die die Speichernutzung visuell darstellt und Ihnen zeigt, wie fragmentiert sie ist und so weiter. Um sie zu verwenden, können Sie so etwas tun wie
emcc test/hello_world.c --memoryprofiler -o page.html
Beachten Sie, dass Sie HTML wie in diesem Beispiel ausgeben müssen, da die Ausgabe des Speicherprofilers auf der Seite gerendert wird. Um sie anzuzeigen, laden Sie page.html in Ihrem Browser (denken Sie daran, einen lokalen Webserver zu verwenden). Die Anzeige wird automatisch aktualisiert, sodass Sie die DevTools-Konsole öffnen und einen Befehl wie _malloc(1024 * 1024) ausführen können. Dadurch werden 1 MB Speicher zugewiesen, die dann auf der Anzeige des Speicherprofilers angezeigt werden.
Der AutoDebugger ist die 'nukleare Option' für das Debugging von Emscripten-Code.
Warnung
Diese Option ist hauptsächlich für Emscripten-Core-Entwickler gedacht.
Der AutoDebugger wird die Ausgabe umschreiben, sodass er jeden Speichervorgang ausgibt. Dies ist nützlich, da Sie die Ausgabe für verschiedene Compiler-Einstellungen vergleichen können, um Regressionen zu erkennen.
Der AutoDebugger kann potenziell jedes Problem im generierten Code finden, daher ist er streng genommen leistungsfähiger als die CHECK_*-Einstellungen und SAFE_HEAP. Eine Verwendung des AutoDebuggers besteht darin, schnell viele Protokollausgaben zu erzeugen, die dann auf ungewöhnliches Verhalten überprüft werden können. Der AutoDebugger ist auch besonders nützlich für das Debugging von Regressionen.
Der AutoDebugger hat einige Einschränkungen
Er erzeugt viel Ausgabe. Die Verwendung von diff kann sehr hilfreich sein, um Änderungen zu identifizieren.
Es werden einfache numerische Werte anstelle von Zeigeradressen ausgegeben (da sich Zeigeradressen zwischen den Läufen ändern und daher nicht verglichen werden können). Dies ist eine Einschränkung, da manchmal die Inspektion von Adressen Fehler aufzeigen kann, bei denen die Zeigeradresse 0 oder unmöglich groß ist. Es ist möglich, das Tool in tools/autodebugger.py zu ändern, um Adressen als Ganzzahlen auszugeben.
Um den AutoDebugger auszuführen, kompilieren Sie mit der Umgebungsvariablen EMCC_AUTODEBUG=1 gesetzt. Zum Beispiel
# Linux or macOS
EMCC_AUTODEBUG=1 emcc test/hello_world.cpp -o hello.html
# Windows
set EMCC_AUTODEBUG=1
emcc test/hello_world.cpp -o hello.html
set EMCC_AUTODEBUG=0
Verwenden Sie den folgenden Workflow, um Regressionen mit dem AutoDebugger zu finden
Kompilieren Sie den funktionierenden Code mit der Umgebungsvariablen EMCC_AUTODEBUG=1.
Kompilieren Sie den Code erneut mit EMCC_AUTODEBUG=1 in der Umgebung, diesmal jedoch mit den Einstellungen, die die Regression verursachen. Nach diesem Schritt haben wir einen Build vor der Regression und einen danach.
Führen Sie beide Versionen des kompilierten Codes aus und speichern Sie deren Ausgaben.
Vergleichen Sie die Ausgabe mit einem diff-Tool.
Jede Differenz zwischen den Ausgaben ist wahrscheinlich auf den Fehler zurückzuführen.
Hinweis
Sie können -sDETERMINISTIC verwenden, um sicherzustellen, dass Timing und andere Probleme keine Fehlalarme verursachen.
Die Emscripten Test Suite enthält gute Beispiele für fast alle von Emscripten angebotenen Funktionen. Wenn Sie ein Problem haben, ist es eine gute Idee, die Suite zu durchsuchen, um festzustellen, ob Testcode mit ähnlichem Verhalten ausgeführt werden kann.
Wenn Sie die hier vorgeschlagenen Ideen ausprobiert haben und weitere Hilfe benötigen, nehmen Sie bitte Kontakt mit uns auf.