Autor Thema: Paging und Multitasking  (Gelesen 9601 mal)

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« am: 24. February 2012, 15:34 »
Hallo Forum,

ich bin gerade dabei, ein kleines OS zu schreiben. Vieles funktioniert schon zufriedenstellend, jedoch habe ich Probleme mit Paging in Verbindung mit Multitasking. Beides alleine funktioniert, jedoch bekomme ich Page Faults, wenn ich einen Ring 3 Task aufrufe (genauer: nachdem ich den Speicherkontext des Tasks für das Paging gesetzt habe). Meine Vermutung ist, dass ich nicht die richtigen Speicherbereiche für meinen Ring 3 Task mappe. Bis jetzt mappe ich eigentlich den gesamten Speicherbereich des Kernels, der bis zum Zeitpunkt, an dem der Task erstellt wird, reserviert ist. Dieser Speicherbereich sollte doch auf jeden Fall alles benötigte einschließen, oder? Welche Speicherbereiche sind für einen Ring 3 Task notwendig und mit welchen Rechten müssen diese gemappt sein?

Danke für Antworten und Grüße
BurningWave

Svenska

  • Beiträge: 1 792
    • Profil anzeigen
Gespeichert
« Antwort #1 am: 24. February 2012, 16:09 »
Hallo,

der Speicherbereich des Kernels muss auch weiterhin mit Kernel-Berechtigungen gemappt bleiben, schließlich soll dein Task ja mit dem Kernel kommunizieren können. Das schließt auch IDT, GDT, Page-Directory, Page-Tables und das TSS mit ein.

Für die Tasks selbst musst du eigentlich nur Code, Daten und Stack mit Usermode-Berechtigungen zusätzlich mappen.

Du siehst im Pagefault ja in CR2 die Adresse, auf die zugegriffen werden sollte. Ist das eine Kernel-Adresse, eine Task-Adresse oder etwas ungültiges?

Gruß,
Svenska

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #2 am: 24. February 2012, 17:11 »
Ich habe gedacht, dass der Speicherkontext (also Page Directory & Tables) eines Prozesses nicht von diesem Prozess selbst gemappt werden müssen, da dieser ja an dem Mapping nichts ändern soll (das soll nur der Kernel über Sys-Calls können). Dem ist wohl nicht so, oder? Page Directory und Tables des Kernelkontexts müssen den Tasks nicht bekannt sein?
Gibt es eine Möglichkeit, nur die relevanten Teile des Kernels in den Kontexten der einzelnen Tasks zu mappen oder muss immer der komplette Kernel gemappt werden. Falls ja, könnten so doch andere Tasks wieder Daten eines bestimmten Tasks und Systemdaten zumindest lesen.
Und noch eine Verständnisfrage: Das User-Flag muss also in jedem Eintrag einer Page Table eines User-Tasks gesetzt werden (selbst, wenn es sich um Kernel-Code handelt)?
« Letzte Änderung: 24. February 2012, 17:20 von BurningWave »

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #3 am: 24. February 2012, 18:18 »
Du kannst den Kernel teilweise mappen (absolute Pflicht sind GDT, IDT und TSS), aber das macht nur unnötigen Ärger. Am einfachsten ist es, den Kernel komplett zu mappen und überhaupt kein gesonderes Kernel-PD zu haben.

Das User-Flag setzt du nur für Pages, die vom Usermode-Code zugegriffen werden können sollen. Das ist der Mechanismus, wie du deinen Kernel schützt, obwohl er gemappt ist.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #4 am: 03. March 2012, 13:42 »
OK, danke. Soweit habe ich das nun verstanden. Doch bei meinem Kernel wird Speicher - auch für User-Tasks - nur vom Kernel selbst reserviert und freigegeben. Also die User-Tasks müssen eigentlich nichts an ihrem jeweiligen Pagedirectory oder dessen Tables ändern. Müssen diese trotzdem von dem jeweiligen Task selbst gemappt werden oder reicht es, wenn sie für den Kernel gemappt sind (da er ja der einzige ist, der sie verändern können muss)? Im Pagedirectory befinden sich nur physische Adressen der Tables, deshalb muss ich die Tables und das Directory eines Tasks nur für den Kernel und nicht für den jeweiligen Task mappen, oder?

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #5 am: 03. March 2012, 18:34 »
Gut, Paging sollte nun endlich funktionieren. Jedoch bekomme ich beim Wechsel Ring0 -> Ring3 einen Pagefault. Ich vermute, dass das irgendwie mit dem Userstack bzw. dem normalen Stack zu tun hat (wieso braucht man für einen Task eigentlich zwei? - Das habe ich noch nicht ganz verstanden). Mein Taskmanager orientiert sich an diesem Tutorial: http://www.lowlevel.eu/wiki/Teil_6_-_Multitasking

Hier ist die Funktion, die einen Task erstellt:
void TaskManager::CreateTask(TASK_FUNCTION f, const char *Name, VideoPipe *VPipe)
{
    Task t;
    t.ID = ++TaskRunCount;
    memcpy(t.Name, Name, 39);
    t.Name[39] = '\0';
    t.MContext = M.CreateContext(); // Speicherkontext für Task erstellen (Mapping des Kernelcodes und des gesamten Stacks werden in neus Mapping aufgenommen)
    t.Stack = (uint8_t *)M.alloc(t.MContext); // Ich vermute, dass der Fehler an dieser oder der fogenden Zeile liegt.
    t.UserStack = (uint8_t *)M.alloc(t.MContext); // Parameter gibt den Kontext an, für den der zu reservierende Speicher gemappt wird (hier also für den Task)
    t.VPipe = VPipe;

    CPU_State CPU;
    CPU.eax = CPU.ebx = CPU.ecx = CPU.edx = CPU.esi = CPU.edi = CPU.ebp = 0;
    CPU.esp = (uint32_t)t.UserStack + 4096;
    CPU.eip = (uint32_t)f;
    CPU.cs = 0x18 | 0x03;
    CPU.ss = 0x20 | 0x03;
    CPU.eflags = 0x200;
    t.CPU = (CPU_State *)(t.Stack + 4096 - sizeof(CPU));
    *t.CPU = CPU;

    TaskList.Append(t);
}

Wie soll der jeweilige Speicher für die Stacks gemappt werden? Für den Kernel oder den Task oder beide? Wo kann sonst ein Fehler liegen, wenn beim Rücksprung aus dem Interrupthandler, der den Task (Ring) gewechselt hat, ein Pagefault auftritt?

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #6 am: 03. March 2012, 22:44 »
Die zwei unterschiedlichen Stacks brauchst du, weil der Prozessor beim Ringwechsel (also beim iret oder umgekehrt bei einem Interrupt) auch den Stack wechselt. Dass er das macht, ist auch sinnvoll, ansonsten hättest du ein Sicherheitsproblem: Wenn der Userspace sich einen Stack mit dem Kernel teilen würde, könnte der Userspace-Code den Stackpointer irgendwo in die Botanik setzen und der Kernel würde beim Versuch, seinen Interrupthandler auszuführen einen Page Fault bekommen (oder noch ungünstiger: Irgendwelche Kernelstrukturen überschreiben).

Zum zweiten Teil: Wenn man einen Page Fault debuggt, schaut man sich die Werte von eip, cr2 und evtl. noch den Fehlercode der Exception an. Wenn du denkst, dass es beim Ringwechsel knallt, dann wird eip auf dein iret zeigen. cr2 sagt dir, welche virtuelle Adresse benutzt werden sollte, und der Fehlercode sagt dir, warum es nicht funktioniert hat (vermutlich einfach nicht gemappt, d.h. not present).
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #7 am: 07. March 2012, 23:24 »
Hm, mir ist schon klar, warum der Kernel und die verschiedenen Tasks je einen egenen Stack haben sollten, aber warum braucht ein Task nach dem Tutorial von hier zwei Stacks (User Stack und Stack)? Würde nicht einer reichen? Wenn ich schon beide brauche, für wen müssen diese jeweils gemappt sein?

Und ich hätte noch eine andere Frage. Wie kann ich möglichst einfach herausfinden, welche Codestelle zu einer bestimmten Adresse, die der Instruction Pointer beinhaltet, gehört? Welche Tools gibt es hierfür?

Sorry, falls meine vielen Fragen etwas nervig sein sollten, aber ich verzweifle echt an diesem Multitasking...

rizor

  • Beiträge: 521
    • Profil anzeigen
Gespeichert
« Antwort #8 am: 07. March 2012, 23:36 »
Hm, mir ist schon klar, warum der Kernel und die verschiedenen Tasks je einen egenen Stack haben sollten, aber warum braucht ein Task nach dem Tutorial von hier zwei Stacks (User Stack und Stack)?
Das liegt daran, dass einmal der User-Thread einen Stack braucht und dann der Kernel, wenn er einen Interrupt behandeln muss.
Auf den Stack des Kernels werden dann die Register des Tasks gelegt.

Würde nicht einer reichen?
Nein, denn sonst könnte ein Thread den CPU-Frame eines anderen Threads verändern. Das darf nicht passieren.

Wenn ich schon beide brauche, für wen müssen diese jeweils gemappt sein?
Einer sollte für den User-Tasks RW gemappt sein (aber NICHT executable) und der andere sollte RW gemappt für den Kernel sein.

Und ich hätte noch eine andere Frage. Wie kann ich möglichst einfach herausfinden, welche Codestelle zu einer bestimmten Adresse, die der Instruction Pointer beinhaltet, gehört? Welche Tools gibt es hierfür?
objdump bewirkt da Wunder. Da kannst du dann nach der Adresse suchen. Es gibt auch ein Flag für objdump, das dir den dazugehörigen C-Code zeigt. Fällt mir aber grad nicht ein.

Sorry, falls meine vielen Fragen etwas nervig sein sollten, aber ich verzweifle echt an diesem Multitasking...
Dafür brauchst du dich nicht entschuldigen, denn wie willst du es sonst lernen ;)

Grüße,
rizor
Programmiertechnik:
Vermeide in Assembler zu programmieren wann immer es geht.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #9 am: 07. March 2012, 23:59 »
Hm, mir ist schon klar, warum der Kernel und die verschiedenen Tasks je einen egenen Stack haben sollten, aber warum braucht ein Task nach dem Tutorial von hier zwei Stacks (User Stack und Stack)? Würde nicht einer reichen? Wenn ich schon beide brauche, für wen müssen diese jeweils gemappt sein?
Du könntest theoretisch auch einen einzigen Stack für den Kernel nehmen (oder genauergesagt einen pro CPU, aber im Moment machst du ja kein SMP). Im Tutorialcode wird aber der Registerzustand des Userspace-Codes auf dem Stack liegen gelassen, solange andere Tasks ausgeführt werden. Das könntest du dann natürlich nicht mehr machen, weil die anderen Tasks den gleichen Stack weiterbenutzen würden. In diesem Fall müsstest du den Registerzustand irgendwo anders hin sichern, z.B. direkt in die struct task.

Zitat
Und ich hätte noch eine andere Frage. Wie kann ich möglichst einfach herausfinden, welche Codestelle zu einer bestimmten Adresse, die der Instruction Pointer beinhaltet, gehört? Welche Tools gibt es hierfür?
Versuch es mal mit objdump -S kernel.elf | less. Wenn du den Code mit gcc -g kompiliert hast, siehst du dann auch den passenden C-Code zu dein einzelnen Assemblerinstruktionen.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #10 am: 11. March 2012, 21:21 »
Ich habe es jetzt mit objdump überprüft. Der Pagefault (Errorcode 5) tritt definitiv bei iret auf. Der Instruction Pointer zeigt auf die 1. Anweisung im User-Task. Woran kann das liegen? Eigentlich ist der komplette Kernel für den Task mit User-Berechtigung gemappt (also alles von der 1MB-Marke, wo der Kernel beginnt), bis zu der Stelle, an der zuletzt Speicher reserviert wurde, bevor Multitasking aktiviert wurde). Aus dem Error-Code schließe ich, dass die angeforderte Seite present ist, jedoch nicht mit dem User-Flag (0x4) gemappt wurde. Wie passt das zusammen?

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #11 am: 11. March 2012, 21:46 »
Hast du das User-Bit auch im Page-Directory-Eintrag gesetzt? Ansonsten kannst du es nämlich in der Page Table setzen sooft du willst und es wird nichts ändern.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #12 am: 12. March 2012, 23:08 »
Ja, das ist definitiv auch gesetzt.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #13 am: 15. March 2012, 18:05 »
Also es gibt neues: Das mit dem Pagefault konnte ich beheben. Es war noch ein Fehler bezüglich des Kontextwechsels im Code. Die 2 Stacks (Userstack und Interruptstack) der Tasks werden nun nur für den jeweiligen Task (NICHT für den Kernel) gemappt. Ist das in Ordnung oder muss der Kernel Zugriff auf den Interruptstack haben?
So weit, so gut, jetzt habe ich ein neues Problem: Ich bekomme einen General Protection Fault bei iret, bevor mein Task aufgerufen wird (Speicherkontext ist zu diesem Zeitpunkt schon gewechselt). Woran könnte das liegen (mein Interrupthandler (insbesondere der Assemblerteil) sieht im Wesentlichen so aus, wie der im Tutorial)?

rizor

  • Beiträge: 521
    • Profil anzeigen
Gespeichert
« Antwort #14 am: 15. March 2012, 18:30 »
Also es gibt neues: Das mit dem Pagefault konnte ich beheben. Es war noch ein Fehler bezüglich des Kontextwechsels im Code. Die 2 Stacks (Userstack und Interruptstack) der Tasks werden nun nur für den jeweiligen Task (NICHT für den Kernel) gemappt. Ist das in Ordnung oder muss der Kernel Zugriff auf den Interruptstack haben?

Der hat da auch so Zugriff drauf. Der Kernel darf alles machen.
Es bleibt halt zu überlegen, ob der User Zugriff auf den Stack haben sollte. Aber das ist mehr eine sicherheitsrelevante Frage.

So weit, so gut, jetzt habe ich ein neues Problem: Ich bekomme einen General Protection Fault bei iret, bevor mein Task aufgerufen wird (Speicherkontext ist zu diesem Zeitpunkt schon gewechselt). Woran könnte das liegen (mein Interrupthandler (insbesondere der Assemblerteil) sieht im Wesentlichen so aus, wie der im Tutorial)?

Lässt sich nicht pauschal beantworten, aber ich würde mal vermuten, dass es daran liegt, dass du die Segment-Register nicht richtig setzt.
Programmiertechnik:
Vermeide in Assembler zu programmieren wann immer es geht.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #15 am: 15. March 2012, 23:02 »
Dieses Problem wäre auch gelöst. Es hatte nichts mit den Segmentregistern zu tun, diese dürften richtig gesetzt sein, sondern vielmehr mit einem Stückchen Assemblercode, das an der falschen Stelle stand. Aber geholfen hat das nichts. Jetzt bekomme ich wieder Pagefaults :S Zum einen bekomme ich beim Eintritt in den Ring 3 einen Page Fault, wenn der Kernel nicht zum Schreiben gemappt ist. Das ist das geringere Übel, ich muss eben noch herausfinden, welche Kernelbereiche für Ring 3 Tasks zum Schreiben gemappt sein müssen. Durch ein wenig Experimentieren müsste ich die Lösung finden. Das größere Übel besteht darin, dass es bei der Rückkehr in den Ring 0 crasht. Irgendwie werden die Kernelsegmente bzw. der Kernelstack nicht geladen.

Vielleicht wäre der Code des Interrupthandlers relevant:
extern "C" CPU_State *InterruptCallback(CPU_State *cpu)
{
    Task *task = NULL;

    if(cpu->intr <= 0x1f)
    {
        if(cpu->intr == 8 || cpu->intr == 18)
        {
            [...] Fehler behandeln
        }
    }
    else if(cpu->intr >= 0x20 && cpu->intr <= 0x2f)
    {
        if(cpu->intr == 0x20 && TaskMgr.GetSchedulingState())
        {
            task = TaskMgr.Schedule(cpu);
            gdt.TSSSetCPU(uint32_t(task->CPU + 1));     // void GDT::TSSSetCPU(uint32_t cpu) {tss[1] = cpu;}
        }

        if(cpu->intr >= 0x28)
            outportb(0xa0, 0x20);   // EOI an Slave-PIC
        outportb(0x20, 0x20);       // EOI an Master-PIC
    }
    else if(cpu->intr >= 255)
    {
        [...] Fehler behandeln
    }

    cpu = idt.callback(cpu);

    if(task)
    {
        cpu = task->CPU;
        if(task->MContext)
        {
            M.ActivateContext(task->MContext);
            asm volatile (          // User-Datensegmente laden
                "mov $0x23, %ax;"
                "mov %ax, %ds;"
                "mov %ax, %es;"
            );
        }
        else
            M.ActivateKernelContext();          // Kernel-Datensegmente wurden schon in Assemblerteil, der diese Routine aufruft, geladen
    }

    return cpu;
}

Ich bin so langsam wirklich am Verzweifeln -.-
« Letzte Änderung: 15. March 2012, 23:03 von BurningWave »

rizor

  • Beiträge: 521
    • Profil anzeigen
Gespeichert
« Antwort #16 am: 16. March 2012, 08:42 »
Wenn dein Ring-3 Task Schreibzugriff im Kernel braucht, ist das definitiv nicht gut. Sowas sollte über Syscalls gelöst werden.
Ist denn dein TSS richtig gesetzt?
Programmiertechnik:
Vermeide in Assembler zu programmieren wann immer es geht.

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #17 am: 16. March 2012, 11:01 »
Ich gehe davon aus, dass er nicht wirklich den Ring-3-Task meint, sondern alles, was im Speicherkontext von dem Task läuft. Also zwangsläufig zumindest die Interruptstubs des Kernels, die selbstverständlich irgendwie auf Kernelspeicher schreiben können müssen. Wenn es tatsächlich Ring-3-Code wäre, dann müsste der Ring-3-Code gefixt werden anstatt zusätzlich Zeug freizugeben.

Ich halte die Idee, dass der Kernel ein separates Page Directory bekommt, sowieso für unsinnig (produziert nur massenweise unnötige TLB-Flushes und bringt eigentlich keinen Vorteil), aber ich glaube, das habe ich hier schonmal erwähnt.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

BurningWave

  • Beiträge: 14
    • Profil anzeigen
    • JB Technologies
Gespeichert
« Antwort #18 am: 16. March 2012, 14:23 »
Vielleicht liegt es tatsächlich am TSS. Es beinhaltet eigentlich nur zwei Werte. SS0 ist 0x10 und bei ESP0 bin ich mir nicht ganz sicher. Ich muss ja diesen Wert bei jedem Task Change anpassen. Ich habe noch nicht ganz verstanden, was ich angegeben soll. Muss ich die Adresse des Stacks des aufzurufenden Tasks angeben?

kevin

  • Administrator
  • Beiträge: 2 767
    • Profil anzeigen
Gespeichert
« Antwort #19 am: 16. March 2012, 14:43 »
Das TSS wird benutzt, wenn du aus dem Userspace-Code zurück in den Kernel springst. Benutzt heißt, dass ss:esp aus den Feldern ss0 und esp0 im TSS geladen wird. Auf diesen neu geladenen Kernelstack werden dann Sachen wie Fehlercode, Rücksprungadresse usw. gepusht und das ist der Stack, den dein Interrupthandler benutzt.
Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end.

 

Einloggen