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 *&current, 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?


Log in to reply