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

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

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

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)

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

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