How computers start up! Part 3: Moving on to Protected Mode(32-bit)

Moving on from 16 bit Real Mode to 32-bit Protected Mode post setting up GDT

Preparing for the transition to Protected Mode

How Segmentation works in P-mode

In Real mode, segmentation works by initializing segment registers with segment base addresses. Then, to address a memory in code segment we use a syntax like, [cs:memory_address].
Segmentation isn't that straight up and works differently in Protected mode. We define segments as descriptors inside the GDT(Global Descriptor Table). Memory access is now slightly more complicated with some extra steps until the required physical address is calculated.
This image shows the workflow of segmentation in P-mode.segmentation-1

Algorithm:

  • Break your logical address into segment selector + offset.
  • Register GDTR holds the address of the global descriptor table. segment selector part of the address is used to select a segment descriptor which resides inside the descriptor table.
  • Add offset to the base address of the segment descriptor to get the physical address

Structure of Segment Selector(eg: cs,ss,ds...)

Segment Selector(16-bit)

As we can see, bit 3 to bit 15 is occupied by an index to the descriptor inside the descriptor table, TI states whether it is a local descriptor table(ldt) or global descriptor table(gtd), and of course the self-explanatory Request Privilege Level(RPL).

Prepare Global Descriptor Table

GDT is a region of memory starting with 8-bytes of null(0) data, then at least 2 descriptors are required to be there, one code segment and one data segment.

A segment descriptor has a structure as shown in the image.

Segment Selector structure(32 bits)

Code and data segments are segment descriptors, the most interesting parts of the segment descriptors are the base and limit values. As the name suggests base stands for the starting address of this segment in the memory and the limit gives the length of the segment. We define these values as follows,

gdt_start: ; labels are needed to compute sizes and jumps
    ; the GDT starts with a null 8-byte
    dd 0x0 ; 4 byte
    dd 0x0 ; 4 byte
    
    
; GDT for code segment. base = 0x00000000, length = 0xfffff
; for flags, refer to os-dev.pdf document, page 36
gdt_code: 
    dw 0xffff    ; segment length, bits 0-15
    dw 0x0       ; segment base, bits 0-15
    db 0x0       ; segment base, bits 16-23
    db 10011010b ; flags (8 bits)
    db 11001111b ; flags (4 bits) + segment length, bits 16-19
    db 0x0       ; segment base, bits 24-31
    
; GDT for data segment. base and length identical to code segment
gdt_data:
    dw 0xffff
    dw 0x0
    db 0x0
    db 10010010b
    db 11001111b
    db 0x0
    
gdt_end:

NOTE: Both the segments have a base address 0x00 which means both segments overlap and occupy full memory. This is called a Flat Memory Model

Now, all we need is to set up a memory region that contains the length and the base addresses of the GDT. We do this by

; GDT descriptor
gdt_descriptor:
    dw gdt_end - gdt_start - 1 ;always one less of its true size
    dd gdt_start ; address (32 bit)

Finally, let's define some variables which will be needed for accessing code segment and data segment descriptors

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

Now, all is left is to tell the processor where the GDT is. This is done using a special instruction lgdt [gdt_descriptor], short for LOAD GDT. But we need to make sure that no interrupts are coming this way while we do that. the reason for this is that we are going to set up our own Interrupt Vector Table(IVT) in the form of an Interrupt Descriptor Table(IDT), foregoing the IVT which we inherited from BIOS when the boot-loader started execution.

Switching to Protected Mode

An x-86 CPU has 8 control registers, cr0 is the first one. According to Wikipedia,

A control register is a processor register which changes or controls the general behavior of a CPU or other digital device. Common tasks performed by control registers include interrupt control, switching the addressing mode, paging control, and coprocessor control.

The first bit of the register cr0 controls the execution mode for the CPU:
control

The steps to perform the switching are:

  1. Disable interrupts
  2. Load our GDT
  3. Set a bit on the CPU control register cr0
  4. Flush the CPU pipeline by issuing a carefully crafted far jump
  5. Update all the segment registers
  6. Update the stack
  7. Call to a well-known label that contains the first useful code in 32 bits
[bits 16]
switch_to_pm:
    cli ; 1. disable interrupts
    lgdt [gdt_descriptor] ; 2. load the GDT descriptor
    mov eax, cr0
    or eax, 0x1 ; 3. set 32-bit mode bit in cr0
    mov cr0, eax
    jmp CODE_SEG:init_pm ; 4. far jump by using a different segment

[bits 16] tells the CPU that this code is to be executed using 16-bit processing, or in Real Mode. A far jump is simply jumping to an xx:yy address instead of jumping to yy offset. This is important to switch from 16 to 32-bit mode.
Let's see what init_pm looks like,

[bits 32]
init_pm: ; we are now using 32-bit instructions
    mov ax, DATA_SEG ; 5. update the segment registers
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000 ; 6. update the stack right at the top of the free space
    mov esp, ebp

    call BEGIN_PM ; 7. Call a well-known label with useful code

BEGIN_PM can be any function that is designed to work on 32 bits system. As of this moment, we are in Protected Mode!

One thing that we are not going to cover here is the installation and setup of Interrupt Descriptor Table. See the extras section for more information on that.

One last job that is remaining here with the boot-loader is to call the kernel, that can be done with the following assembly code,

BEGIN_PM:
    call KERNEL_OFFSET ; Give control to the kernel

Extras

Interrupt Descriptor Table

An interrupt indexes into an interrupt descriptor table (IDT) in memory, which yields a 64-bit gate that points to the interrupt handler and indicates its privilege level. There is a IDTR register in the processor that points to the IDT.

If the interrupt handler’s privilege level is numerically lower than that of the interrupted task, the processor also switches to another stack. The location of this new stack is available in a task state segment (TSS) in memory, which is pointed to by a task register (TR) in the processor.

The task state segment is private to each thread in the system. The CPU state is always stored task state segment and thus it helps in harware context switch.

For more information, go visit my page on interrupts here.

Sign up for the blog-newsletter! I promise that we do not spam...