Boot LogoBoot
Notes

CPU System Management Modes

Disclaimer

This guide is meant for people with very little experience. If you follow along and have a curious mind, this will teach you extremely important concepts in osdev.

Information

This guide is valid only for Intel architecture.

Concepts about CPU System Management Modes

CPUs have different operating modes, which can only be accessed progressively and according to certain criteria. Most modern Intel CPUs support a total of four modes, each with different features and characteristics.

The four modes are:

  • Real Mode
  • Protected Mode
  • Virtual 8086 Mode
  • Long Mode

Let's see in the details the four modes.

The Real Mode

Real Mode is the first mode the CPU boots into.

It is a simple 16-bit mode with strong practical limitations, but it is also the only mode from which you can completely control every single CPU operation.

Limitations

The limitations are:

  • Less than 1 MB of RAM available.
  • There isn't any memory protection or virtual memory.
  • There aren't security mechanisms to protect against crash or malwares.
  • The default CPU operand length is only 16 bits.
  • The memory addressing modes provided are more restrictive than other CPU modes.
  • Accessing more than 64k requires the use of segment registers that are difficult to work with.

Advantages

The advantages are:

  • We can use BIOS drivers to control devices and handle interrupts.
  • We can use an advanced collection of low level API functions provided by the BIOS.
  • Memory access is really faster.

Code example

[ORG 0x7C00]
[BITS 16]

mov si, msg

print:
    mov ah, 0x0e
    mov al, [si]
    cmp al, 0
    je done
    int 0x10
    inc si
    jmp print

done:
    jmp $

msg:
    db "Hello World!", 0

times 510-($-$$) db 0
db 0x55, 0xAA

Explanation

This example prints "Hello World!" in Real Mode. Let's see every row what it do.

[ORG 0x7C00]    ; Offset at 0x7C00, where the program starts
[BITS 16]       ; Explain that the code is in Real Mode (isn't required to made it works)
mov si, msg     ; Copy in the si register the address of the phrase
print:              ; Function print
    mov ah, 0x0e    ; Say to the CPU that you want to print a value
    mov al, [si]    ; Load in al the value saved in the address stored in si
    cmp al, 0       ; Compare al's value to 0, if true it means that the phrase is ended
    je done         ; je - Jump if Equal, jump to done if al == 0
    int 0x10        ; If here, the phrase isn't ended. Call the interrupt and print the char
    inc si          ; Add 1 to the address saved in si (pass to next char)
    jmp print       ; Jump to print
done:       ; Function done
    jmp $   ; Jump forever here -> while(true) {}
msg:                        ; Save the first address of the phrase in msg
    db "Hello World!", 0    ; Save the text "Hello World!" with a 0 at the end.
times 510-($-$$) db 0   ; Add 0 until the whole file is 510 byte of size
db 0x55, 0xAA           ; Add the last 2 bytes of end program

The Protected Mode

Protected Mode is the main operating mode of modern Intel processors.

This mode, also called 32-bit mode, when enabled permit to unleash the real power of the CPU. However, it will prevent really low-level calls like BIOS interrupts.

Limitations

The limitations are:

  • Very complicated initial setup.
  • No direct access to hardware from userspace.
  • Complex virtual memory management.
  • Debugging very difficult.
  • Not compatible with Real Mode code.

Advantages

The advantages are:

  • Memory isolation and protection with segmentation and paging.
  • Support for 3 Privilege Levels.
  • Secure Multitasking Support.
  • Kernel bug protection.

Intel Manual Reference

To enter in Protected Mode, following the official Intel manual, you have to follow these steps:

  • Disable interrupts
  • Load the GDT
  • Set the PE flag in the register cr0 (the Protected mode bit is set, so with value 1) (The cr0 is the Control Register 0)
  • Execute a far jump immediately after the cr0 set
  • If paging is enabled, the cr0 set and the far jump must be from a page that is identity mapped (that is, the linear address before the jump is the same as the physical address after paging and protected mode is enabled)
  • If a LDT is going to be used, load the LDT
  • After the jump, in Protected Mode, reset the segment registers values (DS, SS, ES, FS and GS)
  • If an IDT is going to be used, load the IDT
  • If loaded an IDT enable maskable hardware interrupts and NMI interrupts (Non Maskable interrupts) (STI command)

The Manual also say that if other instructions exists between the cr0 set and the far jump, there may be random failures.

Code example

[ORG 0x1000]
[BITS 16]

CODE_SEG equ codeDescriptor - GDTStart
DATA_SEG equ dataDescriptor - GDTStart

cli
lgdt [GDTDescriptor]

mov eax, cr0
or eax, 1
mov cr0, eax

jmp CODE_SEG:startProtectedMode
jmp $

align 8
GDTStart:
    nullDescriptor:
        dd 0
        dd 0

    codeDescriptor:
        dw 0xffff
        dw 0
        db 0

        db 0b10011010

        db 0b11001111

        db 0

    dataDescriptor:
        dw 0xffff
        dw 0
        db 0

        db 0b10010010

        db 0b11001111

        db 0
GDTEnd:

GDTDescriptor:
    dw GDTEnd - GDTStart - 1
    dd GDTStart

[BITS 32]
startProtectedMode:
    jmp $

Explanation

Okay, I know, it's scary, but don't let this get you down. This code took me months to write, but now I'll explain it to you in the simplest and most complete way possible.

This program enter in Protected Mode. Yes, it doesn't do anything else.

If you try to copy the entire code, compile it, and run it, it won't work because it's not designed to run in the first-stage bootloader. I ran it in the second stage of my bootloader, so I know it works perfectly. For more information on multi-stage bootloaders, I refer you to the dedicated guide.

Note that all parts of this code are actually required to properly enter Protected Mode. Certain values can be changed, and there's obviously room for programming style, but skipping steps described in this code risks crashing the system.

Before to pass to the code explanation, you need to know the concept of GDT (Global Descriptor Table). I recommand reading the Global Descriptor Table guide before continue.

Below is a simple but very detailed explanation of the program.

[ORG 0x1000]    ; Organise the program with an offset of 0x1000 (starts from 0x1000)
[BITS 16]       ; The code is in Real Mode
CODE_SEG equ codeDescriptor - GDTStart  ; CODE_SEG = relative address of the code descriptor in the GDT
DATA_SEG equ dataDescriptor - GDTStart  ; DATA_SEG = relative address of the data descriptor in the GDT
cli                     ; Clear Interrupt - Disable BIOS interrupt
lgdt [GDTDescriptor]    ; Load the GDT, saved at the GDTDescriptor address
mov eax, cr0    ; Save in eax (32-bit register) the value of cr0 (Control Register 0)
or eax, 1       ; Set the bit 0 to 1 (cr0 has 32 bits, the bit 0 is the "Protected Mode Enable" bit)
mov cr0, eax    ; Update the cr0 value with the eax updated value

The piece of code above is the main of the Protected Mode enable process: it enable the Protected Mode by setting the last bit of the cr0 register (the Control Register 0). To do this you need to use these three lines of code as you cannot directly access the value in cr0.

jmp CODE_SEG:startProtectedMode     ; Jump to the 32-bit code part using the GDT code info
jmp $                               ; Fallback if something get wrong
align 8                 ; Align the GDT to 8-bit blocks (REQUIRED!)
GDTStart:               ; The address of the start of the GDT
    nullDescriptor:     ; The null descriptor
        dd 0            ; Allocate 32 bits of 0
        dd 0            ; Allocate 32 bits of 0

    codeDescriptor:     ; The code descriptor
        dw 0xffff       ; Allocate 16 bits with value 0xFFFF (all bits to 1)
        dw 0            ; Allocate 16 bits of 0
        db 0            ; Allocate 8 bits of 0

        db 0b10011010   ; Allocate 8 bits with value 10011010

        db 0b11001111   ; Allocate 8 bits with value 11001111

        db 0            ; Allocate 8 bits of 0

    dataDescriptor:     ; The data descriptor
        dw 0xffff       ; Allocate 16 bits with value 0xFFFF (all bits to 1)
        dw 0            ; Allocate 16 bits of 0
        db 0            ; Allocate 8 bits of 0

        db 0b10010010   ; Allocate 8 bits with value 10010010

        db 0b11001111   ; Allocate 8 bits with value 11001111

        db 0            ; Allocate 8 bits of 0
GDTEnd:                 ; The address of the end of the GDT

For the detailed explaination of the GDT, please read the Global Descriptor Table guide.

GDTDescriptor:
    dw GDTEnd - GDTStart - 1    ; The size of the GDT in 16 bits
    dd GDTStart                 ; The 32 bits address of the start of the GDT
[BITS 32]               ; Finally we are in 32-bits mode (Protected Mode)
startProtectedMode:     ; The address where we have to arrive with the far jump
    jmp $               ; Stop the program

This part must be at the end of the file, because is the only part in Protected Mode.

The Virtual 8086 Mode

Virtual 8086 mode is a sub-mode of the Protected mode. When the CPU is in Virtual 8086 mode, it's running an "Emulated" 16-bit Real Mode machine.

Limitations

The limitations are:

  • All the limitations of the Real Mode.
  • You can't enter in Protected Mode, you need to switch back to the Protected Mode.
  • Some stuff about the fact that the low level commands are emulated and not really executed as in the Real Mode, but is problems too technical to be important to us.

Advantages

The advantages are:

  • We can use all the Real Mode functions, while we are in fact in Protected Mode.

Code example

[ORG 0x4000]
[BITS 32]

SEGMENT_VIRTUAL_8086_MODE equ 0x0000

enterVirtual8086:
    push 0x0000         ; GS
    push 0x0000         ; FS
    push 0x0000         ; DS
    push 0x0000         ; ES
    push 0x0000         ; SS
    push 0x7000         ; ESP

    push dword (1<<17) | (1<<9) | (1<<1) | (3<<12)  ; EFLAGS

    push SEGMENT_VIRTUAL_8086_MODE  ; CS
    push startVirtual8086Mode       ; EIP

    iret 

[BITS 16]
startVirtual8086Mode:
    jmp $

Explanation

[ORG 0x4000]
[BITS 32]       ; We start in Protected Mode
SEGMENT_VIRTUAL_8086_MODE equ 0x0000    ; The address of the segment

enterVirtual8086:       ; We have to create the stack for the physical addressing
    push 0x0000         ; GS
    push 0x0000         ; FS
    push 0x0000         ; DS
    push 0x0000         ; ES
    push 0x0000         ; SS starting point
    push 0x7000         ; ESP - stack grows down from 0x0000:0x7000

    push dword (1<<17) | (1<<9) | (1<<1) | (3<<12)  ; Set EFLAGS
    ; bit 17 - VM | Activate Virtual 8086 Mode
    ; bit 9 - IF | Enable hardware interrupts
    ; bit 1 - Reserved | Must always be 1
    ; bits 12-13 - IOPL=3 | Allow in/out and cli/sti

    push SEGMENT_VIRTUAL_8086_MODE  ; CS (segment of entry point)
    push startVirtual8086Mode       ; EIP (entry point offset)

    iret                            ; jump in VM86 mode
[BITS 16]               ; We're in Virtual 8086 Mode that's a 16-bits mode
startVirtual8086Mode:
    jmp $

The Long Mode

The Long Mode, sometimes also called IA32e Mode, is the most userfull and powerfull mode that the Intel CPUs can provide to the OS devs. The Long Mode consists of two sub modes: the 64-bit mode and the compatibility mode (with the 32-bit mode).

Limitations

The limitations are:

  • Setup really more difficult than the Protected Mode one.
  • Need to enable paging.
  • Real Mode and Virtual 8086 Mode aren't usable anymore.
  • Granularity required with paging.
  • Segmentation isn't usable anymore.
  • Debug almost impossible (I'm serious, debug may require days for the smallest mistakes).

Advantages

The advantages are:

  • We have an extended set of registers:
    • 64-bits registers: rax, rbx, rcx, rdx, rsp, rbp, rip, etc.
    • Eight new general-purpose registers: r8-r15
    • Eight new multimedia registers: xmm8-xmm15
  • Addressing to 64-bit.
  • Each process can address up to 128TB of virtual memory.
  • Paging is required.
  • Syscall and Sysret!

Code example

[ORG 0x9000]
[BITS 32]

mov ax, DATA_SEG
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax

mov esp, 0x90000
mov ebp, esp

CR0_PAGING equ 1 << 31
CR0_PAGING_DISABLE equ ~CR0_PAGING
mov eax, cr0
and eax, CR0_PAGING_DISABLE
mov cr0, eax

CR4_PAE equ 1 << 5
mov eax, cr4
or eax, CR4_PAE
mov cr4, eax

mov eax, pdpt_table
or eax, 0x03
mov [pml4_table], eax

mov eax, pd_table
or eax, 0x03
mov [pdpt_table], eax

mov dword [pd_table], 0x00000083
mov dword [pd_table+4], 0x00000000

mov eax, pml4_table
mov cr3, eax

IA32_EFER equ 0xC0000080
IA32_EFER_LME_BIT equ 1 << 8
mov ecx, IA32_EFER
rdmsr
or eax, IA32_EFER_LME_BIT
wrmsr

mov eax, cr0
or eax, CR0_PAGING
mov cr0, eax

CODE_SEG64 equ codeDescriptor64 - GDTStart64
DATA_SEG64 equ dataDescriptor64 - GDTStart64

cli
lgdt[GDTDescriptor64]

jmp CODE_SEG64:startLongMode

align 4096
pml4_table:
    times 512 dq 0

align 4096
pdpt_table:
    times 512 dq 0

align 4096
pd_table:
    times 512 dq 0

align 8
GDTStart64:
    nullDescriptor64:
        dd 0
        dd 0

    codeDescriptor64:
        dw 0xffff
        dw 0
        db 0

        db 0b10011010
        
        db 0b10101111

        db 0

    dataDescriptor64:
        dw 0xffff
        dw 0
        db 0

        db 0b10010010
        
        db 0b10101111

        db 0
GDTEnd64:

GDTDescriptor64:
    dw GDTEnd64 - GDTStart64 - 1
    dd GDTStart64

[BITS 64]
startLongMode:
    jmp $

Explanation

I said that the setup is really really difficult, but don't worry, I'm going to explain every single row of this code. Some details like the GDT and the cr0 register are similar to the Protected Mode ones, but we have to do more advanced stuff like setup paging, enable phisical address extension and a lot of other stuff.

This code is functional for entering Long Mode, but you need to be in Protected Mode already, and due to the size of the assembled file, which is over 512 bytes long, you need to make a multi-stage bootloader.

[ORG 0x9000]
[BITS 32]       ; We must be in Protected Mode
mov ax, DATA_SEG    ; Setup the segments with the data info of the Protected Mode GDT
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax

mov esp, 0x90000    ; Setup stackpointer
mov ebp, esp        ; Setup basepointer
CR0_PAGING equ 1 << 31              ; cr0 flag to enable paging
CR0_PAGING_DISABLE equ ~CR0_PAGING  ; cr0 paging flag to reset it
mov eax, cr0
and eax, CR0_PAGING_DISABLE         ; Disable paging
mov cr0, eax
CR4_PAE equ 1 << 5  ; Phisical Address Extension pin of cr4
mov eax, cr4
or eax, CR4_PAE     ; Enable the Phisical Address Extension
mov cr4, eax
mov eax, pdpt_table     ; Load the Page Directory Pointer Table
or eax, 0x03            ; Set last two bits to 1
mov [pml4_table], eax   ; Move in Page Map Level 4 the PDPT data

mov eax, pd_table       ; Load the Page Directory Table
or eax, 0x03            ; Set last two bits to 1
mov [pdpt_table], eax   ; Move in Page Directory Pointer Table the PDT data

mov dword [pd_table], 0x00000083    ; Move in the PDT the value 0x0000000000000083
mov dword [pd_table+4], 0x00000000  ; Move in the PDT with offset 4 the value 0x0000000000000000

mov eax, pml4_table     ; Load the PML4T
mov cr3, eax            ; Update the cr3 register

The code above is a fundamental part to enable the 64-bit paging.

The PAE paging is the Physical Address Extension that allow the CPU to pass from 32-bit linear addresses to 52-bit phisical addresses. To use the PAE paging is required to prepare a set of four PDPTE (Page Directory Pointer Table Extension), that the CPU use as paging registers loading them from the info saved in the cr3 register. At the last two rows of this section of the code, we load the PML4 Table in eax to load in cr3 32-bit Page Directory Pointer Table reference.

In this example we are setting the Page Map Level 4, that's a bit more advanced than the lowest levels.

To enable the PML4 we have to:

  • Load in the Page Map Level 4 Table the reference to the Page Directory Pointer Table.
  • Set the bit 0 of the Page Map Level 4 Table to 1.
    • In this example we set also the bit 1 of the Page Map Level 4 Table to enable write operations.
  • Load in the Page Directory Pointer Table the reference to the Page Directory Table.
  • Set the bit 0 of the Page Directory Pointer Table to 1.
    • In this example we set also the bit 1 of the Page Directory Pointer Table to enable write operations.
  • Set the bit 0 of the Page Directory Table to 1.
  • Set the bit 7 of the Page Directory Table to 1.
    • In this example we set also the bit 1 of the Page Directory Table to enable write operations.

The Page Directory Table is a 64-bit table, now we are still in the Protected Mode, so we can't load directly a 64-bit value in the Table, but we can load 2 sets of 32-bit data. To set 0x0000000000000083 in pd_table we can load first the lowest 32-bit value, then with an offset of 4 bytes ([pd_table+4]) we can load the 32-bit hightest value in the table.

The bit 0 must be 1 in all the three Tables. The bit 7 of the Page Directory Table must be 1, otherwise the CPU will think that's a Page Directory Pointer Table.

On the last part of the code, we load the ready PML4 Table in a register to load it in cr3.

IA32_EFER equ 0xC0000080        ; Address of the IA32e Mode Settings
IA32_EFER_LME_BIT equ 1 << 8    ; The LME (Long Mode Enable) bit that must be set at 1 to enter in Long Mode
mov ecx, IA32_EFER              ; Move in ecx the IA32e Mode Address
rdmsr                           ; Read MSR (Model Specific Registers)
or eax, IA32_EFER_LME_BIT       ; Set in eax (that have the MSR data) the Long Mode Enable bit
wrmsr                           ; Write MSR (Model Specific Registers)

In this part of the code, we enable the Long Mode, being ready to enter in Long Mode.

mov eax, cr0
or eax, CR0_PAGING      ; Re-enable paging (now it will be PML4 paging)
mov cr0, eax
CODE_SEG64 equ codeDescriptor64 - GDTStart64    ; Calcolate the Code Descriptor of the 64-bit GDT
DATA_SEG64 equ dataDescriptor64 - GDTStart64    ; Calcolate the Data Descriptor of the 64-bit GDT

cli                             ; Disable interrupts
lgdt[GDTDescriptor64]           ; Load the 64-bit GDT

jmp CODE_SEG64:startLongMode    ; Far jump to the Long Mode code (using the GDT Code Descriptor)
align 4096
pml4_table:             ; Declare an empty PLM4 Table
    times 512 dq 0      ; 64-bit full of 0

align 4096
pdpt_table:             ; Declare an empty PDP Table
    times 512 dq 0      ; 64-bit full of 0

align 4096
pd_table:               ; Declare an empty PD Table
    times 512 dq 0      ; 64-bit full of 0

This part of the code above is the declaration of the three tables needed to enable PML4 paging. The tables are initialized empty and then setted the bits that have to be 1.

align 8                     ; Align the GDT to 8-bit blocks (REQUIRED!)
GDTStart64:                 ; The address of the start of the GDT
    nullDescriptor64:       ; The null descriptor
        dd 0                ; Allocate 32 bits of 0
        dd 0                ; Allocate 32 bits of 0

    codeDescriptor64:       ; The code descriptor
        dw 0xffff           ; Allocate 16 bits with value 0xFFFF (all bits to 1)
        dw 0                ; Allocate 16 bits of 0
        db 0                ; Allocate 8 bits of 0

        db 0b10011010       ; Allocate 8 bits with value 10011010
        
        db 0b10101111       ; Allocate 8 bits with value 10101111

        db 0                ; Allocate 8 bits of 0

    dataDescriptor64:       ; The data descriptor
        dw 0xffff           ; Allocate 16 bits with value 0xFFFF (all bits to 1)
        dw 0                ; Allocate 16 bits of 0
        db 0                ; Allocate 8 bits of 0

        db 0b10010010       ; Allocate 8 bits with value 10010010
        
        db 0b10101111       ; Allocate 8 bits with value 10101111

        db 0                ; Allocate 8 bits of 0
GDTEnd64:

For the detailed explaination of the GDT, please read the Global Descriptor Table guide.

GDTDescriptor64:
    dw GDTEnd64 - GDTStart64 - 1    ; The size of the GDT in 16 bits
    dd GDTStart64                   ; The 32 bits address of the start of the GDT
[BITS 64]       ; Finally, we are in Long Mode
startLongMode:
    jmp $

This part must be at the end of the file, because is the only part in Long Mode, and must be keeped separated from the Protected Mode part.

Conclusion

We have explored the four main operating modes of modern Intel processors, each with distinctive characteristics and specific use cases.

  • Real Mode is the starting point of every Intel CPU, where we obtain complete control and access to BIOS functions, but with severe memory and protection limitations.
  • Protected Mode unlocks the true potential of the processor with 4 GB of memory and memory protection, although with a complex initial setup.
  • Virtual 8086 Mode allows us to run Real Mode code while maintaining the advantages of Protected Mode.
  • Long Mode provides us with an extended set of registers, 64-bit addressing, and massively superior addressing capability, while requiring the most complex configuration of all.

Understanding these modes is fundamental for OS development, as every OS must follow a logical progression through these modes: starting in Real Mode, moving to Protected Mode, and potentially reaching Long Mode. The choice of which mode to use depends on the specific needs of your operating system and the features you want to provide.

In the context of OS development, Real Mode is the first mandatory step, but for a modern and performant operating system, the final goal is usually to reach Long Mode, which offers the best performance and capabilities.