Heute habe ich es endlich geschafft, den „Fehler“ in einem Firefox-Plugin (man könnte auch sagen „plag ihn“) zu finden. Es geht um das evince-browser-plugin welches für einen Firefox 10 auf einer embedded-Linux Plattform basierend auf ARM zu übersetzen war. Übersetzt hat es auch ganz gut, aber laufen wollte es nicht. Das öffnen von PDFs im Firefox endete in schneeweißen Browserfenstern und auf einem xterm von dem der Firefox augerufen wurde las man:Â toolkit is not Gtk2
Das war die Ausgangssituation und natürlich war ich mir sicher, dass der Browser mit Gtk2 gebaut war. Also hieß es nun rausfinden, was da schiefgeht.
Dieser Fehlertext war sehr schnell in der Initialisierungsfunktion des Plugins entdeckt. In der Datei evbp.c steht folgendes:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* Check that the browser supports XEmbed */ ret = npn_funcs->getvalue(NULL, NPNVSupportsXEmbedBool, (void *)&xembed); if (ret != NPERR_NO_ERROR || xembed != PR_TRUE) { g_critical("browser does not support XEmbed"); return NPERR_INCOMPATIBLE_VERSION_ERROR; } /* Check that the toolkit is Gtk2 */ ret = npn_funcs->getvalue(NULL, NPNVToolkit, (void *)&toolkit); if (ret != NPERR_NO_ERROR || toolkit != NPNVGtk2) { g_critical("toolkit is not Gtk2"); return NPERR_INCOMPATIBLE_VERSION_ERROR; |
Dieser Fehlertext war schnell um ein %d %d erweitert um die Werte für ret und toolkit auszugeben. Â Ein Testlauf zeigte dann auch, dass nach dem API-Aufruf über den Funktionsvektor in ret der Wert 2 stand. Was will mir diese 2 sagen? Ein Blick nach npapi.h vom Firefox-Browser bringt Klarheit:
1 2 3 4 5 6 7 |
/* * Values of type NPError: */ #define NPERR_BASE 0 #define NPERR_NO_ERROR (NPERR_BASE + 0) #define NPERR_GENERIC_ERROR (NPERR_BASE + 1) #define NPERR_INVALID_INSTANCE_ERROR (NPERR_BASE + 2) |
Der Wert 2 ist also gleichzusetzen mit NPERR_INVALID_INSTANCE_ERROR.
Jetzt habe ich mich mal auf die Suche nach der Funktion in den Firefox-Sourcen gemacht und wurde in der Datei PluginModuleChild.cpp im Verzeichnis dom/plugins/ipc fündig. Da sieht man dann die Funktion die bestimmte Parameter des Browsers abfragen kann. Und das sieht so aus.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
NPError NP_CALLBACK _getvalue(NPP aNPP, NPNVariable aVariable, void* aValue) { PLUGIN_LOG_DEBUG_FUNCTION; ENSURE_PLUGIN_THREAD(NPERR_INVALID_PARAM); switch (aVariable) { // Copied from nsNPAPIPlugin.cpp case NPNVToolkit: #if defined(MOZ_WIDGET_GTK2) || defined(MOZ_WIDGET_QT) *static_cast<NPNToolkitType*>(aValue) = NPNVGtk2; return NPERR_NO_ERROR; #endif return NPERR_GENERIC_ERROR; case NPNVjavascriptEnabledBool: // Intentional fall-through case NPNVasdEnabledBool: // Intentional fall-through case NPNVisOfflineBool: // Intentional fall-through case NPNVSupportsXEmbedBool: // Intentional fall-through case NPNVSupportsWindowless: // Intentional fall-through case NPNVprivateModeBool: { NPError result; bool value; PluginModuleChild::current()-> CallNPN_GetValue_WithBoolReturn(aVariable, &result, &value); *(NPBool*)aValue = value ? true : false; return result; } default: { if (aNPP) { return InstCast(aNPP)->NPN_GetValue(aVariable, aValue); } NS_WARNING("Null NPP!"); return NPERR_INVALID_INSTANCE_ERROR; } } NS_NOTREACHED("Shouldn't get here!"); return NPERR_GENERIC_ERROR; } |
Und da war dann für mich erstmal der Moment erreicht an dem ich an meinen Programmierkünste zu zweifeln anfing. Die obige Funktion ist ja durchaus überschaubar und bei der switch-Anweisung ist der Zweig für den Wert NPNVToolkit so überschaubar, dass man ungläubig den Kopf schüttelt, denn der Wert 2 gehört nicht zu den Werten die dieser Programmzweig zurückliefern kann.
Also habe ich mal den gdbserver auf meinem ARM-Board angeworfen und versucht, das remote zu debuggen. Leider ist das Debuggen eines Firefox eine deutlich größere Herausforderung wie das Debuggen von „Hello World“ wie man es in den Tutorials findet. Eine Schwierigkeit ist, dass der Browser sich selbst forked wenn ein Plugin gestartet wird und einen neuen Job „plugin-container“ erzeugt. D.h. man braucht dann wieder irgendwie eine weitere Debugger-Instanz die den Kind-Prozess traced und Dinge wie follow-fork-mode funktionieren beim Remote-Debugging auch nicht. Auch catchpoints laufen nicht im remote-debugging Modus.
Es ist mir dann schon gelungen, das Plugin in dieser Routine anzuhalten, nur wenn man zulange wartet, dann schlägt wohl irgendein Watchdog im Parent zu und das Browserfenster schaltet auf schwarz und ab hier hat man einen Zombie-Prozess der plötzlich nicht mehr zu debuggen ist.
Das was ich über Tracing auf Source-Level-Ebene rausfand, war dass der switch wohl noch angesteuert wird und in der Variablen die da geprüft wird der Wert 13 drin stand.
Gucken wir in npapi.h wie dieser NPNVToolkit definiert ist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
typedef enum { NPNVxDisplay = 1, NPNVxtAppContext, NPNVnetscapeWindow, NPNVjavascriptEnabledBool, NPNVasdEnabledBool, NPNVisOfflineBool, NPNVserviceManager = (10 | NP_ABI_MASK), NPNVDOMElement = (11 | NP_ABI_MASK), NPNVDOMWindow = (12 | NP_ABI_MASK), NPNVToolkit = (13 | NP_ABI_MASK), NPNVSupportsXEmbedBool = 14, . . . |
Also 13 ge-oder-t mit einer NP_ABI_MASK die wohl 0 ist, denn der Debugger zeigte ja die 13 an. Aber warum gibt das blöde Teil dann einen falschen Fehlerwert zurück. Um hier weiter zu kommen habe ich die Routine dann mal mit dem gdb disassembliert. Das sieht dann so aus, wenn man den Schalter „/m“ mit angibt, der dann auch Source-Zeilen einstreut.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
1106 switch (aVariable) { 0x40dbaa5c : ldr r1, [sp, #4] 0x40dbaa60 : cmp r1, #14 0x40dbaa64 : beq 0x40dbaaa4 0x40dbaa68 : bgt 0x40dbaa7c 0x40dbaa6c : sub r3, r1, #4 0x40dbaa70 : cmp r3, #2 0x40dbaa74 : bhi 0x40dbaad0 0x40dbaa78 : b 0x40dbaaa4 0x40dbaa7c : cmp r1, #17 0x40dbaa80 : blt 0x40dbaad0 0x40dbaa84 : cmp r1, #18 0x40dbaa88 : ble 0x40dbaaa4 0x40dbaa8c : cmp r1, #268435469 ; 0x1000000d 0x40dbaa9c : bne 0x40dbaad0 0x40dbaaa0 : b 0x40dbaae8 1107 // Copied from nsNPAPIPlugin.cpp 1108 case NPNVToolkit: 1109 #if defined(MOZ_WIDGET_GTK2) || defined(MOZ_WIDGET_QT) 1110 *static_cast(aValue) = NPNVGtk2; 0x40dbaa90 : moveq r3, #2 0x40dbaa98 : streq r3, [r4] 1111 return NPERR_NO_ERROR; |
Bei dieser Darstellung muss man aufpassen, dass man nicht reinfällt. Die vermeintliche Abfolge der Assembleranweisungen ist nämlich nicht so, wie sie oben zu sehen ist, sondern hier zeigt der Disassembler erst mal eine Zeile aus dem Source an und dann, was er daraus gemacht hat. Die beiden Assemblerzeilen nach der Sourcezeile 1110 sind also eigentlich adressmäßig im Block darüber stehen. Wenn man den tatsächlichen Maschinencode sehen will muss man mit dem Schalter „/r“ disassemblieren, damit gibt es dann „raw“-Format.
Aber auch so bemerkte ich eine sehr interessante Zeile ab Adresse 0x40dbaa8c, hier wird nämlich auf den Wert 268435469 geprüft der in hexadezimaler Darstellung 0x1000000d entspricht. Das „d“ am Ende steht für die 13 (dezimal) die mir der Debugger anzeigt. Geht vielleicht der Firefox davon aus, dass oben erwähnte NP_ABI_MASK gar nicht Null ist..?
Also nochmal ein Blick in npapi.h. Da findet man dann folgenden Code-Block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#define NP_ABI_GCC3_MASK 0x10000000 /* * gcc 3.x generated vtables on UNIX and OSX are incompatible with * previous compilers. */ #if (defined(XP_UNIX) && defined(__GNUC__) && (__GNUC__ >= 3)) #define _NP_ABI_MIXIN_FOR_GCC3 NP_ABI_GCC3_MASK #else #define _NP_ABI_MIXIN_FOR_GCC3 0 #endif #if defined(XP_MACOSX) #define NP_ABI_MACHO_MASK 0x01000000 #define _NP_ABI_MIXIN_FOR_MACHO NP_ABI_MACHO_MASK #else #define _NP_ABI_MIXIN_FOR_MACHO 0 #endif #define NP_ABI_MASK (_NP_ABI_MIXIN_FOR_GCC3 | _NP_ABI_MIXIN_FOR_MACHO) |
Und hier sieht man, dass der Wert dieser Maske wohl von diversen Umgebungsvariablen abhängt. Also habe ich als nächstes mal die Compile-Logs geprüft die mir OpenEmbedded dank auskommentiertem „inherit rm_work“ Â aufgehoben hat. Und siehe da, im Firefox sehe ich bei den Compilerflags „-DXP_UNIX=1“. Bei den Compile-Logs des Plugins ist jedoch kein solches Flag drin.
Das erklärt nun auch das absonderliche Verhalten. Klar, beide Teile der Plugin-API nutzten die Datei npapi.h, nur ist für den Browser der Wert für NPNVToolkit eben 268435469 und für das Plugin 13. Klar, dass die beiden sich dann nicht verstehen und die API-Funktion den Fehlerausgang mit dem NPERR_INVALID_INSTANCE_ERROR nimmt.
Also mal kurz das Bitbake-Rezept für das Plugin um dieses Compilerflag erweitert und schon funktioniert alles wie es soll.
Im Nachhinein betrachtet war der ganze Schlamassel mehreren Ursachen geschuldet:
- Die Fehlermeldung des Plugins könnte in die Kategorie „irreführende Fehlermeldungen“ einsortiert werden.
- Beim Rezept-Schreiben habe ich zwar das Include-Verzeichnis in den CFLAGS angegeben, nicht aber den Parameter von dem ich erst durch das Debuggen erfuhr.
- In einer idealen Welt hätte Firefox bei der Installation eine PackageConfig-Datei (Endung „.pc“) angelegt in der die richtigen Compilerflags stehen. Hat er aber nicht.
- In einer idealen Welt hätte die configure-Routine diese Compiler-Flags ausgelesen und verwendet ohne dass ich sie explizit angeben muss. Tut sie aber nicht, was aber für mich nachvollziehbar ist nachdem ich mich ein wenig mit den Autotools auseinandergesetzt habe. Da verstehe ich schon, dass man solche Checks nicht so einfach implementiert.
Fazit: Problem erkannt, Problem gelöst. Und jede Menge gelernt über Dinge wie Autotools, gdb, remote-debugging, ARM-Assemblercode und wie man Fehler in fremden Programmen aufspüren kann. War anstrengend, aber eben auch eine schöne geistige Herausforderung. Das nächste mal aber bitte ein Debug-Kandidat auf dem Niveau von „Hello World“… 🙂
[ratings]