The Long, Hard Road out of Segmentation

As of last night’s Pre-MAGUS check-in, ArcanOS now uses paging rather than segmentation.  Of course, this is a pretty critical step in the MAGUS goals.  Segmentation is really just a legacy aspect of x86 these days, and there are really no meaningful examples of its use in modern OS development.  So, why did I ever use segmentation in the first place?  Well, the biggest reason is that I inherited it.  Remember that ArcanOS began life as an early coursework lab for a teaching operating system called JOS, and very specifically an early introductory form of it that focused on the boot loader.  ArcanOS, like JOS, and like a lot of other operating systems, is designed to be a “higher half” kernel, which means that it is linked to 0xC00000, but it’s actually loaded at a much lower address (right at 1MB, for those playing along at home).  If you really want to split hairs, ArcanOS is linked at 0xC0100000 (which will change soon, possibly in the next check-in).  In order to really “run” ArcanOS, the x86 processor has to be set so that address 0xC0100000 is mapped to 0x00100000.

This was originally done with something the hobby OS community generally calls “the GDT trick“.  In order to run in 32-bit mode, an x86 requires you set up a GDT, and this will set the values of the segment registers.  These registers will be added to every address to get a final result that goes out onto the memory bus.  It turns out that, in this addition process, overflowing 32 bits will just cause a wrap-around.  So, you can exploit this to get your kernel’s link addresses to refer to its actual load addresses (0xC0000000 + 0x4000000 = 0x00000000 after wrap-around).  It’s good for getting things up and running, but you can’t really stake much on it after that.  Segmentation is inflexible and you can’t do any swapping with it.  On top of that, there are all sorts of memory addresses that correspond to physical hardware, and you have to remember to adjust those addresses to compensate for your segmentation trick.  Even more so, though, it seems that the GDT trick isn’t universally supported.  ArcanOS uses Bochs as its reference platform, but it would appear that VirtualBox doesn’t support the GDT trick.  Long story short– getting onto paging is pretty important.

So, this left me with some questions about how I wanted to get onto paging.  My first thought was to employ the GDT trick to jump into my proper kernel code, then set up paging and reload the GDT with segment descriptors that used a base of 0x00000000 (effectively turning off segmentation).  The more I considered this, though, the less I liked it.  Part of the reason ArcanOS started out using the GDT trick was because it also started out with its own boot loader and you have to set up a GDT to get into 32-bit mode in the first place.  ArcanOS now uses GRUB for its boot loader, though, and GRUB already sets up 32-bit mode and installs a GDT to support a flat memory model.  Why was I going to change the GDT only end up setting it back the way it was before?  No, this was a good opportunity to separate ArcanOS from its roots by just going straight to paging.

Setting up paging, it turns out, is a little bit easier than I thought.  You need about 12KB of memory to establish some initial page tables, and if you’re loading the kernel at 1MB, there’s usually plenty of room just under the kernel to use for those purposes.  After that, it’s really just zeroing-out the memory and stuffing some values in the tables.  Info on the layout of the tables, with reference for making a higher half kernel, is in an article on the OSDev Wiki.  The sticky wicket, in my mind, was that I would really just rather not write it in assembly.  I don’t mind writing in assembly, but for anything non-trivial (especially in a hobby project where my free time is at a premium), I trust a compiler more.  But…if I wanted to do this in C, wouldn’t I have to write it in the kernel proper, thus requiring I rely on the GDT trick?  As it turns out, the answer is “no.”  What I realized is that the code would be location-independent as long as all I did was write some simple loops and not refer to any memory except the page tables which I was trying to fill.  All of the control flow would be PC-relative (based on adding offsets to the current code location), so it would never care where it was really located as long as I called into it correctly.  The result is the init_paging( ) function in kernel/memmgr.c.  All I have to do to call the function is to store its address in a register, adjust its link address to its load address, and then call the function from the register.  I call this function, let it do its thing, and then I finish up setting paging in assembly.

Originally, I paged the first megabyte to itself and paged 0x00100000 to 0xC0100000.  This makes the hardware exposed in low memory easy to access, ensures my first page tables are mapped in, etc, and it ensures the symbols in the kernel were all mapped in.  And, then I set up paging and….crash.  Why?  Well, because I’m already running my code at 0x00100000…which is in the second megabyte.  The address of the current instruction was not mapped to anything!  So, once paging was turned on, my own code became inaccessible.  In the end, I ended up deciding to map the first four megabytes to themselves.  I’ll want to clean that out eventually, but for initialization, it’s something I can live with.  So, this was enough to get me into the ArcanOS kernel proper, but then…crash.  What was it this time?  Well…GRUB put the multiboot info struct below 0xC0100000, so that wasn’t mapped in.  Again, just to keep the bases covered, I mapped the first four megabytes to the address space starting at 0xC0000000.  This does mean there are currently two ways to access the same 4MB of memory, but it gets ArcanOS up and booting, and there should be some easy ways to clean this up.

Technically, with this view of memory, ArcanOS would have run just like the GDT trick was still being used, but I still did due diligence and I went through and stopped referring to addresses as being relative to KERNBASE.  My word of advice to any OS hobbyist who’s starting out– I really encourage you to consider doing rudimentary paging right from the start.  If you don’t, you’re going to get married to an address management scheme you’re likely to throw away later, and if you’ve been using it for a while, then it’ll take a harder portage effort to get off of it.  Just page your memory in from the get go.  If you’re using GRUB, even more to the point, because GRUB already set up the GDT for you to use paging with a flat memory model.  The biggest concern I had with setting up paging was that it looked hard at a time when I just wanted to get a kernel booting and doing some interesting things.  In reality, I was able to sling together a C function which would set up the page tables, and this made things very easy indeed.  Don’t get into using segmentation.  If you do, it’ll be a long, hard road getting back out.

So, from here, what I really need to do is clean up the paging a little bit.  Once I’m jumped into the kernel, I can stop identity-paging the first 4MB.  I also want to map only enough pages to hold the kernel.  Also, I don’t see a good reason to link the kernel at 0xC01000000 any more, since doing that was mostly to make the GDT trick a little bit simpler.  I have a good map of physical memory thanks to GRUB and I can make and manipulate pages, so this is everything the memory manager needs to be pretty full-featured.

Leave a Reply

Your email address will not be published. Required fields are marked *