Going Further on x86: Difference between revisions

From OSDev.wiki
Jump to navigation Jump to search
[unchecked revision][unchecked revision]
(Add information about code structuring)
m (moved User:Glauxosdever/Going Further on x86 to Going Further on x86: Put it in the main namespace; it doesn't make a real difference)
(No difference)

Revision as of 18:34, 4 June 2016

You have completed bare bones for x86. Now what, you may be wondering. Welcome to the OS development world!

The following tutorial assumes you are doing the things discussed below in order from top to down.

Preparing For Real

Before going any further:

  • You should grab a copy of the Intel manuals. Most of the processor-specific things discussed below are best described in the Intel manuals.
  • You should ensure you have enough patience and time. Operating system development is one of the most time-consuming projects.
  • You should decide how your code should be structured. Consider that you will eventually port your operating system to different architectures, with different assembly instructions, with different initialisation sequences, with different hardware, with different memory structures, etc. You have to ensure you don't mix files from one architecture with files from another architecture. Meaty skeleton serves as a minimal example of how to structure your code.

Paging

From here on, we will assume you have completed the bare bones tutorial, and have obtained a copy of the Intel manuals.

  • You may wish to enable paging early in boot.s. Paging is nice because it lets you map memory however you want and it lets processes see the full address space. It also provides advanced protection capabilities.
  • You should continue with a higher half kernel, so user-space programs can be loaded at 4 MiB (or lower if you like) without conflicting with the kernel binary. An important decision to take is where to exactly map the kernel.
    • Many prefer mapping the kernel to 0x80000000, leaving 2 GiB for kernel data and 2 GiB for processes. Their main argument is that ptrdiff_t issues are prevented when user-space is not allowed to use more than 2 GiB.
    • Others prefer mapping it to 0xC0000000, leaving 1 GiB for kernel data and 3 GiB for processes. Their main argument is that it integrates better with PAE, since the whole kernel space fits exactly in one page directory.
    • Some others (including the original author of this page) go extreme and map the kernel at 0xE0000000, leaving the tight space of 512 MiB for kernel data and 3.5 GiB for processes. Their main argument is that user-space should be able to use as much memory as possible.

Considering Security

You should have paging enabled at this point. You are also using an ELF format for the kernel binary, right? Do you know what do .text, .data, .rodata, .bss mean? Right, they are the sections of the executable. In .text the instructions for the processor are stored, in .data there is data, in .rodata there is read-only data, in .bss there is uninitialised data. There may be more sections, but let's focus on these for now.

  • You should tell the linker to align them at 4 KiB boundaries. This way the next step is much easier to accomplish.
  • Then, apply read-only/read-write permissions in your page tables, according to your sections. For x86-64 and PAE make sure you disable execution of non-.text sections.
    • Hint: Tell the linker to insert symbols indicating the start and the end of specific sections, so you can access them from your code.
  • Finally, set also the WP bit in the appropriate register, so these permissions are applied for the kernel too (and not only for the userspace). Better coding practices are enforced this way and in general the whole kernel is considered to be more secure.

More x86 Specific Things

An operating system should be self-reliant as much as possible. The bootloader may have left the environment in a "working" state, but it is not convenient in the long run.

  • Create a GDT before the first segment change, because the one that GRUB has setup is no longer valid (the entries are simply cached in the processor still, that's why it "works").
    • Hint: You need at least these entries: null segment entry, kernel code segment entry, kernel data segment entry, user code segment entry, user data segment entry, task state segment entry.
  • Create an IDT and write interrupt handlers. Every real operating system handles exceptions (for example page faults), and reads from peripherals only when data is received (instead of polling).
    • Hint: Make sure to save all registers at the start of the handlers, and restore them at the end of the handlers.
    • Hint: Keep in mind that some exceptions cause an error code to be pushed to the stack, while some others do not.
  • Initialise a timer to generate an interrupt at a convenient interval. PIT ticking every 1 or 10 ms is easy enough for a start.
    • Hint: PIT is not a very accurate timer, therefore it should not be used for every timing purpose later on.

Managing Memory

Soon you will need to allocate something.

  • You first need to obtain the memory map, so you know which physical areas are free.
  • You for sure also need a list of free physical pages, so you know where to physically allocate next.
    • A common way to do it is to create a linked list, that is to store the physical address of the next free page at the start of the previous free page, so only free memory is used to store it. But, you have paging enabled, so you can't arbitrarily write to every part of memory. You can instead map a bit of memory at a time, and write to it the address of the next free page.
  • Now, creating a physical memory manager, a virtual memory manager and a kernel heap allocator is totally in order.

Conclusion

Operating system development is not easy and is not hard. It's pretty hard. The above (incomplete) list is nothing compared to the complexity involved in a mature operating system.