CPU System Management Modes
Disclaimer
Information
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, 0xAAExplanation
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 phraseprint: ; 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 printdone: ; 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 programThe 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 ModeCODE_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 GDTcli ; Clear Interrupt - Disable BIOS interrupt
lgdt [GDTDescriptor] ; Load the GDT, saved at the GDTDescriptor addressmov 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 valueThe 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 wrongalign 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 GDTFor 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 programThis 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 ModeSEGMENT_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 Modemov 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 basepointerCR0_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, eaxCR4_PAE equ 1 << 5 ; Phisical Address Extension pin of cr4
mov eax, cr4
or eax, CR4_PAE ; Enable the Phisical Address Extension
mov cr4, eaxmov 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 registerThe 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, eaxCODE_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 0This 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.