The XamOS — Let’s Build an OS!!!

The 4th step of OS development - Integrate segmentation

Nipuni Perera
9 min readAug 13, 2021

Welcome back!

We’ve gone a long way in building our own operating system, but there’s still a long way to go. In this installment of the series, we’ll look at segmentation in OS development, which is the process of accessing memory using segments.

If you’re new to OS development and my article series you better come from this first article.Otherwise it would be hard to understand what’s going on.🤔

Segmentation

In x86, segmentation refers to how memory is accessed. Based on base sites and restrictions, sections are part of the address space and may overlap. Use 48-bit logical addresses to address the bytes in the segmentation memory: 16 bits for selected segments and 32 bits for the offset inside the desired segment. After adding the offset to the base address, check the resultant linear address against the segment’s limit. The result is a linear address if everything is OK (including a momentarily disregarded access check). The linear address space translates 1: 1 to the physical address space when deactivated, allowing access to physical memory.

Segment Selector

The first component of a logical address contains a segment selector, which is the segment’s unique identity. It’s a unique pointer that identifies a memory section. This diagram depicts the content of a segment selector.

A segment selector’s value is stored in a segment register. The segment selector for that segment must be present in the relevant segment register to access that segment in memory.

The CPU contains six 6, 16-bit segment registers, each of which is completely independent of the others:

CS — Code Segment

DS — Data Segment

SS — Stack Segment

ES — Extra Segment

FS/GS — General Purpose Segments

The OS is allowed to use the ES, GS, and FS registers in any way it sees fit. When accessing memory, most of the time there is no need to specify the segment to use directly.

An example of implicit usage of the segment registers is shown below:

func:
mov eax, [esp+4]
mov ebx, [eax]
add ebx, 8
mov [eax], ebx
ret

The above example can be compared to the following one, which uses segment registers explicitly:

func:
mov eax, [ss:esp+4]
mov ebx, [ds:eax]
add ebx, 8
mov [ds:eax], ebx
ret

It is not necessary to utilize SS for storing the stack segment selector or DS for storing the data segment selector in explicit use. Stack segment selector, on the other hand, can be stored in DS and vice versa. To utilize the implicit approach described above, however, the segment selectors must be stored in their indented registers.

Segment Descriptor

The memory segment referred to in the logical address is described by segment descriptors. The following fields are found in the segment descriptor:

01.A base address for a segment

02.The segment size limit is a parameter that determines the size of a segment.

03.The protection method information is stored in the access rights byte.

04.Control bits

The x86 and x86–64 segment descriptors are written as follows:

The terms “fields” refer to:

Base Address: 32-bit starting memory address of the segment

Segment Limit: The segment is 20 bits long. (More precisely, the address of the most recently accessed data, thus the length is one more than the value stored here.)

G=Granularity: If clear, the limit is in units of bytes, with a maximum of 220 bytes. If set, the limit is in units of 4096-byte pages, for a maximum of 232 bytes.

D=Default operand size: If clear, this is a 16-bit code segment; if set, this is a 32-bit segment.

B=Big: If set, the maximum offset size for a data segment is increased to 32-bit 0xffffffff. Otherwise, it’s the 16-bit max 0x0000ffff.

L=Long: If set, this is a 64-bit segment (and D must be zero), and code in this segment uses the 64-bit instruction encoding. “L” cannot be set at the same time as “D” and “B”.

AVL=Available: For software used, not used by hardware

P=Present: If clear, a “segment not present” exception is generated on any reference to this segment

DPL=Descriptor privilege level: Privilege level required to access this descriptor

Type: If set, this is a code segment descriptor. If clear, this is a data/stack segment descriptor. Can not be both writable and executable at the same time.

C=Conforming: Code in this segment may be called from less-privileged levels.

E=Expand-Down: If clear, the segment expands from base address up to base + limit. If set, it expands from maximum offset down to limit, a behavior usually used for stacks.

R=Readable: If clear, the segment may be executed but not read from.

W=Writable: If clear, the data segment may be read but not written to.

A=Accessed: This bit is set to 1 by hardware when the segment is accessed, and cleared by software.

To allow segmentation, create a table called a segment descriptor table that describes each segment. The Global Descriptor Table (GDT) and Local Descriptor Tables are the two types of descriptor tables in x86 (LDT).

Local Descriptors Table

A Local Descriptor Table (LDT) is a memory table that contains memory segment descriptors in protected mode on the x86 architecture. User-space processes create and maintain LDTs, and each process has its own LDT. In most cases, one LDT per user process will describe privately owned memory. When scheduling a new process, using the LLDT machine instruction, or using a TSS, the operating system will alter the current LDT. If a more complicated segmentation model is needed, LDTs can be utilized. As a result, we’ll be using GDT in this tutorial.

Global Descriptors Table

The Global Descriptor Table (GDT) specifies the base address, size, and access rights such as executability and writability for the different memory regions known as segments that are used during program execution. Because the GDT is shared by everyone, it is considered global. Both GDT and LDT are 8-byte segment descriptor arrays.

The GDT’s initial descriptor is always a null descriptor that can never be used to access memory. Because the descriptor carries more information than simply the base and limit fields, the GDT requires at least two segment descriptors in addition to the null descriptor.
The Type field and the Descriptor Privilege Level (DPL) field, which I discussed previously in the essay, are the two most important elements in segment descriptors.
At the same time, a type field cannot be both readable and executable. As a result, two segments are required: one for executing code in CS (Type is Execute-only or Execute-Read) and another for reading and writing data in the other segment registers (Type is Read or Write).
The privilege levels necessary to utilize the segment are specified in the DPL. The four privilege levels in x86 are PL0, PL1, PL2, and PL3, with PL0 being the most privileged. Because the kernel is supposed to be able to perform anything, it utilizes segments with DPL set to 0. Kernel mode is another name for this. The segment selector in CS determines the current privilege level (CPL).
The required segments are shown in the table below:

If you recall, it was said at the start that segments overlap and that they both cover the full linear address space. We’ll merely utilize segmentation to acquire privilege levels in our basic configuration.

Loading the GDT

The lgdt assembly code instruction loads the GDT into the CPU by taking the address of a struct that defines the GDT’s start and size. This information may be easily encoded using a “packed struct,” as illustrated in the following example:

struct gdt {
unsigned int address;
unsigned short size;
} __attribute__((packed));

What is a packed struct?

This part will teach you the fundamentals of structure packing and padding in C programming.
Configuration bytes are a set of bits that are arranged in a specified order. An example with 32 bits is shown below.

Bit:     | 31 24 | 23 8    | 7 0    |
Content: | index | address | config |

For managing such setups, it is considerably easier to utilize “packed structures,” as illustrated below, rather than an unsigned integer ( unsigned int).

struct example {
unsigned char config; /* bit 0–7 */
unsigned short address; /* bit 8–23 */
unsigned char index; /* bit 24–31 */
};

Structure padding is a C concept that aligns data in memory by inserting one or more empty bytes between memory locations. The compiler may add padding between items when utilizing the struct for a variety of reasons. However, when using a struct to represent configuration bytes, the compiler mustn’t add any padding because the struct will be regarded by the hardware as a 32-bit unsigned integer in the end.
The property packed can be used to compel GCC to not add any padding between items, as illustrated below.

struct example {
unsigned char config; /* bit 0–7 */
unsigned short address; /* bit 8–23 */
unsigned char index; /* bit 24–31 */
} __attribute__((packed));

Because attribute ((packed)) isn’t part of the C standard, it may not be compatible with all C compilers.

If the address of such a struct is contained in the EAX register, the GDT can be loaded using the assembly code shown below:

lgdt [eax]

It would be ideal if you could access this lgdt instruction from C in the same manner that you could access assembly code instructions in and out in the previous article. The cdecl calling standard allows lgdt assembly code instructions to be wrapped in a function in assembly code that can be accessed from C. To begin, create a file in your working directory called gdt.s (these names may be altered, but these are more clear) and store the following code in it:

;Load the Global Descriptor Table global segments_load_gdt segments_load_gdt:      lgdt [esp + 4]

Under the segment selector section, the layout of a segment selector was discussed. To retrieve the address of the segment descriptor, the offset of the segment selector is appended to the start of the GDT (0x08 for the first descriptor and 0x10 for the second, since each descriptor is 8 bytes). The RPL (Requested Privilege Level) should be 0 since the OS kernel should run at privilege level 0.
For the data registers, loading the segment selector registers is simple; we just transfer the necessary offsets to the registers as follows:

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

To do so, add the following assembly code to your gdt.s file:

The following is an example of how to load GDT using the C programming language. Let’s start by making a segmentation. h file in our working directory and saving the following code in it:

Then we may add function definitions for the aforementioned declarations to the segmentation. c file. It may be done with the following source code:

We must perform a “far leap” to load CS. A distant jump is one in which the complete 48-bit logical address, the segment selector to utilize, and the absolute address to jump to are all explicitly specified. For a long leap, we may use the following code:

Update your gdt.s file as follows

This will first set CS to 0x08 and then jump to flush_cs using its absolute address.

Using the “make run” command boot your OS, if the process ends successfully you have integrated segmentation.

You’ve now completed the integration of segmentation in x86, which allows you to access memory via segments. You’ll be able to learn about interrupt handling and reading keyboard inputs in the following article.

Thank you very much for reading!

I’ll hope to get back to you with the chapter four, “interupt_and_inputs” as soon as possible.Till then,

Stay Safe!!! 👋

-Nipuni Perera-

--

--