Build your own OS (part 4)
Integrating segmentation
Hello and welcome to the fourth instalment of my article series on OS development. In the last article, we used C and assembly languages to integrate outputs through Framebuffer and the Serial Port. If you missed it, here’s the link to that article:
In this article, I’ll explain the concept of segmentation, how it works internally, and how we can practically implement it in the operating system we’re working on.
What exactly is segmentation?
Segmentation in operating systems means accessing the memory through segments. Segments are chunks of address space defined by a base address and a limit.
To address a byte in segmented memory, you use a 48-bit logical address: 16 bits for the segment and 32 bits for the offset within that segment (See the figure below).
Bit: | 48 47 46... 18 17 16| 15 14 13 ... 2 1 0 |
Content: | offset | base address |
The offset is added to the segment’s base address, and the resulting linear address is compared to the segment’s limit.
As a result, we have a linear address.
When paging is disabled, the linear address space is mapped 1:1 onto the physical address space, allowing access to physical memory (We will discuss paging in an upcoming article, don't worry about it for now).
Segment Descriptors
To enable segmentation, we must first create a table that describes each segment — a segment descriptor table.
In x86, there are two types of descriptor tables:
- Global Descriptor Tables (GDT)
- Local Descriptor Tables (LDT)
An LDT is set up and managed by user-space processes, and it can be used if a more complex segmentation model is desired. Since GDT is global, we will use it for our implementation.
The Global Descriptor Table (GDT)
A GDT is a collection of 8-byte segment descriptors. The first descriptor of GDT is always null and can never be used to access memory. At least two segment descriptors (plus the null descriptor) are needed for the GDT. The Type field and the Descriptor Privilege Level (DPL) field are the two most important fields for us. The required segments are listed in the table below:
Accessing memory with segments
When accessing memory, most of the time there is no need to explicitly specify the segment to use. There are 16-bit segment registers on the processor. There are six of them, and listed below are those with their uses:
cs : specifies the segment to use
ss : used whenever accessing the stack
ds : used for other data access
es,gs, and fs: The OS is free to use these registers however it want
Most of the time when accessing memory there is no need to explicitly specify the segment to use.
Practical Implementation
Let’s now see how we can load the global descriptor table and practically implement segmentation in our operating system. Following is the assembly code to load the segment selectors:
Save the above code in a file named gdt.s
.
Now we need to implement the C header file to define a structure for the GDT. code. We can use the following header file to achieve that:
Copy the above code to a file named memory_segments.h
and save it in your working directory.
Now that we have defined the structure for the GDT, we need to load values into it. Use the following code can be used to achieve that:
Those are the required files for implementing segmentation. Now we need to link these files to our Makefile so that they can be compiled. For this, we only have to adjust the first line of the Makefile with the relative file paths for the files which need to be compiled, namely and gdt.s
and memory_segments.c
.
Following is the first line of my Makefile after adding the new file names (note that yours might be vary depending on the file paths).
OBJECTS = loader.o kmain.o drivers/io.o segmentation/gdt.o segmentation/memory_segments.o
Now call the segments_install_gdt() function from your main C file (kmain.c
) after importing the header file that we created (memory_segments.h
).
After completing all the above steps, you can run your OS with the same command as before (make run). However, Since this is an internal change, you won't be able to notice any change in the operating system. If the code runs without errors, that guarantees that you have done the job correctly.
Following is the implementation that I’ve done so far. This will you a good idea of how to put the theories into practice:
That’s it for this article. Now our operating system is capable of accessing memory through segments. In the next article, I will explain interrupts and how we can get input to our OS from the user. Until then, Goodbye & Stay Safe!
Reference: The little book about OS development (Erik Helin, Adam Renberg)
-Pasan Devin Jayawardene-