Skelix OS Tutorial
Prev Tutorial 02: Protected Mode Next

Our Goal

As I mentioned in last tutorial, the processor is placed in real-address mode after power-up or reset, it provides a environment of the Intel 8086 processor. As a matter of fact, there is another operation mode, called the protected mode which is going to be used after Skelix boot up from disk. We are going to enter protected mode in this tutorial and print "Hello World!" on screen in protected mode.

Download source file

Benefit of Protected Mode

In real mode, the processor can not access more than 1MB of physical memory easily, that is not enough for us, so 386 provides protected mode to provide several privilege protection schemes and the ability of accessing far more memory than it in real mode. Well, the protected mode we are talking about here means 32-bit protected mode, we are not concerned about 16-bit protected mode anyway.

The most benefit we can get from protected mode is the ability to access a 4GB memory space directly, but even after several decades development, we still do not have 4GB memory on our PC (or do you?), so the virtual memory feature was introduced, it uses hard disk as physical memory. As mentioned before, it also provides memory protection, it prevents the invalid access to kernel code from user programs and one process crashing also does not affect the whole system. It allows every single process have the ability to access isolate 4GB memory space in their own memory space, to prevent from messing up the whole memory space, an address translation feature was introduced which allows processes work in logical address, the mamory management unit translates the logical address to physical address, that makes every process thinks it owns isolate memroy space. For the other details you may want to check Intel's architectural documents.

How does it work......roughly

Okay, now let's finish the boring theoretical part, our goal for this tutorial is to let our code enter protected mode.
In the protected mode, surprisingly, we still use segments (actually we can not disable segmentation feature of the processor), every segment can contain a memory space up to 4GB. The segment is presented by the register called selector, which is the segment register in real mode like CS, DS, etc. Let's put it in this way: in a memory segment which is described by selector CS = 0x8, we may have the ability to access 0~4G-1 bytes directly, I said "may" just because we can choose how large the segment is, not just fixed 64KB in real mode.

I mentioned the segment was described by selector, that is not so precisely, actually the selector is kind of an index into a segment descriptor entry in system tables which store the information about all segments the system can use, the information includes where the segment starts, the length of the segment called limit and the type and privilege of the segment etc. To access a location in memory, a segment selector and an offset must be supplied still in the format as selector:offset just like it in real mode. For example, we can let selector 0x8 points to a descriptor which refer to a segment starts at B8000, then we can use 8:00000000 to access the first byte of video memory which affects the first character on screen. There are three different type of tables in system: GDT (global descriptor table), LDT (local descriptor table), IDT (interrupt descriptor table).  Once the processor in protected mode, all memory accesses pass through GDT or LDT,

We are going to use GDT in this tutorial, GDT can be shared by all tasks as its name implied. We are going to use one data segment and one code segment.

Now let look at the format of the code/data descriptor, one descriptor is 64-bit long:
XDT format

Limit(Bits 15-0) lower 16-bit limit
Base Address(Bits 15-0) lower 16-bit base address
Base Address(Bits 23-16) middle 8-bit base address
A
access information, whether it was read from(=0) or written to(=1) by the last access
Type
Bit 41 for date/stack segment it can be written to (=1)
for code segment it can be read from(=1)
Bit 42 for date/stack segment it indicates expansion direction, it grows downside(=1)
for code segment, confirming
Bit 43 whether it is code segment(=1) or it is a date/stack segment(=0)
Bit 44 must be 1 for code/data segment
DPL descriptor privilege level, we are going to use 0-kernel privilege and 3-user privilege in Skelix
P
whether the segment is present. It is always be 1 in this tutorial.
Limit(19-16) middle 8-bit limit
U user defined
X not used
D whether handle instructions and data as 32-bit(=1) or 16-bit(=0)
G whether the limitation use unit 4K or 1 byte
Base Address(Bits 31-24) higher 8-bit base address
As we can see, a descriptor actually include a 32-bit base address and a 20-bit limit and some attributes, the 32-bit base address indicate where the segment starts from, and the 20-bit limit indicates the length of the segment, but a problem will come up, 20-bit limit can only present 2^20 = 1MB memory, for accessing a 4GB memory space, it uses G bit to indicate whether the limit use 4K or 1 byte for one unit, that means if G bit is set then we get 2^20*4K = 4GB memory, if it is unset then we can use a memory space under 1MB.

Privilege protection is where the protected mode gets its name from, to explain how it works we have to take a look at the selector. The selector as mentioned above, it is sort of an index into descriptor tables.
Selector format
RPL requester privilege level
TI whether it is an index into GDT(=0) or LDT(=1)
Index index into the table
Program's privilege level(PL) is equal to the RPL field in the selector in CS register, it is equal to the current privilege level(CPL) in general. Programs at lower privilege level(PL) can not access data segment which at higher level and can not execute certain instructions. When a selector is loaded to an segment register, the processor will check the CPL and the RPL then make the lower privilege level as effective privilege level (EPL), then compare the EPL with DPL in descriptor, if EPL has higher privilege then the access is allowed. @_@b, It works in this way roughly, actually it also check the write/read attribute, present attribute etc.

As we can see in selectors, the Index field is 13-bit long, so it can present 2^13=8096 descriptors in one table.
There is only one GDT table in system but each process can have their own LDT. The processor reserves the first descriptor of the GDT, it should be set to zero and can not be used based on manuals, however, it does seem can be used safely, I seemed to remember I read some code about it somewhere, well it is out of topic.

Entering Protected Mode

We boot Skelix from a floppy disk in last tutorial, now we have the ability of executing code in real mode, for entering the protected mode, a mode switch must be performed, and we will not let Skelix go back to real mode anymore. Before entering the protected mode, there are some preparations we have to do, we have to create a GDT at first:
02/bootsect.sgdt:
        .quad    0x0000000000000000 # null descriptor
        .quad    0x00cf9a000000ffff # cs
        .quad    0x00cf92000000ffff # ds
        .quad    0x0000000000000000 # reserved for further use
        .quad    0x0000000000000000 # reserved for further use 
There are 5 entries in GDT table, we are going to use first three of them.

The first one is the null descriptor as demanded, the second descriptor is for CS segment, let's check it out from higher bits to lower bits
Bits 15-0 FFFFh lower 16-bit limit
Bits 39-16 000000h lower 24-bit base address
Bit  40 0b just set it to 0
Bit  41 1b readable
Bit  42 0b confirming
Bit  43 1b code segment
Bit  44 1b must be 1
Bits 45,46 00b kernel privilege
Bit  47 1b presented
Bits 48-51 Fh middle 8-bit limit
Bits 52 0b just set it to 0
Bits 53 0b just set it to 0
Bits 54 1b 32-bit instructions and data
Bits 55 1b use 4KB unit for limit
Bits 63-56 00h higher 8-bit base address
So this descriptor refer to a presented code segment which starts from 00000000, the limit is FFFFF*4K = 4G bytes works at kernel privilege, all data and instruction in this segment should be handled as 32-bit.

The third descriptor is used for date and stack segment, the difference exists at Bit 43, it is set to 0 means it is a
data segment.

Now, we got GDT ready, but how can the processor finds this table? There are several new system registers that we can use in protected mode, we are going to use GDTR, it is loaded by instruction LGDT, GDTR is 48-bit long, it is grouped with one WORD indicates the length of GDT in byte and one DWORD indicates the start address of GDT.
02/bootsect.sgdt_48:
        .word  .-gdt-1
        .long  GDT_ADDR
There are several constants defined at below, includes GDT_ADDR.
02/include/kernel.inc.set CODE_SEL, 0x08        # code segment selector in kernel mode
This selector is 00001000 in binary, it refers to the second descriptor in GDT, that is the CS segment descriptor
.set DATA_SEL, 0x10        # data segment selector in kernel mode
.set IDT_ADDR, 0x80000     # IDT start address
We set all system information at a fixed address, IDT (will be introduced in next tutorial) is the beginning of all informations.
.set IDT_SIZE, (256*8)     # IDT has fixed length
.set GDT_ADDR, (IDT_ADDR+IDT_SIZE)
                           # GDT starts after IDT
We use GDT_ADDR instead of the address of gdt because we are going to move all system tables to a fixed place before we enter the protected mode, and the 7C00 area will be overlapped.
.set GDT_ENTRIES, 5        # GDT has 5 descriptors
                           # null descriptor
                           # cs segment descriptor for kernel
                           # ds segment descriptor for kernel
                           # current process tss
                           # current process ldt
We are going to use 5 GDT descriptors in Skelix, we introduced the first three, the last two will be explained in the future
.set GDT_SIZE, (8*GDT_ENTRIES)
                           # GDT length
One descriptor is 8 bytes long, so the total GDT is 8*GDT_ENTRIES long. Actually we do not use this value in GDTR.
.set KERNEL_SECT, 72       # Kernel lenght, counted by sectors
We set the kernel length manually, because the kernel will be larger than one boot sector, so we need to use boot code to read the rest of kernel into memory. Here we set how many sectors we are going to read from disk
.set STACK_BOT, 0xa0000    # stack starts at 640K
The kernel stack starts at address 640K to downside, because the space above 640K is used by some other hardwares.

Let's look at the boot sector code
02/bootsect.s         .text
        .globl    start
        .include "kernel.inc"
include the above file
        .code16
start:
        jmp      $0x0,  $code
gdt:  
        .quad    0x0000000000000000 # null descriptor
        .quad    0x00cf9a000000ffff # cs
        .quad    0x00cf92000000ffff # ds
        .quad    0x0000000000000000 # reserved for further use
        .quad    0x0000000000000000 # reserved for further use
gdt_48:
        .word    .-gdt-1
        .long    GDT_ADDR
code:
        xorw    %ax,    %ax
        movw    %ax,    %ds    # ds = 0x0000
        movw    %ax,    %ss    # stack segment = 0x0000
        movw    $0x1000,%sp    # arbitrary value
                               # used before pmode
Puts the stack top at an arbitrary value, don't let it overwirte the boot sector code at 7c00
        ## read rest of kernel to 0x10000
        movw    $0x1000,%ax
        movw    %ax,    %es
        xorw    %bx,    %bx    # es:bs destination address
        movw    $KERNEL_SECT,%cx
        movw    $1,     %si     # 0 is boot sector
rd_kern:
        call    read_sect
        addw    $512,    %bx
        incw    %si
        loop    rd_kern
read the rest of kernel to memory 0x10000 temporarily, they are going to be removed to address 0 after entering
protected mode. The function read_sect is hard to explain in details, and I'm not going to do that, you can read it on your own if you like :(
        cli
 CLI instruction disables maskable hardware interrupts because we are entering protected mode, all interrupts work in real mode will not be available in protected mode.
        ## move first 512 bytes of kernel to 0x0000
        ## it will move rest of kernel to 0x0200,
        ## that is, next to this sector
        cld
        movw    $0x1000,%ax
        movw    %ax,    %ds
        movw    $0x0000,%ax
        movw    %ax,    %es
        xorw    %si,    %si
        xorw    %di,    %di
        movw    $512>>2,%cx
        rep
        movsl
Move the first 512 bytes of new read kernel code to 0x0000, this part starts with the code compile by another file load.s which will be introduced in a short while, it will read rest of kernel to 0x0200, just follows the first 512 bytes in memory, but in this tutorial, load.s does nothing more than display "Hello World!" on screen.
        xorw    %ax,    %ax
        movw    %ax,    %ds    # reset ds to 0x0000
        ## move    gdt
        movw    $GDT_ADDR>>4,%ax
        movw    %ax,    %es
        movw    $gdt,   %si
        xorw    %di,    %di
        movw    $GDT_SIZE>>2,%cx
        rep
        movsl
Moves GDT to a predefined address GDT_ADDR for further use.
enable_a20:       
        ## The Undocumented PC
        inb    $0x64,   %al   
        testb  $0x2,    %al
        jnz    enable_a20
        movb   $0xbf,   %al
        outb   %al,     $0x64
This method of enable A20 is introduced in book "The Undocumented PC". A20 is a bus gate in keyboard controller (don't ask me why Intel put it there), after power-up it is close, enabling it gives us the ability to access the memory beyond 1MB.
        lgdt    gdt_48
Now we load GDT descriptor into register GDTR
        ## enter pmode
        movl   %cr0,    %eax
        orl    $0x1,    %eax
        movl   %eax,    %cr0
A MOV instruction that sets the PE flag in control register CR0 at bit 0 switch the processor into the protected mode, surprisingly easy, right? Only if we ignore those preparations.
        ljmp   $CODE_SEL, $0x0
Now we are in protected mode, but before doing anything we like, this far jump is absolutely necessary because there are some 16-bit instructions are still in the prefetched pipeline, we have to flush them out because we are going to use 32-bit addresses and operands instead of 16-bit. And this instruction starts to execute the code at address 0 with selector 0x8 which is the second descriptor in GDT. Note that we have move the first 512 bytes of kernel to this address, which is load.s.
        ## in:    ax:    LBA address, starts from 0
        ##        es:bx address for reading sector
read_sect:
        pushw   %ax
        pushw   %cx
        pushw   %dx
        pushw   %bx

        movw    %si,    %ax       
        xorw    %dx,    %dx
        movw    $18,    %bx    # 18 sectors per track
                               # for floppy disk
        divw    %bx
        incw    %dx
        movb    %dl,    %cl    # cl=sector number
        xorw    %dx,    %dx
        movw    $2,     %bx    # 2 headers per track
                               # for floppy disk
        divw    %bx

        movb    %dl,    %dh    # head
        xorb    %dl,    %dl    # driver
        movb    %al,    %ch    # cylinder
        popw    %bx            # save to es:bx
rp_read:
        movb    $0x1,   %al   # read 1 sector
        movb    $0x2,   %ah
        int     $0x13
        jc      rp_read
        popw    %dx
        popw    %cx
        popw    %ax
        ret
Above code for reading sector from disk, ES:DX indicates start address of where the sector should be loaded to, SI indicates which sector is going to be read, like 0 means the boot sector, CX indicates how many sectors are going to be read. If you really want to read through it, then enjoy your slow death :)
.org    0x1fe,  0x90
.word   0xaa55

Hello World Comes Back

After entering protected mode, all general and segment registers still hold the values they had in read mode and the code begins with CPL 0, that means we can execute any instructions and access any ports and memory addresses. load.s will be executed at address 0.
02/load.s        .text
        .globl    pm_mode
        .include "kernel.inc"
        .org 0
Tell the loader, this code will start executing at logical address 0, in this case the physical address is also 0.
pm_mode:
        movl    $DATA_SEL,%eax
        movw    %ax,    %ds
        movw    %ax,    %es
        movw    %ax,    %fs
        movw    %ax,    %gs
        movw    %ax,    %ss
        movl    $STACK_BOT,%esp
We load all date and stack segment registers with selector 0x10, which is 00010000 in binary, refer to the third descriptor in GDT, RPL is also 0. This step is extremely important, the value in segment registers must refer to valid descriptors.
        cld
        movl    $0x10200,%esi
        movl    $0x200, %edi
        movl    $(KERNEL_SECT-1)<<7,%ecx #Bug 1
        rep
        movsl
Move the rest of kernel right after load.s. (#Bug 1: Song Jiang has pointed out that we should move KERNEL_SECT-1 sectors instead of KERNEL_SECT because we have moved the first kernel sector to 0x0000 and he is correct. Since KERNEL_SECT is a arbitrary value which big enough we can use for reading whole kernel into memory, so we use KERNEL_SECT won't cause any trouble)
        movb    $0x07, %al
        movl    $msg,  %esi
        movl    $0xb8000,%edi
that's exciting, we can use 32-bit address now!!
1:
        cmp     $0,    (%esi)
        je      1f
        movsb
        stosb
        jmp     1b
1:      jmp     1b
msg:
                .string "Hello World!\x0"

Let me use graphs to make all those movement clear, at first boot sector was loaded at 00007C00, it sets the stack top at memory 00001000, then it read the rest of kernel to memory 00010000, after that, move the first sector of kernel which contains the code in load.s to address 0, Figure 1 illustrates this memory image
Figure 1movement 1Figure 2movement 2      
After entering the protected mode, load.s move the rest of kernel after it and set stack at address A0000, illustrated by Figure 2

At last, let's check out the Makefile
02/MakefileAS=as -Iinclude
-I option tells assembler where to find those files included by .include, so we have to put kernel.inc file under directory include.
LD=ld

KERNEL_OBJS= load.o
At this moment, the kernel only include the module assembled from load.s
.s.o:
    ${AS} -a $< -o $*.o >$*.map

all: final.img

final.img: bootsect kernel
    cat bootsect kernel > final.img
    @wc -c final.img

bootsect: bootsect.o
    ${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<

kernel: ${KERNEL_OBJS}
    ${LD} --oformat binary -N -e pm_mode -Ttext 0x0000 -o $@ ${KERNEL_OBJS}
    @wc -c kernel
Kernel code start at address 0x0000.
clean:
    rm -f *.img kernel bootsect *.o

Then let's generate the final.img
result of tutorial2
Hello world come back!

Subject:

Your Name:

Your Email Address:

Comments:


Prev Home Next
Up