When I send the search-words "switch to real mode" to Google, I got about
7,5 million hits. But I could not find any precisely done description, how to
do this. Every of the found hints was either not useable, or not related to the
very important "architecture dependand" details of the Intel-CPU, type i486, or
even wrong. Even a thick and exspensive book (published 30 years ago), which
normally is the starting point, when I go under the hood of my machines,
contains a partly wrong and not really reasoned description (explicitely based
on intel-documentation!)
So I did myself, what there is obviously of interest for millions of people.
And I will describe here the complicated way to the desired state "real mode".
I do this readable online in .html, so that everyone can easily get it, and I
do not claim for any copyright besides GNU. Feel free to taste my fruit!
The needed assembler-code is made in NASM-dialect and my ASMat-dialect. Both
dialects are useable under my assembler operating system ASMOS, running NOT in
page mode, but protected mode.
If You want to read more about ASMOS click this link:
my homepage www.rcfriz.de
OVERWIEW:
The first step has to be, to get rid of the page mode and to get back to the
normal protected mode, where segment-descriptors and selectors, which adress
them, are to use to define base-adresses, where base-adresses =0 of programs
are useable and relocateble.
This step is done with the following two assembler commands:
mov eax,1 The PE-bit =switch to protected mode is bit1
The PG-bit =switch to page mode is bit31
mov cr0,eax
This step can not be done, if Your program does not run in a segment in
priviledge-level 0 .And of course, You will have to know, where the "Global
Descriptor Table" (="GDT") is positioned in the memory. This must be an
absolute adress, which You can get using the following commands:
mov eax,b b=adress of length and base-adress of the GDT (2*DD)
sgdt .eax store descriptor of GDT in variables
The variable "b" contains after these steps the length in bits0-15 and
the base-adress in bits16-47. The same format is used as IDT-descriptor, while
the bits48-31 always need to be =0. These are also commands, which can not be
done without priviledge level 0 !
These steps are not to do under ASMOS, where every program can contain every
command (besides some not genuine i486-commands). Keep this fact in mind, when
You read the example code below!
When You are in priviledge level 0 and protected mode, knowing where not only
GDT but also IDT are positioned, then You can do next steps, if You also know,
how Your operating system initialized the interrupt-controler and where its
interrupt-vectors are positioned in the IDT.
This very important table is always in use! But the CPU makes a setting for the
IDT-descriptor, which is still the original AT-design: length in bits0-15 =3FFh
and base-adress=0. This is the not altered starting point for BIOS.
As in protected mode the first interrupt vectors (lowest adress and number)
are in use of the CPU ("faults",traps" a.s.o.), every operating system first
has to re-define not only the base-adress of IDT, but the interrupt-vectors
too - especially those, which are in use of the interrupt-controler.
This details, You will have to know, if You want to get back to protected mode,
while the original state, made by the BIOS is to know, before You can get
really to real mode.
Before You can move any real mode code to memory below 1M, You will have to save
every code and data, which is positioned there. Else You can't get to real mode
nor back to protected mode! Mostly You will have to define new descriptors for
GDT and IDT, which are defined in that space below 1M and are not useable
without new base-adresses - normally not to know during program-time but only
during run-time.
The next step is to define one new descriptor in the moved GDT, describing a
16-bit-protected-mode-segment, which has the same base-adress as the
real-mode-segment, You will use. In my example code below this absolute adress
is 80000h. And You must define another real-mode-segment for the stack in Your
moved binary!
The binary to code has to be done due to the assembler-dialect, You want to use.
There are a lot of different ways to get binaries made out of mixed code for
three states of the machine, which are to pass through. I will explain this in
detail below...
Your ready coded program will have to do at run-time additional steps to make
that part, which is to move below 1M, ready for use. At least the new
descriptor for GDT and IDT are to write into that code. And of course the
pointer back to Your protected-mode-program!
After completing the binary to move, it can be moved to an absolute adress
below 1M and then be started by a FAR-jump in 32-bit-protected-mode. This
command has to contain the new selector of the 16-bit-protected-mode-segment,
You made before. You cannot use a FAR-jump to the real-mode-segment, because the
CPU exspects a segment-adress in the command after switching off the protected
mode before the jump. In protected mode 32 bits offset-adress are used, which
are NOT the format in real mode. That's why You need the 16-bit-descriptor,
which allows a 16-bit-offset in the next jump to real mode.
Reaching the 16-bit-protected-mode-segment below 1M, there is nothing more to
do before the FAR-jump to real mode, than switching off the protected mode.
You need not to define segment-register contents or any stack. But switching
off the protected mode inside that 16-bit-segment is a special problem, which
I describe below in detail (and for the first time).
Inside the real mode code You can do the well known steps to get back to the
protected mode (every bootloader contains such steps). But because You are in a
16-bit-protected-mode-segment, You may get problems with the 16-bit size of the
offset-adress in the jmp-command. Then You will need the solution, I describe
below.
IN DETAIL:
My intention was, to make a program, which allows to call VBE-functions,
using certain settings in registers. So I call in my program a procedure, as if
I used a BIOS-int.
As ASMOS defines GDT, IDT, stack and other important things between the
adresses 1000h and 9FFFFh, the real-mode-int-table is untouched, but every byte
till the video-memory-hole is to save. This is easily done, because ASMOS
contains at an absolute adress the first adress of available memory space above
1M. That adress has to be hold inside the program, while the ASMOS-variable has
to be re-defined, because some action could need other free space above the
save memory contents.
This example is written in ASMat-dialect. The rules are: Pr„fix ":" marks a
labels name, praefix " " marks following string as command, separated by " ",
a comment can follow or it can start without those praefixes at the left hand
end of line (NO comment-signs!), operand-adresses in commands are marked by the
praefix ".", which replaces silly branches.
-----------------------------------other code
mov edi,.es:100024h destination adress="membegin" in ASMOS
...becomes now base-adress of moved low mem!
mov .lowMEM,edi
mov esi,0
call P_Move_mem
-----------------------------------other code
:P_Move_mem call with base-adress of source in esi, of destination in edi
mov ecx,28000h count of DDs, which are to move
:Move_mem
dec ecx
mov eax,.es:esi+ecx*4
mov .es:edi+ecx*4,eax
jne Move_mem
ret
As You can see, I make use of the adress-calculation in protected mode, which
is alien to C-programers and not really useable inside page-mode (You must know
absolute adresses, base-adresses in descriptors and selectors in
segment-registers - in "es" normally an overall-segment is selected under ASMOS
with base-adress =0 .I do this step before I do something below 1M and restore
it only at the end of the program. So GDT, IDT and stack can be re-defined with
adresses in variables inside the program and the binary to move below 1M. You
cannot use such coding inside the page mode of Your operating system!
During run-time of this program I use the moved GDT, IDT and stack after doing
this:
-----------------------------------other code
mov eax,.lowMEM
add esp,eax new stackpointer / selector is overall-selector =8
add eax,1000h abs.base-adress of normal GDT in ASMOS
mov .gdt1,eax 17-48 DWord of absolute adress
add eax,10000h abs.base-adress of normal IDT in ASMOS
mov .idt1,eax 17-48 DWord of absolute adress
mov eax,gdt0 first of four byte-adresses holding the descriptor
lgdt .eax
mov eax,idt0 first of four byte-adresses holding the descriptor
lidt .eax
define base of buffer space for menu-mode to come:
add edi,A0000h last Dword of moved data is at 9FFFCh, the stack
mov .es:100024h,edi =new "membegin"
-----------------------------------other code
The procedure to call as if a BIOS-int was "called" is preceeded by the code,
which has to be translated separately and inserted as binary, before the
program can be translated. While the program is translated using ASMat, the
code to move is to translate using ASMnr, which translates only real-mode-code
using the same dialect as NASM. My assembler translation programs do not allow
mixed code (speeds up translation). Normally You will need expressions as
"USE 16" or "BIN 16" or anything like that at the start of such code. In
ASMat-dialect, this is a comment, which is copied, translated and as binary
copied again and inserted below as constant binary string. ASMOS eases such
actions very much.
_We are in a 16-bit-protected-mode-segment first.
_ jmp START
_ nop
_;This buffer will store base-adresses of GDT and IDT and the register contents
_;defined by the caller. After action in real mode, the registers may be
_;re-defined and may be stored here again at absolute adresses, available for
_;the caller.
_RegAX: DW 0 ; adress:4 / ...in each 2 bytes ax,bx,cx,dx
_RegBX: DW 0
_RegCX: DW 0
_RegDX: DW 0
_IDT0: DW 0FFFFh ; adress:0Ch / IDT in protected mode / 1-16 length
_IDT1: DW 0 ; adress:0Eh / 48-bit-pointer to IDT in high memory
_IDT2: DW 0
_IDT3: DW 0 ; always =0
_IDTrm0: DW 003FFh ; adress:14h / IDT in real mode / 1-16 length
_IDTrm1: DW 0 ; adress:16h / 48-bit-pointer to IDT in low memory
_IDTrm2: DW 0
_IDTrm3: DW 0 ; always =0
_START:
_;Here the 16-bit-register-parts are normal, You need an "operand-size-praefix"
_;=66h to make a "xor ax,ax" acting like "xor eax,eax", while this is changed
_;in 32-bit-protected-mode. Then most assembler translating programs insert
_;automatically the operand-size-praefix before "xor ax,ax", which is the same
_;opcode as "xor eax,eax". This is of interest, if You use translation for
_;real mode code.
_;There is no need to load any segment-register, because we instantly switch
_to real mode. We only switch off protected mode (and page mode too).
_;WARNING!
_;The command "lmsw" does NOT work in 16-bit-protected mode !!!!!
_;As most assembler translating programs (in fact my ASMnr) do not recognize
_;the command "mov cr0,eax" nor translate "mov eax,60000000h" with the needed
_;length of 32 bit for the immediate value, I define the opcodes binary as
_;constants (of course such values need to be in the right sequence of the
_;commands!).
_Rmop_mov: DW B866h ; operand-size-praefix 66h forces loading of eax instead
_ ; of "normal" ax in 16-bit-Pmode. "B8h" is the opcode.
_Rmimm_movL: DW 0 ;="mov eax,60000000h"
_Rmimm_movH: DW 6000h ; This value in eax switches cache and PE-bit off
_Rmop: DB 0FH ; Opcode of "mov cr0,eax" / uses the whole 32 bits
_ ; independant of operand-size !
_;After this switch, the CPU exspects a segment-adress in cs !
_;As I know, where this segment shall be, I can define it before run-time and
_;let the translation program do the rest...
_ jmp 8000h:Realmode ; real mode segment and NOT a selector!
_;The Offset will be in 16 bits, because translation is done for real mode.
_;Only after using a segment-register, the changed mode is in fact recognized
_;by the CPU! Then the other segment-register have to contain segment-adresses
_;too instead of selectors.
_Realmode:
_ mov ax,8000h
_ mov ds,ax
_ mov ax,8800h ; Buffer region for video-BIOS, readable by
_ ; caller at abs.adress 88000h
_ mov es,ax ; most BIOSints exspect buffer-definition es:di
_ mov di,0
_ mov ax,9000h
_ mov ss,ax
_ mov sp,8000h ; will be decremented...
_;Define real mode IDT as BIOS-default:
_ mov si,IDTrm0
_ lidt [si]
_;Initialize Interrupt-Controller like BIOS (NO eoi).
_;Interrupt table and vectors at adress=0 are untouched by ASMOS!
_;Mask all ints: ; Not all of them are masked inside ASMOS!
_ mov al,FFh ; De-mask with 0, mask with 1 / Bits are related to IRQ#
_ out 21h,al ; Master
_ out 0A1h,al ; Slave
_;Init, 1.Parameter:
_ mov eax,00010001b
_ out 20h,al;Master
_ out 0A0h,al ; Slave
_;Init, 2.Parameter: (BIOS default is changed inside ASMOS)
_ mov al,8 ; set INT# to IRQ#0 of master
_ out 21h,al
_ mov eax,70 ; decimal!
_ out 0A1h,al ; set INT# to IRQ#0 of Slave
_;Init,3.Parameter:
_ mov eax,00000100b
_ out 21h,al ; IRQ# of slave, bitoriented to master
_ mov al,00000010b
_ out 0A1h,al ; IRQ# of slave at master-inputs, dual coded to slave
_;Init, 4.Parameter: ; ( bit1: 1=auto, 0=eoi explizit)!
_ mov al,1 ; means: Intel-environment
_ out 21h,al ; BIOS default to master
_ mov al,1
_ out 0A1h,al ; BIOS default to slave
_;Make an unspecific end of interrupt: (needed in real mode!)
_ mov al,20h
_ out A0h,al
_ out 20h,al
_;De-mask all ints: ; Mostly masked inside ASMOS!
_ mov al,0 ; De-mask =0, mask =1 / Bits are related to IRQ#
_ out 21h,al ; master
_ out 0A1h,al ; slave
_;Restore contents of registers, defined by caller:
_ mov ax,[RegAX]
_ mov bx,[RegBX]
_ mov cx,[RegCX]
_ mov dx,[RegDX]
_ sti
_ int 10h ; This is an example....Do, what You like instead.
_ mov [RegAX],ax
_ mov [RegBX],bx
_ mov [RegCX],cx
_ mov [RegDX],dx
_ push 0
_ popf ; clear interrupts before returning...
_;Go back to caller in 32-bit-protected mode using ASMOS-standard:
_;Mask ints:
_ mov al,10001b
_ out 21h,al
_ out 0A1h,al
_;Init, 1.Parameter:
_ mov eax,10001b
_ out 20h,al
_ out 0A0h,al
_;Init, 2.Parameter:
_ mov al,0F0h
_ out 21h,al
_ mov eax,0F8h
_ out 0A1h,al
_;Init,3.Parameter:
_ mov eax,100b
_ out 21h,al
_ mov al,10b
_ out 0A1h,al
_;Init, 4.Parameter: ( bit1=1=auto-eoi)!
_ mov al,11b
_ out 21h,al
_ mov al,11b
_ out 0A1h,al
_;Establish IDT:
_ mov si,IDT0
_ lidt [si]
_ mov ax,[Selector32] ; get the program-selector for direct jump
_ mov [FARjmpSELrm],ax
_;Switch to protected mode (cli, switch PE-bit in machine status word and jump
_;FAR to following code):
_;As this code is translated for real mode, the following command works in
_;in real mode...
_ mov ax,1
_ lmsw ax ; set PE-Bit in cr0
_;FAR jump with selector:offset-argument. This jump is really the switch to
_;protected mode, because of the first use of a segmentregister after
_;switching PE-bit!
_;An indexed jump would only work here, if there were a fitting selector in ds
_;loaded!
_;We easier use a direct jump, which must be made out of constants.
_FARjmpOPrm: DB EAh ; opcode of direkt FAR jump
_FARjmpOFrm: DW 32Pmode ; offset 0-15 of destination label
_FARjmpSELrm: DW 0 ; selector to descriptor, which defines 32-bit-segment
_32Pmode: ; We are now in a 32-bit-protected-mode-segment
_ mov ax,8 ; selector of overall-segment
_ mov ds,ax
_ mov es,ax
_ mov fs,ax
_ mov gs,ax
_ mov ss,ax
_;Now the FAR jump to 32-bit-protected mode above 1M can be done.
_;As the command would be translated by ASMnr with an offsetadress in 16
_;instead of needed 32 bit, here the opcode is defined as constant
_FARjmpre: DB 66h ; operand-size-praefix 66h forces 32 bits offset
_FARjmpOP: DB 0EAh ; opcode of direkt FAR jump
_FARjmpOF0: DW 0 ; offset 0-15 of destination label
_FARjmpOF1: DW 0 ; offset 16-32 of destination label
_FARjmpSEL: DW 0 ; selector to descriptor, defines adress size 32 bits
_Selector32: DW 0 ; selector to descriptor, defining a 16-bit-segment,
_ ; which is congruent to the real-mode-segment
_PROGAMMend: DB "END " ; This string is needed in ASMOS to ease copying.
_;First sign NOT to copy the binary is the "E" of the string!
After translating the above code, insert it in the below constant, which is of
a special type, recognized by the translation in ASMn and ASMat.
If You use other translation programs and dialects for coding this, then You
will need an aequivalent way to make the binary starting at a label and ending
exactly below the next one. By such a way, adresses inside the binary can be
re-defined during run-time - very important in this context!
The original NASM-Assembler offers a quite similar type "INCBIN"...
:16-bit-PROTECTEDM i 63h first byte of binary at "f", last at "d" of end
The following label of a procedure is the next adress above the "d"
This is called after defining registers in bits 0-15 and moving contents below
1M separately (Before and after this call, using the procedure "P_Move_mem",
shown above).
:P_VBE-call
mov .callerESP,esp save the original stack-pointer
Buffer to define in es:di will always be at abs. 88000h (es=8800h,edi=0)
mov esi,16-bit-PROTECTEDM save register contents of caller:
NO push/pop available, because the stack will change!
mov .esi+4,eax
mov .esi+6,ebx
mov .esi+8,ecx
mov .esi+Ah,dx
Define constants in code of 16-bit-protected & real mode:
The following adresses are related to the label of the start of the translated
binary!
mov eax,.lowMEM
add eax,11000h adress of new IDT in high memory
mov .esi+Eh,eax
The following adresses are related to the label below the end of the translated
binary!
mov edi,P_VBE-call
Define pointer in FAR-Jump back to 32-bit-protected-mode:
mov .edi-8h,eax
mov eax,cs selector in opcode here
mov .edi-4,ax
We now make a program-segment below 1M with operand and adress size of 16 bit.
A program-data-segment is NOT needed.
Base-adress of the segment to define is 80000h. The raster of the sequence of
descriptors in the GDT-stack is 8 byte-adresses.
Inside ASMOS there is a variable named "GDTtop", which is the actual
offset-adress of the first available adress above the stacked, already defined
descriptors. This is in fact the selector-value too, if the bits0-2 are =0 !
Under ASMOS these 3 bits, which define the priviledge level, are not used resp.
always =0.
But you need not descriptors in sequence to already defined ones. You only have
to be shure, that You do not replace any already defined descriptor! The here
made descriptor shall be forgotten after returning from this procedure...
The absolute adress of the new GDT is already defined above...
mov edx,.gdt1 absolute adress of new GDT above 1M
mov eax,.es:100038h ="GDTtop"
cmp eax,FFF8h
jc Progdescon
The following is needed under ASMOS to make an error-output in the screen.
mov ebx,2 "OUT OF RANGE"
stc to sense by caller of this procedure...
ret
:Progdescon
add edx,eax selector of 16-bit-program-descriptor
mov w.es:edx,FFFFh 1-16 length
mov w.es:edx+2,0 1-16 segmentadress
mov b.es:edx+4,8 17-24 segmentadress
mov b.es:edx+5,10011010b Bitfield
mov b.es:edx+6,0 defines 16-bit-descriptor
mov b.es:edx+7,0
These descriptors will be forgotten after returning from this procedure, because
we do not increment "GDTtop".But we need the actual value, which is the selector
of the just made program-descriptor for 16-bit-segment.
mov eax,.es:100038h ="GDTtop"
program-selector, used in FAR-jump to 16-bit-protected mode
mov .FARjmpS,ax
mov .edi-2,ax value of "FARjmpSELrm" in binary
Move the binary code to low memory:
mov ecx,edi
mov edi,80000h Destination for code to move
calculate length of code to move:
sub ecx,esi
shr ecx,2
inc ecx
:MoveRM
dec ecx
mov .es:edi+ecx*4,eax
jne MoveRM
Clear Buffer space for BIOS:
mov edi,88000h
xor eax,eax
mov ecx,8000h
:ClearRM
dec ecx
mov .es:edi+ecx*4,eax
jne ClearRM
push 0
popfd Reset EEFLAGS, means ;cli; too
mov eax,60000001h setting in cr0 switches cache off,
but we keep the PE-bit untouched (still in protected mode).
mov cr0,eax
jmp f.FARjmpO
:FARjmpO d 0 offset 0-32 of destination label
:FARjmpS w 0 selector of 16-bit-descriptor
a align with nops....
:BackAgain we are now back in 32-bit-protected mode above 1M....
mov eax,cs
add eax,8 Standard under ASMOS: The congruent
data-program-descriptor is defined above the program-descriptor.
mov ds,eax selector of program-datasegment
mov esp,.callerESP stores the ret-adress of this procedur
mov eax,1
setting in cr0: PE=H=on, CD,NW=L=cache switched on
mov cr0,eax switch to 32-bit protected mode
sti allow interrupts again
ret
If Your operating system runs in page mode, then You will have to do additional
steps to get back in that stoneage...
The page mode once was created because of very little and exspensive memory.
It reduces in fact speed to less than the halve. Also multi-tasking was created
only because of slow CPUs. Multi-tasking reduces speed much more. Much slower
is additional swapping...
Modern machines contain CPUs, running up to 1000 times faster, and memory with
up to 2000 times more storage place. Only without intelligence, you can praise
page mode and multi-tasking as an advantage...