Toby Opferman
http://www.opferman.net
programming@opferman.net
Protected Mode Tutorial II
If you have read PM Tut 1, this is the supplementary Tutorial that
tells you how to set protected mode physically. So, after you have read
the basics from the first tutorial, let's talk about setting protected mode!
This tutorial only tells you how to set pmode. You should get supplementary material
to perform more operations and use protected mode. There is a lot *NOT* covered
because it is not needed to set pmode.
Descriptor Tables
We got to start with Descriptor Tables. What are they? They hold information
about mapping physical memory to virtual memory. Each entry in a descriptor table has
a certain format. Each holds the length, the physical address and they have some
attributes assocaited. The attributes can set readonly/execute permissions, as well
as what DPL the memory will belong. The privledge level. Application programs should
be in Ring3, lowest privledge level and the OS can limit what Ring3 can do. Ring0 is
where the OS goes, it's the most privledged level.
Setting the Tables
Before you switch to protected mode, you should set these tables. Actually, you only
need to set IDT and GDT, you can leave LDT alone when first setting protected mode.
So, I am going to show you how to set up your GDT. For the IDT, we can cheat and you can
set it up later and LDT we are going to ignore since it is not required to switch to pmode.
First, we just want to do this. Find a location in memory for the IDT and just set it to 0.
Then, we use CLI so when we switch, the interrupt table is not being used :). This simplifies
things.
LIDT [QWORD PTR IDT_BASE_ADDR] ; Load Interrupt Descriptor Table
The IDT_BASE_ADDR is set like so:
IDT_BASE_ADDR dw 256*8 ; Size of IDT
dd 6000h ; Physical address in Memory
First Word is the size. The next DWORD is the physical address in memory where it will be.
Now, we do the same for the GDT.
GDT_BASE_ADDR dw (8*8192) - 1 ; Size of GDT
dd 6000h + 256*8 ; Physical address in Memory
We are going to put the GDT right after the IDT in memory.
LGDT [QWORD PTR GDT_BASE_ADDR] ; Load Global Descriptor Table
Before you do this though, copy the GDT/IDT to their positions in memory.
Next, I will show you how to create the GDT (That you will copy to the physical location in
memory before setting, that way, it's set when you jump to pmode).
The first descriptor is the NULL descriptor, and, of course, it's NULL!
Descriptors are also 8 bytes. Also, Descriptors in the GDT start at 8h. This is because
of the format of the selector:
The bit layout:
ssss stpp
s = Selector. These start at 0 (NULL Descriptor which isn't used), then go to 1.
1, 2, 3, 4, 5, ... entries in the GDT.
But, since they are mapped to higher bits, the actual values are:
8h, 10h, 18h, 20h, 28h, etc.
t means table number. 0 = GDT, 1 = LDT. So, it knows what table to look in.
pp means privledge level. 0 - 3. But, in a flat model, only 0 and 3 are used.
You need to use a multisegmented model to have protection at the 1 and 2 levels.
Which provides protection at the page and the segment, but is very complicated model.
Most OSes use Flat model, which isn't using the Processor to it's full capabilities, but
it's simple to implement. Window uses a flat model.
This is how the GDT entries are layed out:
1st Word = Bottom word of Segment Length
2nd Word = Bottom Word of Physical address
Next Byte = Low byte of high word to phyisical address.
Next Byte = Attributes = Accessed bit, DPL, Present bit, System bit and others.
Next Byte = Low nibble is the Upper part of the Segment Length. High nibble has
Granularity bit, Available and Stack size.
Last Byte = High byte of base phyiscal address.
(For better descriptoins, please referr to the intel manuals or other materials)
dq 0 ; NULL Descriptor
; 8h Selector
dw 0FFFFh ; Segment Length
dw 0h ; Low Word Base Address
db 8 ; Low Byte of High Word of Base Address
db PRESENT_BIT or DPL0 or SYSTEM_BIT or READ_EXE_BIT or CODE_BIT
db 0Fh or GRAN_PAGE_BIT or DEFAULT_SIZE32 ; High Nibble of Segment Length
db 0 ; High Byte of Base Address
And we'll set up the first segment and that's it. 8h selector,we'll jump to 8h:0 when we
set p mode. See below, I have made some contants in order to make setting the
bits you want easier. This will point to code at the phyiscal address 8000h. This is where
our 32bit pmode code should be loaded to.
; Equate Constants
DPL0 EQU 00h
DPL1 EQU 20h
DPL2 EQU 40h
DPL3 EQU 60h
SYSTEM_BIT EQU 10h
NO_SYSTEM_BIT EQU 0h
PRESENT_BIT EQU 80h
NO_PRESENT_BIT EQU 0h
GRAN_PAGE_BIT EQU 80h
GRAN_BYTE_BIT EQU 0h
AVL_BIT EQU 10h
UNAVL_BIT EQU 0
DEFAULT_SIZE32 EQU 40h
DEFAULT_SIZE16 EQU 0
DATA_BIT EQU 0
CODE_BIT EQU 8
CONFORM_BIT EQU 4
NO_CONFORM_BIT EQU 0
READ_EXE_BIT EQU 2
EXE_ONLY_BIT EQU 0
ACCESSED_BIT EQU 1
CLR_ACCESSED_BIT EQU 0
Switching to Protected Mode
Doing the switch is simple.
MOV EAX, CR0
OR AL, 1
MOV CR0, EAX
That's it.
But, let me warn you. If you are setting it in the bootstrap for your
own OS, this is fine. If you're doing a DOS pmode extender, this is NOT fine. There are
two problems. 1) Windows and 2) Memory Managers
If a memory mangaer is running, you will not be able to set pmode. To test for a DOS
pmode extender, do like so:
MOV EAX, CR0
TEST AL, 1
JNZ SHORT ERROR_MEMORY_MANAGER
OR AL, 1
MOV CR0, EAX
That will find that a DOS Memory Manager is present and not load. If the PM bit is already set,
there is a DOS Memory manager loaded. In Windows 95, however, DOS is in a virtual machine.
And, the PM bit will NOT be set! So how can you tell if you're under Windows 95? Like so:
MOV EAX, CR0
TEST AL, 1
JNZ SHORT ERROR_MEMORY_MANAGER
OR AL, 1
MOV CR0, EAX
MOV EAX, CR0
TEST AL, 1
JZ SHORT ERROR_WINDOWS95
You can tell because you CANNOT SET the PMBit! So, Set it, check it. If it's not set,
you're under Windows 95 and you won't be able to continue, so exit. I figured this out a few
years ago when I was fooling with switching to pmode.
Anyway, after you set protected mode, you need to clear the prefetch queue.
Some people do this before they do the actual jump:
JMP $+2
NOP
NOP
But I've seen code without it as well, so you do what you want.
Finally, you should have set a selector in your GDT that was compiled as 32bit binary and
loaded into memory at a certain location that you have a selector pointing to. Now,
we hard code a far jump. This enables us to have the assembler not optimize the jump.
Of course, I've seen code without this and there is debate on this, some peopel say you
don't need to and others say you do. I just do it anyway.
db 67h ; 32 Bit Memory Address
db 66h ; 32 Bit Instruction
db 0Eah ; Far Jump Opcode
dd 0h ; Memory Offset in Selectory
dw 8h ; Selector
That should be it. Another thing of warning. When I set my selector registers before I jumped
to pmode and tried to use them, they no longer had the values I put in them before the jump.
So, as a precautionary measure, I would reset the selector registers with the correct
descriptors, it could save you a lot of headache if you found something not working and
didn't think of this! But, I would reccomend setting them before you jump as well.
I have heard people's code crashing because of the Stack Selector not being set properly
before the jump. But I would reset them again after the jump. Whatever works for you.