Memwatch: Modifikation kommt außer Mode

Schreiben Sie selber Programme für Atari ST und TT? Wenn ja, beachten Sie doch hoffentlich die Grundregeln der sauberen Programmierung. Dazu zählt halt das Übliche: nur dokumentierte Systemvariablen verwenden, keine direkten Bildschirmzugriffe usw. Man kennt das ja.

Als Programmierer sollte man jedoch auch darauf achten, insofern sauber zu arbeiten, als daß kein selbstmodifizierender Code verwendet wird. Bei Prozessoren, die wie der 68030 des TT einen internen Cache besitzen, sind andernfalls Fehler während der Programmausführung nicht auszuschließen. Probleme gibt es genau dann, wenn der Inhalt des Caches nicht mit dem des Hauptspeichers übereinstimmt und der Prozessor für die Bearbeitung des nächsten Befehls die Daten im Cache verwendet. Diese sind unter Umständen nicht mehr auf dem neuesten Stand, falls die korrespondierenden Bereiche im Hauptspeicher zwischenzeitlich modifiziert wurden. Ein Programmabsturz ist somit mehr als wahrscheinlich.

Die obige Situation kann eintreten, wenn sich ein Programm selbst modifiziert, ohne anschließend den Cache zu löschen. Liegt ein Teil des modifizierten Programmcodes direkt hinter der Stelle, auf die der Programmzähler zeigt, so befinden sich die inzwischen ungültigen Daten möglicherweise bereits im Cache und sorgen im weiteren Programmverlauf für einen Fehler. Wenn es sich wirklich nicht umgehen läßt, das laufende Programm zu modifizieren, dann muß nach einer solchen Manipulation unbedingt der Cache gelöscht werden.

Programme, die auf einem 68040-Prozessor laufen sollen, werden es übrigens besonders schwer haben. Der MC68040 kann seine beiden Caches (Daten- und Befehls-Cache) im sogenannten „Copy Back“-Modus einsetzen. In dieser Betriebsart werden neue Daten nicht sofort in den Speicher geschrieben, sondern nur bei Bedarf. Verändert sich ein Programm auf dem 68040 selbst, so hat dies zunächst lediglich Auswirkungen auf den Daten-Cache. Der Prozessor holt sich seine Befehle jedoch aus dem Befehls-Cache, der von selbstmodifizierenden Programmen nicht beeinflußt wird und somit noch den nicht modifizierten Code enthält...

An der Wurzel gepackt

Nun sollte man es sich aber nicht so einfach machen, bei Verwendung von selbstmodifizierendem Code lediglich den Cache zu invalidieren und es dabei bewenden zu lassen. Dies stellt schließlich nur eine Notlösung dar. Gerade durch das Vorhandensein des Caches erreicht der 68030 seine hohe Rechenleistung. Wird der Cache gelöscht, sinkt der Datendurchsatz, was natürlich nicht wünschenswert ist.

Programme, die sich selbst modifizieren, bringen noch weitere Nachteile mit sich. Was geschieht, wenn ein solches Programm in ein Eprom gebrannt wird? Klar, daß beim Brennen selber natürlich nichts weiter passiert. Allerdings darf man sich anschließend nicht wundem, daß man mit seinem Eprom wenig anfangen kann, denn hier läßt sich nun gar nichts mehr modifizieren. Nun dürfte man beim ST oder TT nicht allzu häufig das Verlangen verspüren, Programme in Eproms zu verewigen. Schließlich lassen sich über den ROM-Port lediglich 128 kByte ROM ansprechen, was nicht gerade viel ist, wenn man sich den Umfang heutiger Programme vor Augen führt. Aber gerade für kleinere residente Routinen wie RAM-Disks oder Gerätetreiber kann der Einsatz von Eproms durchaus sinnvoll sein.

Schließlich noch ein Blick in die Zukunft. Wer garantiert, daß es nicht einmal eine TOS-Version geben wird, die das TEXT-Segment einer Applikation gegen Schreibzugriffe schützt? Gerade im Hinblick auf die Tendenz, mehrere Programme parallel laufen zu lassen, erscheint ein solcher Schutzmechanismus durchaus sinnvoll. Ein fehlerhaftes Programm wäre dann nicht mehr so schnell in der Lage, ein anderes Programm zum Absturz zu bringen. Die Betriebssicherheit eines Multitasking-Systems ließe sich somit deutlich erhöhen.

Es gibt also gute Gründe, die dafür sprechen, grundsätzlich auf selbstmodifizierenden Code zu verzichten. In den weitaus meisten Fällen ist dies mit durchaus vertretbarem Aufwand möglich. Oft genügt es, einen kleinen Programm teil aus dem TEXT-Segment in das DATA- oder BSS-Segment zu kopieren und dort zu verändern. Hierzu später mehr.

Das TEXT-Segment schützen

Es hätte zwar etwas für sich, wenn das TT-TOS Schreibzugriffe auf den Programmcode einer Applikation verhindern würde, aber dies ist (noch) nicht der Fall. Zum jetzigen Zeitpunkt wäre ein solches Verhalten des TOS auch absolut nicht wünschenswert, denn es gibt eine ganze Reihe von Programmen, die ihr TEXT-Segment verändern. Programme, die für den Atari ST gedacht waren, brauchten sich schließlich nicht um solche Feinheiten zu kümmern.

Auch wenn TOS selbstmodifizierenden Code nicht verhindert, muß es nicht dabei bleiben, lediglich über die Frage zu spekulieren, inwiefern ein Programm sich selbst modifiziert. Die Hardware des TT bietet ideale Voraussetzungen, sich Gewißheit hierüber zu verschaffen. Mit seiner PMMU stellt der 68030-Prozessor alle Funktionen zur Verfügung, die benötigt werden, um einen Schreibschutz von TEXT-Segmenten zu realisieren.

Daß es die MMU prinzipiell erlaubt, einen Speicherbereich gegen Zugriffe zu schützen, wurde bereits in [1] verdeutlicht. Hier ging es jedoch um einen Schutz für den Bildschirmspeicher, der sich nicht nur auf Schreib-, sondern auch auf Lesezugriffe erstreckte. Für unseren Zweck brauchbar ist ein solches Verfahren nicht, da Lesezugriffe auf den Programmcode weiterhin zulässig sein sollen. Es stellt sich also die Frage, wie man es anstellen kann, ausschließlich Schreibzugriffe auf das TEXT-Segment zu unterbinden.

Wer schreibt, der bleibt?

Wirft man einen Blick auf den Aufbau der Seitendeskriptoren der 68030-MMU, so läßt sich bereits ablesen, welcher Weg gegangen werden kann. Das W-Bit erlaubt es, einzelne Speicherseiten gegen Änderungen zu schützen. Es kommt also darauf an, für jedes Programm, das auf dem TT gestartet wird, genau in den Deskriptoren das W-Bit zu setzen, die für die Adreßübersetzung des TEXT-Segments zuständig sind.

Eigentlich wäre es angebracht, Änderungen im Programmcode sowohl für User- als auch für den Supervisor-Zugriff zu unterbinden. Dies hätte auf dem TT jedoch fatale Folgen, die eng mit der ST-Kompatibilität Zusammenhängen. Durch einen Design-Fehler beim 68000 ist es bei diesem Prozessor erlaubt, Befehle des Typs MOVE SR,Dn im User-Modus zu verwenden. Eigentlich sollte es in dieser Betriebsart nicht gestattet sein, auf Daten zuzugreifen, die dem Supervisor Vorbehalten sind. Obiger Befehl erlaubt es nämlich, das System-Byte des Statusregisters auch im User-Modus auszulesen. Alle Prozessoren seit dem 68010 erlauben einen Zugriff auf den kompletten Prozessorstatus deshalb nur noch im Supervisor-Modus. Um die Prozessor-Flags (Condition Code Byte) anzusprechen, existieren neue Befehle wie MOVE CCR,Dn. Da diese beim 68000 jedoch nicht implementiert sind, ist man gezwungen, bei diesem Prozessor weiterhin das komplette Statusregister anzusprechen. Diese Operation führt auf den restlichen Motorola-Prozessoren jedoch zu einer Exception-Behandlung aufgrund einer Privilegverletzung.

CCR statt SR

Das TOS des TT löst dieses Problem, indem innerhalb der Exception-Routine Befehle wie MOVE SR,Dn in MOVE CCR,Dn modifiziert werden. Das TEXT-Segment eines Programms wird also vom Betriebssystem verändert! Auf diese Weise ist es möglich, Programme, die für einen 68000-Prozessor geschrieben wurden, ohne Änderungen auch auf einem 68030 ablaufen zu lassen. Die bei TOS gewählte Methode birgt den Vorteil, daß jeder Zugriff auf das System-Byte des Statusregisters nur ein einziges Mal zu einer Exception führt. Beim erneuten Durchlaufen des Codes ist das SR bereits durch CCR ersetzt, und es tritt nicht nochmals ein Fehler auf. (Wie es sich gehört, wird nach einer solchen Manipulation übrigens der Cache gelöscht.)

Wenn also TOS unter gewissen Bedingungen Eingriffe in das TEXT-Segment eines Programms vornimmt, müssen wir zulassen, daß der Programmcode aus dem Supervisor-Modus heraus verändert werden darf. Damit ist natürlich nicht mehr möglich, selbstmodifizierenden Code dann zu entlarven, wenn dieser im Supervisor-Modus abläuft. Die weitaus meisten Programme beschränken sich jedoch korrekterweise auf den User-Modus, so daß wir in der Regel durchaus feststellen können, ob ein Programm sich selbst modifiziert. Nur Programme, die ständig im Supervisor-Modus arbeiten, entgehen unserer Überprüfung.

Der SRP tritt in Aktion

Es stellt sich nun die Frage, wie man es in den Griff bekommen kann, einen Schreibschutz ausschließlich für den User-Modus zu realisieren. Hierzu bietet der 68030 zwei Möglichkeiten. Zum einen können die Deskriptortabellen so aufgebaut werden, daß eine Übersetzung in Abhängigkeit vom Zustand der Function Code Bits FC2-FC0 erfolgt [1]. (Diese Bits geben Aufschluß darüber, ob ein Befehls- oder Datenzugriff im User- oder Supervisor-Modus erfolgt.) Eine andere Lösung besteht darin, für User- und Supervisor-Zugriffe mit zwei unterschiedlichen Deskriptortabellen zu arbeiten. Genau dieses Verfahren wird vom Programm MEMWATCH eingesetzt.

Auf dem TT wird im Normalfall nicht unterschieden, ob auf eine Adresse im User- oder Supervisor-Modus zugegriffen wird. Daher kann sich TOS mit einer einzigen Deskriptortabelle begnügen, auf deren Beginn der CRP (CPU Root Pointer) zeigt. Die MMU stellt jedoch auch einen SRP (Supervisor Root Pointer) zur Verfügung. Soll im Supervisor-Modus eine andere Deskriptortabelle als im User-Betrieb verwendet werden, so zeigt der SRP auf den Start eben dieser Tabelle. Um dem Prozessor zu signalisieren, daß der Inhalt des SRP gültig ist, muß im TC-Register (Translation Control) lediglich des SRE-Bit (Supervisor Root Enable) gesetzt werden.

Da das Betriebssystem in der Lage sein soll, ohne Einschränkungen auf den Hauptspeicher zuzugreifen, ist es sinnvoll, an den Übersetzungsvorschriften für den Supervisor-Modus nichts zu ändern. Der SRP kann also (wie sonst der CRP) auf die standardmäßige Deskriptortabelle zeigen. Lediglich für den CRP muß eine neue Tabelle angelegt werden. In dieser wird dann von MEMWATCH je nach Bedarf vermerkt, ob eine Speicherseite gegen Schreibzugriffe geschützt ist.

Im Mittelpunkt steht MSHRINK

Eine Routine, die jedes auf dem TT gestartete Programm überwachen soll, muß sich in den GEMDOS-Vektor einklinken und in Aktion treten, sobald ein neues Programm gestartet wird. Diesen Zeitpunkt erkennt man am sichersten daran, daß hierzu die PEXEC-Routine des GEMDOS aufgerufen wird. Es liegt also nahe, auf einen PEXEC-Aufruf zu warten, um anschließend die MMU so zu konfigurieren, daß Schreibzugriffe auf den Code des gestarteten Programms verhindert werden. Das hört sich zwar einfach an, ist es aber leider nicht. Wird PEXEC auf gerufen, befindet sich das betreffende Programm noch gar nicht im Speicher. Man hat somit keine Informationen darüber, in welchem Bereich des Hauptspeichers das TEXT-Segment zu liegen kommen wird. Es ist zwar durchaus realisierbar, daß ein residentes Programm dafür sorgt, andere Programme zu laden und zu starten. Hierzu stellt das GEMDOS diverse PEXEC-Modi zur Verfügung. Dummerweise müßte man sich jedoch darum kümmern, das Programm zu relozieren, bei Bedarf den Speicher zu löschen und einen Teil der Basepage mit gültigen Daten zu füllen. Außerdem wären noch die Bits im Programm-Header zu analysieren, die ausschlaggebend dafür sind, ob ein Programm im ST- oder TT-RAM ausgeführt werden soll. Um diesen Aufwand zu vermeiden, greift MEM-WATCH zu einer alternativen Methode.

Wenn man davon ausgeht, daß selbst unsauber geschriebene Programme sich an gewisse Richtlinien halten müssen, um überhaupt korrekt zu laufen, liegt es nahe, sich nicht in PEXEC, sondern in MSHRINK einzuklinken. Diese Routine sollte von jedem Programm kurz nach dem Programmstart aufgerufen werden, um dem Betriebssystem den nicht benötigen Speicher zurückzugeben. Zum Zeitpunkt eines MSHRINK-Aufrufs befinden sich bereits alle für einen Schreibschutz des TEXT-Segments relevanten Daten im Speicher. Anhand dieser Angaben kann man Beginn und Länge des Programmcodes ermitteln. MEMWATCH besorgt sich hierzu den Pointer auf die Basepage des aktuellen Prozesses. Hierzu existiert seit TOS 1.02 („Blittertos“) die Systemvariable _run, deren Adresse über die _sysbase-Struktur erhalten werden kann und die einen Zeiger auf die Basepage der aktuellen Applikation darstellt. Die Basepage wiederum enthält Informationen über Start und Länge der einzelnen Programmsegmente.

Beschränkung auf das TEXT-Segment

Per MMU ist der Adreßraum beim TT in Speicherseiten mit einer Größe von 32 KByte aufgeteilt. Dieser Wert stellt somit die kleinste Einheit dar, die schreibgeschützt werden kann. Nun ist es natürlich nicht so, daß ein TEXT-Segment genau an einer Seitengrenze liegt und einen Umfang von 32 KByte hat. Ein Schreibschutz per MMU wird sich also in der Regel auch auf Bereiche vor und hinter dem Programmcode erstrecken. Hinter dem TEXT-Segment befindet sich beispielsweise das DATA-Segment, für das MEMWATCH keinen Schreibschutz einrichten soll. Tritt nun ein Busfehler auf, darf nicht gleich davon ausgegangen werden, daß das TEXT-Segment modifiziert wurde. Zunächst muß geprüft werden, ob die Adresse, auf die zugegriffen wurde, tatsächlich im Programmcode der aktiven Applikation liegt. Hierzu bedient sich MEMWATCH erneut der Basepage. Ein Vergleich der Zugriffsadresse mit Start und Länge des TEXT-Segments sorgt für Klarheit. Nur dann, wenn tatsächlich versucht wurde, auf eine Adresse innerhalb des Programms zu schreiben, wird ein Warnton ausgegeben. Andernfalls wird der Zugriff lediglich im Supervisor-Modus wiederholt. Was die Geschwindigkeit bei der Programmausführung betrifft, leidet diese je nach Lage des TEXT-Segments selbstverständlich darunter, daß auch Zugriffe auf benachbarte Speicherbereiche einen Busfehler auslösen. Die Exception-Behandlung braucht schließlich ihre Zeit, auch wenn ein Schreibzugriff erlaubt ist.

Das Ende vom Lied

Wie genau MEMWATCH vorgeht, um den Schreibschutz zu installieren, kann man dem kommentierten Assembler-Quelltext entnehmen. Es darf natürlich nicht vergessen werden, daß nach dem Beenden eines Programms die schreibgeschützten Speicherseiten wieder für Schreibzugriffe aus dem User-Modus zugänglich sein müssen. Um dies zu erreichen, genügt es, Aufrufe von PTERM0 und PTERM zu überwachen. Eine dieser GEMDOS-Funktionen muß jedes Programm aufrufen, um sich.ordnungsgemäß zu beenden. Somit bietet es sich an, während eines Aufrufs obiger Routinen die nötigen MMU-Manipulationen vorzunehmen, um den Schreibschutz zu entfernen. Programme, die sich resident im Speicher verankern, beenden sich mittels PTERMRES. Hier greift MEMWATCH nicht ein, so daß die TEXT-Segmente residenter Routinen weiterhin gegen Überschreiben geschützt bleiben.

Die Bedeutung des VBR

Alle Programme, die sich wie MEMWATCH in ihrer Funktion darauf stützen, daß der Busfehler-Vektor zur Realisierung von MMU-Manipulationen geändert wird, haben mit einem nicht unerheblichen Problem zu kämpfen. Es darf auf keinen Fall Vorkommen, daß ein anderes Programm, das nicht mit der Programmierung der MMU vertraut ist, diesen Vektor in seinem Sinne umbiegt. So gibt es einige Programme, die beim Auftreten einer Exception versuchen, diese eigenständig zu behandeln. Solche Programme geben dann nicht einfach zwei Bomben auf dem Bildschirm aus, wie es TOS tut, sondern warten mit einer Fehlermeldung auf. Nun wäre es fatal, wenn MEMWATCH beim Auftreten eines Busfehlers nichtmehr dazu käme, die Ursache für eine Busfehler-Exception zu überprüfen. Wurde diese nämlich durch einen Schreibzugriff auf das TEXT-Segment einer Applikation verursacht, muß sichergestellt sein, daß das Hauptprogramm nach der Ausgabe des Warntons weitergeführt wird. MEMWATCH muß aus diesem Grund also unbedingt das erste Programm sein, das nach einem Busfehler aufgerufen wird.

Dies läßt sich dadurch sicherstellen, daß man das VBR (Vector-Base-Register) in geeigneter Weise einsetzt. Dieses Register existiert bei den Prozessoren der 68000-Familie erst seit dem 68010. Das VBR definiert, an welcher Adresse sich die Tabelle mit den Exception-Vektoren befindet. Beim 68000-Prozessor liegen diese Vektoren stets am Beginn des Adreßraums, also ab Adresse 0. Alle Programme, die nicht an neuere Prozessoren angepaßt sind (also auch nicht in der Lage sind, das VBR selber zu manipulieren), gehen davon aus, daß die Exception-Vektoren ab Adresse 0 angelegt sind. Das TOS des TT sieht die Situation ähnlich. (Dies zeigt eine unzureichende Anpassung des TOS an den 68030.) Werden Vektoren über den SETEXEC- Aufruf des BIOS geändert, spricht TOS unabhängig vom Inhalt des VBR stets Adressen am Beginn des Hauptspeichers an. Obwohl dieses Verhalten von TOS nicht unbedingt korrekt ist, ist es für unseren Zweck überaus praktisch.

Hinters Licht geführt

MEMWATCH richtet nun die Vektortabelle anderswo im Speicher ein. Programme, die die Exception-Vektoren an der üblichen Stelle verändern, bewirken damit überhaupt nichts mehr. Die alten Vektoren werden schließlich gar nicht mehr verwendet, so daß deren Inhalt zunächst keine Rolle spielt. Tritt nun ein Busfehler auf, wird über den neuen Busfehler-Vektor stets die Exception-Behandlung von MEMWATCH aufgerufen. Auch der GEMDOS-Vektor wird auf diese Weise verlegt* so daß bei GEMDOS-Aufrufen ebenfalls zuerst in MEMWATCH eingesprungen wird. Da für das Programm lediglich zwei Vektoren von Bedeutung sind, genügt es, die Exception-Behandlung so einzurichten, daß beim Auftreten einer Exception, die nicht von MEMWATCH verwaltet wird, die alten Vektoren ab Adresse 0 angesprungen werden. Dies läßt sich mit minimalem Aufwand dadurch verwirklichen, daß man den Vektor-Offset, den der 68030 bei jeder Exception auf dem Stack ablegt, entsprechend auswertet. Mit einer einzigen Routine ist es so möglich, für alle Vektoren eine einheitliche Exception-Behandlung zu gewährleisten.

Geschwindigkeitsgewinn

Bleiben wir noch ein wenig beim VBR. Läuft MEMWATCH im TT-RAM, liegt selbstverständlich auch die neu eingerichtete Vektortabelle im schnellen RAM. Da es dem Prozessor so möglich ist, schneller auf die Exception-Vektoren zuzugreifen, als wenn diese im ST-RAM lägen, kann man durch geeignete Manipulationen, des VBR eine leichte Beschleunigung des TT erzielen. Bei MEMWATCH kommt dieser Effekt natürlich kaum zum Tragen, da hier innerhalb der Exception-Bearbeitung eine Neuberechnung der Vektoradressen erfolgt. Bei anderen Anwendungen kann das aber durchaus anders aussehen. Wird die Tabelle der Exception-Vektoren per VBR verschoben, muß die neue Adresse unbedingt durch 16 teilbar sein. Die Vektortabelle muß also stets auf einer sogenannten Cacheline beginnen.

Unabhängig vom VBR hat es mit solchen Cachelines eine besondere Bewandtnis. Routinen, die auf einer Cacheline beginnen, werden vom 68030 schneller ausgeführt, als wenn sie sich an einer Adresse befinden, die sich nicht durch 16 teilen läßt. Dieser Effekt hängt damit zusammen, daß es der Burst-Modus des 68030 erlaubt, eine komplette Cacheline in kürzester Zeit in den internen Prozessor-Cache zu übertragen. Arbeitet man mit einem Assembler, der 68020/68030-Code erzeugen kann, steht in der Regel ein Befehl zur Verfügung, der ein „Alignment“ einer Routine erlaubt. Beim MAS handelt es sich um die align-Direktive. So besagt align16, daß der nächste Befehl so assembliert wird, daß er auf einer Cacheline zu liegen kommt.

Ataris Versäumnis

Es läßt sich zeigen, daß Programme, die häufig benötigte Routinen auf einer Cacheline plazieren, um bis zu 8% schneller ablaufen, als wenn diese Routinen lediglich auf einer Wortgrenze liegen. Es empfiehlt sich also, insbesondere Unterprogramme, die periodisch (z.B. während des VBL) aufgerufen werden, auf eine Cacheline zu legen. Nun hört sich das zunächst ganz einfach an, denn man kann dazu die entsprechenden align-Befehle einsetzen. aber die Sache hat leider einen Haken, der mit der Struktur der bisherigen TOS-Versionen zusammenhängt. Zwar kann ein Assembler dafür sorgen, daß ein Befehl relativ zum Programmbeginn an einer ganz bestimmten Adresse zu liegen kommt, aber damit das Alignment auch wirklich funktioniert, muß der Beginn eines Programms unbedingt auf einer Cacheline liegen. TOS gewährleistet dies jedoch nicht. Ein Programm wird beim Start mittels PEXEC lediglich an eine gerade Adresse geladen. Ob es sich dabei um eine Cacheline handelt, bleibt dem Zufall überlassen. Meist ist dies nicht der Fall. Somit lassen sich Befehle zum Alignment von Programmcode bisher nicht sinnvoll einsetzen. Dennoch sollte man diese Möglichkeit in eigenen Programmen berücksichtigen. Sollte es einmal eine TOS-Version geben, die den Beginn eines Programms auf eine Cacheline plaziert, so profitiert man von der erhöhten Ausführungsgeschwindigkeit. Man kann nur hoffen, daß Atari diese Problematik überdenkt und im Sinne des Anwenders handelt.

Übeltäter en masse

Nach diesem kurzen Exkurs nun aber zurück zu MEMWATCH. Sie sind jetzt in der Lage, sich ein Bild darüber zu machen, wie sauber die von Ihnen genutzte Software programmiert ist. Im großen und ganzen zeigt sich, daß eine ganze Reihe an Programmen für ST und TT mit selbstmodifizierendem Code arbeitet. In den meisten Fällen zeigt MEMW ATCH eine Codemanipulation jedoch nur kurz nach einem Programmstart an. Hier modifiziert sich ein Programm lediglich in seiner Initialisierungsphase. Es gibt jedoch auch Programme, die ihren Code ständig manipulieren. Dabei handelt es sich insbesondere um Software, die bereits an anderer Stelle [1] unangenehm aufgefallen ist. Zu wundern braucht man sich über diese Feststellung wohl nicht.

XBRA-Dilemma

Nun mag sich der eine oder andere Programmierer gar nicht darüber im klaren sein, daß er in seinen Programmen selbstmodifizierenden Code verwendet. So verleitet beispielsweise der Einsatz des XBRA-Verfahrens [2] dazu, entsprechende Routinen zu verwenden. Häufig wird die XBRA-Struktur direkt vor dem Einsprungpunkt für den umgebogenen Vektor errichtet und befindet sich somit im TEXT-Segment eines Programms. Das muß jedoch nicht zwangsweise so sein.

Zur Verdeutlichung noch einmal kurz die Grundlagen des XBRA-Systems. Residente Programme, die einen Systemvektor verbiegen, überschreiben diesen Vektor nicht einfach mit einem neuen Inhalt, sondern merken sich den alten Vektor anhand einer XBRA-Struktur, die den folgenden Aufbau hat:

typedef struct 
{
    char xb_magic[4 ];	 /* XBRA-Kennung "XBRA" */
    char xb_id[4];	     /* ID des residenten Programms */ 
    long xb_oldvec;	     /* ursprünglicher Vektorinhalt */
}

Das XBRA-Protokoll ermöglicht es residenten Programmen unter anderem, sich zu deinstallieren, ohne dabei andere Programme, die die gleichen Vektoren verbiegen, in ihrer Funktion einzuschränken. Darüber hinaus kann ein Programm anhand seiner eigenen XBRA-Struktur erkennen, ob es bereits installiert ist. Mehrfachinstallationen lassen sich so leicht verhindern.

Der XBRA-Datenblock muß sich direkt vor der Routine befinden, auf die der umgebogene Vektor zeigt. Da sich der Programmcode im TEXT-Segment befindet, hat dies zur Folge, daß sich die XBRA-Struktur oft ebenfalls in diesem Segment wiederfindet. Bei der Programminitialisierung wird hier dann der alte Vektorinhalt eingetragen - und schon ist es passiert: das TEXT-Segment wurde modifiziert.

XBRA im DATA-Segment

Damit es beim Einrichten der XBRA-Struktur nicht notwendig ist, das TEXT-Segment eines Programms zu beeinflussen, muß diese Struktur anderswo angelegt werden, beispielsweise im DATA-Segment. Schauen wir uns an, wie man hierzu vorgehen kann.

Da der Programmcode, den der verbogene Vektor anspringt, direkt hinter der XBRA-Struktur hegen muß, bietet es sich an, an dieser Stelle keinen kompletten Programmteil, sondern lediglich einen Sprungbefehl auf die eigentliche Routine abzulegen. Dies würde einer erweiterten Struktur entsprechen, die im DATA-Segment eingerichtet werden kann:

typedef struct 
{
    char xb_magic[4];   /* XBRA-Kennung "XBRA" */
    char xb_id[4];      /* ID des residenten Programms */
    long xb_oldvec;     /* ursprünglicher Vektorinhalt */
    int  xb_jmp;        /* $4EF9 als JMP-Opcode */
    long xb_newvec;     /* neuer Vektor */
}

Bei der Initialisierung dieser Daten muß ein Programm lediglich das Langwort xb_newvec durch die Adresse jener Routine ersetzen, die über den geänderten Vektor angesprungen werden soll. Diese Routine befindet sich logischerweise im TEXT-Segment.

Wie man sieht, läßt sich mit wenig Aufwand dafür sorgen, daß die Verwendung des XBRA-Protokolls nicht in selbstmodifizierendem Code endet. Auch in anderen Fällen kann man mit durchdachten Routinen auf solchen Code verzichten und so dem Ziel der sauberen Programmierung ein weiteres Stück näherkommen.

PMMU-Handler

Abschließend möchte ich noch auf ein System hinweisen, das Kollisionen vermeiden soll, falls mehrere Programme gestartet werden, die die 68030-MMU nutzen. In den allermeisten Fällen vertragen sich solche Programme nicht miteinander, da sich die MMU nicht ohne weiteres für mehrere Aufgaben gleichzeitig einsetzen läßt. Aus diesem Grund wird eine möglichst einheitliche Methode benötigt, die eine Überprüfung der MMU-Aktivität erlaubt. Einige Programme für den TT nutzen den cookie-jar [4], um bei dieser Problematik Abhilfe zu schaffen.

Programme, die die MMU programmieren, sollten einen cookie mit der Kennung PMMU einrichten. Ist der zugehörige cookie-Parameter kein Null-Pointer, zeigt er auf die Adresse eines Handlers, der über definierte Aufrufe gewisse MMU-Manipulationen erlaubt. In der nächsten Ausgabe der ST-Computer wird diese Thematik voraussichtlich ausführlich behandelt. An dieser Stelle soll lediglich der Hinweis stehen, daß die bloße Anwesenheit des PMMU-cookies signalisiert, daß die MMU bereits genutzt wird. So haben Programme die Möglichkeit, sich nicht zu installieren, falls dieser cookie vorhanden ist. Selbstverständlich kann ein Programm neben dem MMU-cookie einen weiteren, programmspezifischen cookie einrichten. Schließlich ist es nicht untersagt, mehr als einen cookie pro Programm zu verwenden. Es existiert bereits Software, die den PMMU-cookie einrichtet bzw. auswertet.

Dazu zählen neben MEMWATCH beispielsweise die virtuelle Speicherverwaltung OUTSIDE ab Version 1.03, ROMSPEED V1.2, ROMRAM V1.1J, VRAM sowie neue SYS_MON-Versionen.

Literatur:

[1] „Screenwatch - Direkter Bildschirmzugriff -nein danke", ST-Computer 7,8/91
[2] Jankowski, Reschke, Rabich „ATARI ST Profibuch", SYBEX-Verlag
[3] Steve Williams, „68030 Assembly Language Reference", Addison-Wesley Publishing Inc.
[4] Rolf Kotzian, „Das cookie-Jar-Prinzip", ST-Computer 12/90

*****************************
*                           *
* MEMWATCH V1.0             *
*                           *
* Schreibschutz für das     *
* TEXT-Segment auf dem TT   *
*                           *
* (c) 1991 MAXON Computer   *
* by Uwe Seimet             *
*                           *
*****************************

GEMDOS  = 1
CCONWS  = 9
SUPER   = 32
PTERMRES= 49 
MSHRINK = 74 
PTERM   = 76

BIOS    = 13
BCONOUT = 3

_sysbase = $4f2 
_p_cookies = $5a0

        text

        move.l 4(sp),a6 ;Pointer auf Basepage

        lea     stack+400,sp
        move.l  12(a6),a1   ;Länge des
                            ;TEXT-Segments 
        add.l   20(a6),a1   ;DATA-Segment 
        add.l   28(a6),a1   ;BSS-Segment
        lea     $100(a1),a1 ;für Basepage 
        move.l  a1,prglen   ;Programmlänge merken
        pea     (a1) 
        pea     (a6) 
        clr     -(sp)
        move    #MSHRINK,-(sp) 
        trap    #GEMDOS     ;Restspeicher freigeben
        lea     12(sp),sp
        tst.l   d0          ;alles klar?
        bne     quit        ;leider nicht-

        clr.l   -(sp)
        move    #SUPER,-(sp);Supervisor
        trap    #GEMDOS     ;Modus
        addq.l  #6,sp
        move.l  d0,d7       ;SSP merken

        lea     sterr,a5
        move.l  _p_cookies,d0 ;kein 
        beq     error       ;cookie jar-
        move.l  d0,a0 
testjar:movem.l (a0)+,d0-d1 
        tst.l   d0 
        beq     endjar
        cmp.l   #"_MCH",d0  ;Computertyp? 
        bne     nomch       ;nein-
        swap    d1
        subq    #2,d1       ;TT?
        bne     error       ;nein-
nomch:  cmp.l   #"PMMU",d0  ;MMU-Programm aktiv? 
        bne     testjar     ;nein-
        lea     mmuerr,a5 
        bra     error

endjar:move.l   #"PMMU",-8(a0) ;kein
        clr.l   -4(a0)      ;PMMU-Handler
        movem.l d0-d1,(a0)

        move.l  #table+15,d6 ;Deskriptor-/Tabelle
        and     #$fff0,d6   ;auf Cacheline ausrichten 
        pmove   crp,crpreg  ;alter CRP 
        pmove   crpreg,srp 
        move.l  crpreg+4,a1 ;Pointer auf alte Tabelle
        move.l  a1,a2
        move.l  d6,crpreg+4 ;für neuen CRP

        move.l  d6,a0 
        moveq   #63,d0 
tcopy:  move.l  (a1)+,d1
        move.l  d1,d2
        and     #$02,d2     ;Tabellen-
        cmp     #$02,d2     ;Deskriptor?
        bne     notable     ;nein-
        sub.l   a2,d1 
        add.l   d6,d1 
notable:move.l  d1,(a0)+    ;Tabelle 
        dbra    d0,tcopy    ;kopieren

        move.l  a0,d1 
        or      #2,d1 
        moveq   #13,d0 
        move.l  d6,a1 
        lea     $c0(a1),a1 
cdes:   move.l  d1,(a1)+    ;Deskriptoren
        add.l   #128,d1     ;für ST-RAM
        dbra    d0,cdes

        moveq   #1,d1
        move    #511-64,d0  ;Seiten-

dloop0: move.l  d1,(a0)+    ;Deskriptoren 
        add.l   #$8000,d1   ;für ST-RAM 
        dbra    d0,dloop0

        move.l  a0,d2 
        move.l  a0,d1 
        add.l   #64,d1 
        or      #2,d1 
        moveq   #15,d0 
dloop1: move.l  d1,(a0)+
        add.l   #128,d1
        dbra    d0,dloop1

        move.l  #$01000001,d1 
        move    #511,d0     ;Seiten-
dloop2: move.l  d1,(a0)+    ;Deskriptoren
        add.l   #$3000,d1   ;für TT-RAM
        dbra    d0,dloop2 
        or      #2,d2

        move.l  d2,($44,d6.l)

        move.l  #vectors+15,d0 ;VBR auf 
        and     #$fff0,d0   ;Cacheline
        move.l  d0,a1       ;ausrichten
        move.l  d0,a2 
        movec   vbr,a0  
        move.l  $08(a0),o_bus 
        move.l  $84(a0),o_gemdos 
        move.w  #255,d0 
copyvec:move.l  #newvec,(a1)+ 
        dbra    d0,copyvec 
        move.l  #buserr,$08 
        move.l  #gemdos,$84 
        movec   a2,vbr

        lea     dummy,a0    ;keine
*       pmove   (a0),tt0    ;transparente Übersetzung 
        dc.l    $f0100800   ;PMOVE (A0) TT0
        pmove   crpreg,crp  ;neuer Rootpointer
        pmove   tc,tcreg
        bset    #1,tcreg    ;SRE setzen
        pmove   tcreg.tc

        move.l  d7,-(sp)
        move    #SUPER,-(sp);Rückkehr in 
        trap    #GEMDOS     ;User-Modus
        addq.l  #6,sp

        pea     message 
        move    #CCONWS,-(sp) 
        trap    #GEMDOS 
        addq.l  #6,sp 
        clr     -(sp)
        move.l  prglen,-(sp)
        move    #PTERMRES,-(sp) ;zurück
        trap    #GEMDOS     ;zum TOS

gemdos:
        lea     8(sp),a0    ;Supervisor-
        btst    #5,(sp)     ;Modus?
        beq     user        ;nein-
cont:   jmp     ([o_gemdos])
user:   move.l  usp,a0      ;sonst User-Stack
        tst     (a0)
        beq     term        ;PTERM0-
        cmp     #PTERM,(a0)
        beq     term        ;PTERM-
        cmp     #MSHRINK,(a0)
        bne     cont
        move.l  ([_sysbase],40), a0 
        move.l  (a0),a0     ;zeigt auf PD 
        move.l  12(a0),d0 
        ptestw  #2,([a0],8),#7,a0 
wrset:  bset    #2,3(a0)    ;W-Bit
        addq.l  #4,a0       ;setzen
        sub.l   #$8000,d0
        bcc     wrset       ;nächste Page-
        pflusha
        bra     cont

term:
        move.l  ([_sysbase],40),a0
        move.l  (a0),a0     ;zeigt auf PD
        move.l  12(a0),d0 
        ptestw  #2,([a0],8),#7,a0 
wrclr:  bclr    #2,3(a0)    ;Schreibschutz
        addq.l  #4,a0       ;entfernen
        sub.l   #$8000,d0
        bcc     wrclr       ;nächste Page-
        pflusha
        bra     cont


error:
        pea     (a5)        ;Fehlermeldung
        move    #CCONWS,-(sp) ;ausgeben 
        trap    #GEMDOS 
        addq.l  #6,sp
        pea     inserr      ;nicht
        move    #CCONWS,-(sp) ;installiert 
        trap    #GEMDOS 
        addq.l  #6,sp 
exit:   move.l  d7,-(sp)
        move    #SUPER,-(sp) ;zurück in 
        trap    #GEMDOS     ;User-Modus
        addq.l  #6,sp 
quit:   clr     -(sp)
        trap    #GEMDOS


        align 16

newvec:
        move.l  4(sp),-(sp) ;Vektoroffset 
        and.l   #$fff,(sp)  ;isolieren
        move.l  ([sp]),(sp) 
        rts


buserr:
        movem.l d0/a6,-(sp) 
        lea     8(sp),a6        ;Throwaway-
        cmp     #$1008,6(a6)    ;Stackframe?
        bne     is_isp          ;nein-
        movec   msp,a6 
is_isp: move.b  11(a6),d0
        movec   d0,dfc          ;Deskriptor
        ptestw  dfc,([16.a6]),#7 ;prüfen 
        subq.l  #2,sp 
        pmove   psr,(sp)
        btst    #3,(sp)+        ;W-Bit testen
        bne     wrterr
        movem.l (sp)+,d0/a6
        move.l  ([o_bus]),-(sp) ;weiter im TOS
        rts

wrterr:
        move.l  16(a6),d0
        move.l  ([_sysbase],40),a6
        move.l  (a6),a6         ;zeigt auf PD
        sub.l   8(a6),d0        ;oberhalb oder
        bcs     noerr           ;unterhalb des
        cmp.l   12(a6),d0       ;TEXT-
        bcc     noerr           ;Segments?
        movem.l a0-a2/d0-d2,-(sp)
        move    #7,-(sp)        ;BEL
        move    #2,-(sp)
        move    #BCONOUT,-(sp)  ;Warnton 
        trap    #BIOS           ;ausgeben
        addq.l  #6,sp
        movem.l (sp)+,a0-a2/d0-d2 
noerr:  movem.l (sp)+,d0/a6
        bset    #2,11(sp)       ;Befehl im
        rte                     ; Supervisor-Modus wiederholen


        data

message: dc.b $0d,$0a,"MEMWATCH V1.0 " 
         dc.b "installiert",$0d,$0a 
         dc.b "(c) 1991 by Uwe Seimet",$0d,$0a,0

*Diverse Fehlermeldungen

Uwe Seimet
Aus: ST-Computer 12 / 1991, Seite 152

Links

Copyright-Bestimmungen: siehe Über diese Seite