Du musst doch im Prinzip nur sicherstellen, dass der Stackpointer SP innerhalb eines bestimmten Intervalls liegt. Dazu baust du zwei Register in deine CPU ein: SP_min und SP_max, und bei jeder Stackoperation bzw. Zugriff über den Stackzeiger prüfst du, ob SP_min <= SP <= SP_max (+- Wortgröße je nach Design). Wenn diese Bedingung falsch ist, dann löst du eine Exception aus und das Programm wird aufgrund eines ungültigen Zugriffs terminiert.
Wenn du zusätzlich einen Framepointer hast (wie z. B. BP bei x86) dann behandelst du den genauso wie SP. Außerdem solltest du dir überlegen, ob du verhindern willst, dass der Wert des Stackpointer/Framepointers in ein anderes Register kopiert wird oder in den Speicher geschrieben wird. Sonst lässt sich der Mechanismus nämlich aushebeln, weil normale indirekte Speicherzugriffe ja nicht der oben genannten Prüfung unterlegen. Das ist natürlich eine massive Einschränkung und macht es z. B. unmöglich C vollständig auf die CPU zu portieren.
Beispiel: Wenn du es wie die meisten anderen CPUs machst, und SP wie ein normales Register behandelst, kann man einfach folgenden C-Code schreiben (trifft natürlich auf äquivalenten Code in beliebigen anderen Sprachen inkl. Assembler zu):
int *px;
void foo() {
int x;
px = &x; // Zeiger auf x
*px = 12; // Setze x auf 12. Gültig, weil x existiert.
}
void bar() {
foo();
// Zwei fehlerhafte Stackzugriffe:
*px = 123; // Fehler: x existiert nicht mehr
px[456] = 789; // Möglicherweise Stackoverflow
}
Wie kann man also dafür sorgen, dass das korrekt erkannt wird? Eine Möglichkeit wäre jedes Wort im Speicher mit einem speziellen Bit auszustatten, auf das von Programmen normalerweise nicht zugegriffen werden werden kann. Das heißt wenn du zum Beispiel eine Architektur mit 16-Bit-Worten hast, musst du RAM verwenden, der 17-Bit-Worte unterstützt. 16-Bit von diesem 17-Bit-Wort werden ganz normal vom Programm gelesen/beschrieben. Das eine zusätzliche Bit wird hingegen verwendet, um den Ursprung des Wertes zu kennzeichnen.
Bei jedem schreibenden Speicherzugriff wird dieses Bit gesetzt, wenn der Wert eine gültige Stackadresse ist. Das Bit wird gelöscht, wenn es "normale Daten" (Zahlenwerte oder Zeiger auf was anderes als den Stack) sind. Die CPU-Architektur muss intern auch zu jedem Register ein zusätzliches Bit speichern, das genau besagt, ob es eine Stackadresse ist oder nicht. Wenn du also aus dem Speicher etwas liest, wird das 17. Bit aus dem RAM in das zu dem Register gehörende Bit kopiert. Jede Operation (Indirekter Speicherzugriff, Arithmetik, ...) verhält sich nun unterschiedlich jenachdem, ob in einem Register oder in einem Wort im RAM eine Stackadresse steht. Zum Beispiel muss das Ergebnis einer Addition einer Konstante und einem Register, das als Stackadresse markiert ist, weiterhin eine gültige Stackadresse sein. Oder wenn zum Beispiel zufällig in einem Register ein Wert steht, der einer Stackadresse entspricht, aber das Register nicht als Stackadresse gekennzeichnet ist, musst du eine bei einem indirekten Speicherzugriff über dieses Register eine Exception auslösen.
Wenn man das etwas ausweitet, lässt sich vermutlich der fehlerhafte Zugriff auf die nicht mehr existierende Variable x durch
*px = 123; auch erkennen. Man könnte z. B. bei Speicherzugriffen überprüfen, ob die Adresse zwischen SP und SP_max liegt (wenn der Stack nach unten wächst). Aber wenn der alte Speicherplatz von x inzwischen von einer andere Variablen verwendet wird, lässt sich das nicht mehr erkennen (aber das ist ja dann kein Stack Overflow mehr, sondern ein anderer Fehler).
Diesen Mechanismus mit dem speziellen Bit kannst du weiter ausbauen, um z. B. ein
Sicherheitskonzept basierend auf Capabilities umzusetzen.
Ich halte das hier beschriebene allerdings nicht für sinnvoll. Stack Overflows muss man nicht so präzise erkennen.