Lua Koroutinen - setjmp longjmp Clobbering?

8

In einem Blogbeitrag von vor nicht allzu langer Zeit beschreibt Scott Vokes ein technisches Problem verbunden mit luas Implementierung von Koroutinen unter Verwendung der C-Funktionen setjmp und longjmp :

  

Die Haupteinschränkung von Lua-Routinen besteht darin, dass sie, da sie mit setjmp (3) und longjmp (3) implementiert sind, nicht dazu verwendet werden können, um von Lua in C-Code zu rufen, der zurück in Lua ruft, das in C zurückruft, weil Der verschachtelte longjmp wird die Stack-Frames der C-Funktion überlisten. (Dies wird zur Laufzeit erkannt und nicht im Hintergrund ausgeführt.)

     

Ich habe nicht festgestellt, dass dies ein Problem in der Praxis ist, und ich weiß nicht, wie ich es reparieren kann, ohne Lua's Portabilität zu beschädigen, eines meiner Lieblingsdinge an Lua - es wird buchstäblich alles mit ANSI laufen C-Compiler und eine bescheidene Menge an Speicherplatz. Mit Lua kann ich leicht reisen. :)

Ich habe Coroutines in einer angemessenen Menge verwendet und ich dachte, ich verstehe im Großen und Ganzen, was los war und was setjmp und longjmp tun, aber ich las dies irgendwann und erkannte, dass ich es nicht wirklich verstand. Um zu versuchen, es herauszufinden, habe ich versucht, ein Programm zu machen, von dem ich dachte, dass es aufgrund der Beschreibung ein Problem verursachen sollte, und stattdessen scheint es gut zu funktionieren.

Allerdings gibt es ein paar andere Orte, an denen ich Leute gesehen zu haben scheinen, dass es Probleme gibt:

Die Frage ist:

  • Unter welchen Umständen funktionieren Lua-Coroutinen nicht, weil C-Funktion-Stack-Frames durcheinander geraten?
  • Was genau ist das Ergebnis? Bedeutet "zur Laufzeit erkannt", lua panic? Oder etwas anderes?
  • Betrifft das immer noch die neuesten Versionen von lua (5.3) oder ist das tatsächlich ein 5.1-Problem oder etwas?

Hier war der Code, den ich produziert habe. In meinem Test ist es mit lua 5.3.1 verknüpft, kompiliert als C-Code, und der Test selbst wird als C ++ - Code nach C ++ 11-Standard kompiliert.

%Vor%

Die Ausgabe, die ich bekomme, ist:

%Vor%

Vielen Dank an @ Nicol Bolas für die sehr detaillierte Antwort!
Nachdem ich seine Antwort gelesen, die offiziellen Dokumente gelesen, einige E-Mails gelesen und etwas mehr damit herumgespielt habe, möchte ich die Frage präzisieren / eine bestimmte Folgefrage stellen, wie auch immer Sie sie ansehen möchten.

Ich denke, dieser Begriff "Clobbering" ist nicht gut für die Beschreibung dieses Problems, und das war ein Teil dessen, was mich verwirrte - nichts wird "durchnässt" in dem Sinne, dass es zweimal geschrieben wird und der erste Wert verloren geht ist nur, wie @Nicol Bolas darauf hinweist, dass longjmp einen Teil des C-Stacks wirft, und wenn Sie hoffen, den Stack später wiederherzustellen, schade.

Das Problem wird in Abschnitt 4.7 des Lua 5.2-Handbuchs sehr gut beschrieben Link von @ Nicol Bolas zur Verfügung gestellt.

Seltsamerweise gibt es in der lua 5.1-Dokumentation keinen entsprechenden Abschnitt. Jedoch hat lua 5.2 dies zu sagen über lua_yieldk :

  

Liefert eine Coroutine.

     

Diese Funktion sollte nur als Rückgabeausdruck einer C-Funktion wie folgt aufgerufen werden:

     

return lua_yieldk (L, n, i, k);

Lua 5.1-Handbuch sagt etwas Ähnliches , etwa lua_yield stattdessen:

  

Liefert eine Coroutine.

     

Diese Funktion sollte nur als Rückgabeausdruck einer C-Funktion wie folgt aufgerufen werden:

     

return lua_yieldk (L, n, i, k);

Einige natürliche Fragen dann:

  • Warum spielt es eine Rolle, ob ich return hier verwende oder nicht? Wenn lua_yieldk longjmp aufruft, dann wird lua_yieldk niemals zurückgeben, also sollte es egal sein, ob ich dann zurückkomme? Das kann also nicht geschehen, oder?
  • Nehmen wir an, dass lua_yieldk nur eine Notiz im lua-Zustand macht, dass der aktuelle C-API-Aufruf angegeben hat, dass er nachgeben will, und wenn es dann endlich zurückkehrt, wird lua herausfinden, was als nächstes passiert. Dann löst dies das Problem des Speicherns von C-Stapelrahmen, nein? Da, nachdem wir normalerweise nach Lua zurückgekehrt sind, diese Stack Frames sowieso schon abgelaufen sind - also werden die im @ Nicol Bolas Bild beschriebenen Komplikationen umgangen? Und zweitens, in 5.2 zumindest ist die Semantik nie, dass wir C Stack Frames wiederherstellen sollten, es scheint - lua_yieldk wird zu einer Fortsetzungsfunktion fortgesetzt, nicht zum lua_yieldk Aufrufer, und lua_yield wird augenscheinlich wieder zur Aufrufer des aktuellen api-Aufrufs, nicht an den lua_yield -Aufrufer selbst.

Und die wichtigste Frage:

  

Wenn ich lua_yieldk in der in den Dokumenten angegebenen Form return lua_yieldk(...) konsistent verwende und von einem lua_CFunction , das an lua übergeben wurde, zurückkehrt, ist es weiterhin möglich, den Fehler attempt to yield across a C-call boundary auszulösen?

Schließlich, (aber das ist weniger wichtig), würde ich gerne ein konkretes Beispiel sehen, wie es aussieht, wenn ein naive Programmierer "nicht vorsichtig ist" und den attempt to yield across a C-call boundary Fehler auslöst. Ich habe die Idee, dass es Probleme mit setjmp und longjmp geben könnte, die Stack-Frames werfen, die wir später brauchen, aber ich möchte einen echten lua / lua capi-Code sehen, auf den ich zeigen und sagen kann " tu das nicht ", und das ist überraschend schwer fassbar.

Ich habe diese E-Mail gefunden, wo jemand diesen Fehler mit einigen Lua 5.1 gemeldet hat Code, und ich habe versucht, es in lua 5.3 zu reproduzieren. Was ich jedoch fand, war, dass dies nur ein Fehlerbericht aus der lua-Implementierung ist - der eigentliche Fehler wird verursacht, weil der Benutzer seine Coroutine nicht richtig eingerichtet hat. Die richtige Methode zum Laden der Coroutine besteht darin, den Thread zu erstellen, eine Funktion auf den Thread-Stack zu setzen und anschließend lua_resume im Thread-Status aufzurufen. Stattdessen verwendete der Benutzer dofile auf dem Thread-Stack, der die Funktion dort nach dem Laden ausführt, anstatt sie wieder aufzunehmen. Also ist es effektiv yield outside of a coroutine iiuc, und wenn ich dies patch, funktioniert sein Code gut, mit lua_yield und lua_yieldk in lua 5.3.

Hier ist die Liste, die ich erstellt habe:

%Vor%

Hier ist die Ausgabe, wenn USE_YIELDK auskommentiert ist:

%Vor%

Hier ist die Ausgabe, wenn USE_YIELDK definiert ist:

%Vor%     
Chris Beck 16.12.2015, 03:33
quelle

2 Antworten

9

Denken Sie darüber nach, was passiert, wenn eine Coroutine eine yield ausführt. Es stoppt die Ausführung, und die Verarbeitung kehrt zu dem zurück, der resume in dieser Coroutine genannt wurde, richtig?

Nun, sagen wir, Sie haben diesen Code:

%Vor%

Zum Zeitpunkt des Aufrufs von yield sieht der Lua-Stack wie folgt aus:

%Vor%

Wenn Sie yield aufrufen, wird der Lua-Aufrufstack, der Teil der Coroutine ist, beibehalten. Wenn Sie resume ausführen, wird der beibehaltene Aufruf-Stack erneut ausgeführt und beginnt dort, wo er vorher unterbrochen wurde.

OK, jetzt sagen wir, dass middle tatsächlich keine Lua-Funktion war. Stattdessen war es eine C-Funktion, und diese C-Funktion ruft die Lua-Funktion top auf. So gesehen sieht dein Stack folgendermaßen aus:

%Vor%

Bitte beachten Sie, was ich vorher gesagt habe: so sieht Ihr Stack konzeptionell aus .

Weil Ihr tatsächlicher Aufrufstapel nicht so aussieht.

In Wirklichkeit gibt es wirklich zwei Stapel. Es gibt Lua's internen Stack, definiert durch lua_State . Und da ist Cs Stack. Lua's interner Stack, zu dem Zeitpunkt, wenn yield aufgerufen wird, sieht ungefähr so ​​aus:

%Vor%

Also, wie sieht der Stack zu C aus? Nun, es sieht so aus:

%Vor%

Und genau da ist das Problem. Sehen Sie, wenn Lua eine yield macht, ruft sie longjmp auf. Diese Funktion basiert auf dem Verhalten des C-Stacks. Es wird nämlich dorthin zurückkehren, wo setjmp war.

Der Lua-Stapel wird beibehalten, da der Lua-Stapel vom C-Stapel getrennt ist. Aber der C-Stapel? Alles zwischen longjmp und setjmp ?. Weg. Kaputt. Verlor für immer .

Nun kannst du gehen, "warte, weiß der Lua-Stapel nicht, dass er in C und zurück in Lua gegangen ist"? Ein bisschen. Aber der Lua-Stapel ist unfähig, etwas zu tun, zu dem C nicht in der Lage ist. Und C ist einfach nicht in der Lage, einen Stapel zu konservieren (gut, nicht ohne spezielle Bibliotheken). Während also der Lua-Stack vage weiß, dass ein C-Prozess in der Mitte seines Stacks stattgefunden hat, kann er nicht rekonstruieren, was da war.

Was passiert also, wenn Sie diese yield ed-Coroutine fortsetzen?

Nasale Dämonen. Und niemand mag diese. Zum Glück, Lua 5.1 und höher (zumindest) wird Fehler, wenn Sie versuchen, über C zu liefern.

Beachten Sie, dass Lua 5.2+ Möglichkeiten hat, dies zu beheben . Aber es ist nicht automatisch; es erfordert eine explizite Codierung Ihrerseits.

Wenn Lua-Code, der sich in einer Coroutine befindet, Ihren C-Code aufruft und Ihr C-Code Lua-Code aufruft, können Sie lua_callk oder lua_pcallk verwenden, um die möglicherweise ergebenden Lua-Funktionen aufzurufen. Diese aufrufenden Funktionen nehmen einen zusätzlichen Parameter: eine "Fortsetzung" -Funktion.

Wenn der von Ihnen aufgerufene Lua-Code liefert, wird die lua_*callk -Funktion niemals wirklich zurückkehren (da Ihr C-Stack zerstört wurde). Stattdessen wird die Fortsetzungsfunktion aufgerufen, die Sie in Ihrer lua_*callk -Funktion angegeben haben. Wie Sie anhand des Namens erraten können, besteht der Job der Fortsetzungsfunktion darin, dort fortzufahren, wo Ihre vorherige Funktion nicht mehr aktiv war.

Nun behält Lua den Stapel für Ihre Fortsetzungsfunktion, so dass er den Stapel in demselben Zustand erhält wie Ihre ursprüngliche C-Funktion. Nun, außer dass die Funktion + Argumente, die Sie aufgerufen haben (mit lua_*callk ), sind entfernt, und die Rückgabewerte von dieser Funktion werden auf Ihren Stapel geschoben. Abgesehen davon ist der Stapel alle gleich.

Es gibt auch lua_yieldk . Dadurch kann Ihre C-Funktion zu Lua zurückkehren, so dass bei Wiederaufnahme der Coroutine die angegebene Fortsetzungsfunktion aufgerufen wird.

Beachten Sie, dass Coco Lua 5.1 die Möglichkeit gibt, dieses Problem zu lösen. Es ist in der Lage (obwohl OS / Assembly / etc Magie) von Erhaltung der C-Stapel während einer Yield-Operation. LuaJIT-Versionen vor 2.0 bieten diese Funktion ebenfalls.

C ++ Hinweis

Sie haben Ihre Frage mit dem C ++ - Tag markiert, also nehme ich an, dass das hier eine Rolle spielt.

Zu den vielen Unterschieden zwischen C und C ++ gehört die Tatsache, dass C ++ weit mehr von der Art seines Callstacks abhängt als Lua. Wenn Sie in C einen Stapel verwerfen, können Ressourcen verloren gehen, die nicht bereinigt wurden. C ++ ist jedoch erforderlich, um Destruktoren von Funktionen aufzurufen, die auf dem Stapel an einem bestimmten Punkt deklariert sind. Der Standard erlaubt es nicht, sie einfach wegzuwerfen.

Fortsetzungen funktionieren also nur in C ++, wenn nothing auf dem Stack ist, der einen Destruktoraufruf benötigt. Oder genauer gesagt können nur trivial zerstörbare Typen auf dem Stapel sitzen, wenn Sie eine der Lua-APIs der Fortsetzungsfunktion aufrufen.

Natürlich behandelt Coco C ++ gut, da es tatsächlich den C ++ - Stack enthält.

    
Nicol Bolas 16.12.2015, 05:00
quelle
1

Veröffentlichen Sie dies als eine Antwort, die @ Nicol Bolas Antwort ergänzt, und so Ich kann Platz haben, um aufzuschreiben, was ich gebraucht habe, um das Original zu verstehen Frage und die Antworten auf die sekundären Fragen / eine Code-Liste.

Wenn Sie die Antwort von Nicol Bolas gelesen haben, aber noch Fragen wie ich haben, hier sind einige zusätzliche Hinweise:

  • Die drei Ebenen im Aufruf-Stack, Lua, C, Lua, sind für das Problem essentiell. Wenn Sie nur zwei Schichten haben, Lua und C, bekommen Sie das Problem nicht.
  • Wenn man sich vorstellt, wie der Coroutine-Aufruf funktionieren soll, sieht der Lua-Stack aus In gewisser Weise sieht der C-Stack in gewisser Weise so aus, dass der Call (longjmp) und später wird wieder aufgenommen ... das Problem passiert nicht sofort wenn es ist wieder aufgenommen.
    Das Problem passiert, wenn die wiederaufgenommene Funktion später versucht, zu Ihrem zurückzukehren C-Funktion.
    Weil die Koroutinensemantik funktionieren soll, soll sie zurückkehren in einen C-Funktionsaufruf, aber die Stapelrahmen dafür sind weg und können nicht sein restauriert.
  • Die Problemumgehung für diese fehlende Möglichkeit, diese Stack-Frames wiederherzustellen, besteht darin, Verwenden Sie lua_callk , lua_pcallk , wodurch Sie einen Ersatz bereitstellen können Funktion, die anstelle der C-Funktion aufgerufen werden kann, deren Frames waren ausgelöscht.
  • Das Problem mit return lua_yieldk(...) scheint nichts zu tun zu haben irgendeins von diesen. Wenn Sie die Implementierung von lua_yieldk überfliegen, scheint dies der Fall zu sein es ist tatsächlich immer longjmp , und es kann nur in irgendeinem obskuren Fall zurückkehren mit Lua Debugging-Haken (?).
  • Lua intern (in der aktuellen Version) verfolgt, wann Ertrag nicht sein sollte erlaubt, indem eine Counter-Variable nny (Anzahl non-yieldable) zugeordnet wird in den Lua-Zustand und wenn Sie lua_call oder lua_pcall von einem CAPI aufrufen Funktion (a lua_CFunction , die du vorher nach lua geschoben hast), nny ist inkrementiert und wird nur dekrementiert, wenn dieser Aufruf oder pcall zurückkehrt. Wann nny ist ungleich Null, es ist nicht sicher zu liefern, und Sie erhalten diesen yield across C-api boundary Fehler, wenn Sie versuchen, trotzdem zu liefern.

Hier ist eine einfache Auflistung, die das Problem erzeugt und die Fehler meldet, wenn du wie ich bist und gerne konkrete Code-Beispiele hast. Es demonstriert einige der Unterschiede bei der Verwendung von lua_call , lua_pcall und lua_pcallk innerhalb einer Funktion, die von einer Coroutine aufgerufen wird.

%Vor%

Beispielausgabe:

call

%Vor%

pcall

%Vor%

pcallk

%Vor%     
Chris Beck 18.12.2015 07:16
quelle

Tags und Links