How To Build An Operating System: Segmentation- Part 04
This is the fourth article of this building an own operating system article series. If you didn’t read the first three articles in this series, I recommend you to read those articles first.
What is Segmentation in OS?
Memory management is the functionality of an operating system that handles or manages primary memory and moves processes back and forth between main memory and disk during execution. Memory management keeps track of each and every memory location, regardless of either it is allocated to some process or it is free. It checks how much memory is to be allocated to processes. It decides which process will get memory at what time. It tracks whenever some memory gets freed or unallocated and correspondingly it updates the status.
- Like Paging, Segmentation is another non-contiguous memory allocation technique.
- In segmentation, the process is not divided blindly into fixed-size pages.
- Rather, the process is divided into modules for better visualization.
Characteristics
- Segmentation is a variable size partitioning scheme.
- In segmentation, secondary memory and main memory are divided into partitions of unequal size.
- The size of partitions depends on the length of the modules.
- The partitions of secondary memory are called segments.
To enable segmentation you need to set up a table that describes each segment-a segment descriptor table. In x86, there are two types of descriptor tables: the Global Descriptor Table (GDT) and Local Descriptor Tables (LDT). An LDT is set up and managed by user-space processes, and all processes have their own LDT. LDTs can be used if a more complex segmentation model is desired-we won’t use it. The GDT is shared by everyone-it’s global.
Accessing Memory
Most of the time when accessing memory there is no need to explicitly specify the segment to use. The processor has six 16-bit segment registers: cs
, ss
, ds
, es
, gs
and fs
. The register cs
is the code segment register and specifies the segment to use when fetching instructions. The register ss
is used whenever accessing the stack (through the stack pointer esp
), and ds
is used for other data accesses. The OS is free to use the registers es
, gs
and fs
however, it wants.
Below is an example showing implicit use of the segment registers:
func:
mov eax, [esp+4]
mov ebx, [eax]
add ebx, 8
mov [eax], ebx
ret
The above example can be compared with the following one that makes explicit use of the segment registers:
func:
mov eax, [ss:esp+4]
mov ebx, [ds:eax]
add ebx, 8
mov [ds:eax], ebx
ret
You don’t need to use ss
for storing the stack segment selector, or ds
for the data segment selector. You could store the stack segment selector in ds
and vice versa. However, in order to use the implicit style shown above, you must store the segment selectors in their indented registers.
Local Descriptor Table (LDT)
A Local Descriptor Table (LDT) is a memory table used in the x86 architecture in protected mode and containing memory segment descriptors. An LDT is set up and managed by user-space processes, and all processes have their own LDT. There will be generally one LDT per user process, describing privately held memory. The operating system will switch the current LDT when scheduling a new process, using the LLDT machine instruction or when using a TSS. LDTs can be used if a more complex segmentation model is desired. So, in this article, we will use GDT.
The Global Descriptor Table (GDT)
The list of selectors that the processor uses is not actually kept inside the processor. Instead, it’s kept somewhere in memory (in an area carefully guarded by our kernel) and our code lets the CPU know at some point where to find it. This is in fact a required step for getting into protected mode. This list of selectors in memory is called the global descriptor table (GDT) and it must carefully follow a specific format.
A GDT/LDT is an array of 8-byte segment descriptors. The first descriptor in the GDT is always a null descriptor and can never be used to access memory. At least two segment descriptors (plus the null descriptor) are needed for the GDT because the descriptor contains more information than just the base and limit fields. The two most relevant fields for us are the Type field and the Descriptor Privilege Level (DPL) field.
The Type field can’t be both writable and executable at the same time. Therefore, two segments are needed: one segment for executing code to put in cs
(Type is Execute-only or Execute-Read) and one segment for reading and writing data (Type is Read/Write) to put in the other segment registers.
The DPL specifies the privilege levels required to use the segment. x86 allows four privilege levels PL0, PL1, PL2, and PL3, where PL0 is the most privileged. The kernel should be able to do anything, therefore it uses segments with DPL set to 0. This is also called kernel mode. The current privilege level (CPL) is determined by the segment selector in CS.
The segments overlap -they both encompass the entire linear address space. In our minimal setup, we’ll only use segmentation to get privilege levels.
Loading the GDT
The processor is loaded into the GDT with the lgdt assembly code instruction which contains the struct address which determines the beginning and the size of the GDT. (Inside the memory_segment.h file)
struct gdt {
unsigned int address;
unsigned short size;
} __attribute__((packed))
If the eax registry content contains the address of a structure of this kind, the GDT may be loaded with the following assembly code:
lgdt [eax]
It might be easier if you make this instruction available from C, the same way as was done with the assembly code instructions in
and out
.
After the GDT has been loaded the segment registers need to be loaded with their corresponding segment selectors. The content of a segment selector is described in the figure and table below:
Bit: | 15 3 | 2 | 1 0 |
Content: | offset (index) | ti | rpl |
The offset of the segment selector is added to the start of the GDT to get the address of the segment descriptor: 0x08
for the first descriptor and 0x10
for the second, since each descriptor is 8 bytes. The Requested Privilege Level (RPL) should be 0
since the kernel of the OS should execute in privilege level 0.
Loading the segment selector registers is easy for the data registers — just copy the correct offsets to the registers:
mov ds, 0x10
mov ss, 0x10
mov es, 0x10
.
.
.
To load cs
we have to do a “far jump”:
; code here uses the previous cs
jmp 0x08:flush_cs ; specify cs when jumping to flush_cs flush_cs:
; now we've changed cs to 0x08
A far jump is a jump where we explicitly specify the full 48-bit logical address: the segment selector to use and the absolute address to jump to. It will first set cs
to 0x08
and then jump to flush_cs
using its absolute address.
After all steps, the gdt.s file will look like this,
By following this repository you can find the full code set that you need for this segmentation process.
Thanks for reading! See you next week with part 05.
Reference: The little book about OS development/Erik Helin, Adam Renberg