Build Your Own OS #4

Niroshan Pushparaj
5 min readAug 13, 2021

--

Hello everyone, In the previous part we discuss about printing characters and serial port communication. End of that process we got file called com1.out . In this article we discuss about Segmentation in x86 Operating Systems which means accessing the memory through the segments.

Segmentation Process

When a program is loaded into memory, the segmentation system tries to locate space that is large enough to hold the first segment of the process, space information is obtained from the free list maintained by memory manager. Then it tries to locate space for other segments. Once adequate space is located for all the segments, it loads them into their respective areas.

With the help of segment map tables and hardware assistance, the operating system can easily translate a logical address into physical address on execution of a program.

The Segment number is mapped to the segment table. The limit of the respective segment is compared with the offset. If the offset is less than the limit then the address is valid otherwise it throws an error as the address is invalid. In the case of valid addresses, the base address of the segment is added to the offset to get the physical address of the actual word in the main memory.

The above figure shows how address translation is done in case of segmentation.

Segmentation in x86

In x86, segmentation refers to the use of segments to access memory. Segments are overlapping parts of the address space defined by a base address and a limit. A 48-bit logical address is used to address a byte in segmented memory: The segment is specified by 16 bits, while the offset inside that segment is specified by 32 bits. The offset is added to the segment’s base address, and the resulting linear address is compared to the segment’s limit, as shown in the diagram below.

To allow segmentation, you’ll need to create a segment descriptor table, The Global Descriptor Table (GDT) and Local Descriptor Tables (LDT) are the two types of descriptor tables in x86 . User-space processes create and manage LDTs, and each process has its own LDT. If a more complex segmentation model is desired, LDTs can be employed; however, we will not use them. Everyone has access to the Global Descriptor Table because it is global.

Accessing Memory

When accessing memory, most of the time there is no need to specify the segment to use directly. cs, ss, ds, es, gs, and fs are the six 16-bit segment registers on the processor. The code segment register (cs) defines which segment should be used for fetching instructions. When accessing the stack (through the stack pointer esp), the register ss is used, while ds is used for other data access.

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 matched 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

The Global Descriptor Table (GDT)

A GDT/LDT is an eight-byte segment descriptor array. The GDT’s initial descriptor is always null, and it 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 privilege levels required to use the section are specified in the DPL. x86 supports four privilege levels (PL) ranging from 0 to 3, with PL0 being the most privileged.

The segments needed are described in the table

We’ll simply use segmentation to get privilege levels in our basic configuration.

Loading the GDT

The lgdt assembly code instruction loads the GDT into the Processor by taking the address of a struct that specifies the GDT’s start and size.

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

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]

After the GDT has been loaded the segment registers needs to be loaded with their corresponding segment selectors. The content of a segment selector is described in below table:

Bit:     | 15                                3 | 2  | 1 0 |
Content: | offset (index) | ti | rpl |

rpl — Requested Privilege Level-we want to execute in PL0 for now.

ti — Table Indicator. 0 means that this specifies a GDT segment, 1 means an LDT Segment.

offset (index) — Offset within descriptor table.

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.

gdt_asm.s file to load GDT in assembly code as shown below,

then, I create header files for memory segmentation. Finally you are able to call GDT function from kmain.c file. You can see my newly created files for this segmentation part here.

Hope you all understand this blog and I will publish more articles to achieve our main goal “Build Your Own OS”. Keep reading.

THANK YOU!!!

Reference

--

--