Hallo Besucher, der Thread wurde 7,5k mal aufgerufen und enthält 58 Antworten

letzter Beitrag von Zirias/Excess am

Mit Branch-Befehl eine Subroutine abbrechen - okay oder böse?

  • Ich bastle gerade an einem gridbasierten Movement, in dem ein Sprite immer x Pixel in eine Richtung läuft und dann stoppt. Dabei nutze ich u.a. eine Subroutine, die prüft, ob die max. Anzahl der Pixel erreicht ist. In grid befindet sich der aktuelle Stand (der beim Bewegen hochgezählt wird) und in grid_default die max. Länge.


    Code
    1. test_grid
    2. ldx grid ; Lade aktuelle Position im Tile
    3. cpx grid_default ; Vergleiche mit max. möglicher Schritte
    4. beq joystickloop ; Falls erreicht, springe zur Joystick-Routine
    5. rts ; Ansonsten verlasse die Subroutine

    Das funktioniert soweit auch ganz gut. Was ich mich aber gerade frage: Ist es okay, so eine Subroutine mit einem Branch-Befehl abzubrechen? Oder kann es da später zu Problemen kommen? Ich kehre ja nicht mehr zu aufrufenden Stelle zurück. Der Aufruf sieht übrigens so aus:


    Code
    1. north_grid_loop
    2. jsr test_grid ; Ist eine weitere Bewegung möglich
    3. jsr dec_y_grid ; Bewege das Sprite in y-Richtung nach oben
    4. jmp north_grid_loop ; Falls weitere Bewegung möglich, wiederholen
  • Irgendwann aber sicherlich. Dann mit RTS oder JMP?

    Falls die Kondition nicht eintritt, dann natürlich schon mit RTS. Danach würde ich das Sprite bewegen. Falls die Kondition aber eingetreten ist, muss ich das Sprite nicht mehr bewegen und kann wieder zurück zur Joystickeingabe.


    Ich frag mich halt, ob es in Assembler so etwas wie einen Stack Overflow gibt. Ich glaube, in BASIC würde ich den so früher oder später provozieren.

  • Ist es okay, so eine Subroutine mit einem Branch-Befehl abzubrechen? Oder kann es da später zu Problemen kommen?

    Das kann man nur beurteilen, wenn man weiß, wo in Deinem Source das Label "joystickloop" steht. Denn falls "joystickloop" seinerseits "north_grid_loop" aufruft, entsteht eine Rekursion und der Stack läuft irgendwann über.


    Grundsätzlich muss nicht jede per JSR aufgerufene Funktion direkt auf RTS enden: Man kann ja durchaus per JMP (oder natürlich auch einem Branch) zu einer anderen Funktion springen, und diese kann das ihrerseits ebenfalls tun - aber irgendwann sollte halt per RTS der Rücksprung erfolgen. Passiert stattdessen ein "JMP Hauptschleife" ist man vom Basic GOTO-geschädigt. :D


    EDIT:

    Ich frag mich halt, ob es in Assembler so etwas wie einen Stack Overflow gibt.

    Ja.

  • Denn falls "joystickloop" seinerseits "north_grid_loop" aufruft, entsteht eine Rekursion und der Stack läuft irgendwann über.

    Nein, ich bin mir relativ sicher, dass das nicht passiert. "joystickloop" ruft "north" auf. Die ganze Funktion sieht so aus:


    Code
    1. north
    2. lda spr1_y ; Aktueller Testcode, der auf Bildschirmrand
    3. cmp #42 ; prüft. Kommt später weg.
    4. beq joystickloop
    5. north_grid_loop
    6. jsr test_grid
    7. jsr dec_y_grid
    8. jmp north_grid_loop


    Und ja, wahrscheinlich bin ich wirklich vom BASIC-Code geschädigt. Aber sind ja die ersten "ernsten" Schritte in Assembler seit bestimmt 25 Jahren. ;)


    Aktuell sieht das ganze live so (Link zu Twitter) aus.

  • Ich frag mich halt, ob es in Assembler so etwas wie einen Stack Overflow gibt. Ich glaube, in BASIC würde ich den so früher oder später provozieren.

    Den Overflow kann es immer geben, da ja der Stack endlich ist. Die Frage ist nur, was passiert im Falle eines Overflows. Bei BASIC kommt in der Regel in OUT OF MEMORY ERROR oder eine FORMULA TOO COMPLEX ERROR. Es passiert kein Schaden (abgesehen vom Programmabbruch). Bei einem Maschinencode-Programm, wir die Einhaltung der Grenze selten überprüft (weil zu aufwändig und zeitintensiv ständig die Schranken des Stack-Pointers zu überprüfen bei jeder stack-orientierten Aktion). Bei einem Programm, dass sich korrekt verhält, ist das auch eher selten notwendig.


    In deinem Fall würde sozusagen laufend die Rücksprungadresse am Stack liegen bleiben. Der Stack-Pointer wandert ständig immer weiter und weiter Richtung niedriger Adressen (also $0100) und würde dann wieder auf $01FF springen (ist immer auf Page 1 beschränkt) und den Bereich weiter mit einer Rücksprungadresse überschreiben.
    An sich sollte man sowas vermeiden auch etwaige schrägen Manipulationen des Stacks, damit man sauber wieder rauskommt. Das lässt sich üblicherweise strukturell besser lösen.
    Aber im konkreten Fall (wo ja das Gesamtprogramm nicht bekannt ist) müsste man das Verlassen der Unterroutine anders gestalten, damit der Stack im Reinen bleibt, z.B.

    Code
    1. ldx grid ; Lade aktuelle Position im Tile
    2. cpx grid_default ; Vergleiche mit max. möglicher Schritte
    3. bne back ; Subroutine sauber verlassen
    4. pla ; Rücksprungadresse vom Stack entfernen
    5. pla ; (zerstört Akku): erst Low-Byte, dann High-Byte
    6. bne joystickloop ; High-Byte üblicheweise <> 0, also immer verzweigen
    7. ; zur Joystick-Routine
    8. back rts ; Ansonsten verlasse die Subroutine
  • "joystickloop" ruft "north" auf.

    Per JSR? Dann geht es schief, da läuft der Stack voll (und über).

    Die ganze Funktion sieht so aus:

    Die Definition von "joystickloop" muss schon drin sein, damit man was sagen kann...

  • Ohne den kompletten Code zu kennen, würde ich hinter (und in)


    "jsr test_grid"


    einen Flag einbauen, ob die Routine angesprungen wurde oder nicht, und dann ggf. eben gleich zurück zu


    "north_grid_loop"

  • Die Definition von "joystickloop" muss schon drin sein, damit man was sagen kann...


    Okay, ich probier's mal nur für Norden:


    Die Joystickroutine kann man bestimmt verbessern, ich hab sie aus "Das große Commodore 64 Buch".

  • Anmerkungen im (gekürzten und umsortierten) Code:

    Wenn der Stack überläuft, merkt das Programm selbst das nicht sofort: Allerdings kann der System-Interrupt nicht damit umgehen (er liest dann dank LDA$0104,X ggfs. von außerhalb der Stackpage), und das Programm kann nicht mehr zur aufrufenden Routine zurückkehren, weil diese Adresse längst überschrieben ist. In diesem Beispiel fällt das nicht auf, da die oberste Schleife eh eine Endlosschleife ist.

  • north_grid_loop

    Code
    1. joystickloop
    2. lda #0
    3. sta $02
    4. lda #0
    5. sta grid
    6. [...]


    Heute im Sonderangebot! Sparen Sie zwei Bytes mit:


    Code
    1. joystickloop
    2. lda #0
    3. sta $02
    4. sta grid
    5. [...]


    Bei north_grid_loop ruft das Programm als Unterroutine (jsr) test_grid auf. Von dort wird u.U. per beq weiterverzweigt nach joystickloop, von wo es aber kein RTS gibt, sondern ggf. per beq zu north gebrancht wird, das direkt wieder zu north_grid_loop führt. Dadurch kann der Stack irgendwann überlaufen.


    Edit: Mac Bacon war nicht nur schneller, sondern ausführlicher.

  • Das der Stack-Überlauf vorliegt war doch schon in Post #5 klar, oder? Ich wäre eher geneigt gewesen, dass wir hier über entsprechende Abhilfen (wie es schon @Hexworx angedeutet hat) diskutieren, statt über die Feststellung des Überlaufs zu elaborieren, was in @Mac Bacons Art natürlich auch ein Genuss ist und damit gleich erfährt, welch ein Gräuel sich dabei mitunter offenbart. ^^

  • Wenn du mit jsr aufrufst und dann in einen Loop weiterspringst (der dann irgendwann wieder die jsr aufruft) hast du logischerweise einen Stackoverflow, was nicht gerade günstig ist.
    Was da natürlich machen kannst ist, dass du, bevor du den Loop anspringst, den Stack bereinigst 2x pla und das ist erledigt. Dann kannst du einfach in den Loop weiter springen.
    Alternativ kannst du auch die Rücksprungadresse am Stack einfach überschreiben mit der Adresse des Joystickloops und dann mit rts zurückverzweigen. Was besser ist, hängt vom Programm ab, und ob so eine Konstruktion überhaupt sinnvoll ist.

  • Ich frag mich halt, ob es in Assembler so etwas wie einen Stack Overflow gibt.

    "In Assembler" gibt's nichts, aber JSR/RTS nutzen den 256 Bytes großen hardware-stack, und der kann natürlich "überlaufen" (wobei er dann einfach auf den Anfang "wrappt"). Auf jeden Fall ist das keine gute Idee. Eine Möglichkeit fehlt noch: warum überhaupt JSR und nicht einfach per branch oder JMP deine Routine anspringen? Dann wird auch kein Stack verwendet und du musst eben selbst wissen, wohin du zurückspringen musst.

  • @JeeK hat doch schon geschrieben, wie man quasi aus JSR ein JMP macht: einfach zweimal PLA schreiben. Dann gibt's hier keine rekursiv aufgerufenen Subroutines und der Stack verhält sich "ganz normal"...

    Nur ist das völlig überflüssig falls die Routine eh nur von einer Stelle angesprungen wird -- dann spart man sich den Stack einfach ;) Ohne den kompletten Code zu sehen wäre es möglich, dass das die einfachste und sinnvollste Variante ist.


    edit: falls der Code im OP tatsächlich die komplette Routine zeigt würde ich auch mal darüber nachdenken, ob da nicht ein Makro sinnvoller wäre ;)

  • @JeeK hat doch schon geschrieben, wie man quasi aus JSR ein JMP macht: einfach zweimal PLA schreiben. Dann gibt's hier keine rekursiv aufgerufenen Subroutines und der Stack verhält sich "ganz normal"...

    Das ist aber in diesem Fall m.E. ein übler Hack.
    Sauberer würde ich das hier lösen, indem ich die Fallunterscheidung aus der Subroutine herausnehme:

  • Sauberer würde ich das hier lösen, indem ich die Fallunterscheidung aus der Subroutine herausnehme:

    Auch das ging mir durch den Kopf (beliebt ist in etwas komplexeren Fällen auch setzen/löschen von C innerhalb der Subroutine) -- das ist auf jeden Fall sauberer und lesbarer.


    Am Stack "herumfummeln" würde ich nur, wenn man genau dadurch dringend benötigte Cycles oder Bytes einspart. Ansonsten immer die sauber strukturierte und damit lesbare Variante nehmen.

  • Klar ist PLA PLA nicht die Schulungslösung, und klar gibt es auch in diesem Beispiel andere, bessere Programmstrukturen. Aber ein "Hack" ist das wohl auch nicht. Regulärer Befehl, der eine klare Aufgabe hat und auf jedem C64 funktioniert, nichts Unvorhergesehenes. Mir ist schon klar, dass auch ich 99,8% meiner JRSs per RTS wieder verlasse. Aber in Einzelfällen tut PLA PLA genau das, was es soll: Es hält den Stack im Gleichgewicht ;)

  • Finde schon dass das ein Hack ist. Es macht den Code nicht gerade lesbarer. Das es funktioniert steht ausser Frage, aber ob es die Beste Lösung ist?


    Immerhin muss bei einem jsr die Rücksprungadresse auf den Stack geladen werden (kostet Zeit) und wenn ich dann weiss dass ich das eigentlich eh nie verwende, dann macht es keine richtigen Sinn das per jsr aufzurufen. Dass man den Stack dann wieder per pla bereinigen muss kostet auch wieder Zeit und Speicher (auch wenn es nicht viel ist).


    Kann mir aber vorstellen dass es Sinn machen kann, wenn man die Funktion an verschiedenen Stellen aufruft, und nur dann eine Rückkehr erwartet wenn auch tatsächlich was passiert ist auf das reagiert werden muss. Quasi sowas wie ein umgekehrter Eventhandler. :)