This post is related to rev. 5

At boot time, the CPU runs in 16 bits real mode. This is a ancient execution mode of the Intel CPUs that is characterized by a 20 bit segmented memory address space (meaning that a maximum of 1 MiB of memory can be addressed), direct software access to BIOS routines and peripheral hardware, and no concept of memory protection or multitasking at the hardware level. In order to maintain backwards compatibility, the processor always starts in real mode. Therefore, we need to use a special mechanism to switch to an enhanced and more secured mode, called protected mode.

Protected mode adds the following features, used by most modern OSes:

  • 32 bits address space: 4 GiB of memory can be addressed
  • Paging: the OS could manage access rights of each page (4 KiB) of memory. This is also used for virtual memory.
  • Privilege levels: 4 privilege levels, or rings, are used to restrict tasks from accessing data or executing several privileged CPU instructions.
  • Multitasking: x86 processors introduced a mechanism of preemptive multitasking, but number of OSes don't use it.

To switch into protected mode, we just have to set the Protection Enable (PE) bit in the Control Register 0 (CR0). But before doing that, we have to setup two other things.

For historical reasons, all the memory cannot be accessed without enabling a feature called the "A20 line" (see this article for more information). A traditional way to enable the A20 line is to program the keyboard controller.

    cli                     ; no more interruptions
    
    xor cx, cx
    
clear_buf:
    in al, 64h              ; get input from keyboard status port
    test al, 02h            ; test the buffer full flag
    loopnz clear_buf        ; loop until buffer is empty
    mov al, 0D1h            ; keyboard: write to output port
    out 64h, al             ; output command to keyboard

clear_buf2:
    in al, 64h              ; wait 'till buffer is empty again
    test al, 02h
    loopnz clear_buf2
    mov al, 0dfh            ; keyboard: set A20
    out 60h, al             ; send it to the keyboard controller
    mov cx, 14h

wait_kbc:                   ; this is approx. a 25uS delay to wait
    out 0edh, ax            ; for the kb controler to execute our 
    loop wait_kbc           ; command.

The second thing is due to the fact that, in protected mode, the memory management is controlled through tables of descriptors. A descriptor is basically a structure of flags and addresses used by the CPU to perform safety checks and controls access to memory. Each descriptor defines a memory segment with a base address, a size, a granularity value and several security flags. All these descriptors are grouped into the Global Descriptor Table (GDT). We have to set up this table and to inform the CPU to use it.

We're going to set up a GDT with 3 entries: a mandatory NULL entry, and 2 entries for code and data segment in ring 0 (Kernel). In the future, we will add 2 other entries for code and data segment in ring 3 (User mode).

Moreover, this GDT will describe a flat memory model: each segment will start at address 0 and will cover the full range of memory.

I will not describe here the format of these entries because a lot of web pages have already done this better than me (see this article or this one)

So, here are the GDT definition:

gdt:
        dd 0            ; Null Segment
        dd 0

        dw 0FFFFh       ; Code segment, read/execute, nonconforming
        dw 0
        db 0
        db 10011010b
        db 11001111b
        db 0

        dw 0FFFFh       ; Data segment, read/write, expand down
        dw 0
        db 0
        db 10010010b
        db 11001111b
        db 0

gdt_desc:                       ; The GDT descriptor
        dw gdt_desc - gdt - 1    ; Limit (size)
        dd gdt                  ; Address of the GDT

and the code to load this GDT

    ; load GDT
    xor ax, ax
    mov ds, ax              ; Set DS-register to 0 - used by lgdt

    lgdt [gdt_desc]         ; Load the GDT descriptor

Once these operations are complete, we can switch into protected mode:

    
    ; enter pmode
    mov eax, cr0            ; load the control register in
    or  al, 1               ; set bit 0: pmode bit
    mov cr0, eax            ; copy it back to the control register

The CPU now runs in protected mode. To finalize the switch, we just have to set its different segments registers: DS and SS are set explicitely to point to the third decriptor (10h), whereas the code segment CS is set during the jmp instruction. This code jump is also needed to flush the CPU instruction pipeline.

    jmp 08h:protected_mode

[Bits 32]
protected_mode:   
    mov ax, 10h             ; Save data segment identifier
    mov ds, ax              ; Move a valid data segment into the data segment register
    mov ss, ax              ; Move a valid data segment into the stack segment register
    mov esp, 090000h        ; Move the stack pointer to 090000h