Lowlevel
Lowlevel => Lowlevel-Coding => Thema gestartet von: BurningWave 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
-
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
-
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)?
-
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.
-
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?
-
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 (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?
-
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).
-
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...
-
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
-
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.
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.
-
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?
-
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.
-
Ja, das ist definitiv auch gesetzt.
-
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)?
-
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.
-
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 -.-
-
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?
-
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.
-
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?
-
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.
-
Das zweite Problem mit den Page Faults konnte ich endlich lösen :)
Doch zum ersten Problem: Es kann ja keine Lösung sein, den kompletten Kernel für User-Tasks writeable zu mappen (in diesem Fall wäre Paging komplett sinnlos). Wenn ich nun nur bestimmte Stellen für User-Tasks sichtbar machen möchte, ergibt sich das Problem, wie ich den Adressraum der Funktion des jeweiligen Tasks bekomme. Ein Funktionszeiger auf die Funktion sagt mir, wo diese anfängt, aber wie bekomme ich deren Länge?
Ich habe noch eine Frage: Ein return innerhalb eines Tasks lässt den Instruction Pointer doch irgendwo ins Nirvana springen. Wie kann man dieses Problem handhaben?
-
Irgendwo sitzt bei dir noch ein großes Missverständnis.
Ring-3-Code braucht gar keinen Zugriff auf den Kernel, Ring-0-Code braucht ihn. Du hast Ring-0-Code, der mit dem PD des Tasks läuft, also musst du in diesem PD alle Pages gemappt haben, die dieser Ring-0-Code benutzt. Du musst das User-Bit für diese Pages aber nicht gesetzt haben, weil es ja schließlich Ring-0-Code ist, der auch auf Kernelpages zugreifen darf.
Ein Task sollte sich nicht durch return, sondern mit einem Syscall beenden. Wenn es das nicht tut, gibt es halt eine Exception und der Kernel entsorgt den Task...
-
Ring-3-Code braucht gar keinen Zugriff auf den Kernel, Ring-0-Code braucht ihn. Du hast Ring-0-Code, der mit dem PD des Tasks läuft, also musst du in diesem PD alle Pages gemappt haben, die dieser Ring-0-Code benutzt. Du musst das User-Bit für diese Pages aber nicht gesetzt haben, weil es ja schließlich Ring-0-Code ist, der auch auf Kernelpages zugreifen darf.
Genau das ist das Problem. Im PD des Tasks ist der komplette Kernel ohne User-Flag gemappt. Jetzt brauche ich den Speicherbereich der Task-Funktion, um in diesem Bereich das User-Bit zu setzen.
-
Ach so, du hast den Userspace-Task in den Kernel einkompiliert? Dann würde ich erstmal das ändern.
-
Es funktioniert :) :)
Ich musste nur noch das PD des Tasks mit dem User-Flag und writeable mappen, sonst funktioniert es nicht. Bis man darauf kommt...
Auf jeden Fall vielen Dank für eure Hilfe.
-
So mein kleines OS ist nach langer Arbeit endlich in einen ansehnlichen Zustand gekommen:
http://www.jbtechnologies.de/projects/programms/badmem-os.html
Danke nochmal für eure Hilfe.