If you want to write 32-bit code (as indicated by the BITS32), you will have to set up a protected mode environment. Right now, the CPU is in real mode and has to be put in protected mode.
I see that you are trying to link the boot sector and the boot loader executable into one file. This may be possible with a proper linker script, but GCC cannot fix-up 16-bit addresses if you are still in real mode and does not support segmented memory models, which means that protected mode is a necessity. You are also generating a PE executable with the boot sector, which will certainly not work. The BIOS will just load 512 bytes and jump to it (DL
is set to the boot disk by the way).
A better way to do this is to build the boot loader as a separate binary and load it from the disk using INT 13h
. Enter the second stage in a 32-bit protected mode environment with the global descriptor table set up for the flat model. The second stage loader can be a flat binary built to run at a specific load address.
Because all your addresses are going to be fixed up by the linker to wherever you want to load your second stage boot loader, it is necessary to subtract the base address when accessing memory outside the loader (e.g video memory).
#define LOAD_POINT 0x7E00 /* Right after the boot sector */
#define PHYS(a) (void*(a-LOAD_POINT))
Do note that if you decide to enter a protected mode environment (which is highly recommended when using GCC), BIOS services will NOT be available unless you set up virtual 8086 mode and a task state segment.
Entering the Protected Mode Environment
BITS 32 is an assembler directive and does not actually change the CPU state. You must set the first bit of CR0 to enter, load the GDT with flat model.
mov eax,cr0
inc ax
mov cr0,eax
lgdt [gdtr]
Even then, you are still only in 16-bit protected mode. It is necessary to jump to the code segment which is marked as 32-bit.
The Global Descriptor Table
Each entry is 8 bytes long. The first is the NULL segment and must be zeroed. The GDT can be placed anywhere in memory. To load the GDT, we must set the GDTR with LGDT.
The following sample shows the relationship between the GDTR and the GDT, as well as flat model segments.
org 7C00h
_GDTR:
DW _GDT_end - _GDT - 1 ; GDT limit in bytes (highest offset)
DD _GDT ; Linear address of GDT
_GDT:
DQ 0 ; NULL segment
; Flat code segment (selector 0x8)
DB 0FFh,0FFh,0,0,0,1_00_11010b,11_001111b,0
; Flat data segment (selector 0x10)
DB 0FFh,0FFh,0,0,0,1_00_10010b,11_001111b,0
_GDT_end:
The exact structure of the GDT is specified here: https://i.stack.imgur.com/KNZeO.gif
GDT entries are referenced with a selector. Bit 0 and 1 are the privilege level requested (zero for out purposes), bit 2 is GDT/LDT (also zero here for GDT). The rest is a 13-bit index to the table. Basically, the segments are referenced by simply shifting the index by three in this case.
The Interrupt Descriptor Table
If you want to receive interrupts in protected mode, the IDT is essential. It contains a code segment selector and a 32-bit address. This can be done in the boot loader environment.
Reprogramming the PIC to a proper base vector is required so that they do not collide with x86 exceptions as they do in real mode.
Here is a hardware specification of the 8259 PIC (the standard PC interrupt controller) https://stanislavs.org/helppc/8259.html
An example in C: https://wiki.osdev.org/8259_PIC#Code_Examples
Virtual 8086 Mode
Virtual 8086 is a complex topic, so I will give some brief details that should help avoid pitfalls. V86 mode allows for running real mode code in protected mode. Using it requires an IDT with at least a #GP handler.
V86 automatically sets the current privilege level to 3 for user. Any privileged instructions or the INT instruction will cause a general protection fault so that the monitor can emulate it. On the stack of the #GP, the saved instruction pointer points to the exact instruction that caused it, so be sure to increment it after emulating the instruction.
BIOS routines are sure to have IO instructions, so make sure that the TSS has an IO permission bitmap and set all the bits to zero so that all ports are allowed.
The TSS also has a field called ESP0 which is the stack pointer value that will be loaded to ESP when entering ring 3. This has to be set before entering ring 3 or V86. An assembly routine for entering V86 can set this to the current stack pointer.
See the OSDev article (which I have contributed to a while ago) about it for more specific details: https://wiki.osdev.org/Virtual_8086_Mode
Further Reading and Recommendations
Here is an excellent resource for the specifics of the IA32 architecture. This is the official Intel manual for the i386.
https://pdos.csail.mit.edu/6.828/2014/readings/i386/toc.htm
The following topics should be of interest:
- Virtual 8086 mode
- IDT/GDT/TSS
- Exceptions