Create Your Own Operating System

Malshani Dahanayaka
7 min readAug 13, 2021

--

PART 04 — Segmentation

In this article, I’m going to explain you about Segmentation in x86 which means accessing the memory through segments. In Operating Systems, Segmentation is a memory management method wherein the memory is separated into variable size parts. Each part is known as a segment which can be apportioned to a process.

There are types of segmentation:

  1. Virtual memory segmentation –
    Each process is divided into a number of segments, not all of which are residents at any one point in time.
  2. Simple segmentation –
    Each process is divided into a number of segments, all of which are loaded into memory at run time, though not necessarily contiguously.

Translation of logical addresses to linear addresses.

Segment Table

There is a table called segment table which stored the details about each segment. The segment table is stored in one (or many) of the segments.

This segment table contains mainly two information about the segment:

  1. Base: It is the base address of the segment
  2. Limit: It is the length of the segment.

When we consider, why Segmentation is required?…that’s because till now, we were using Paging as our main memory management technique. It separates all the processes into the type of pages regardless of the fact that a process can have some overall parts of functions which should be stacked on the same page.

The operating system doesn’t care about the User’s view of the process. It might partition the same function into various pages and those pages could possibly be loaded at the same time into the memory. It diminishes the effectiveness of the system.

It is smarter to have segmentation what partitions the process into segments. Each segment contains the same type of functions.

Translation of Logical address into a physical address by segment table

CPU generates a logical address which contains two parts:

  1. Segment Number
  2. Offset

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.

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

The operating system also generates a segment map table for each program. This figure shows how address translation is done in case of segmentation.
The operating system also generates a segment map table for each program. This figure shows how address translation is done in case of segmentation.

As you can see we could use either segment to reach physical addresses between 0x10100 and 0x1FFFF since the segments overlap.

The x86 line of computers have 6 segment registers (CS, DS, ES, FS, GS, SS). They are totally independent of one another.

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:

The Global Descriptor Table (GDT)

The Global Descriptor Table is a data structure which is used by Intel x86-family processors starting with the 80286 for the purpose of defining the characteristics of the various memory segments which are used during program execution, including the size, the base address, and access privileges like write and executable.

Structure

The GDT is loaded using the LGDT assembly instruction. It expects the location of a GDT description structure:

The offset is the linear address of the table itself, which means that paging applies. The size is the size of the table subtracted by 1. This is because the maximum value of size is 65535, while the GDT can be up to 65536 bytes (a maximum of 8192 entries). Further, no GDT can have a size of 0.

The table contains 8-byte entries. Each entry has a complex structure:

The NULL-descriptor

The first entry in the Global Descriptor Table (GDT) is called the null descriptor. The NULL descriptor is unique to the GDT, as it has a TI=0, and INDEX=0. Most printed documentation states that this descriptor table entry must be 0. Even Intel is somewhat ambiguous on this subject, never saying what it CAN’T be used for. Intel does state that the 0'th descriptor table entry is never referenced by the processor.

The code descriptor

The code descriptor should be configured like this:

  • Base address = 0x0
  • Limit = 0xffff (with page granularity turned on, this is actually 4GB)
  • Access byte
  • Present = 1
  • Privilege level = 0 (privilege level 0 is for kernel code)
  • Executable = 1 (this is a code segment)
  • Direction = 0
  • Readable = 1 Combining all these bits gets us the value 1001 1010b, or 0x9a.
  • Flags
  • Granularity = 1 (for 4KB pages)
  • Size = 1 (32-bit style) Combining all these bits gets us the value 1100 1111b, or 0xcf.

The data descriptor

The data descriptor should be configured like this:

  • Base address = 0x0
  • Limit = 0xffff (with page granularity turned on, this is actually 4GB)
  • Access byte
  • Present = 1
  • Privilege level = 0 (privilege level 0 is for kernel code)
  • Executable = 0 (this is a data segment)
  • Conforming = 0
  • Writable = 1 Combining all these bits get us the value 1001 0010b, or 0x92.
  • Flags
  • Granularity = 1 (for 4KB pages)
  • Size = 1 (32-bit style) Combining all these bits get us the value 1100 1111b, or 0xcf.

The DPL specifies the privilege levels required to use the segment. x86 allows for four privilege levels (PL), 0 to 3, where PL0 is the most privileged. In most operating systems (eg. Linux and Windows), only PL0 and PL3 are used. However, some operating systems, such as MINIX, make use of all levels. The kernel should be able to do anything, therefore it uses segments with DPL set to 0 (also called kernel mode). The current privilege level (CPL) is determined by the segment selector in cs.

The segments needed are described in the table below.

Loading the GDT

Loading the GDT into the processor is done with the lgdt assembly code instruction, which takes the address of a struct that specifies the start and size of the GDT. It is easiest to encode this information using a “packed struct” as shown in the following example:

If the content of the eax register is the address to such a struct, then the GDT can be loaded with the assembly code shown below:

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 needs to be loaded with their corresponding segment selectors. The content of a segment selector is described in the figure and table below:

The layout of segment selectors.

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:

To load cs we have to do a “far jump”:

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.

Now you can create gdt_asm.s file using these codes.

gdt_asm.s

Also, you can make gdt.c file for GDT.

It is easiest to encode this information using a “packed struct”.

gdt.c file

And also you need to update Makefile in OS files.

Add gdt.o and gdt_asm.o file names under the OBJECTS in Makefile.

After the setup of all files, you can boot the OS using “make run” command. If the process ended successfully you can see the below output in BOCHS interface.

Hope you all get a good idea on how to access the memory through segments in the operating system.

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

Thank You For Reading………..

— Malshani dahanayaka —

--

--

Malshani Dahanayaka

Software Engineering Undergraduate of University of Kelaniya