PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : Kompliziertes Stackproblem


Demirug
2005-12-28, 15:50:11
Eines Vorweg. Ich habe jetzt zwei Tage (und Nächte) damit verbracht das Problem zu suchen. Bin also gerade etwas angesäuert deswegen.

Zur Situation:
Eine Anwendung (liegt nur Binär vor) ruft über COM-Interfaces Methode in einer DLL auf. Mit einer veränderten Version der DLL kommt es nun plötzlich zu Fehlverhalten und Abstürzen. Selbst dann wenn die neue DLL nichts anderes macht als die Aufrufe direkt an die alte weiter zu geben. Nach den besagten zwei Tagen im Debugersumpf habe ich nun herausgefunden das es immer dann zum Fehlverhalten kommt wenn die neue Funktion „zu viel“ Stackspeicher braucht. Die Anwendung hat offensichtlich in einem Bereich des Stackspeichers der sie eigentlich nichts angeht Daten abgelegt. Benötigt die neue Methode nun mehr Speicher als die alte werden diese Daten überschrieben und es kommt zum Fehlverhalten.

Musste sich hier jemand schon mal mit einem solchen Problem rumschlagen? Unabhängig davon würde ich gerne Vorschläge zur Lösung des Problems hören. Die Anwendung zu verändern fällt aus. Die ist von 1999 und die Firma wurde schon lange geschlossen.Ich habe ja selber schon eine Idee aber vielleicht kennt jemand ja noch eine Lösung dafür.

PS: Bevor jemand fragt. Ja es geht um den Tweaker und mein aktuelles Hassspiel.

zeckensack
2005-12-28, 16:14:18
Da gibt's nur zwei Möglichkeiten.

a)Bringe die Daten "unter" dem Stack in Sicherheit. Sehr blöd, weil man dazu wissen sollte wie groß die sind. Auf Verdacht Daten zu sichern könnte schnell zu einem Performanceproblem werden.

b)Wechsle auf einen anderen Stack.
struct
ReplacementStack
{
void* new_esp;
void* old_esp;
void* allocation;
} stack2; //*muss* global sein! Lokale Variablen sind uU nicht auffindbar ...

//init:
stack2.allocation=malloc(1<<20); //1MiB sollte reichen
stack2.bottom=(void*)(((size_t)stack2.allocation)+(1<<20)));

...Das Umschalten musst du dann wohl oder übel in Assembler machen (kein Inline-ASM).

Ein gemeinsames Problem bei beiden Verfahren (wobei ich die zweite Variante klar bevorzugen würde) ist das ganze gegen Multithreading-Schranz immun zu machen. Optimalerweise würde man pro Thread einen eigenen Ersatzstack anlegen, und die Adresse in einen TLS-Slot packen.

Solche Probleme sollte es eigentlich garnicht geben :|
Darf man fragen wie das Spiel heißt? Need for Speed Irgendwas?

Demirug
2005-12-28, 16:32:41
Ich hatte noch eine dritte Variante. Den kritischen Bereich durch Veränderung des Stackpointers „überspringen“. Allerdings habe ich da auch wieder das Problem das ich nicht weiß wie viel Raum ich lassen muss. Ein zweiter Stack war auch meine Idee. Scheint also nicht so falsch zu sein. Was du allerdings gegen den Inline Assembler hast ist mit nicht ganz klar. Bei der Vielzahl von Methode die ich „Schützen“ muss tendiere ich allerdings zu einem generischen Ansatz. Nachdem ich gerade ~200 GDI Funktionen gehooked habe muss ich mir sowas nicht nochmal von Hand antun.

Das Spiel ist Jane's F/A-18. Die gehörten damals aber AFAIK auch schon zu EA.

Da sind noch mehr „interessante“ Dinge zu finden. Zum Beispiel ein „Render2Texture“ Effekt für animiertes Wasser ohne allerdings das Rendertarget zu wechseln. Wahrscheinlich werde ich für SimHQ an einem Artikel dazu mitschreiben.

Coda
2005-12-28, 16:59:42
Ist der Aufwand für ein Spiel nicht etwas extrem?

Demirug
2005-12-28, 17:02:54
Ist der Aufwand für ein Spiel nicht etwas extrem?

Letzen Endes verbessert das alles die allgemeine DX6/7 Kompatibilität.

zeckensack
2005-12-28, 17:19:50
Was du allerdings gegen den Inline Assembler hast ist mit nicht ganz klar.Die Calling Conventions. Und lokale Variablen.

Du musst beim Verlassen der Funktion den alten Stack-Pointer wiederherstellen, und falls du eine __stdcall-Konvention implementierst dann noch mit ESP+=x die Parameter wegreißen.
Wenn du Inline-ASM in eine ~C-Funktion einfügst, wird dir der Compiler da Ärger machen, weil er ja nicht wissen kann was du vorhast.

Lokale Variablen -- explizite und implizite -- gehen in dem Moment verloren wo du ESP neu lädst. Ebenso die Funktionsparameter.

Deswegen würde ich die Funktionen für den Stack-Switch (falls es Funktionen werden, und ich denke das sollten sie) direkt in Assembler schreiben. Dann kann der Compiler nichts mehr kaputt machen.

Möglicherweise geht das irgendwie mit dem Attribut "naked", ja, aber vom Compiler erzeugte Standard-Prolog- und -Epilog-Code ist IMO ganz falsch.

... generische Lösung ...Du kannst aus dem Assembler-Code ja mehr oder weniger problemlos C-Funktionen aufrufen. Wenn der Stack dafür ausreicht, könntest du mehrere Varianten für den Stack-Switcher schreiben, eine pro Calling Convention, pro Rückgabetyp und pro Größe der Funktionsparameter, und dann einen Zeiger auf die so geschützte Funktion mitgeben.

;NASM-Syntax

;Prototyp:
;uint __stdcall void stack_wrap_stdcall_12argbytes_uintrv(void* fn,RS* stack)

global stack_wrap_stdcall_12argbytes_uintrv@8

stack_wrap_stdcall_12argbytes_uintrv@8:
MOV EDX,[ESP+12] ;RS holen
MOV EAX,[ESP+8] ;fn holen
MOV [EDX+4],ESP ;RS->old_esp speichern
LEA ESI,[ESP+20] ;Zeiger auf die 12 Quell-Argument-Bytes basteln

MOV ESP,[EDX] ;Stack umschalten
SUB ESP,20
MOV [ESP+12],EBX ;EBX auf *neuen* Stack sichern
MOV [ESP+16],EDX ;EDX (=RS) sichern
;Argumente Kopieren
MOV EBX,[ESI]
MOV [ESP],EBX
MOV EBX,[ESI+4]
MOV [ESP+4],EBX
MOV EBX,[ESI+8]
MOV [ESP+8],EBX

CALL EAX ;fn aufrufen
;fn ist __stdcall, nimmt also die Argumente vom Stack!
;ansonsten käme hier ADD ESP,12

;der Rückgabewert ist jetzt in EAX

POP EBX ;gesichertes EBX zurückholen
POP EDX ;RS zurückholen
;auf den alten Stack zurückschalten
MOV ESP,[EDX+4]
;und raus
RETN 8//C oder C++

static uint __stdcall
handlerFunctionX(int arg0,int arg1,int arg2)
{
//was immer du brauchst, 1MiB Stack zur Verfügung
}

APIEXPORT uint __stdcall
apiFunctionX(int arg0,int arg1,int arg2)
{
return(stack_wrap_stdcall_12argbytes_uintrv(&handlerFunctionX,get_thread_stack()));
//Bei Rückgabe werden die 12 Argument-Bytes dieser Funktion
//vom Compiler gefressen.
}Ich weiß jetzt nicht ob der Assembler-Teil so stimmt, weil ich vergessen habe welche Register laut x86/C-ABI über Funktionsaufrufe hinweg gesichert werden (müssen). Solltest du's besser wissen, dürfte eine Korrektur nicht allzu schwierig sein.

Soweit ist das IMO alles noch machbar. Das richtig große Problem dürfte get_thread_stack() sein. Falls du die Sache mit dem TLS machst, musst du prüfen ob der Ersatzstack für den aktuellen Thread initialisiert wurde, und falls nicht, malloc oä aufrufen. Und wenn dann malloc zuviel Stack verbraucht, dann gnade dir Anubis. Dann kannst du nichts mehr machen. In erster Näherung.

Demirug
2005-12-28, 17:27:19
Mit "naked" bekommt man keinen Prolog/Epilog Code. Habe das schön öfter benutzt.

Ich werde dann mal ein wenig "basteln". Ist ja derzeit das letzte bekannte Problem das ich noch lösen muss.

Demirug
2005-12-29, 16:22:17
Zecki, ich glaube in deinem Code sind ein paar Fehler bezüglich der Indices bei den Stackzugriffen. Ich habe es jetzt aber sowieso etwas anders gemacht.

Interesant ist allerdings das sobald man auf den zweiten Stack umgeschaltet hat ein OutputDebugString sofort zu einem Absturz führt.

Landmann
2005-12-30, 11:33:26
Ich hatte mal aehnliche Probleme, weil ich an DLLs rumgewerkelt habe, deren Funktionen wohl jemand noch in ASM programmiert hatte, bzw. die eine nicht-standardmaessige Calling-Convention verwendet haben.

Dann passiert genau das, was Du schreibst. Wenn du WINAPI-Convention annimmst, aber mit C-Convention gecallt wird, dann wird zuviel Stack abgeraeumt (umgekehrt dann eben zuwenig).
Hast Du sicherheitshalber mal untersucht, wie die originale Funktion damit umgeht (im Disassembler das 'ret' suchen und schauen, ob es C/Pascal Convention ist) ?
Hast Du Dir mal angeschaut, wie die Funktion in der DLL aufgerufen wird ? Bzw., was sie am Anfang mit dem Stack macht ? Wie hast Du das erwaehnte "einfache weiterleiten" der Parameter erledigt ? Nur ein "jmp" ? Dann waere es wirklich extrem seltsam, wenn dabei noch Probleme auftreten...oder hast Du ein eigenes Call-Ret-Paerchen benutzt ? Dann koennen wieder die Calling-Conventions dazwischenpfuschen.

zeckensack
2005-12-30, 12:26:55
Zecki, ich glaube in deinem Code sind ein paar Fehler bezüglich der Indices bei den Stackzugriffen.Das war ungetestet. Habe ich mir extra für dich aus den Fingern gesogen :)
Was habe ich denn vergessen? Nur den Frame Pointer, oder noch was anderes?

Demirug
2005-12-30, 12:42:12
zecki, die Mühe hättest du dir nicht machen brauchen. Soweit ich sehen konnte fehlt nur der Framepointer. Allerdings musste ich von der alternativen Stack Methode nun vorerst doch die Finger lassen. Sobald man nämlich auf dem zweiten Stack läuft funktionieren die Exceptions nicht mehr. Scheint ein Problem beim Unwinden zu sein. Muss ich mir irgendwann mal genauer ansehen.

Landmann, die Calling Conventions wurden soweit ich das sehen kann eingehalten. Beim weiterleiten habe ich natürlich noch etwas Code dazwischen gepackt der Stackspeicher benötigt hat. Solange diese menge unter einem bestimmten Limit (ich habe es nicht genau bestimmt) blieb hat auch alles funktioniert. Kommt man über das Limit gibt es Fehler.

Demirug
2005-12-30, 22:48:25
Für den Fall des es noch jemanden interssiert. Ich habe jetzt die Lösung gefunden (zumindestens im WIN32 Umfeld) mit der auch die Exceptions usw. funktionieren.

Man erzeugt sich einen Fiber und wechselt auf diesen um die Funktion auszuführen. Der Wechsel ist zwar etwas aufwendiger als ein reines Umbiegen des Stacks aber dafür wird auch der Rest korrekt gesetzt. Die Umschaltfunktion selbst braucht nur 8 Byte auf dem Stack was in diesem Zusammenhang ja auch wichtig ist.

Gast
2005-12-30, 23:10:49
Für den Fall des es noch jemanden interssiert. Ich habe jetzt die Lösung gefunden (zumindestens im WIN32 Umfeld) mit der auch die Exceptions usw. funktionieren.

Man erzeugt sich einen Fiber und wechselt auf diesen um die Funktion auszuführen. Der Wechsel ist zwar etwas aufwendiger als ein reines Umbiegen des Stacks aber dafür wird auch der Rest korrekt gesetzt. Die Umschaltfunktion selbst braucht nur 8 Byte auf dem Stack was in diesem Zusammenhang ja auch wichtig ist.Mit Fibers hatte ich schon sehr schlechte Erfahrungen. Was machst du wenn der Kunde ebenfalls Fibers nutzt? IsThreadFiber oä existiert nämlich nicht (ist IIRC erst für Vista geplant), und wenn du's trotzdem machst (dh aus einem Thread heraus zweimal ConvertThreadToFiber aufrufst), schmiert dir knallhart und ohne Fehlercode der Prozess ab.

-zecki

Demirug
2005-12-30, 23:22:17
Mit Fibers hatte ich schon sehr schlechte Erfahrungen. Was machst du wenn der Kunde ebenfalls Fibers nutzt? IsThreadFiber oä existiert nämlich nicht (ist IIRC erst für Vista geplant), und wenn du's trotzdem machst (dh aus einem Thread heraus zweimal ConvertThreadToFiber aufrufst), schmiert dir knallhart und ohne Fehlercode der Prozess ab.

-zecki

Die ganze Schutzfunktion ist ja sowieso nur eine Option die man zuschalten kann. Ich patche dann bei Bedarf die VTable.

Durch das reine zweimallige Aufrufen von ConvertThreadToFiber ist bei mir nichts passiert.

Zudem ist die Wahrscheinlichkeit das DX6.1/DX7 Spiele Fibers benutzten doch eher gering. Falls es doch mal Probleme geben sollte kann ich mir immer noch anschauen wie CreateFieber einen Win32 konformen Stack baut. Das umschalten mit SwitchToFiber habe ich mir schon angeschaut. Das ist sehr einfach.

Coda
2005-12-31, 00:15:16
Ich patche dann bei Bedarf die VTableRein aus Interesse: Wie machst du das und ist es nicht compilerspezifisch wie diese aussieht?

Demirug
2005-12-31, 00:32:48
Rein aus Interesse: Wie machst du das und ist es nicht compilerspezifisch wie diese aussieht?

Die VTable von COM Objekten ist Compiler unabhängig sonst würde COM ja nicht funktionieren. Was danach kommt (Thunks um this gerade zu biegen usw.) ist dann aber Compiler spezifisch.

Eigentlich ist das ganz einfach. Sobald man die Adresse hat hebt man mit VirtualProtect den Schreibschutz auf und schreibt dann die neuen Zieladdressen in die Tabelle. Danach setzt man den Schreibschutz wieder zürck.

VTables sind einfach. System Funktionen sind komplizierter weil man dafür ein Katapult bauen muss. Dafür wiederum braucht man einen mini Disassembler um die Länge der einzelnen Assembler Anweisungen zu ermitteln.

Coda
2005-12-31, 15:24:39
Ach für COM-Objekte. Ich dachte du meinst virtuelle Funktionen.

Demirug
2005-12-31, 15:42:44
Ach für COM-Objekte. Ich dachte du meinst virtuelle Funktionen.

Das ist letzen Endes nichts anderes. Vorallem weil DirectX COM objekte ja sowieso nicht auf die übliche Art erzeugt werden. Ein COM Interface ist ja nichts anderes als eine Klasse die nur aus rein virtuellen Funktionen besteht.

Coda
2005-12-31, 17:42:31
Ja, aber die VTable für "normale" virtuellen Funktionen ist ja compilerspezifisch.

Demirug
2005-12-31, 18:34:47
Ja, aber die VTable für "normale" virtuellen Funktionen ist ja compilerspezifisch.

Durchaus aber letztend Endes müssen das alle in etwa auf die gleiche Art und weiße mache. Das ergibt sich aus dem Befehlssatz für X86 CPUs. Zudem macht es ja auch normalweise keinen Sinn die internen VTables zu patchen.