Fragen zu Systemprogrammieren



  • Hi,

    Ich lese gerade ein Buch zu Bestriebssystemen und Systemprogrammierung.
    Nun kommen mir langsam Fragen, die aber irgendwie auch mit Nachlesen nicht wirklich beantwortet wurden.
    Ich dachte das ist der richtige Teil des Forums, weil sich hier ja die richtigen Freaks rumtreiben, die wirklich wissen sollten wie, wo, wann, was passiert 🙂

    Bitte bessert mich aus wenn schon mein Grundverständnis vollkommen falsch ist.

    So wie ich das sehe wird jedem Programm bei der Ausführung eine gewisse Menge an Speicher zugeteilt, HEAP und STACK. Nun kann man ja aber zb in C++ selbst Speicher reservieren mit new().

    1. Frage: Wird dieser innerhalb vom HEAP reserviert, oder kommt da zum kompletten Speicher, über den der Prozess verfügt etwas hinzu? bzw kann der Prozess überhaupt zusätzlichen Speicher anfordern? Falls ja, wie kann es dann sein das Programmen der Speicher ausgeht, obwohl noch RAM frei wäre?

    Das nächste Verständnisproblem hab ich beim Binding. Da gehts es ja afaik darum die Programmadressen in Speicheradressen umzuwandeln. In meinem Buch steht, je später das passiert, also zb erst bei der Ausführung, desto aufwändiger ist es.

    2. Frage: Wie kann das Binding überhaupt vor der Ausführung passieren ? Woher soll man denn vor der Ausführung wissen welchen Adressraum der Prozess vom OS zugeteilt bekommt ?

    Ich hoffe ihr könnt mir die Fragen beantworten.

    Danke schon mal !



  • Der Operator new in C++ reserviert auf allen mir bekannten Implementierungen Speicher auf dem Heap - was nicht heissen muss, dass das ueberall so ist. Der C++-Standard kennt weder Heap noch Stack, da werden abstraktere Begriffe verwendet, bei denen jeder, der den Standard implementiert entscheiden muss was jetzt dynamischer Speicher ist. De facto kannst du aber auf Desktop OSs davon ausgehen, dass new Speicher auf dem Heap reserviert.
    Um dir jetzt zu erklaeren, wie man das intern machen koennte, muss ich dir erstmal die zweite Frage beantworten.

    Stichwort hier ist allgemein erstmal Virtual Memory. Du hast momentan noch ein falsches Verstaendnis davon, wie das Memory-Managment auf modernen Betriebssystemen funktioniert. Seit dem Intel 368 gibt es den 32 bit Protected Mode und seit dem auch das Paging. Die meisten heute modernen Systeme laufen mindestens im Protected Mode, wenn nicht schon im Long Mode (64 bit Protected Mode), wobei letzterer hauptsaechlich nur die Adress- und Registerbreite vergroessert. In meiner weiteren Erklaerung gehe ich mal vom 32PM aus.
    Der Grund, warum das OS schon vor Ausfuehrung der Anwendung festlegen kann, wo ihr Adressraum ist, liegt einfach darin, dass die Anwendung ihren eigenen Adressraum hat. Unter Windows werden Anwendungen standardmaessig an die Adresse 0x400000 (kann aber in den Linkeroptionen geandert werden) geladen. Da das aber in ihren eigenen 4GB geschieht, merken die Anwendungen das nicht. 0x400000 ist die virtuelle Adresse, wo die Anwendung im physischen Speicher genau liegt, weiss sie nicht. Das muss sie aber auch nicht, weil sie sich nur um ihren Adressraum kuemmern muss.
    OS- un Prozessorintern wird das folgendermassen gemanaget:
    Beim Taskswitch, also dem Prozesswechsel (Threads lasse ich hier erstmal aus, weil die das verkomplizieren), wird das Prozessor-Register CR3 aka Page Directory Base Address vom Betriebssystem mit einer physischen Adresse geladen. Hier liegt dann im physischen Speicher das Page-Directory. Das enthaelt 1024 eintraege mit physischen Adressen (und einigen Verwaltungsinformationen), die auf Page-Tables zeigen. Diese Page-Tables enthalten ihrerseits wieder 1024 eintraege mit physischen Adressen, welche angeben, wo sich die Page befindet. Im 32 bit Protected Mode ist eine Page standardmaessig 4 KB gross (laesst sich auf neueren Prozessoren auch aendern, aber der einfachheit halber gehen wir mal davon aus). Wenn du jetzt im Prozess auf eine Virtuelle Adresse zugreifst passiert folgendes:

    v. Adresse |--10 bit--|--10 bit--|---12 bit---|
                            31                                 0
                                    |            |        |
    CR3-phys->Pagedirectory         |            |        |
           |    0     |             |            |        |
           |    1     |             |            |        |
           |    2     |--+  <-Index-+            |        |
           |    3     |  |                       |        |
           |    ...   |  +--phys-->Page-Table    |        |
           |   1023   |       |    0     |       |        |
                              |    1     |-+<-I*-+        +---------+
                              |    ...   | |                        |
                              |    1023  | +--phys-->Page           |
                                                   +------+         |
                                                   |      |         |
                                                   | 4KB  |<-Offet--+
                                                   |      |
                                                   +------+
    *Index
    

    Wie ich bereits erwaehnt habe, tragen die Page-Directory und -Table Eintraege neben der Adresse auch noch Verwaltungsinformationen. Anahnd eines bestimmten Bits kann die CPU lesen, ob der Eintrag gueltig ist, d.h. wirklich eine Adresse enthaelt, oder ungueltig, weil auf die Festplatte ausgelagert oder noch nicht erzeugt. Sollte auf einen ungueltigen Page-Eintrag oder Table-Eintrag zugegriffen werden, schmeisst der Prozessor eine Exception ans OS. Das entscheidet dann, ob die Page eingelagert werden muss, weil sie vorher aus Speichernot ausgelagert, wurde, oder ob sie noch nicht benutzt wurde und einfach ein freies Stueck Speicher dafuer reserviert wird. Intern behaelt das OS uebersicht wo im physischen Speicher schon was ist und wo nicht, z.B. in dem man ausrechnet, wie gross der Speicher ist, dann diese Zahl (x) durch 4096 teilt (4KB) und dann ein Bitset mit x Bits anlegt. Nun kann man sagen: Bit gesetzt = Speicher belegt, bit nicht gesetzt = Speicher frei. Aber das ist bei jedem OS anders, so wird es z.B. von PrettyOS implementiert.

    Nun zurueck zum Heap. Im Prinzip kann man es so machen. Der Heap inkl. Verwaltungsinformationen wird von API-Funktionen verwaltet, die im Usermode laufen. Wenn nun etwas auf dem Heap reserviert werden soll, checken die API-Funktionen ob der bereits reservierte Speicher, der ja nur mit einer Granulaitaet von 4KB durch Pages reserviert werden kann, genug Platz fuer das zu erzeugende Objekt + Verwaltungsinfos bietet. Sollte das nicht der Fall sein, muessen neue Pages beim Betriebssystem angefordert werden. Dazu berechnet man zunaechst den benoetigten Speicher fuer das Objekt + Verwaltungsinfos, rundet ihn zur naechsten 4KB-Grenze auf, und fordert entsprechend viele zusammenhaengende Pages per Syscall an*. Dann kann man die eigentlichen Heaap-Arbeiten machen, auf die ich hier mal nicht weiter eingehen will, entsprechende Implementierungen kann man sicherlich im Internet finden.

    *Bedingt durch die Tatsache, dass das Betriebssystem auch einfach auf den PageFault des Prozessors warten koennte ist das kein muss, aber dennoch sehr unsauber. Besser ist es explizit Pages zu reservieren und dann darauf zuzugreifen. So kann das OS beispielsweise bei Pagefaults checken, ob die Page nur ausgelagert ist -> wieder einlagern, oder auf eine falsche Adresse zugegriffen wird -> Anwendung fehlerhaft, Prozess beenden (wobei man zugriffe auf die Adresse NULL gesondert behandeln sollte weil Programmierer ja uebermaessig gern darauf Zugreifen :p ).

    Ich hoffe ich konnte dir einen Ueberblick verschaffen. Solltest du Fragen haben melde dich einfach 🙂

    EDIT I: Inhaltl. Verbesserung.
    EDIT II: Wir haben auch ein Forum speziell fuer OS-Programmierung, lass dich ggf. mal dorthin schieben.



  • Zu 1:
    AFAIK implementierungsabhaengig (sowohl vom Compiler als auch OS). Oft reserviert sich ein Programm der Einfachheit halber beim Start eine gewisse Menge Speicher und verwaltet diesen als Heap selbst, damit nicht fuer jede popelige new-Anfrage von ein paar Byte das OS genervt werden muss. Ist der voll, kann aber natuerlich idR. auch vom OS weiterer Speicher angefordert werden. Das laeuft haeufig aehnlich wie beim guten alten "malloc" ab: "Ich haette gern x byte Speicher!" => "Ok, ab Adresse x darfst du deinen Kram ab jetzt auch abladen, ohne gekillt zu werden."
    Ab wann Speicheranforderungen nicht mehr erfuellt werden, ist wie gesagt stark implementierungsabhaengig, bzw. evtl. wurde die Menge Speicher fuer diesen Prozess begrenzt.

    Zu 2:
    Es gibt durchaus Faelle, wo das alles im Grossen und Ganzen schoen standardisiert war/ist. Eben mit dem Gedanken im Hinterkopf, dass es viel einfacher ist, schon beim Zusammenbauen eines Programms ueberall feste Adressen eintragen zu koennen, statt quasi jedes Mal beim Starten alle Adressen neu berechnen und ersetzen zu muessen, was im Wesentlichen einem neuen Linken gleichkommt.
    In DOS .com-Programmmen war das z.B. so geloest, dass Programme im Real Mode immer an eine feste Adresse eines Segments geladen wurden. In Windows starte(te)n Programme immer an Adresse 0x00400000 ... wobei hier eh eigentlich schon immer auch umgelinkt wird - siehe dlls.

    Edit: Zu spaet... also eine kleine Ergaenzung. 😉



  • Vielen lieben Dank euch für die wirklich ausführlichen Erklährungen.

    Wenn ich das richtig verstanden habe sind auch die Speicheradressen also eigentlich nur virtuelle Adressen. Wo diese dann wirklich im Speicher hinzeigen steht dann in den Page-Tables, die soetwas wie ein Mapping von virt. Speicheradressen zur realen. Speicheradressen sind.
    Jedes Programm bekommt also einen Adressraum mit virtuellen Adressen zugeordnet und greift darauf zu. Auf welche Adressen dann physisch zuggriffen wird steht in den Page-Tables bzw. wird eigenltich vom OS gehandelt.

    Stimmt das soweit ?

    Falls ja kommt mir da andere Fragen.

    1. Wieso muss man die Programmadressen dann überhaupt übersetzen, wenn die Speicheradressen dann ja auch wieder "nur" virtuell sind ? Könnten die Programadressen nicht auch in die Page-Table eingetragen werden ?

    2. Wird ein bestimmte Programmadresse, immer zur gleichen virt. Speicheradresse ? Soweit ich das verstanden habe sind Progammaressen ja nur "Adresshafte" darstellung von den Befehlen die ich im Programm verwende. Wird dann cout() immer zur gleichen Programmadresse und weiter dann auch zur gleichen virt. Speicheradresse?



  • Speicheradressen und Programmadressen SIND bereits virtuelle Adressen. Da wird nix nochmal uebersetzt. Jeder Zeiger im Programm, jedes Adressregister (auch der Instruction Pointer!) beinhaltet nur eine virtuelle Adresse.

    Nehmen wir an, deine "Speicheradresse" im Programm ist 0x0BADF00D. Wir lesen nun ein DWORD an dieser Adresse. Was macht der Prozessor?

    1. Er bekommt beim Instruction fetch mitgeteilt, dass er ein DWORD lesen soll, und das dieses an der Adresse 0x0BADF00D steht.
    2. Nun wird gecheckt, ob Paging aktiviert ist (anhand des PG-Bits im CR0; fuer die genaue Bitnummer Google bemuehen, ich weiss sie grad nicht)
    3. Paging ist aktiviert. Also lesen wir nun die Page Directory Adresse aus dem CR3. Die ersten 10 Bits stecken in 0x0BA, was binaer 000010111010 entspricht. Da wir aber nur 10 Bits benoetigen schneiden wir die letzten beiden ab. Nun haben wir 0000101110, was dezimal 46 entspricht. Also greift er auf den 47. (weil 0 ja der erste Eintrag waere) Eintrag zu und liest die Adresse der Pagetable.
    4. Die naechsten 10 Bit stecken in 0xADF, 101011011111. Die obersten 2 des As hat er ja schon verarbeitet, also schnippelt er wieder etwas: 1011011111, dez. 735. Nun greift er auf den 736. Eintrag der Page-Table zu und lesen die Startadresse der Page.
    5. Innerhalb der Page brauchen wir jetzt noch das Offset. Das ist der letzte Teil der Adresse (puh, einmal ohne Geschnipsel), sprich 0x00D.
    6. Der die jetzt legen wir die Adresse 0xyyyyy00D an den Adressbus an, wobei yyyyy die Adresse ist, die wir aus der Page-Table gelesen haben.
    7. Nun erfolgt der eigentliche Speicherzugriff.

    Die Pruefung, ob die Eintreage ueberhaupt valide sind und auch die Rechte-Checks (man kann angeben ob auf best. Pages nur Ring 0 oder jeder Code zugreifen darf) hab ich jetzt zur Vereinfachung weggelassen. Wenn dich auch das interssiert kannst du die Intel Manuals lesen, besonders 3A.
    Noch eine Anmerkung: Dieses verfahren klingt vielleicht ineffizient, weil ja dauernd noch das Directory und die Tables gelesen werden muessen, aber heutige Prozessoren halten das i.d.R. im Chache.



  • Ah okay verstehe. Vlt. ist im Buch einfach nur gemeint das Befehle eigenltich nur Adressen sind, und nicht auch noch extra in welche umgewandelt werden.

    Dann vielen Dank für die ausführlichen Erkährungen.



  • Jain. Befehle sind keine Adressen. Es ist wohl eher gemeint, dass Befehle an bestimmten Adressen stehen.
    Man nehme mal eine Stark vereinfachte CPU mit 3 Registern, die nur MOV und ADD befehle kennt.
    Nun sind die OPCodes folgendermassen definiert:
    MOV-Instruktion reg, Zahl = 0x1A
    MOV-Instruktion reg, reg = 0x2B
    ADD-Instruktion reg, Zahl = 0x3C
    ADD-Instruktion reg, reg = 0x4D
    Und die Register-Nummern: reg1 = 1, reg2 = 2, reg3 = 3
    Wir gehen davon aus, dass ein Register 16 bit breit ist und haben dementsprechend auch als Zahl ein word.

    Man hat nun dieses Assembler-Codestueck:

    MOV reg1, 0x123
    MOV reg2, 0x456 
    ADD reg2, 0x789
    MOV reg3, reg1
    MOV reg1, reg2
    ADD reg2, reg3
    

    Uebersetzt man das kaeme folgendes Heraus:

    codestart+0x00 | 0x1A 0x01 0x01 0x23
    codestart+0x04 | 0x1A 0x02 0x04 0x56
    codestart+0x08 | 0x3C 0x02 0x07 0x89
    codestart+0x0C | 0x2B 0x03 0x01
    codestart+0x0F | 0x2B 0x01 0x02
    codestart+0x12 | 0x4D 0x02 0x03
    

    Dementsprechend sind hier dann codestart (also da wo der Code hingeladen wurde) + 0xYY die Befehlsadressen und Konstrukte wie 0x1A010123 die Instruktionen selbser.



  • Jo, so hatte ich es eigenltich gemeint 🙂
    Danke für das Beispiel, war ne große Hilfe !


Anmelden zum Antworten