In this lecture, we will set up paging and remap our kernel to the higher part of the memory locations.
As you see in the memory map,
the kernel is running at address 200000 so far.
What we want to do is we are going to do map the kernel to the area shown here.
The large chunk of memory addresses in red
is non canonical address. The user programs will run in the lower part of the memory location.
So the non-canonical address area separates the kernel and user space.
So how to do it?
This requires us to set up paging. When we enter the 64-bit mode, paging mechanism is enabled.
But the way we setup paging is making the virtual address
equal to physical address.
Therefore we need to reconfigure it to do the job.
OK, let's see what is paging and how to set it up.
Paging allows us to remap data into physical address space using fixed size blocks called physical pages.
The page sizes the processor supports in 64-bit mode are 1g pages, 2m pages
and 4k pages.
In our system, we use 1g and 2m pages.
So the memory address can be divided into different sets of pages. And page translation will use data structure
called page translation table to translate virtual pages into physical pages.
As you see, we translate it into different pages, or we can translate it into the same page
which is used in the kernel so far.
Let's see the translation table.
The translation table is actually a hierarchical data structure which means we will have several tables.
The virtual address is broken into three pieces in this example. The sign extension is not used here.
We start from Page map level 4 table.
The table is pointed to by the register cr3.
Then the 9-bit pml4 value is used as an index to locate the corresponding item in the table.
Each entry here includes the address of the next lower level table. Page directory pointer table in this example
.
Then the value of pdp is used as an index to locate the correct entry.
And the entry now points to the physical page which is 1g page.
The page offset is used as the offset into the physical page and now we get physical address.
This is a one gigabyte page translation
.
Let's move to 2m page translation.
The virtual address is now divided into more fields. The process is pretty much the same.
Register cr3 holds the address of page map level 4 table and PML4 acts as an index to the entry
which points to the next level table.
Then the value of pdp is used to locate the corresponding entry.
At this point we have one more level.
The next level table is called page directory table
and we can find the entry using pd value. The entry points to the 2m physical page.
And the page offset is the offset into the page.
The 4k page translation is the same except that it requires one more level of translation table
.
And note that the addresses used in register cr3 and table entries are all physical addresses.
Alright this is the simplified process of translation.
What we are going to do next is we are going to talk about the table entries, since setting up paging in our system
is setting those entries.
These three table entries are used for 1g page translation. There are other attributes
in the entry that are not shown in the slide.
What is shown here is enough for our memory module to work properly.
The first entry is stored in register cr3.
So we simply save the address of the page map level 4 table to the register.
The next entry is page map level 4 table entry which holds the address of page directory pointer table
.
In addition, we have some attributes in the entry. The bit p represents present. If the bit is 0,
it means the page table or page is not in the memory and page fault exception will generate
if we access it.
So we set it to 1.
The w means read write with 0 being read only and 1 being read and write.
The bit u means user. If the bit is 0,
it means that the user program in ring3 is not allowed to access the page.
If the bit is 1, then the user program in ring3 can access the page.
Ok, move to the next entry which is page directory pointer table entry.
The table entry holds the address of the 1g physical page.
The p w and u bits have the same meaning.
And the bit 7 must be 1 indicating a 1g physical page translation.
OK, let's take a look at 2mb page translation.
The first two entries are the same as those in the 1g page translation. The third entry is pointing to
page directory table instead of 1g physical page.
So the bit 7 is now cleared.
The last entry points to the 2m physical page and the bit 7 is set to 1
indicating a 2m physical page translation.
OK, next we will do a recap on the code of setting up paging in the loader file and then remap our kernel
to the higher part of the memory location. Let's get started.
Opens the loader file.
The code here is for setting up paging. First off, we zero the 10000 bytes of memory region starting from 70000.
The value of cr3 is 70000.
So the address of page map level 4 table is 70000.
Each entry in the page map level 4 table represent 512g and we only implement the low 1g.
so we set up the first entry of the table.
Each table takes up 4k space, since the table include 512 entries
with each entry being 8 bytes.
So the next table address is set to 71000 and the lower 3 bits are the attribute we need to set
.
We want the memory to be readable and writable and only accessed by the kernel. Also, we set present bit to 1.
So the value is 3. The value we assign to the entry is 71003.
The reason we set the value 7 instead of 3 is that in the previous lectures,
we accessed the memory when we jumped to the ring3 and write the screen buffer to print message on the screen.
If we set it to 3 at that time,
the cpu exception will generate.
For now, we do the initialization in the kernel, so we don’t need to make the memory accessible in ring3
.
Ok let’s continue. In the page directory pointer table,
we also set up the first entry. Because each entry here points to 1g physical page.
This is exactly the entry we want to set.
The base address of the physical page is set to 0
and the attribute here is set to the same value 3.
don’t forget to set the bit 7 to 1 to indicate this is 1g physical page translation
.
Ok, next thing we will do is we are going to remap our kernel. The virtual address the kernel currently running at
is 200000.
And the address we want the kernel to remap to is the virtual address showing here as we have seen it in the slide.
In this example we map the virtual address to the same 1g physical page.
So the two translations we set up will eventually point to the same physical page where the kernel is stored
.
Which means we just change the virtual address of the kernel by simply setting up the table
instead of copying the kernel.
Alright,
with the virtual address we want,
we need to do a simple calculation. Apparently, this virtual address is way beyond 1g
.
So we retrieve the 9-bit page map level 4 value located at the bit 39 of the address.
We get the index by shifting right the value 39 bits. Next we clear other bits using and instruction.
And now we get the 9-bit index value.
What we are going to do next is using the index to locate the corresponding entry in the table.
Each entry takes up 8 bytes,
so we multiply the index by 8.
This is the entry we want.
The value we want to assign is 72003 which means the next level table is at 72000
and the attributes are the same as we saw before.
As you can see the index bits of page directory pointer table entry is 0.
So the first entry in this table is the entry we need to set. Because we want to remap the kernel to the virtual address
without really copying the kernel elsewhere.
So we set the 1g physical page to the same physical page where the kernel locates.
There is one more thing to do before we finish the loader.
Since the kernel is relocated to the new virtual address which is far away from the loader,
we need to save it to the 64-bit register and jump to kernel.
So we move the virtual address to rax
And then jmp rax
OK, let's move to the kernel file.
In the kernel file,
the first thing we are going to do is change the load gdt instruction. Since the kernel is running
at the high memory location.
The memory address of gdt pointer is also at this memory area.
We cannot reference it using 32-bit address.
Instead, we move the address in register rax and load rax.
So we copy address
of gdt pointer to rax.
and load rax
If you want to reference gdt pointer using rip-relative addressing here, it will be fine since our kernel is really small.
In this example we simply move to the register.
The same thing happens with set tss.
We move the address of tss descriptor to rdi and access the memory locations using rdi instead.
Next one is kernel entry.
Because push instruction can only push 32-bit immediate value which cannot represent the address of kernel entry.
We need to move the address to the 64-bit register and then push the value on the stack
.
we move the address of kernel entry to rax
then push rax
Next one is stack address. The stack address copied to the rsp register is at the low memory location.
We change it to the high memory address which points to the same physical address. Remember the virtual address
,
and virtual address 0 we set up in the loader file point to the same physical page.
So adding 200000 to the new virtual address is the new stack address we want
.
And don’t forget to change the kernel stack in the TSS.
we also add it with the virtual address.
Alright that’s it for the kernel file.
In the linker script,
we specify the linker to link the kernel at the desired address. To do it, we use .
which means the current address. And we assign the base address to it. In the loader file, we jump to the new virtual address.
So this is the address we use here. And now the following sections will be linked at this address.
OK, let's build the project and test it out.
Because we only change the memory address of the program, the message printed on the screen remains the same.
But the virtual address for our system is different from those in previous lectures.
OK, that's it for this lecture.