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