Develop our own operating system! #part04

Integrate Segmentation

Waruni Lalendra
8 min readAug 13, 2021

Welcome again to the 4th article on ‘Develop your own operating system’ article series! Last week we have finished an exciting session about printing texts to the console. In this week we are going to work with memory managment process of our operating system. Memory management is the function in charge of managing the primary memory(RAM) of a computer. Before we going in any deeper we need to have a clear idea about two concepts which are called as Paging and Segmentation.

What is Paging?

Paging is a technique for allocating non-contiguous memory. It is a partitioning theme with a fixed size . Both main memory and secondary memory are split into equal fixed size divisions during paging. Pages and frames are the names given to the divisions of the secondary memory area unit and the main memory area unit, respectively.

In paging, each process is divided into sections with the sizes of sections that are same as the page size.

You may have confused with the terms of logical and physical memory.

Logical memory is a correlation between physical memory of the computer system and an address range that is accessible to devices. In a virtual memory system, it may be in main memory or secondary storage. Physical memory refers to the actual RAM of the system.

While a program is running, the CPU generates a logical address. Because the logical address does not exist physically, it is sometimes referred to as a virtual address.

The CPU uses physical address as a reference to reach the actual memory location. The Physical Address indicates the physical location of necessary data in memory. The user never deals with the physical address directly, but rather with the logical address that corresponds to it. The user program produces the logical address and assumes that the program is executing in this logical address; but, the program requires physical memory to execute; hence, the logical address must be mapped to the physical address by memory management unit(MMU).

A page table is the data structure used by a virtual memory system in a computer operating system to store the mapping between virtual addresses and physical addresses.

What is Segmentation?

Like paging, segmentation is a non-contiguous memory allocation technique. In segmentation, unlike paging, the operation is not split into fixed size pages. It is a partitioning theme with various sizes. Segmentation, like paging, does not split secondary and main memory into equal-sized segments. Secondary memory area partitions are referred to as segments. The details for each segment are kept in a table known as the segmentation table. The Segment table provides two major pieces of information about segments: Base, which is the segment’s bottom address, and Limit, which is the segment’s length.

During segmentation, the CPU creates a logical address that includes the segment number and offset. If the segment offset is less than the limit, the address is called valid; otherwise, it throws an exception since the address is incorrect.

Segmentation in x86

x86 segmentation means that accessing the memory by segments. Segments are sections of the address space, which might overlap, depending on a base address and a limit. You need a 48-bit logical address for addressing a byte within a segmented memory: 16 bits that species the segment and 32 bits that species what offset you need within the segment. The offset is added to the base address of the segment, and the resulting linear address is checked against the segment’s limit (That means that resulting value cannot go beyond the segment limit). If it all works properly you can have a linear address of the physical memory.

You must configure the table describing each segment to allow segmentation which is called as a segment descriptor table. Two types of tables are available in x86: the Global Descriptor Table (GDT) and the Local Descriptor Tables (LDT).

Global Descriptor Table (GDT)

A number of items named the Segment Descriptors are included in the GDT table. Each one contains 8 bytes and provides information about the segment start-point, the length of the segment and the segment’s access privileges.

The following NASM-syntax code represents a single GDT entry:

struc gdt_entry_struct

limit_low: resb 2
base_low: resb 2
base_middle: resb 1
access: resb 1
granularity: resb 1
base_high: resb 1

endstruc

Let’s discuss about this with more details later.

Local Descriptor Table (LDT)

A number of different memory segments will be used in each individual program from the operating system. In this local descriptor table, the features of each local memory segment are saved. The GDT contains pointers to each LDT. An LDT is established and controlled by user-space processes and has their own LDT in all processes.

In here we are only focus on implementation of the GDT.

Accessing Memory

The segment to be used is generally not needed when accessing the memory manually. The processor consists of six registers for 16-bit segments: cs, ss, ds, es, gs and fs. The register cs is the register of the code segment and species the section to be used for instructions. When the stack is accessed through the stack pointer esp, the registry ss is used and ds are used for other data accesses. The OS can use the es, gs and fs registers as it want.

This 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

This is an example showing explicit use of the segment registers:

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

The Global Descriptor Table (GDT)

The first GDT descriptor is always a null descriptor that can never be used for memory access. There are at least two segment descriptors required for GDT (also the null descriptor) since the descriptor includes more information than the base and limit fields itself. The Type and the Descriptor Privilege level (DPL) fields are the two most important fields for us.

The field Type cannot be writable or executable simultaneously. Two segments are indeed : a segment in which code is to be executed in cs (Type is to execute only or Execute read) and a segment in which data read and write (Type is to read/write) is needed to be entered in the other segment register.

The DPL sets the privilege levels for the segment to be used. x86 enables four levels of privilege (PL), from 0 to 3 where PL0 is the most prioritized. Only PL0 and PL3 are used in most Operating systems, (Ex: Linux and Windows). Some OS, such as MINIX, still employ all levels. The kernel must be capable of doing anything, thus it uses DPL segments set to 0 (also called kernel mode). A segment selector in cs determines the current privilege level (CPL).

In this step series 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.

The segment registers(The segment selector registers hold the index of a descriptor. The value into a segment selector register is called selector.) must be filled with their respective segment selectors once the GDT has been loaded. In the following picture and table, the content of a segment selector is described:

In order to obtain the address of the segment Descriptor, an offset from the segment selector is inserted at the beginning of the GDT: 0x08 for the first and 0x10 for the second, as each descriptor is 8 octets. The requested level of privilege (RPL) should be 0, as the kernel of the OS should be 0.

Loading the segment selector registers is easy for the data registers . We only have to copy the correct offsets to the registers:

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

Now let’s see how to load cs register.

    ; 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

Far jump is a jump when the complete 48-bit logical address is explicitly specified: the selector of the segment to used and the absolute address to jump. It sets cs at 0x08 first and then springs to flush cs with its absolute address. Now your final gdt.s file will contain following codes.

;Load the Global Descriptor Tableglobal segments_load_gdtglobal segments_load_registerssegments_load_gdt:       lgdt [esp + 4]       retsegments_load_registers:       mov ax, 0x10       mov ds, ax ; 0x10 — an offset into GDT for the third (kernel data segment) record.       mov ss, ax       mov es, ax       mov fs, ax       mov gs, ax       jmp 0x08:flush_cs ; 0x08 — an offset into GDT for the second (kernel code segment) record.flush_cs:       ret

As I mentioned before in here we are only focusing on GDT but not LDT because Loading LDT is much complicated for this beginners level OS development series. For better understanding, this week we have created gdt.s file, memory_segment.h file and memory_segment.c file. By following this repository your can find the full code set that you need for this segmentation process. So until next week, be motivated, keep learning..!

Reference: The little book about OS development/Erik Helin, Adam Renberg

Written by,

R.A.W. Lalendra,

Undergraduate in Bsc(hons) Software Engineering,

University of Kelaniya Sri Lanka.

--

--

Waruni Lalendra

Software Engineering undergraduate at University of Kelaniya Sri Lanka