User:Mduft/HigherHalf Kernel with 32-bit Paging

From OSDev.wiki
Revision as of 08:17, 25 November 2010 by osdev>Mduft
Jump to navigation Jump to search
This page is a work in progress.
This page may thus be incomplete. Its content may be changed in the near future.

Welcome

Hi, and welcome to this small tutorial. It is assembled from the code, that i have written for my own hobby OS. As a prerequisite i recommend that you make yourself familiar with the AT&T x86 assembler syntax used by the GNU assembler (which i will be using here). I will try to explain everything i do in very much detail, but still - it's not trivial :)

I also suggest you have the Intel Processor Documentation(s) at hand, especially Volume 3a, which explains 32-bit paging and all the related structure, etc.

QEMU

As a side note, i want to use this to advertise qemu as a development utility (no i'm not affiliated with 'em ;)). I use it very much, and it is really, really useful. For example you can investigate the current page tables of your kernel right after loading CR3, which prove very usefull when writing this code. Also having a symbolic debugger (although "symbolic" is not so much usefull in assembler ;p) is the bare minimum you should be equipped with when going on the kernel development route...

Additionally, you don't have to worry about boot loaders and such, as long as your kernel is multiboot compliant.

Boot code Structure

I will give a rough outline of the kernel (if you can call my 3-filer a kernel ;p) as i will explain it here:

  • boot.S: This is the main meat of the startup code. it will contain code to:
    • setup a initial stack.
    • setup initial paging structures (the kernel PD, and two PTs)
    • identity map the low 1MB, and all of the kernel.
    • map all the kernel to the higher half
    • enable paging
    • call the C kernel
    • write "PANIC!" on the screen in nice white-on-red letters
      (this is why you read this, right?)
  • link.ld: This will contain the instructions for the linker.

Also, i plan to provide the original files i use in my kernel, so you can have a closer look at them. When reading through them, you will mention, that i have (basic) C++ support in them. You either don't have to worry about it, or can use it to extend your own kernel in this direction.

Thinking about it

At first, i want you to think a little about what we will need, and what we want to accomplish.

We want to have a kernel, which runs (not loads!) at a high address. At such a high address, that it may be, that there is not so much physical memory, that this address is available. So we need virtual memory (or a segmentation trick, described in Higher Half With GDT). We will use paging, to map our kernel to a high address after it has been loaded to a low address (which should be always available physically).

For this to work correctly, the code in our kernel needs to know about this. Most of the kernel will have to be linked to a high address, so we don't need to worry about this anymore, once we're over the bootstrap phase. The initial bootstrap code, in contrast, needs to be linked to a low address, or be completely position independent.

For the paging stuff, we will need to have at least a PD (Page Directory) for the kernel, and two PTs (Page Tables). Why two, you might ask. The reason is, we need one PT for the low addresses (the lower 1MB, and the kernel in lower half), and one for the addresses in the higher half. If your kernel grows larger and larger, it might even require more PTs, and you will for sure need to deal with PDs and PTs a lot when it comes to processes, where probably each process will have it's own PD and associated PTs.

For this tutorial i chose some very common addresses:

  • 0x100000 as the physical load address of the kernel. This is the location the bootloader will load our kernel to.
  • 0xC0000000 as the offset into the higher half.

Those two added together, are the address where the kernel will finally be running (so 0xC0100000). You will see the use of KERNEL_HIGH_VMA all through the code to take into account the offset to the higher half. This is, because the linker assigns higher half addresses to some of the symbols. Since our kernel is loaded to lower half and "thinks" it is running in higher half, we need to be careful when using the addresses of symbols assigned by the linker. During the initial bootstrap, i'm doing a lot of adjustments from higher to lower half, so things are accessible as long as we don't have paging set up.

boot.S

Now for the real code. i will go through it, and explain every section as thorough as possible.

Global symbols

We will have only one single global symbol for this file, and thats the entry point of the kernel. So the file starts with the following line:

.global bootstrap_ia32

The required .data stuff

We will need some room in our kernel to do the things we want to. However, we don't (yet) have means to dynamically allocate things, and we don't want to worry too much, so we will simple reserve all the required space in the executable itself. this way, the linker will take care of the kernels bounds, and the bootloader (or qemu in our case), will take care about loading, and checking for enough room, etc., etc.

Use the .data section

First, we'll tell the assembler, that we want to put the following stuff in the .data section of the output file:

.section .data

Make room for the PD and PTs

Then, we need room for the PD and PTs. I'll reserve some space for them like this:

# the kernel page directory, lowest page table (for the low 1MB identity 
# mapping in kernel space) and the kernel page table, used to map the
# kernel itself from physical 0x100000 to 0xC0000000).

.align 0x1000
_kernel_pd:
   .space 0x1000, 0x00
_kernel_pt:
   .space 0x1000, 0x00
_kernel_low_pt:
   .space 0x1000, 0x00

The above tells the assembler to pad the current position in the output file until it is aligned to 4K (0x1000 -> 4096). The we reserver three times 4K of space for the PD and PTs. These symbols will end up in the .data section of the kernel, and thus will be there as soon as the bootloader loaded the kernel. No need to worry about allocating those from "somewhere".

Make room for the Stack

Another thing we will need from the very start, is the stack. we won't have time to defer it's creation until we can allocate memory, so we need to put it in here too. Of course you can make it way smaller if you like; Linux uses 16K or 8K if you tell it to do so.

# a whopping 64K of initial stack space.
.set INITSTACKSIZE, 0x10000
initstack:
   .space INITSTACKSIZE, 0x00

Strings

The last thing in the .data section are our strings we will be using from within the assembler code. There are not many of them, just the "PANIC!" i promised above :)

_msg_panic:
   .asciz "PANIC!"

The actual code

... or not quite yet. At least we tell the assembler to put the following things in the .text section.

Use the .text Section

The following tells the assembler to use the .text section. Also, i tell it to produce 32-bit code, which is not really a necessety, but i like to make it clear.

.section .text
.code32

Now for the exception to the rule

... the MultiBoot Header. It is put in the .text section for a special reason. The multiboot specification states, that the header must appear longword-aligned aligned somewher in the first 8192 bytes of the kernel. As you will see later on in the linker script, the .text section is the first section in the kernel, and thus has to contain the header.

.set ALIGN,         1<<0             # align loaded modules on page boundaries
.set MEMINFO,       1<<1             # provide memory map
.set FLAGS,         ALIGN | MEMINFO  # this is the Multiboot 'flag' field
.set MAGIC,         0x1BADB002       # 'magic number' lets bootloader find the header
.set CHECKSUM,      -(MAGIC + FLAGS) # checksum required

.align 4

multiboot_header:
   .long MAGIC
   .long FLAGS
   .long CHECKSUM

The above first sets some "absolute" symbols. They have a fixed value, and can be used as constants. The it sets the alignment of the following to 4 (longword; remember? the header has to be aligned like this... most likely, we could omit this, since it is the first thing in the file anyway, and the chance that we are misaligned here is very small to impossible. but still.. better be on the safe side).

A good thing to know is, that the ".set symbols" are present in the symbol table of the output file, but do not take up any space in the file at the current location. This means, for example, that the current alignment does not change because of their declaration. We're still at the very same position in the output file.

Required symbols for the screen buffer

We also need some helpers for handling the screen (clearing it, and then, the point of the whole tutorial: printing "PANIC!"):

.set VIDEO_RAM,     0xB8000          # Video Memory, used to print to the screen.
.set VIDEO_DWORDS,  0x3E8            # The count of DWORDs (!) the screen buffer is large.

The entry point

Now the entry point function, the one that will be visible from the outside world. We will start by setting up the inital stack. Because (as we all know, right?) the stack grows downwards in memory, we need to set the ESP register to point to the end of the stack space.

bootstrap_ia32:
   # setup the stack
   mov   $(initstack + INITSTACKSIZE), %esp
   # adjust address to be physical, stack is in data segment, which is linked to
   # the kernels virtual higher half address.
   subl  $KERNEL_HIGH_VMA, %esp

This will put the address of the initstack symbol, with the length of the stack added to it, into ESP, the stack pointer. What you do not know yet (and what i will explain in detail when we come to the linker script): the stack symbol (as it is in the .data section) is assigned a higher half address by the linker. Thus we cannot yet access it, because we're still running in the lower half now. If we would not subtract the offset into the higher half, where the kernel will be mapped after paging is enabled, the kernel would maybe still run fine - as long as the machine has enough memory, so accessing the higher half address works (however i haven't tried this!).

Next, we'll want to setup some form of initial paging, which enables the kernel to run code off the higher half addresses, and use all the other things that will be linked there.

   # setup boot paging to map kernel to higher half
   call init_boot_paging_ia32

I will discuss the init_boot_paging_ia32 function in detail just a few lines further down. Let's first finish the entry point, as it is not so complex.

Imagine, we have paging setup all right (the init_boot_paging_ia32 function did it's job, whatever this means), and can now use the correct higher half addresses all over the place. The first thing we'll want to do, is to try whether it really works, and relocate the stack from the lower to the higher half.

"Relocate" is maybe a bad word for what we're doing. Either way, we're accessing the same physical memory, because we mapped the higher half to the lower half physical addresses. In fact, we can use both addresses just fine. Also, if you, say, write a byte to 0x100000 (please don't ;)), it will immediately appear at 0xC0100000 too; it's the same memory location.

   # adjust stack registers to point to the now mapped stack as virtual address
   addl  $KERNEL_HIGH_VMA, %esp
   mov   %esp, %ebp

Now the stack is used from it's higher half mapped address.

Since we have all set up now, we can test the lower 1MB mapping very easily, by trying to clear the screen, which will access the screen buffer at 0xB8000. If the mapping doesn't work, this will cause one or the other exception, double fault and tripple fault :)

   mov $VIDEO_RAM, %edi
   mov $VIDEO_DWORDS, %ecx
   mov $0x07200720, %eax
   rep stosl

This fill all the screen buffer with light-grey-on-black colored blanks, and thus clear it. (Each word is built like this: 0x0000 -> background (0 = black), 0x0700 -> foreground (7 = light grey), 0x0020 -> " " (blank)).

The only thing left to do (except error handling), is to call the main C kernel from here. First we push the parameters passed by the Multiboot compliant bootloader on the stack.

   # push parameters to the entry point (grub parameters)
   push  %eax
   push  %ebx
   call boot

My kernel main C function happens to be named "boot", but thats of course up to you. Now, actually there is all we need. Still we will want to make sure, that if boot ever returns, the CPU does not continue to execute junk or code it shouldn't, so we will halt the CPU. Note: You'll probably want to halt all APs too, if you put SMP support in your kernel...

the_end:
   mov $_msg_panic, %eax
   call boot_print_msg
   # halt the cpu... the kernel stopped.
   cli
   hlt

I think it is pretty obvious: the above loads the "PANIC!" message into EAX, calls the print function (i will show it to you in a second...), then disables interupts and halts the CPU.

The simple printing function

Ok, before we start with the paging code, here is the printing function i have to write the "PANIC!" on the screen. I'll leave it to you to figure out how it works, i guess things like these are described a lot in th wiki :)

boot_print_msg:
   # eax: address of the string to print.
   push %edx
   push %ebx

   mov $VIDEO_RAM, %edx

   _print_loop:
       movb (%eax), %bl
       xorb %bh, %bh
       cmpb $0x0, %bl
       je _end_print
       orw $0x4F00, %bx
       movw %bx, (%edx)
       add $0x2, %edx
       inc %eax
       jmp _print_loop
   _end_print:

   pop %ebx
   pop %edx

Initialize Paging

The following is my init function for the paging. It makes the PD point to the correct PTs. It also calls another function to fill the PTs with the correct entries (it "maps" virtual to physical addresses). Let's start with the top of the function:

init_boot_paging_ia32:
   # save registers used here.
   push %eax
   push %ebx
   push %edx
   push %ecx

At the end of the function, we will restore the registers, so the code behaves nicely, and doe not overwrite reigsters set by the bootloader (remember: grub passes two parameters in EAX and EBX).

Now, to insert the PDEs in the PD, we need to habe the correct addresses of the PD and PTs. Those symbols (_kernel_pd, _kernel_pt, _kernel_low_pt; we declared those above in the .data section, remember?) are assigned higher half addresses by the linker, as you will see later on, when i explain the linker script. Since the higher half addresses are merely an "alias" to the lower half addresses, we can simply subtract the offset into higher half, to get the actual physical load address of those symbols. This way, we can access them without paging enabled.

   mov  $_kernel_pd, %eax          # get virtual address of kernel pd
   sub  $KERNEL_HIGH_VMA, %eax     # adjust to physical address

EAX now contains the physical address of the kernels PD. Now the same for the "low" PT (the one mapping the first 1MB of the memory, and all the physical kernel addresses, as the kernel is loaded right above 1MB. With this single PT, we can map up to 4MB, that makes room for a approximately 3MB kernel above the 1MB low memory).

   mov  $_kernel_low_pt, %ebx      # get virtual address of kernel low pt
   sub  $KERNEL_HIGH_VMA, %ebx     # adjust to physical address

If we want to set a PDE containing this PT, we just need to set the present flag. (Note: i don't clear the lower 12 bits of the address intentionally; it is not required, as i can be sure, that the structures are correctly aligned the way i declared them in the .data section - just in case you wonder if i forgot it ;)).

   or   $0x1, %ebx                 # set present flag

EBX is now in the correct form for a PDE, the only thing left to do, is actually set the entry in the PD (whos address is loaded into EAX):

   mov  %ebx, (%eax)               # set the pde

This was easy. The next one is a little more tricky. The low PT has the advantage of beeing at index zero of the PD. The higher half PT for the kernel doesn't have this advantage; we will first have to calculate the offset into the PD, where we want to set the PDE.

   push %eax
   mov  $KERNEL_HIGH_VMA, %eax     # get virtual address offset
   shr  $22,  %eax                 # calculate index in the pd
   mov  $4, %ecx
   mul  %ecx                       # calculate byte offset (4bytes each entry)
   mov  %eax, %edx
   pop  %eax

The above code does this: It first saves the EAX register, as we'll need it's value again later (and restores it at the end, see?). Then it loads the higher half kernel offset into EAX. I need this for a special reason: I assume, that all the kernel fits entirely into the one PT for now. I need to have any virtual address that should be mapped by the kernel PT. From there i can extract the Index that the PDE should have in the PD. It is not really required to load the actual offset here (although it's the clearest way i can think of). Anything linked to higher half will do (for example the address of the _kernel_pd or _kernel_pt symbols themselves). Next, EAX is right shifted by 22 bits to extract the index into the PD. Since all entries are 4 bytes each, we need to multiply the index with 4, to get the offset of the PDE from the PD base address. This offset is saved in the EDX register for later use.

To get the right position for out PDE, we now just have to add the offset to the PD base pointer, we loaded before.

   add  %edx, %eax                 # move the pointer in the pd to the correct entry.