INTRODUCTION:

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...