Hallo,
Was haltet ihr von den beiden Varianten? was für die praktikablere.
Die Antwort auf diese Frage hat mehr Auswirkungen als Du wohl im Moment siehst.
Vorweg: in meiner CPU hab ich mich dazu entschieden keinen expliziten Stack in Hardware zu unterstützen sondern ich mache PUSH und POP mit den ganz normalen Speicherzugriffsbefehlen. Ein 'PUSH R12' ist bei mir 'STAW SegE:[SP+4],R12' (im 32Bit-Modus, im 64Bit-Mode wäre die 4 ne 8 ) und ein 'POP R7' wird ein 'LDBW R7,SegE:[SP-4]' (hier das selbe wegen 32Bit/64Bit-Modi). Das STAW bedeutet
STore and update Address-Register (SP in dem Fall, was nur ein Makro für R62 ist)
After Access and
Write it back. Das LDBW bedeutet
Loa
D and update Address-Register
Befor Access and
Write it back. Daraus erkennt man auch das bei mir der Stack nach oben wächst (auch diese Designentscheidung hat einige Auswirkungen). Meine normalen Speicherzugriffsbefehle haben also immer die Fähigkeit zur eigentlichen Adresse (die normalerweise aus einem Register kommt) noch ein Offset (das eine Konstante ist oder auch aus einem Register kommt und dann noch geshiftet werden kann) dazu zu Rechnen (beim Rechnen unterscheide ich noch explizit zwischen Addieren und Subtrahieren um damit Überläufe sicher verbieten zu können, es gibt bei mir bei den Adressen keinen Warp-Around) und dann diesen Wert auch optional wieder im Adressregister abzulegen, mit dieser Flexibilität hat man einige Vorteile und eben auch gleich noch die Stack-Operationen mit erledigt. Das einzigste was so nicht geht ist das Ablegen von Konstanten auf dem Stack (z.B. PUSH DWORD 4711), dafür müsste ich diese Konstante in ein Register laden und dieses dann auf den Stack speichern, aber das wird IMHO auch nicht so oft benötigt.
Das man für den Stack-Pointer ein Register benötigt ist in beiden Fällen gegeben, ebenso das man dieses Register mit den normalen Befehlen ansprechen können muss (ein 'SUB/ADD SP,16' ist wichtig um mal schnell 16 Bytes Stack-Frame allozieren/freigeben zu können). Größere Auswirkungen hat die Entscheidung für oder gegen richten HW-Stack eher bei anderen Sachen. CPUs ohne echten HW-Stack haben üblicherweise keinen CALL-Befehl sondern einen Linked-Branch was bedeutet das dort die Rücksprungadresse nicht auf dem Stack sondern in einem extra Register landet (dem Link-Register) und die aufgerufene Funktion dieses selber sichern/wiederherstellen muss (im Rahmen des Funktions-Prolog/Epilog) falls sie ihrerseits wieder andere Funktionen aufruft (was auf Leaf-Funktionen eben nicht zu trifft und bei diesen 2 Speicherzugriffe erspart). Ein 'RET' wird dann als 'MOV IP,LR' (also Kopieren vom Link-Register in den Instruction-Pointer) umgesetzt was wieder OpCodes spart.
Ein weiterer wesentlicher Punkt sind Interrupts und die Art wie von der CPU die unterbrochene Umgebung gesichert wird. CPUs die keinen HW-Stack haben benutzen dazu üblicherweise das Konzept der Schattenregister indem die wesentlichen Register (also mindestens der aktuelle Instruction-Pointer und der aktuelle Stack-Pointer) in der CPU mehrfach vorhanden sind und je nach aktuellem Modus ein bestimmtes Set davon tatsächlich benutzt wird. Dies hat auch Auswirkungen darauf wie verschiedene Exceptions/Interrupts verschachtelbar sind. ARM hat für die Register R14 und R15 und Flags gleich für jeden einzelnen Modus ein extra Set, das kostet zwar einiges an Logik bietet aber auch relativ schnelle Modus-Wechsel. Auf meiner CPU gibt es für die Register 60..63 nur 2 Sets, einmal für den User-Mode und einmal für den System-Mode, so das der System-Mode grundsätzlich nicht unterbrechbar ist und eine Exception im Kernel (egal ob Syscall-Handler, IRQ-Handler oder Exception-Handler) grundsätzlich in einem CPU-Shutdown endet, das hat zwar auch ein paar unangenehme Auswirkungen aber bei einem Mikro-Kernel-OS kann man damit ganz gut leben.
Eine weitere wichtige Designentscheidung ist in welche Richtung der Stack wachsen soll. Früher hat man sich dazu entschieden den Stack von oben nach unten wachsen zu lassen weil das dem damaligen Modell eines Prozess (kein Multi-Threading und nur einen zusammenhängenden Speicherbereich) am besten entsprach. Aus heutiger Sicht, wo Multi-Threading und z.B. Buffer-Overflows relevant sind, sieht die Sache anders aus. Mit einer CPU die das nicht per HW macht ist man da grundsätzlich flexibel, auf ARM/PowerPC/SPARC/... (auch auf meiner CPU) ist es eine rein willkürliche Festlegung da die CPUs aufgrund der flexiblen normalen Speicherzugriffsbefehle eigentlich beides unterstützen können. Ich habe mich für nach oben wachsende Stacks entschieden, das ist schon wegen meinen Segmenten die bessere Wahl, außerdem bietet man so weniger Angriffsfläche für Buffer-Overflows u.ä. weil die eigene Rücksprungadresse auf dem Stack dann
vor lokalen Arrays usw. liegt und damit eben nicht erreichbar ist (höchstens mit einem negativen Index und das ist nicht so einfach und auf meiner CPU wegen der expliziten Unterscheidung zwischen + und - beim Offset gar nicht möglich).
Wie eine CPU mit HW-Stack arbeitet kennt hier ja jeder von x86 zur genüge, um auch mal zu sehen wie andere CPUs das ohne expliziten HW-Stack machen kann ich nur empfehlen sich mal die Dokumentationen von ARM/MicroBlaze/PowerPC/SPARC genau durchzulesen (am besten von allen genannten CPUs
).
Grüße
Erik