Stack wechseln für Simulation
-
Für eine Simulationsengine sollen mehrere Kontexte mit kooperativem Multitasking laufen. Dafür möchte ich eine art Kontextswitch bauen.
Das sieht in etwa so aus:
__declspec(naked) void __cdecl Switch(void *&from, void *&to){ /* Aktueller Stackpointer soll in [from] gespeichert und neuer Stackpointer aus [to] geladen werden */ __asm{ mov from, ebp mov ebp, to ret } }
Das klappt irgendwie nicht, sicherlich weil der Stackpointer für den Zugriff auf die auf dem Stack liegenden Variablen der Stackpointer falsch ist.
__asm{ //__cdecl intro push ebp mov ebp, esp //eigentliche funktion mov from, dword ptr ebp[-4] mov dword ptr ebp[-4], to //__cdecl outro mov esp, ebp pop ebp ret } }
Das könnte funktionieren, nur wirft mir Visual Studio Syntax-Fehler entgegen.
Kann mir jemand sagen wie das richtig wäre?
Folgefrage:
void __cdecl foo(); void *stack = malloc(STACKSIZE); void *stackpointer = ???;
Was muss man in stack und stackpointer schreiben, damit man mit obiger Switch-Funktion den eben angelegten Stack benutzen kann, sodass er in Funktion foo springt?
Ich versuche es mit Visual C++ zum Laufen zu kriegen.
-
- schon mal über Fibers oder thread pools nachgedacht (WinAPI)?
- wofür legst du eine stack frame an? (EBP)
- dir Rücksprungadresse (und Argumente) müssen vom stack geholt werden (z.b. mit POP) bevor ESP geändert wird. Die Rückkehr erfolgt dann mittels JMP reg32
- dereferenzieren kann man nur mittels Registern : z.B: mov eax,[edx+4]
- die meisten Befehl in der x86 Architekturen erlauben nur eine Speicher Operanden (z.B. MOV)
- ESP und EBP müssen mindestens getauscht werden.
- es ist hoffentlich klar, dass der Kontext nicht nur aus dem stack besteht. (z.B. dann problemaisch, wenn eins der nicht flüchtigen Register (ESI,EDI,EBX) sich auf eine den vorhereigen stack beziehen.)
- was passiert wenn der stack nicht groß genug ist? Windows kann den selbst erzeugten stack nicht vergrößern.; stdcall vorausgesetzt! pop edx ; ret. addr. pop ecx ; pFrom pop eax ; pTo mov DWORD ptr [ecx][0],ebp mov DWORD ptr [ecx][4],esp mov ebp,DWORD ptr [eax][0] mov esp,DWORD ptr [eax][4] jmp edx
-
nwp3 schrieb:
Für eine Simulationsengine sollen mehrere Kontexte mit kooperativem Multitasking laufen. Dafür möchte ich eine art Kontextswitch bauen.
Dafür hat dein Betriebsystem bereits Mittel. Lass Deinen Arbeitgeber einen I7 kaufen und Du kannst 10Mio Threads halten. Mach die Threads bescheiden.
Hab vor 15 Jahren das letzte Projekt betreut, was unter Win den SP entführt. Rate, wie es ausgegangen ist. Du kannst nicht debuggen.
-
Zu meinem vorigen Code:
dword ptr braucht man nicht und ebp[-4] muss [ebp-4] heißen (nicht immer alles vom Visual Studio Disassembler abkucken).Habe jetzt folgendes gebastelt:
__declspec(naked) void __cdecl yieldTo__(void *¤t, void *&next){ //Need to preserve edi, esi, ebp and ebx //so this function trashes eax and ecx, which is fine //stack's content for __cdecl: param_n, ... , param 3, param 2, param 1, return address __asm{ pop eax //pop return address pop ecx //pop current mov [ecx], eax //save return address in current pop eax //pop next sub esp, 8 //2 useless pushes, can be replaced by "sub esp, 8" if you are sure that the stack grows down and pointers are 4 bytes long //push eax //push back next, but next was already on the stack //push ecx //push back current, but current was already on the stack mov ecx, [eax] //load return address from next push ecx //push new return address //3 pops and 3 pushes, sp should be fine, ebp was not touched, no return value ret } }
Ich meine dass das soweit korrekt ist.
Fehlt noch die Initialisierung des Stacks.
Da der Stack bei mir (Win7 x86_64) nach unten zu wachsen scheint muss ich wohl stackpointer auf stack + STACKSIZE - 4 setzen und den Wert auf die Rücksprungadresse (=Funktionsadresse bei __cdecl). Das habe ich aber noch nicht probiert.Der Grund warum ich nicht einfach Threads nehme ist, dass man da schwer die Reihenfolge der Ausführung bestimmen kann, was aber zwingend notwendig ist. Außerdem soll das auch auf Linux ohne größere Anpassungen laufen und möglichst performant für viele kleine Ausführungseinheiten sein.
Die automatische Stackvergrößerung scheint unter Windows eh nicht richtig zu funktionieren, 1MB Stack pro Thread und fertig. Ist mir schon mehrfach auf die Füße gefallen. Kann man aber bei CreateThread und sicher irgendwie bei CreateProcess einstellen.
Bei mir kann man STACKSIZE entsprechend nahezu beliebig festlegen und wenn es zu wenig ist, dann gibts einen Stackoverflow und das Programm stürzt ab. Man sollte den Compiler fragen können wieviel Stack die jeweiligen Funktionen benötigen, aber das geht wohl nicht.
-
nwp3 schrieb:
Der Grund warum ich nicht einfach Threads nehme ist, dass man da schwer die Reihenfolge der Ausführung bestimmen kann, was aber zwingend notwendig ist.
Erstens kann man doch, zweitens gibt es Fibers.
nwp3 schrieb:
Außerdem soll das auch auf Linux ohne größere Anpassungen laufen
Das Problem wird aber sein,...
nwp3 schrieb:
und möglichst performant für viele kleine Ausführungseinheiten sein.
...dass es bald gar nicht mehr läuft.
nwp3 schrieb:
Die automatische Stackvergrößerung scheint unter Windows eh nicht richtig zu funktionieren,
Klar klappt sie.
nwp3 schrieb:
1MB Stack pro Thread und fertig. Ist mir schon mehrfach auf die Füße gefallen.
Zu klein? Uff. Benutze nur logarithmisch beschränkte Rekursion und fertig.
nwp3 schrieb:
Kann man aber bei CreateThread und sicher irgendwie bei CreateProcess einstellen.
Ok, stell halt 100M ein. Aber bitte nur mit aufgesetzter Narrenkappe.
nwp3 schrieb:
Bei mir kann man STACKSIZE entsprechend nahezu beliebig festlegen und wenn es zu wenig ist, dann gibts einen Stackoverflow und das Programm stürzt ab.
Ähm. Dumme Idee. Belege großzügig Adressraum. Und bleib beim physikalischen Speicher kleinlich. Dazu gibt es doch guard pages.
nwp3 schrieb:
Man sollte den Compiler fragen können wieviel Stack die jeweiligen Funktionen benötigen, aber das geht wohl nicht.
int binQuadratischDoof(int x){ char speicher[x];//benutze GCC mit VLAs if(x!=0) return sqrt(x)+binQuadratischDoof(x-1); else return 42; }
Wenn jetzt x irgendwie von einer Benutzereingabe abgeleitet wurde, ist es doch schon gelaufen.
Wirste threadlokale globale Daten haben (auch nicht versteckt in der C-Bibliothek?), müssen Exceptionkontexte umgeschaltet werden, Debug-Flags, Coprozessorflags?