Adressierung von Speichersegmenten / Sudo Null IT News

Das gebräuchlichste Speicheradressierungsmodell ist flach, wobei jedes Speicherelement eine globale Adresse hat. Dies ist jedoch nicht die einzige Möglichkeit, mit Speicher zu arbeiten. In diesem Artikel möchte ich eine der Alternativen betrachten – die Segmentadressierung. Es werden mehrere historische Systeme betrachtet, die diesen Ansatz implementieren, die Vorteile der Segmentadressierung in Bezug auf Skalierung und Sicherheit und Hypothesen über die Gründe, warum sie sich nicht durchgesetzt hat (Spoiler: Ich werde die Sprache C und das Betriebssystem Unix schelten).

Bei den meisten Computersystemen müssen Sie, um mit einer bestimmten Speicherzelle arbeiten zu können, irgendwie ihre Adresse angeben, normalerweise eine 16-, 32- oder 64-Bit-Zahl. Die Anzahl der Bits in einer Adresse wird oft als Bittiefe des Systems bezeichnet. Häufig wird ein “Seitenübersetzungs”-Mechanismus verwendet, der Bereiche des virtuellen Speichers der Benutzeranwendung auf vom Betriebssystem verwalteten physischen Speicher abbildet. Es ist jedoch immer nur eine “Seitentabelle” aktiv, und aus Sicht der Anwendung (und in vielerlei Hinsicht aus Sicht des Betriebssystemkerns) bleibt der Speicher erhalten eben.

Betrachten Sie einen alten Intel 86/88/186. Die Registergröße dieser Prozessoren beträgt nur 16 Bit, wodurch nur 64 Kilobyte Speicher adressiert werden können. Als diese Chips entwickelt wurden, reichte diese Speichergröße für viele Anwendungen nicht mehr aus und 32-Bit-Prozessoren waren zu teuer. Das Problem wurde gelöst, indem der Architektur Segmentregister hinzugefügt wurden. Beim Zugriff auf den Speicher an einer 16-Bit-Adresse (gespeichert in einem Mehrzweckregister oder direkt im Befehlscode) wurde der Wert des Segmentregisters addiert, um 4 Bit verschoben (was dasselbe ist, multipliziert mit 16) und der Der resultierende Wert wurde als physikalische Adresse verwendet. Dieser Ansatz ermöglichte es, bis zu einem Megabyte Speicher zu adressieren. In der auf diesen Prozessoren basierenden Architektur der IBM PC-Personalcomputer war ein Teil des Adressraums für Systemanforderungen reserviert, und bis zu 640 Kilobyte standen Benutzeranwendungen und dem Betriebssystem zur Verfügung. Aber nicht alles ist so einfach.

Zu dieser Zeit hatte sich die Sprache C im Bereich der Softwareentwicklung sehr gut bewährt, was es ermöglichte, sehr effektive, aber durchaus portierbare Programme zwischen verschiedenen Plattformen zu schreiben. Eines der Hauptmerkmale dieser Sprache war die Adressarithmetik. Ihr war es zu verdanken, dass es möglich war, Code zu schreiben, der Hardware effektiv nutzt, sich aber gleichzeitig nicht auf Assembler beschränkte.

Wie kann die C-Sprache auf i86 implementiert werden? Was ist die Größe des Zeigers auf eine Speicherzelle zu wählen? Die erste Option ist die Verwendung einer 16-Bit-Adresse. Die Prozessorarchitektur unterstützte 4 Segmentregister – CS, DS, ES und SS. CS enthält immer ein Codesegment. Die C-Sprache stellt kein Standardmittel zum spontanen Modifizieren von Code zur Verfügung, wodurch der Compiler und das Betriebssystem mehr oder weniger frei damit umgehen können. Wenn der gesamte Code in 64 KB passt, ist alles in Ordnung, wenn nicht, ist der Compiler gezwungen, teurere Aufrufe und Sprünge zwischen den Segmenten zu verwenden, aber dies ist ein relativ geringer Preis. Zeiger auf Funktionen, die vom C-Standard gefordert werden, stellen eine gewisse Komplexität dar. Die Schnittstelle von Funktionen, die innerhalb desselben Segments aufgerufen werden und Aufrufe zwischen Segmenten verwenden, war anders – Aufrufe zwischen Segmenten speicherten zusätzlich den alten CS-Wert auf dem Stack, und dies war nicht der Fall für Ortsgespräche erforderlich. Außerdem war die Größe der Adresse der Funktion unterschiedlich – 16-Bit reichte für Intrasegment, Intersegment sollte das Segment und den Offset in diesem Segment speichern. (Der Compiler könnte Funktionen an Adressen suchen, die ein Vielfaches von 16 sind, und sich nur auf den Wert des Segments beziehen, aber ich habe keine Implementierung in C oder einer anderen Sprache gesehen, die das tun könnte.)

Das DS-Register ist für das Datensegment bestimmt – es wurde standardmäßig von fast allen Speicherbefehlen verwendet (mit Ausnahme einiger Zeichenfolgenbefehle, die ES verwendeten, und Stack-Zugriffen, die später besprochen werden). Wenn also 64 KB für Daten ausreichen, können Sie alle in einem Segment speichern und einen 16-Bit-Zeiger verwenden.

Die meisten Compiler (Aber nicht alles) wird für jeden Funktionsaufruf ein Stack-Frame erstellt, ein Bereich des Stacks, der die Rücksprungadresse, Argumente und lokale Variablen enthält. Der i86 verwendet das Segmentregister SS für den Stack. Push/Pop-Operationen, Subroutinenaufrufe und Exit benutzten implizit das SP (Stapelzeiger)-Register. Es gab keine Anweisungen in i86, die explizit SP zur Adressierung verwendeten, also kopierte der Compiler am Anfang der Subroutine den Wert von SP in das BP-Register (Basiszeiger) und verwendete ihn, um mit Argumenten und lokalen Variablen zu arbeiten. Bei der Adressierung über BP wurde standardmäßig das Stack-Segment verwendet. In i386 wurde es möglich, SP explizit für den Speicherzugriff zu verwenden, in diesem Fall wurde auch das Stapelsegment verwendet. So ermöglichte die Architektur die unabhängige Nutzung von 64 KB Daten und 64 KB Stack sowie Multitasking mit dem “Shared Hip, Private Stacks”-Modell, aber …

Aber die C-Sprache erfordert die Fähigkeit, einen Zeiger auf ein beliebiges Objekt zu erhalten, global, das im Datensegment oder auf dem Stapel allokiert ist, einschließlich lokaler Variablen und Funktionsargumente. Wenn wir also einen 16-Bit-Zeiger verwenden wollen, müssen wir die Daten verketten und Segmente stapeln.

i86-C-Compiler unterstützten mehrere Gedächtnismodelle, von denen jede ihre Nachteile und Einschränkungen hatte. Darüber hinaus wurden unabhängig vom Modell mehrere zusätzliche Zeigertypen unterstützt – weit und riesig, die das Segment und den Offset speicherten. Der Unterschied lag in der Unterstützung der Adressarithmetik. Weit davon ausgegangen, dass alle darüber adressierten Daten im selben Segment liegen. Riesig unterstützte vollwertige Adressarithmetik, für die der Compiler zusätzlichen Code generierte, der auf Segmentgrenzenüberschreitungen prüfte und den Segmentwert bei Bedarf neu berechnete.

Somit gingen die Hauptvorteile der C-Sprache – Effizienz und Portabilität – auf x86 verloren.

Die x86-Architektur verwendete explizit eine 20-Bit-Speicheradressierung, und es war nicht einfach, sie zu erweitern, ohne die Kompatibilität mit vorhandenen Anwendungen zu verlieren. Allerdings hinein i286 Eine interessante Lösung wurde implementiert. Das Segment wurde nun nicht mehr als Basisadresse, sondern als Index in der Tabelle verwendet; Typ, Berechtigungen, Segmentgröße und Adresse im physischen Speicher. Der Segmentselektorwert wurde in drei Felder unterteilt – einen 13-Bit-Index in der Segmenttabelle, ein 1-Bit-Ortsflag und 2 Bits für die Rechteverwaltung. Je nach Ortszeichen wurde die Segmentbeschreibung aus der globalen oder lokalen Tabelle der Segmentdeskriptoren für die Aufgabe entnommen. Der Aufgabendeskriptor selbst war ein Segmentdeskriptor eines speziellen Typs. Somit könnte das Betriebssystem bis zu 2^13 Tasks erstellen, von denen jeder 2^13 Segmente mit jeweils 64 KB besitzen würde, was bis zu 4096 Terabyte virtuellen Speichers entspricht. Die Tabellenstruktur unterstützte jedoch nur die physische 24-Bit-Adresse des Anfangs des Segments, und es konnten nur 16 Megabyte physischer Speicher verwendet werden. Neben der Erhöhung des verfügbaren Speichers wurde ein Speicherschutz implementiert, der es ermöglichte, recht ernsthafte Betriebssysteme auszuführen – Unix (Xenix), Windows 3.1, OS / 2.

Auf Kosten dessen wurden die Vorgänge zum Laden in Segmentregister teuer – Sie mussten für eine Beschreibung des Segments in den Speicher gehen und die Verfügbarkeit des Segments für die Aufgabe überprüfen. Das Problem hätte durch einen Cache von Segmentdeskriptoren oder durch Umbenennen von Registern behoben werden können, aber die Intel-Ingenieure haben sich nicht für eine solche Komplikation entschieden.

Die Begrenzung auf 64-Kilobyte-Segmente wurde beibehalten, außerdem würde die Implementierung riesiger Zeiger eine komplexe Unterstützung im Betriebssystem erfordern. Das heißt, die meisten Goodies der neuen Architektur standen C-Programmierern nicht zur Verfügung, und es war bereits die beliebteste Sprache.

Der wirkliche Durchbruch in der x86-Architektur kam mit dem Aufkommen des i386-Prozessors. Der Prozessor wurde 32-Bit, 32-Bit und wurde zu Allzweckregistern und Segmentgrößen. Die Menge an verfügbarem RAM hat sich auch zusammen mit der Bitzahl der Adresse des Anfangs des Segments erhöht. Außerdem erschienen zwei weitere Segmentregister – FS und GS, die die Verluste bei Segmentladevorgängen leicht reduziert haben sollten. Alte Programme konnten jedoch keine 32-Bit-Adressierung verwenden.

Zusätzlich zum 32-Bit-Modus führte i386 die Seitenübersetzung ein. Dies ermöglichte den Betrieb moderner Systeme – Unix und Windows NT. 4 Gigabyte adressierbarer Speicher reichen für jede Anwendung, und die Verwendung von Segmenten war nicht mehr erforderlich, alles kann in einem Segment untergebracht werden. Die Unix-Architektur wurde für den flachen Speicher der Sprache C entwickelt (einschließlich Code und Daten, um die Bootloader-Implementierung zu vereinfachen), und dieser Ansatz wurde schnell von anderen Betriebssystemen übernommen.

In der amd64-Architektur ist die Verwendung von Segmenten stark eingeschränkt. Die Register CS, DS, ES und SS zeigen immer auf den gesamten virtuellen Speicher einer Task. Es sind nur FS und GS verfügbar, die einige Betriebssysteme verwenden, um lokale Thread-Daten zu speichern.

x86 ist eine bekannte, aber nicht die einzige segmentadressierende Architektur. Dieser Artikel wurde mit dem Ziel konzipiert, ein wenig darüber zu sprechen Plessey-System 250 und Intel iAPX432.

Der x86-Segmenttyp bestimmt, ob es Code oder Daten enthält. Die Daten enthielten sowohl gewöhnliche Daten als auch Zeiger und Segmentselektoren. Beim Versuch, es in das Segmentregister zu laden oder dorthin zu springen, wurden die Rechte am Segment geprüft. Und wenn wir weiter gehen und spezifizieren, welche Segmente Links zu anderen Segmenten enthalten können und welche nicht? Dies wurde beim Plessey System 250 (PP250) gemacht.

Das PP250 ist ein sehr altes System, das 1974 veröffentlicht wurde (zum Vergleich, der Intel 8086 begann 1978 mit der Produktion). Leider konnte ich keine Beschreibung seines Befehlssystems oder eines fertigen Emulators finden, daher beschreibe ich die Speicherarchitektur basierend auf den gefundenen Artikeln. Dieses System unterstützte mehrere Typen von Segmenten, je nach Typ enthielt das Segment Code, Daten oder Links zu anderen Segmenten. Da es dem Anwendungsprogramm nicht möglich war, die Segmentreferenz aus dem Datensegment zu erhalten, war es nicht erforderlich, den Segmentselektor und den Segmentdeskriptor zu trennen, wie dies beim Intel 286+ der Fall ist. Außerdem hatten Entwickler immer noch die Möglichkeit, das Format von Links zu einem Segment in verschiedenen Versionen des Systems zu ändern, ohne die Kompatibilität der Anwendungssoftware zu verlieren (z. B. durch Erhöhen des verfügbaren Speichers), aber soweit ich weiß, war dies der Fall nie umgesetzt.

Viele Programmiersprachen haben die Fähigkeit, benutzerdefinierte Datentypen zu erstellen, die mehrere Objekte anderer, sowohl integrierter als auch benutzerdefinierter Typen, einschließlich Referenztypen, kombinieren. In PP250 kann ein solches Objekt nicht in einem Segment platziert werden, da das Segment entweder Links oder Daten enthalten könnte. Dieses Problem wurde im Intel iAXP 432 behoben.

Intel iAXP 432 mit Betriebssystem iMAX432 begann 1975 mit der Entwicklung und wurde 1981 veröffentlicht. Die iAXP 432-Architektur hatte einen erheblichen Einfluss auf die Speicherorganisation im Intel 286, der ein Jahr später veröffentlicht wurde. Die Hauptsprache für dieses System war die Sprache Ada, aber auch andere Sprachen sollten unterstützt werden, insbesondere Lisp. Einer der Vorteile solcher Architekturen ist, dass Zeiger und Daten unterscheidbar sind, was die Implementierung der wichtigen Garbage Collection von Lisp vereinfacht.

Das Segment iAPX 432 bestand aus zwei Teilen, einem Verbindungsbereich und einem Datenbereich. Daher gab es auf Befehlssatzebene keine Möglichkeit, zu versuchen, die Daten in einen Zeiger umzuwandeln (im PP250 konnte ein Versuch, eine Referenz aus einem Datensegment zu laden, nur zur Laufzeit verfolgt werden).

Interessanterweise wurde die Trennung von Zeigern und Daten auch in traditionellen Architekturen gefunden, die für flachen Speicher ausgelegt sind. Zum Beispiel in einem Prozessor Motorola 68000 Es gab zwei Registerdateien – Datenregister und Adressregister. Aus Sicht des Hardwaredesigners vereinfacht dies die Implementierung der Pipeline und spart Bits in den Befehlscodes. Richtig, aus Sicht der Compiler-Entwickler ist dies nur kompliziert Registerzuweisungsalgorithmen. Ein ähnliches Schema ist in einigen CPUs für eingebettete Systeme implementiert, wie z Schwarzflosse.

Obwohl die Segmentadressierung erhebliche Sicherheits- und Skalierbarkeitsvorteile hat, ist sie mit der C-Sprache und modernen Betriebssystemen schlecht kompatibel und hat keine nennenswerte Akzeptanz gefunden. Trotzdem ist es nützlich, sich damit vertraut zu machen, ich hoffe, dass es noch zum Einsatz kommt, zum Beispiel beim Entwerfen virtueller Maschinen auf der Grundlage von Bytecodes.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *