Switching to Protected Mode
Table of Content
Protected Mode
Intel introduces protected mode to provide memory protection on its CPUs. Perhaps for providing backward compability to its old CPUs, an Intel CPU enters so called real mode when its powered on. In real mode, the CPU can only address 1MB memory and there is no memory protection. The CPU can switches to protected mode. In protected mode, the CPU enforces strict memory and hardware I/O protection as well as restricting the available instruction set via Rings. In the following, we write boot sector code to switch to protected mode.
Experiment and Programming Environment
Refer to the bootstrap tutorial for the experiment and programming environment, and for how to compile and run the programs here.
Example 1 Switching to Protected Mode
This program has a few modules and subroutines. We break them down as follows.
Boot Sector Code
The boot sector code references to 4 modules
[org 0x7c00]
; set up real-mode stack
mov bp, 0x9000
mov sp, bp
; print a message in real mode
mov bx, MSG_REAL_MODE
call print_msg_rm
; switch to protected mode
call switch_to_pm
; we will never return here if the above is successful
jmp $
%include "gdt0.asm"
%include "print_msg_rm.asm"
%include "print_msg_pm.asm"
%include "switchpm.asm" ; begin_pm is a callback, and to be called therein
[bits 32]
main_pm:
; do something useful, such as, print a message in protected mode
mov ebx, MSG_PROT_MODE
call print_msg_pm
jmp $
MSG_REAL_MODE:
db "Started in 16-bit real mode", 0
MSG_PROT_MODE:
db "Switched to 32-bit protected mode", 0
times 510-($-$$) db 0
dw 0xaa55
Global Descriptor Table (GDT)
An essential role of operating systems is to allocate system resources. Among the resources is the system’s main memory. To switch to protected mode, we first needs to decide how we should use the memory, for which, we define a Global Descriptor Table (GDT). In the following, we define a simple GDT in gdt0.asm
; See https://wiki.osdev.org/Global_Descriptor_Table
; and
; https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html
; a simple flat Global Descriptor Table (GDT)
gdt:
.sd_null: ; 8 bytes
db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
.sd_code: ; 8 bytes
db 0xff, 0xff ; bits 0 - 15 of segment limit of 20 bits
db 0x00, 0x00 ; bits 0 - 15 of segment base of 32 bits
db 0x00 ; bits 16 - 23 of segment base of 32 bits
; Bits
; P = 1 -> segemnt present (1 for present in memory; 0 for not)
; DPL = 00 -> privilege level 00 for highest (11 for lowest)
; S = 1 -> 0 for system; 1 for code or data
; Type = 1010 -> segment type 4 bits as follows
; code:
; 1 for code (0 for data)
; conforming:
; If 1 code in this segment can be executed from
; an equal or lower privilege level.
; For example, code in ring 3 can far-jump to
; conforming code in a ring 2 segment. The
; privl-bits represent the highest privilege
; level that is allowed to execute the segment.
; For example, code in ring 0 cannot far-jump
; to a conforming code segment with privl==0x2,
; while code in ring 2 and 3 can. Note that the
; privilege level remains the same, ie. a
; far-jump form ring 3 to a privl==2-segment
; remains in ring 3 after the jump.
; If 0 code in this segment can only be executed
; from the ring set in privl.
; readable: 1 for readable, 0 for execute-only
; accessed: 0 initially, if accessed, CPU sets it to 1
db 0b10011010 ; 1st flags , type flags
; Bits
; G = 1 -> offset is in unit 4K (2^12 bytes)
; D/B = 1 -> 1 for 32-bit segment; 0 for 16-bit
; L = 0 -> 64-bit code segment? 0 for not, ununsed on 32-bit processor
; AVL = 0 -> Not available to system programmers
; Segment Limit (bits 19:16) = 1111
db 0b11001111 ; 2nd flags , Limit ( bits 16 -19)
db 0x00 ; bits 24 - 31 of segment base of 32bits
.sd_data: ; 8 bytes
db 0xff, 0xff ; bits 0 - 15 of segment limit of 20 bits
db 0x00, 0x00 ; bits 0 - 15 of segment base of 32 bits
db 0x00 ; bits 16 - 23 of segment base of 32 bits
; Bits
; P = 1 -> segemnt present (1 for present in memory; 0 for not)
; DPL = 00 -> privilege level 00 for highest (11 for lowest)
; S = 1 -> 0 for system; 1 for code or data
; Type = 0010 -> segment type 4 bits as follows
; code:
; 0 for data (1 for code)
; conforming:
; If 1 code in this segment can be executed from
; an equal or lower privilege level.
; For example, code in ring 3 can far-jump to
; conforming code in a ring 2 segment. The
; privl-bits represent the highest privilege
; level that is allowed to execute the segment.
; For example, code in ring 0 cannot far-jump
; to a conforming code segment with privl==0x2,
; while code in ring 2 and 3 can. Note that the
; privilege level remains the same, ie. a
; far-jump form ring 3 to a privl==2-segment
; remains in ring 3 after the jump.
; If 0 code in this segment can only be executed
; from the ring set in privl.
; readable: 1 for readable, 0 for execute-only
; accessed: 0 initially, if accessed, CPU sets it to 1
db 0b10010010 ;
; Bits
; G = 1 -> offset is in unit 4K (2^12 bytes)
; D/B = 1 -> 1 for 32-bit segment; 0 for 16-bit
; L = 0 -> 64-bit code segment? 0 for not, ununsed on 32-bit processor
; AVL = 0 -> Not available to system programmers
; Segment Limit (bits 19:16) = 1111
db 0b11001111 ;
db 0x00 ; base (bits 24 - 31)
.gdt_end:
GDT_DESCRIPTOR: ; to be loaded by instruction lgdt
; The size is the size of the table subtracted by 1. This is because the
; maximum value of size is 65535, while the GDT can be up to 65536 bytes (a
; maximum of 8192 entries). Further no GDT can have a size of 0.
dw gdt.gdt_end - gdt - 1
; The offset is the linear address of the table itself
dd gdt
CODE_SEG equ gdt.sd_code - gdt
DATA_SEG equ gdt.sd_data - gdt
; See https://wiki.osdev.org/Global_Descriptor_Table
; and
; https://www.intel.com/content/www/us/en/architecture-and-technology/64-ia-32-architectures-software-developer-vol-3a-part-1-manual.html
; a simple flat Global Descriptor Table (GDT)
gdt:
.sd_null: ; 8 bytes
db 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
.sd_code: ; 8 bytes
db 0xff, 0xff ; bits 0 - 15 of segment limit of 20 bits
db 0x00, 0x00 ; bits 0 - 15 of segment base of 32 bits
db 0x00 ; bits 16 - 23 of segment base of 32 bits
; Bits
; P = 1 -> segemnt present (1 for present in memory; 0 for not)
; DPL = 00 -> privilege level 00 for highest (11 for lowest)
; S = 1 -> 0 for system; 1 for code or data
; Type = 1010 -> segment type 4 bits as follows
; code:
; 1 for code (0 for data)
; conforming:
; If 1 code in this segment can be executed from
; an equal or lower privilege level.
; For example, code in ring 3 can far-jump to
; conforming code in a ring 2 segment. The
; privl-bits represent the highest privilege
; level that is allowed to execute the segment.
; For example, code in ring 0 cannot far-jump
; to a conforming code segment with privl==0x2,
; while code in ring 2 and 3 can. Note that the
; privilege level remains the same, ie. a
; far-jump form ring 3 to a privl==2-segment
; remains in ring 3 after the jump.
; If 0 code in this segment can only be executed
; from the ring set in privl.
; readable: 1 for readable, 0 for execute-only
; accessed: 0 initially, if accessed, CPU sets it to 1
db 0b10011010 ; 1st flags , type flags
; Bits
; G = 1 -> offset is in unit 4K (2^12 bytes)
; D/B = 1 -> 1 for 32-bit segment; 0 for 16-bit
; L = 0 -> 64-bit code segment? 0 for not, ununsed on 32-bit processor
; AVL = 0 -> Not available to system programmers
; Segment Limit (bits 19:16) = 1111
db 0b11001111 ; 2nd flags , Limit ( bits 16 -19)
db 0x00 ; bits 24 - 31 of segment base of 32bits
.sd_data: ; 8 bytes
db 0xff, 0xff ; bits 0 - 15 of segment limit of 20 bits
db 0x00, 0x00 ; bits 0 - 15 of segment base of 32 bits
db 0x00 ; bits 16 - 23 of segment base of 32 bits
; Bits
; P = 1 -> segemnt present (1 for present in memory; 0 for not)
; DPL = 00 -> privilege level 00 for highest (11 for lowest)
; S = 1 -> 0 for system; 1 for code or data
; Type = 0010 -> segment type 4 bits as follows
; code:
; 0 for data (1 for code)
; conforming:
; If 1 code in this segment can be executed from
; an equal or lower privilege level.
; For example, code in ring 3 can far-jump to
; conforming code in a ring 2 segment. The
; privl-bits represent the highest privilege
; level that is allowed to execute the segment.
; For example, code in ring 0 cannot far-jump
; to a conforming code segment with privl==0x2,
; while code in ring 2 and 3 can. Note that the
; privilege level remains the same, ie. a
; far-jump form ring 3 to a privl==2-segment
; remains in ring 3 after the jump.
; If 0 code in this segment can only be executed
; from the ring set in privl.
; readable: 1 for readable, 0 for execute-only
; accessed: 0 initially, if accessed, CPU sets it to 1
db 0b10010010 ;
; Bits
; G = 1 -> offset is in unit 4K (2^12 bytes)
; D/B = 1 -> 1 for 32-bit segment; 0 for 16-bit
; L = 0 -> 64-bit code segment? 0 for not, ununsed on 32-bit processor
; AVL = 0 -> Not available to system programmers
; Segment Limit (bits 19:16) = 1111
db 0b11001111 ;
db 0x00 ; base (bits 24 - 31)
.gdt_end:
GDT_DESCRIPTOR: ; to be loaded by instruction lgdt
; The size is the size of the table subtracted by 1. This is because the
; maximum value of size is 65535, while the GDT can be up to 65536 bytes (a
; maximum of 8192 entries). Further no GDT can have a size of 0.
dw gdt.gdt_end - gdt - 1
; The offset is the linear address of the table itself
dd gdt
CODE_SEG equ gdt.sd_code - gdt
DATA_SEG equ gdt.sd_data - gdt
Subroutine for Printing Message in Real Mode
We use BIOS service int 0x10
to print messages.
;
; print_msg_rm.asm
;
; we implement a function with interface
; void print_msg_rm(char* msg)
; where we pass the argument msg via ds:bx
;
; this function runs in real mode, and as such we can use BIOS int 0x10
;
print_msg_rm:
pusha ; push all registers to stack
mov ah, 0x0e
.loop:
mov al, [ds:bx]
cmp al, 0
je .done
int 0x10
inc bx
jmp .loop
.done:
popa ; pop all registers to stack
ret
Subroutine for Printing Message in Protected Mode
In protected mode, we don’t have access to BIOS services. To print a message, we directly write to VGA memory.
;
; print_msg_pm.asm
;
; function
; void print_msg_pm(char *msg)
; where we pass address to msg via register ebx
;
; since there is no direct BIOS access in protected mode, we directly
; write VGA device controller memory. The VGA device by default is in
; text mode whose memory's base address is 0xb8000 where one character
; uses two bytes, the first byte is character and the second attributes,
; such as foreground and background colors
;
[bits 32]
VGA_TEXT_MEM_BASE equ 0xb8000
TEXT_ATTR equ 0x4f
print_msg_pm:
pusha
mov edx, VGA_TEXT_MEM_BASE
.loop:
mov al, [ebx] ; msg base
mov ah, TEXT_ATTR
cmp al, 0
je .done
mov [edx], ax
add ebx, 1 ; next char
add edx, 2 ; next char position in VGA text memory
jmp .loop
.done:
popa
ret
Switching to Protected Mode from Real Mode
;
; switchpm.asm
;
; function
; void swtich_to_pm(void (*main_pm)())
; that switches to protected mode, and if successful call provided function
; main_pm
[bits 16]
switch_to_pm:
cli
lgdt [GDT_DESCRIPTOR]
mov eax, cr0
or eax, 0x1
mov cr0, eax
jmp CODE_SEG:.init_pm
[bits 32]
.init_pm:
mov ax, DATA_SEG
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000
mov esp, ebp
call main_pm