Operating systems - Entering 32 bit protected mode
Table of Contents
In this post, we will cover how to enter 32-bit protected mode. This is a requirement if we want to be able to create a OS with the features we are used to. Otherwise, we will have very little storage space allowed due to the 16-bit reference limit.
Boot sector
To enter 32-bit protected mode, we first have to bootstrap our code from a 16-bit sector. This is necesary because the initial code location is stored in a space-restricted zone.
Steps
- Create “Main flow”
- Load GDT (Global Descriptor Table)
- Switch to 32-bit protected Mode
Main flow
We want to load the GDT structure, so that the processor knows where each code block is going to be.
; A boot sector that enters 32-bit protected mode
[org 0x7c00]
mov bp, 0x9000 ; Set the stack
mov sp, bp
mov bx, MSG_REAL_MODE
call print_string
call switch_to_pm ; We won't return from here
In the above code segment, we print a message (In 16-bit real mode) to make sure we are on the right track. Then we execute the code that switches to protected mode (32-bit)
Load DGT
We define two variables based on the DGT created. These variables will become very handy, because they are the start of the sections they describe.
; Define some handy constants for the GDT segment descriptor offset, which
; are what segment registers must contain when in protected mode. For example,
; when we set DS = 0x10 im PM, the CPU knows that we mean it to use the
; segment described at offset 0x10 (i.e. 16 bytes) in our GDT, which in our
; case is the DATA segment (0x0 -> NULL; 0x08 -> CODE; 0x10 -> DATA)
CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start
When we load the DGT, we can then jump the the 32-bit code which is stored in another memory location than the 16-bit code.
switch_to_pm:
cli ; We must switch of interrupts until we have
; set-up the protected mode interrupt vector
; otherwise interrupts will run riot.
lgdt [gdt_descriptor] ; Load our GDT, which defines the protected
; mode segments (e.g. for code and data)
mov eax, cr0 ; To make the switch to protected mode, we set
or eax, 0x1 ; the first bit of CR0, a control register
mov cr0, eax
jmp CODE_SEG:init_pm ; Make a far jump (i.e. to a nre segment) to our 32-bit
Now, we have loaded the Global Descriptor Table.
Once the CPU has been switchedinto 32-bit protected mode, the process by which it translates logical addresses (i.e.the combination of a segment register and an offset) to physical address is completelydifferent: rather than multiply the value of a segment register by 16 and then add to itthe offset, a segment register becomes an index to a particular segment descriptor(SD) in the GDT.
32-bit protected mode
[bits 32]
; Initialise registers and the stack once in PM
init_pm:
mov ax, DATA_SEG ; Now in PM, our old segments are meaningless,
mov ds, ax ; so we point our segment registers to the
mov ss, ax ; data selector we defined in our GDT
mov es, ax
mov fs, ax
mov gs, ax
mov ebp, 0x90000 ; Update our stack position so it is right
mov esp, ebp ; at the top of the free space.
call BEGIN_PM ; Finally, call some well-known label
We initialise the segment registers for our new 32-bit structured code. Then we call our first 32-bit code segment.
[bits 32]
; This is where we arrive after switching to and initialising protected mode.
BEGIN_PM:
mov ebx, MSG_PROT_MODE
call print_string_pm ; Use our 32-bit print routine
jmp $ ; Hang
In this ocasion, we are only printing a string, without BIOS help, like we had in 16-bit real mode. But in the future, we will be able to load our compiled C code.
These code segments require further code to be run successfully. The rest of the code needed for the segmemts to work can be obteined in my GitHub repository.