User:Johnburger/Demo/Boot/Boot
The following code is the entire Real Mode code in the system. It prepares the system for Protected Mode by setting up relevant tables, but it also calls the BIOS to perform tasks, or get information to save away for later. The final thing it does is "stamp" the code with a BIOS signature, so that the various boot loaders will recognize it as a valid - albeit never-seen-before - Boot Sector.
Boot Sector size
One thing to note is that we only have 512 bytes to do everything. If all options are enabled (BIOS.VGA.90x60
takes a lot of work!) there are only a handful of bytes left. For this reason the following code doesn't use the formal, correct, and highly-laudable modular programming technique of defined subroutines that are individually CALL
ed - I don't have the space to waste on three bytes for every call, and one byte for every return.
Fall-through Code
Instead, everything is written as a stream-of-consciousness - sorry, -execution - paradigm where the tail end of one module (yes, there are still modules!) simply falls through to the start of the next one. There are places where CALL
/RET
are used - whenever the same bunch of code needs to be executed more than once, I put it into a subroutine. But that pesky RET
at the end interferes with the stream-of-cons... er... -execution paradigm, and I have to JMP
over it.
But sometimes I use a different technique. If I'm about to fall into the subroutine with carefully-set-up register values, and when I've finished I'm about to JMP
past the subroutine anyway, instead I cheat. Instead of
CALL .Sub
JMP .Past
.Sub:
; Do stuff here
RET
.Past:
I use an assembly trick and code the following:
PUSH .Past ; Fake a CALL from Past
.Sub:
; Do stuff here
RET
.Past:
When the RET
is executed, it POP
s off the return address - which is Past where I want to be!
I don't normally condone this kind of coding, but sometimes it's necessary in constrained environments - and this is very constrained indeed!
Common JMP
Code
Having justified my use of a coding technique that would have Niklaus Wirth turning in his (hopefully long-postponed!) grave, I need to mention a more common coding technique that I also use, this time in code that is less constrained. I have some code that is repeated a number of times, but is extremely similar. Given a little initialization code, the rest is identical. In those cases, I do use the following construct:
Entry1:
; Perform setup for situation #1
JMP CommonCode
Entry2:
; Perform setup for situation #2
JMP CommonCode
Entry3:
; Perform setup for situation #3
JMP CommonCode
Entry4:
; Perform setup for situation #4
; JMP CommonCode
CommonCode:
; Do what's common
For this technique, I am not apologetic. In fact, the above also demonstrates another technique I use: sometimes code is "obvious", usually by pattern with other code around it, but the effect of that obvious code is a no-effect - in other words, a waste of space. I have three choices:
- Leave the code in, taking up some measly number of bytes;
- Remove the code completely, meaning that in the future I might just notice the break in the pattern and unthinkingly add it in again; or
- Leave the code in, but comment it out to prevent it from being assembled.
Needless to say, I chose the last option.
Memory Map
At this point in time (BIOS Boot complete), the Memory Map looks something like:
Address | Usage |
---|---|
0000_0000h
|
Interrupt Vector Table |
0000_0400h
|
SS:SP Stack Top *
|
0000_0400h
|
BIOS Data Area |
0000_0500h
|
Available |
0000_0600h
|
Master Boot Record (MBR) **
|
0000_0800h
|
Available |
0000_7C00h
|
BIOS-loaded Boot Sector |
0000_7E00h
|
Available |
0009_F???h ***
|
Extended BIOS Data Area |
000A_0000h
|
Adapter / ROM Area |
000B_8000h
|
Text Video Memory |
000F_0000h
|
BIOS ROM |
0010_0000h
|
High Memory ****
|
*
Yes! Believe it or not, the BIOS initializes the stack to point to the end of the Interrupt Vector table. Every CALL
or PUSH
you make adds a new, interesting vector to the IVT...
**
The MBR only exists here if the PC has booted from a Hard Drive.
***
An INT 12h will help identify this value.
****
Note that this memory is only accessible in Protected Mode - except for the first 64 kiB (less 16 bytes), which you can access with a Segment Register set to 0FFFFh
as long as the A20 Gate is off. By the way: DON'T DO THIS!
Demo/Boot/Boot.inc
;
; Boot/Boot.inc
;
; This is the BIOS Boot entry point. It runs in Real Mode, and assumes the
; standard BIOS Boot Specification 1.01.
;
; It performs the following actions:
; 1) Loads the rest of the code into RAM;
; 2) Gets some information from the soon-to-be-unusable BIOS;
; 3) Sets up the PC for Protected Mode;
; 4) JMPs to the freshly-loaded Protected Mode code
; After that, the memory used to hold this code is avaiable to the system.
;
; Note that one of the goals of the Boot sector is to jettison itself:
; 7C00h is such an awkward location, so ideally after doing all the Real Mode
; initialisation, we can JMP to Protected Mode and simply leave this code behind.
; To save space, I take advantage of some HORRIBLE things you can do with
; assembly language: PUSHing values to fake CALLs, or worse, SELF-MODIFYING CODE!
; In Protected Mode, this last is very difficult to organise - and unreliable
; with later processors that have on-board caches - so not worth it.
; In Real Mode, this can be very useful as a register- and code-saving device!
; To save space, I've done away with byte-wasting CALLs and RETs for "modular"
; programming. Not that modules aren't in and of themselves useful - indeed,
; they're still modularised within their own little files - but the result is
; simply a stream of bytes to execute. Where I have defined subroutines to be
; CALLed, the stream of code has to either JMP over the subroutine's RET, or
; a CALL is faked by PUSHing the desired continuation address onto the stack.
;-------------------------------------------------------------------------------
SEGMENT Real VSTART=0 ALIGN=1
USE16 ; Real Mode? 16 bits!
; At this point, DL is probably the drive number of the Boot Drive.
; ES:SI is possibly a pointer to the MBR Partition table entry (See Load.inc).
; In any case: don't clobber DL, ES or SI before Load.inc!
%include "Boot/Entry.inc" ; Entry code to initialise important registers
;-------------------------------------------------------------------------------
%include "Boot/CPU.inc" ; Check CPU is (at least) a '386
;-------------------------------------------------------------------------------
%include "Boot/Load.inc" ; Load Protected Mode code
;-------------------------------------------------------------------------------
%include "Boot/IDT.inc" ; Move IDT to final location
;-------------------------------------------------------------------------------
%include "Boot/RAM.inc" ; Get RAM size
;-------------------------------------------------------------------------------
%include "Boot/A20.inc" ; Enable A20 gate
;-------------------------------------------------------------------------------
%include "Boot/Key.inc" ; Speed up keyboard
;-------------------------------------------------------------------------------
%include "Boot/VGA.inc" ; Switch Video mode
;-------------------------------------------------------------------------------
%include "Boot/PM.inc" ; Switch to Protected Mode
;-------------------------------------------------------------------------------
; At this point, we still must be inside the first 512-byte Boot sector.
; We need to confirm to the BIOS that this sector is a valid Boot sector by
; having BIOS.Sig.Value at the correct place.
; Note that if the previous code was too large, the following Pad calculation
; will underflow and generate an error.
Real.Padding EQU BIOS.Sig.Pos - ($ - $$)
TIMES Real.Padding DB 00h
DW BIOS.Sig.Value