How computers start up! Part 3: Moving on to Protected Mode(32-bit)
Moving on from 16 bit Real Mode to 32-bit Protected Mode post setting up GDT
Preparing for the transition to Protected Mode
How Segmentation works in P-mode
In Real mode, segmentation works by initializing segment registers with segment base addresses. Then, to address a memory in code segment we use a syntax like, [cs:memory_address]
.
Segmentation isn't that straight up and works differently in Protected mode. We define segments as descriptors inside the GDT(Global Descriptor Table). Memory access is now slightly more complicated with some extra steps until the required physical address is calculated.
This image shows the workflow of segmentation in P-mode.
Algorithm:
- Break your logical address into
segment selector
+offset
. - Register GDTR holds the address of the global descriptor table.
segment selector
part of the address is used to select a segment descriptor which resides inside the descriptor table. - Add
offset
to the base address of the segment descriptor to get the physical address
Structure of Segment Selector(eg: cs,ss,ds...)
As we can see, bit 3 to bit 15 is occupied by an index to the descriptor inside the descriptor table, TI states whether it is a local descriptor table(ldt) or global descriptor table(gtd), and of course the self-explanatory Request Privilege Level(RPL).
Prepare Global Descriptor Table
GDT is a region of memory starting with 8-bytes of null(0) data, then at least 2 descriptors are required to be there, one code segment and one data segment.
A segment descriptor has a structure as shown in the image.
Code and data segments are segment descriptors, the most interesting parts of the segment descriptors are the base and limit values. As the name suggests base stands for the starting address of this segment in the memory and the limit gives the length of the segment. We define these values as follows,
gdt_start: ; labels are needed to compute sizes and jumps ; the GDT starts with a null 8-byte dd 0x0 ; 4 byte dd 0x0 ; 4 byte ; GDT for code segment. base = 0x00000000, length = 0xfffff ; for flags, refer to os-dev.pdf document, page 36 gdt_code: dw 0xffff ; segment length, bits 0-15 dw 0x0 ; segment base, bits 0-15 db 0x0 ; segment base, bits 16-23 db 10011010b ; flags (8 bits) db 11001111b ; flags (4 bits) + segment length, bits 16-19 db 0x0 ; segment base, bits 24-31 ; GDT for data segment. base and length identical to code segment gdt_data: dw 0xffff dw 0x0 db 0x0 db 10010010b db 11001111b db 0x0 gdt_end:
NOTE: Both the segments have a base address 0x00 which means both segments overlap and occupy full memory. This is called a Flat Memory Model
Now, all we need is to set up a memory region that contains the length and the base addresses of the GDT. We do this by
; GDT descriptor gdt_descriptor: dw gdt_end - gdt_start - 1 ;always one less of its true size dd gdt_start ; address (32 bit)
Finally, let's define some variables which will be needed for accessing code segment and data segment descriptors
CODE_SEG equ gdt_code - gdt_start DATA_SEG equ gdt_data - gdt_start
Now, all is left is to tell the processor where the GDT is. This is done using a special instruction lgdt [gdt_descriptor]
, short for LOAD GDT. But we need to make sure that no interrupts are coming this way while we do that. the reason for this is that we are going to set up our own Interrupt Vector Table(IVT) in the form of an Interrupt Descriptor Table(IDT), foregoing the IVT which we inherited from BIOS when the boot-loader started execution.
Switching to Protected Mode
An x-86 CPU has 8 control registers, cr0
is the first one. According to Wikipedia,
A control register is a processor register which changes or controls the general behavior of a CPU or other digital device. Common tasks performed by control registers include interrupt control, switching the addressing mode, paging control, and coprocessor control.
The first bit of the register cr0
controls the execution mode for the CPU:
The steps to perform the switching are:
- Disable interrupts
- Load our GDT
- Set a bit on the CPU control register
cr0
- Flush the CPU pipeline by issuing a carefully crafted far jump
- Update all the segment registers
- Update the stack
- Call to a well-known label that contains the first useful code in 32 bits
[bits 16] switch_to_pm: cli ; 1. disable interrupts lgdt [gdt_descriptor] ; 2. load the GDT descriptor mov eax, cr0 or eax, 0x1 ; 3. set 32-bit mode bit in cr0 mov cr0, eax jmp CODE_SEG:init_pm ; 4. far jump by using a different segment
[bits 16]
tells the CPU that this code is to be executed using 16-bit processing, or in Real Mode. A far jump
is simply jumping to an xx:yy
address instead of jumping to yy
offset. This is important to switch from 16 to 32-bit mode.
Let's see what init_pm
looks like,
[bits 32] init_pm: ; we are now using 32-bit instructions mov ax, DATA_SEG ; 5. update the segment registers mov ds, ax mov ss, ax mov es, ax mov fs, ax mov gs, ax mov ebp, 0x90000 ; 6. update the stack right at the top of the free space mov esp, ebp call BEGIN_PM ; 7. Call a well-known label with useful code
BEGIN_PM
can be any function that is designed to work on 32 bits system. As of this moment, we are in Protected Mode!
One thing that we are not going to cover here is the installation and setup of Interrupt Descriptor Table. See the extras section for more information on that.
One last job that is remaining here with the boot-loader is to call the kernel, that can be done with the following assembly code,
BEGIN_PM: call KERNEL_OFFSET ; Give control to the kernel
Extras
Interrupt Descriptor Table
An interrupt indexes into an interrupt descriptor table
(IDT) in memory, which yields a 64-bit gate
that points to the interrupt handler and indicates its privilege level. There is a IDTR
register in the processor that points to the IDT
.
If the interrupt handler’s privilege level is numerically lower than that of the interrupted task, the processor also switches to another stack. The location of this new stack is available in a task state segment
(TSS) in memory, which is pointed to by a task register
(TR) in the processor.
The task state segment
is private to each thread in the system. The CPU state is always stored task state segment and thus it helps in harware context switch
.
For more information, go visit my page on interrupts here.