Showing posts with label operating systems. Show all posts
Showing posts with label operating systems. Show all posts

Tuesday, July 8, 2014

DarkOS: Hobbyist OS architecture taking shape.

I've been tinkering with some things in my "hobbyist OS" for a bit now. There was a bit of hiatus when my son was born but he's getting into a sleeping schedule more conducive to projects.

First, I've been messing around with a few emulators. VirtualBox seems to be the best for my work. A few key bits of functionality make it favorable:

  • It doesn't require some of the more advanced CPU virtualization extensions needed by HyperV.
  • It does run using some virtualization, so it's much faster than something like Bochs.
  • It has a "debugger" mode with windbg style commands to dump registers, gdt, etc.

Bochs is still pretty useful, but it started throwing odd crashes during protected-to-real transitions (more on this in a bit).

http://wiki.osdev.org/ has been very helpful in all my development.

I've got a plan of attack for the OS now:

  • Using long mode (amd64).
  • Using ELF x86-amd64 Linux ABI. Cross compiler running in Cygwin has been built.
  • No real/protected mode ping-ponging.
    • The real/protected transitions were proving more annoying (and variable) between Bochs and VirtualBox. I'd get one working and then the other would fail. Since this whole idea of switching back and forth is full of issues I'm just not doing it.
    • This requires slightly odd bootstrapping of components (loading kernel, drivers from disk but not processing the ELF format at first).
    • Doing this, I hope to spend more time focused on the OS and less on the quirkiness of various BIOS implementations.
  • POSIX user mode environment with a Windows NT -style kernel.

Key components and TODO list:

  • Bootloader
    • Enable A20 support.
    • Get RAM configuration from BIOS.
    • Read kernel boot options from disk or perhaps keyboard.
    • Load kernel stub, kernel, and all modules indicated as required for boot. (The kernel and modules are streamed into higher address RAM directly from disk, no ELF image loading done yet).
    • Switch video to VESA linear frame buffer mode (preferably 1024x768 24/32bit color.
    • Fill out a table of boot and machine parameters for use by the kernel stub.
    • Setup basic identity paging for long mode (amd64).
    • Enter long mode and jump to the kernel stub.
  • Kernel Stub
    • Load kernel from raw file images the Bootloader copied from disk to full on ELF relocated modules.
    • Convert the Bootloader "boot and machine parameters" to a table suitable for calling KeMain(...) with.
    • Calling KeMain(...) to initiate kernel startup.
  • Kernel
    • Initialize a very basic set of kernel debugger stubs accessed through a serial port.
    • Initialize the memory manager based on Kernel Stub provided parameters (used memory, etc.)
    • Initialize the object manager.
    • Create a "system" process (virtual address space, etc.)
    • Call the loader functions in the kernel for each of the Bootloader provided boot critical modules.
    • Call ModuleInit(...) for each boot critical modules.
    • Call ModuleEnter(...) for each of the boot critical modules to initiate PCI/PnP/Hardware enumeration.
    • Using file system and storage modules loaded as part of the boot critical modules group, load and start additional modules required by the system.
    • Create a user mode process ("init") to bootstrap the rest of the system from user mode startup scripts.

Once I get the basic kernel booting I hope to crib quite a few module tidbits from a free *nix distribution like FreeBSD or NetBSD. I'll have to evaluate the licenses I'm choosing for my project and the ones provided by FreeBSD and/or NetBSD.

Some modules will be pretty straight forward. For example, the VESA module will mainly just be a set of helper functions for drawing and outputting text to the linear frame buffer. Other modules like EHCI/XHCI will be much more in-depth and probably won't every be tackled.

Ideally I'd like to be able to boot from USB thumb drive, access the network card, and if the POSIX environment works out, perhaps get enough libraries and components running in user mode for a web browser and self hosting a build of the OS.

Well, that's the plan anyway. I hope to make gradual progress with a few planned breaks in the next few months (moving houses... maybe a vacation...).

Wednesday, December 11, 2013

Dispatch Objects

It seems like the next step in my OS development is going to be laying down some kernel work. I thought I'd be able to delay this, but doing so would only result in less useful work being done. A kernel is responsible for many things. I'm sure many more things than I even know about right now (such is the nature of this whole undertaking). However, my experience writing Windows drivers has offered a decent skeleton to start with.

One piece of this skeleton is dispatch objects. Basically, this is any sort of thing that can be waited upon. A timer, an event, a lock, a thread (joining a thread, etc.). I figure this is as decent a place to start as any. The way a dispatch object will work in my kernel is it will be a light weight structure containing a list of waiting threads. Of course, this means I need to start defining a thread structure as well. I imagine I'll continue to flesh out the guts of the kernel (or at least their structures) by jumping off on this one spot.

Since a dispatch object is essentially a list, the first structure I need to create is a list structure. There are many ways to create a list. You can create a linked list, a vector, or various other structures. Again, leaning on my Windows background I'll choose something very similar to the LIST_ENTRY structure in the NT kernel. In more data structure books I've seen a doubly-linked list defined as:

typedef struct _LIST_NODE
{
    struct _LIST_NODE* pPrev;
    struct _LIST_NODE* pNext;
    void* pData;
} LIST_NODE;


Here, I'll rely on something clever that Windows does with it's list structure -- there's no "data" pointer. The structure is intended to be embedded directly into any other data structure:

typedef struct _LIST_NODE
{
    struct _LIST_NODE* pPrev;
    struct _LIST_NODE* pNext;
} LIST_NODE;

typedef struct
{
    LIST_NODE  ThreadListHead;
    ...
} DISPATCH_HEADER;

typedef struct
{
    UINT32     ThreadId;
    ...
    LIST_NODE  DispatchListNode;
    ...
} THREAD_OBJECT;


The way this works is the list pointers are offset from to find the original object the list points to. In the DISPATH_HEADER above, this is simply the same address. However, say I had additional members before the ThreadListHead member, you'd then SUBTRACT back to get the original object pointer. This is done using a member offset macro, where the offset of a member into a structure is computed and then subtracted off the list pointer. While this may appear clunky at first it has one very nice advantage - you don't need to dynamically allocate small chunks of memory. In the example above, you would simply have a list insertion function that you'd pass a pointer to the THREAD_OBJECT::DispatchListNode member variable to.

There are multiple reasons this is a good thing. First, the previously mentioned avoidance of small memory allocations. Second, by not relying on any sort of memory allocation this data structure can be used in the kernel before memory allocation is even possible -- obviously, you need to write an allocator before you can use one. Third (but related to the first), the absence of smaller allocations for what is a widely used construct can add up to some pretty substantial memory savings. Any memory allocation will require a chunk of memory for bookkeeping. This is usually going to be at least the size of a few pointers. For small allocations of only 3 pointers (in the first list structure definition) the overhead of the allocation may very well be larger than the actual data usage. Considering how widely used the LIST_NODE structure will be, this is a substantial savings in memory as well as access speed (there isn't an indirect memory access to get to the actual structure, if you have the pointer to the list, it's only a subtraction to get the actual pointer versus another read).

So, there it is, the beginnings of a DISPATCH_HEADER, a THREAD_OBJECT, and a LIST_NODE. This kernel is practically writing itself...

Friday, November 29, 2013

Rolling Your Own Operating System

I started an extremely ambitious project a while back -- working on my own operating system. This isn't one that I ever intend to gain any ground or even boot on real hardware. (I'm targeting VirtualBox, VirtualPC/HyperV, and Bochs emulator environments right now.) It's just a place for me to play around with OS concepts. At work I write driver code for windows, but this code relies on a system of interfaces and mechanisms that have been designed and implemented by people much smarter than I. I'd like to start working on those more basic concepts. Sort of like peeling back the layers of an onion by recreating it. I anticipate I will learn much not only about how my OS works and can work, but about the tradeoffs that most OS designers have encountered along the way.

My current "OS" is very limited. It uses the standard DOS (well, Windows 7 -- because there are differences...) bootloader from the partition table. I created my own volume boot record to receive control from there. I'm able to load from the beginning of the partition. I'm still working on the loader. This is not be choice, but actually some unexpected first bit of learning. Here's the deal...

The BIOS loads and calls into the MBR code. The MBR code loads and calls into my volume boot record code. Now the fun starts. There are numerous extremely important interfaces the BIOS provides to early boot code. Before protected mode, paging, multi-tasking, and all the other things that really make an operating system an operating system you could just happily keep calling these BIOS interfaces to get this functionality. However, the BIOS code is legacy... It's so legacy that it isn't all that compatible with the way a modern OS sets up the CPU. (There are a few more compatible extensions -- VESA graphics BIOS stuff for example, but even this is severely limited)

So, it's already decision time. A lot sooner than I thought it would be. How do I start laying the ground work for the more modern features of my OS when I need to use the BIOS routines to talk to the hardware? Well, the short answer is I need to stop at some point, and probably the sooner the better. I don't want to litter my kernel code with a bunch of BIOS dependencies -- anything I write that requires this cannot really be reused anywhere else. Ideally, I would switch over from BIOS interfaces to my own all at once, however that will be difficult. Instead, I've decided a better approach is to rely on the BIOS interfaces until I'm ready to create my own. So, what does the BIOS give me that I need to reproduce?

  1. VGA/VESA support -- in order to know my OS is doing something, I should probably have a way to see it working. 1
  2. Keyboard support.
  3. Disk IO support.
  4. ...

I'm sure there are many more but these are the interfaces I'm immediately aware of. The first item, VGA support was easiest. This is probably because it's also the only one that's moved past legacy support. I am able to select a video mode which allows me to draw 24/32 bit RGB directly into a flat buffer space. This buffer space is directly mapped to the screen buffer, so I'm set as far as rudimentary drawing is concerned. I don't get any hardware acceleration and I'm stuck having to implement all my own line drawing, bit-blitting, and other APIs, but at least it's doable. The most annoying thing about this is I cannot adjust for changing the monitor resolution, but if I look at my above goals of not really caring if this runs on real hardware this is acceptable.

Now comes disk IO. This is a bit sticky, since there is no easy way to go about this. I'm probably going to have to support at least IDE and AHCI. I'm not exactly sure if I can get away with IDE only and rely on hard disks to support the legacy commands... I think so, but not sure. I also cannot keep thunking back and forth between BIOS code for this support -- every time I do that I'll probably have to tear down the things I really wanted to learn about -- memory paging, scheduling, etc. I see some work on a storage driver in my near future... It does indeed look like I can just go with IDE. Hopefully, IDE will be pretty easy to work with. It looks like the specs are open as well -- http://www.t10.org/t13/technical/d98120r0.pdf

Here's what my volume boot record code looks like so far: http://www.darkautomata.com/blog/os/vbr_2013_11_29.asm.txt


1 I can just use a COM port interface for logging what is actually happening in the OS, but there is going to come a time relatively early in the OS where I'll actually want to SEE what I've created. Maybe this isn't as big of a requirement as I think it is right now...