Sitemap
CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

Writing your own Operating System: Segmentation

6 min readAug 13, 2021

--

Press enter or click to view image in full size
People vector created by pch.vector — freepik.com

This article is part of a series of articles explaining the development of an x86 Operating System. It can be used as a stand-alone guide but it will make a lot more sense if you follow the series from the beginning. If you are all caught up, we can move along to our next step in OS development.

So far we have only been working in real mode, which limits us to 1MB of RAM. Since this isn’t nearly enough to do anything useful, we need to make the jump into protected mode, which will allow us to access the rest of the memory. However, before we can do that, the processor needs at least two things set up: segmentation and interrupts. In this article, I’ll be talking about Segmentation.

Segmentation

Imagine the whole memory as a loaf of bread. And you are cutting it into slices.

Segmentation is a method of organizing memory. Just like the name suggests, we access the memory through segments. If the memory is the loaf of bread, each slice is a segment. Each segment has a base address and a limit.

A 48-bit logical address is used to address a byte in segmented memory. This 48-bits defines both the segment and the offset within that segment. See this diagram below to see how this 48-bit logical address is transformed to a linear address (which will be checked against the segment’s limit).

Press enter or click to view image in full size

To allow segmentation, you’ll need to create a table that that describes each segment. In x86, there are two types of descriptor tables: the Global Descriptor Table (GDT) and Local Descriptor Tables (LDT). LDTs are used in more complex segmentation models. The GDT is global and shared by everyone.

The Global Descriptor Table (GDT)

Just to be clear the GDT is not strictly a requirement for writing a kernel, it’s just a requirement for writing a useful one.

The GDT defines base access privileges for certain parts of memory. This allows the kernel to handle an exception if a process is attempting to violate these constraints, and kill the process in some way. Most modern operating systems use a mode of memory called “Paging” to do this. It is a lot more versatile and allows for higher flexibility. I’ll be talking about Paging in another article soon.

Now that we have a basic idea about segmentation and the GDT, let’s move on to the actual coding.

Accessing Memory

First, we need to reserve space in memory for the GDT. For that, we’ll be writing an assembly code function called gdt_flush().

gdt_flush() is the function that actually tells the processor where the new GDT exists, using our special pointer that includes a limit.

The GDT can be loaded with the assembly code shown below:

lgdt [eax]

Then we need to reload new segment registers.

The processor has six 16-bit segment registers: cs, ss, ds, es, gs and fs. The CS register is also known as the Code Segment. The Code Segment tells the processor which offset into the GDT that it will find the access privileges in which to execute the current code.

The DS register is the same idea, but it’s not for code, it’s the Data segment and defines the access privileges for the current data. ES, FS, and GS are simply alternate DS registers and are not important to us.

Loading the segment selector registers is easy for the data registers. You just need to copy the correct offsets to the registers. 0x10 is the offset in the GDT to our data segment.

    mov ds, 0x10
mov ss, 0x10
mov es, 0x10
.
.
.

And finally, do a far jump to reload our new code segment. 0x08 is the offset to our code segment.

    jmp 0x08:.flush   
.flush:
ret

Configuring the GDT

We need a struct that specifies the start and size of the GDT. We need to write it in the format required by the lgdt instruction.

The GDT itself is a list of 64-bit long entries. These entries define where in memory the allowed region will start, the limit of this region, and the access privileges associated with this entry.

For this tutorial, we will create a GDT with only 3 entries. Why 3? We need one ‘dummy’ descriptor, in the beginning, to act as our NULL segment for the processor’s memory protection features. We need one entry for the Code Segment, and finally, we need one entry for the Data Segment registers.

Just like when cutting bread the first slice is useless. No one needs the all crust slice. The null descriptor is never referenced by the processor. Certain emulators, like Bochs, will complain about limit exceptions if you do not have one. Some use this descriptor to store a pointer to the GDT itself (to use with the LGDT instruction).

So we now need a structure that contains the value of a GDT entry. We use the attribute ‘packed’ to tell GCC not to change any of the alignment in the structure.

Loading the GDT

It’s not enough to actually reserve space in memory for a GDT. We need to write values into each GDT entry, set the GDT pointer, and then we need to call gdt_flush() to perform the update.

A special function follows, called ‘gdt_set_gate()’, which does all the shifts to set each field in the given GDT entry to the appropriate value using easy-to-use function arguments.

For the null descriptor, we can just pass all null values. But for our second and third entries, we need to assign the correct values. The base address is 0 and the limit is 4GBytes for both of them.

For both entries Granularity = 0xCF as far as highest bits of segment limit is 0xFFFFFFFF. Note that, with the Granularity bit set to 1, a single segment descriptor can represent the entire 4 Gbyte address space.

But for Access = 0x9A privilege 0 for the code segment and Access = 0x92 for privilege 0 the data segment.

gdt_set_gate(0, 0, 0, 0, 0);
gdt_set_gate(1, 0, 0xFFFFFFFF, 0x9A, 0xCF);
gdt_set_gate(2, 0, 0xFFFFFFFF, 0x92, 0xCF);

Initialize the GDT

We need to initialize the GDT. For that, we need to add another function. I have added the initialization of our GDT pointer to it as well, so you can get a better idea of the full function.

Now that our GDT loading infrastructure is all in place. Don’t forget to add your .o files ( In my code gdt.o and memory_segments.o) to the list of files that LD needs to link to create your kernel!

Now just call the initialization function from kmain. And you are done!

Done? But how can I check? You might be a bit confused right now because there’s no visible change in output. We just changed the internal memory calling structure in our OS.

If you can run your OS without any errors as before, then you have successfully configured segmentation into your OS. But if you are having any errors don’t worry. You can obtain the complete code from my GitHub below.

Hope to see you in the next article as well!

Thank you!

Reference: Helin, E., & Renberg, A. (2015). The little book about OS development

--

--

CodeX
CodeX

Published in CodeX

Everything connected with Tech & Code. Follow to join our 1M+ monthly readers

No responses yet