It’s finally time to put together the first and second stage bootloaders we’ve seen so far and make them work – although if you’ve been programming along with the past few installments of this series, you may already have done so. In the last few sections, we looked at the Intel 80386 processor’s protected mode and what’s required to get to it.
In this part, we’ll take a short break and put everything we’ve done so far together to see it run. Might as well have a little payoff for all the work, right?
This article is going to repeat all the code presented in the previous articles to see how it all goes together. You can read through it, or you can grab all the source code here.
This article is part of a series on toy operating system development.
How it all works
Before we get to code, let’s have a summary of everything that happens during the boot process.
Loading the boot sector
- When it’s switched on, the computer performs a Power-On Self Test (POST). It then detects whether a bootable disk is inserted in the disk drive, and loads its boot sector (the first 512 bytes on the floppy disk). A boot sector is executable if its last two bytes are the magic value 0xaa55.
- The entire boot sector is loaded at address 0000:7c00, and the CPU starts executing the very first instruction found at that address.
- Only the first 3 bytes of the boot sector can be code. The next 59 are boot data bytes that describe the floppy disk. The executable code continues after that. In that code, we have to do everything necessary to load the second-stage bootloader.
First-stage bootloader
In the first-stage bootloader, we must do the following:
- Setup the memory segments and stack used by the bootloader code
- Reset the disk system
- Display a string saying “Loading OS…”
- Find the second-stage boot loader in the FAT directory
- Read the second-stage boot loader image into memory at 1000:0000
- Transfer control to the second-stage bootloader
We must leave the rest of the steps to get to protected mode and load the kernel to the second-stage bootloader, as there simply isn’t enough space available in the first-stage bootloader to do all this.
Second-stage bootloader
In the second-stage bootloader, we must do the following:
- Copy the boot sector data bytes to a local memory area, as they will be overwritten
- Find the kernel image in the FAT directory
- Read the kernel image into memory at 2000:0000
- Reset the disk system
- Enable the A20 line
- Disable interrupts, and leave them disabled
- Setup the interrupt descriptor table at 0000:0000
- Setup the global descriptor table at 0000:0800
- Load the descriptor tables into the CPU
- Switch to protected mode, and far jump so that
CSis reloaded from the GDT - Setup protected mode memory segments and stack for use by the kernel code
- Transfer control to the kernel code using a long jump
Boot sector structure
Both of our bootloaders will need to know the structure of the boot sector data bytes:
bootsector:
iOEM: .ascii "DevOS " # OEM String
iSectSize: .word 0x200 # Bytes per sector
iClustSize: .byte 1 # Sectors per cluster
iResSect: .word 1 # #of reserved sectors
iFatCnt: .byte 2 # #of fat copies
iRootSize: .word 224 # size of root directory
iTotalSect: .word 2880 # total # of sectors if below 32 MB
iMedia: .byte 0xF0 # Media Descriptor
iFatSize: .word 9 # Size of each FAT
iTrackSect: .word 18 # Sectors per track
iHeadCnt: .word 2 # number of read-write heads
iHiddenSect: .int 0 # number of hidden sectors
iSect32: .int 0 # # sectors if over 32 MB
iBootDrive: .byte 0 # holds drive that the boot sector came from
iReserved: .byte 0 # reserved, empty
iBootSign: .byte 0x29 # extended boot sector signature
acVolID: .ascii "seri" # disk serial
acVolLabel: .ascii "MYVOLUME " # just placeholder. We don't yet use volume labels.
acFSType: .ascii "FAT12 " # file system type
Required macros
Some of the assembler code that we write for the bootloaders takes the form of functions that we will call multiple times. Other code is called only once and implemented in the form of macros, which serve merely to give structure to the code (we could not use macros, but then our code would become a very long and unreadable list of assembly instructions). For reference, I will present the macros used here.
mInitSegments
The first-stage bootloader must setup real-mode memory segments to work with, as well as a stack. The boot sector is loaded by the BIOS at 0000:7c00. We will define a stack at 0000:7c00, which will grow downward from there (as stacks always do). We must disable interrupts while we define our memory segments.
It opens with a far jump to itself: the BIOS may have entered at 0000:7C00 or at 07C0:0000, the same byte of memory reached through a different CS. Since the next instruction copies CS into the other segment registers, we settle the question ourselves first. See the first bootloader code for an explanation.
.macro mInitSegments
jmp 0x0000:real_start\@ # Canonicalise CS:IP. Some BIOSes enter at
real_start\@: # 0000:7C00, others at 07C0:0000.
cli # Disable interrupts
mov ax, cs # CS = 0x0, because the far jump above made it so
mov ds, ax # DS = CS = 0x0
mov es, ax # ES = CS = 0x0
mov ss, ax # SS = CS = 0x0
mov sp, 0x7C00 # Stack grows down from offset 0x7C00 toward 0x0000.
cld # String ops move forward (rep stosb/movsb rely on it)
sti # Enable interrupts
mov iBootDrive, dl # save what drive we booted from (BIOS put it in DL)
.endm
mResetDiskSystem
Our first-stage bootloader will need to read the FAT table from disk and find the second-stage bootloader image in it. To do so, it must first reset the disk system through the floppy disk controller. If this should fail, we cannot continue and must reboot.
.macro mResetDiskSystem
mov dl, iBootDrive # drive to reset
xor ax, ax # subfunction 0
int 0x13 # call interrupt 13h
jc bootFailure # display error message if carry set (error)
.endm
mFindFile
This macro loads the root directory of the floppy disk’s FAT into memory at the provided segment loadsegment. It then looks for filename and finds the disk sector where this file begins as well as the number of sectors it occupies. These values are stored for later use. If the filename is not found in the FAT, then the system shows an error message and reboots.
This macro makes use of the ReadSector function which we will show below.
.macro mFindFile filename, loadsegment
# The root directory will be loaded in a higher segment.
# Set ES to this segment.
mov ax, \loadsegment
mov es, ax
# The number of sectors that the root directory occupies
# is equal to its max number of entries, times 32 bytes per
# entry, divided by sector size.
# E.g. (32 * rootsize) / 512
# This normally yields 14 sectors on a FAT12 disk.
# We calculate this total, then store it in cx for later use in a loop.
mov ax, 32
xor dx, dx
mul word ptr iRootSize
div word ptr iSectSize # Divide (dx:ax,sectsize) = (ax,dx)
mov cx, ax
mov root_scts, cx
# root_scts is now the number of sectors in root directory.
# Calculate start sector root directory:
# root_strt = number of FAT tables * sectors per FAT
# + number of hidden sectors
# + number of reserved sectors
xor ax, ax # find the root directory
mov al, byte ptr iFatCnt # ax = number of FAT tables
mov bx, word ptr iFatSize # bx = sectors per FAT
mul bx # ax = #FATS * sectors per FAT
add ax, word ptr iHiddenSect # Add hidden sectors to ax
add ax, word ptr iResSect # Add reserved sectors to ax
mov root_strt, ax
# root_strt is now the number of the first root sector
# Load a sector from the root directory.
# If sector reading fails, a reboot will occur.
read_next_sector:
push cx
push ax
xor bx, bx
call ReadSector
check_entry:
mov cx, 11 # Directory entries filenames are 11 bytes.
mov di, bx # es:di = Directory entry address
lea si, \filename # ds:si = Address of filename we are looking for.
repz cmpsb # Compare filename to memory.
je found_file # If found, jump away.
add bx, word ptr 32 # Move to next entry. Complete entries are 32 bytes.
cmp bx, word ptr iSectSize # Have we moved out of the sector yet?
jne check_entry # If not, try next directory entry.
pop ax
inc ax # check next sector when we loop again
pop cx
loop read_next_sector # try the next root directory sector
jmp bootFailure # could not find file: abort
found_file:
# The directory entry stores the first cluster number of the file
# at byte 26 (0x1a). BX is still pointing to the address of the start
# of the directory entry, so we will go from there.
# Read cluster number from memory:
mov ax, es:[bx+0x1a]
mov file_strt, ax
.endm
mReadFAT
The mReadFAT macro loads the floppy disk’s FAT into memory at a specified segment. This macro also makes use of the ReadSector function. Once the FAT is loaded, it will be used by the mReadFile macro to actually read a file.
.macro mReadFAT fatsegment
# The FAT will be loaded in a special segment.
# Set ES to this segment.
mov ax, \fatsegment
mov es, ax
# Calculate offset of FAT:
mov ax, word ptr iResSect # Add reserved sectors to ax
add ax, word ptr iHiddenSect # Add hidden sectors to ax
# Read all FAT sectors into memory:
mov cx, word ptr iFatSize # Number of sectors in FAT
xor bx, bx # Memory offset to read into (es:bx)
read_next_fat_sector:
push cx
push ax
call ReadSector
pop ax
pop cx
inc ax
add bx, word ptr iSectSize
loop read_next_fat_sector # continue with next sector
.endm
mReadFile
This macro reads a file image into memory at the specified memory segment. It uses the FAT table in memory read earlier by the mReadFAT macro. It also makes use of the start sector and number of sectors of the file to be read, which were earlier determined by the mFindFile macro.
.macro mReadFile loadsegment, fatsegment
# Set memory segment that will receive the file:
mov ax, \loadsegment
mov es, ax
# Set memory offset for loading to 0.
xor bx, bx
# Set memory segment for FAT:
mov cx, file_strt # CX now points to file's first FAT entry
read_file_next_sector:
# Locate sector:
mov ax, cx # Sector to read is equal to current FAT entry
add ax, root_strt # Plus the start of the root directory
add ax, root_scts # Plus the size of the root directory
sub ax, 2 # ... but minus 2
# Read sector:
push cx # Read a sector from disk, but save CX
call ReadSector # as it contains our FAT entry
pop cx
add bx, iSectSize # Move memory pointer to next section
# Get next sector from FAT:
push ds # Make DS:SI point to FAT table
mov dx, \fatsegment # in memory.
mov ds, dx
mov si, cx # Make SI point to the current FAT entry
mov dx, cx # (offset is entry value * 1.5 bytes)
shr dx
add si, dx
mov dx, ds:[si] # Read the FAT entry from memory
test cx, 1 # Is the CLUSTER NUMBER odd or even?
jnz read_next_file_odd
and dx, 0x0fff # even: our entry is the low 12 bits
jmp read_next_file_cluster_done
read_next_file_odd:
shr dx, 4 # odd: our entry is the high 12 bits
read_next_file_cluster_done:
pop ds # Restore DS to the normal data segment
mov cx, dx # Store the new FAT entry in CX
cmp cx, 0xff8 # If the FAT entry is greater or equal
jb read_file_next_sector # to 0xff8, then we've reached end-of-file
.endm
mStartSecondStage
This macro sets up data segments for the second stage bootloader code which was loaded at 1000:0000. It then performs a long jump to the second-stage code, thus causing the code segment to point to the code’s location.
.macro mStartSecondStage
# Make es and ds point to segment where 2nd stage was loaded.
mov ax, LOAD_SEGMENT
mov es, ax
mov ds, ax
# Jump to second stage start of code:
jmp LOAD_SEGMENT:0
.endm
mCopyBootSector
The second-stage bootloader requires the data in the boot sector as well. When the second stage runs, the first thing it does is copy the boot sector data to a local memory area, before it gets overwritten.
.macro mCopyBootSector
push ds # The original boot sector as loaded at 0000:7c00
xor ax, ax # The actual bootsector data there starts at 0000:7c03
mov ds, ax # Set ds:si to point to 0000:7c03
mov si, 0x7c03
lea di, bootsector # Set es:di to point to bootsector area in our code.
mov cx, 34 # Copy 34 bytes: through iBootDrive, the last field
# the second stage actually reads. (The full BPB and
# EBPB run to 59 bytes if you want all of it.)
rep movsb
pop ds
.endm
mGoProtected
This macro switches the CPU to protected mode by setting the least significant bit in the CR0 register, and then, just as importantly, jumps.
Immediately after CR0.PE is set, CS must be reloaded from the new global descriptor table, because the processor is still using the segment information it cached when CS was last loaded - in real mode. Only a far jump reloads it. Earlier versions of this series called this step “clearing the prefetch queue” and used a near jump followed by a couple of nops, which does not accomplish it; see jumping to protected mode for the full explanation, including why the jump takes a linbase argument and why the .code32 below is not optional.
.macro mGoProtected linbase
mov eax, cr0
or eax, 1
mov cr0, eax
data32 jmp 0x08:(\linbase + in_pmode\@) # reloads CS from GDT entry 1
.code32
in_pmode\@:
.endm
mSetup386Segments
Before we jump to the kernel code, we must set up the data segments and stack that the kernel will use. Since we are in protected mode at this point, we no longer specify segmented addresses but must use a selector instead. Also, we define a stack at 0x30000 (linear address) that will grow downwards.
.macro mSetup386Segments
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
mov esp, 0x2ffff
.endm
mJumpToKernel
This final macro transfers control to the kernel code. It does this by executing a long jump, which will set the code segment cs to point to the kernel code. This has to be a 32-bit long jump, since we are now in protected mode and the kernel is 32-bit code - and since mGoProtected has already switched the assembler to .code32, we can simply write it:
.macro mJumpToKernel
jmp 0x08:0x20000
.endm
Required functions
Apart from the above macros, we must write a number of functions that our bootloaders will use.
WriteString
The WriteString function writes a string to the terminal. It does this using BIOS interrupt 0x10, function 0xe. This can only be done while we’re still in real mode. After the switch to protected mode, we’ll need to write our own functions to write to the screen, since we can’t use the BIOS anymore.
This function expects a NULL-terminated string at ds:si.
.func WriteString
WriteString:
lodsb # load byte at ds:si into al (advancing si)
or al, al # test if character is 0 (end)
jz WriteString_done # jump to end if 0.
mov ah, 0xe # Subfunction 0xe of int 10h (video teletype output).
mov bx, 9 # Set bh (page number) to 0, and bl (attribute) to white (9).
int 0x10 # call BIOS interrupt.
jmp WriteString # Repeat for next character.
WriteString_done:
retw
.endfunc
Reboot
This function displays a message “Press any key…”, waits for a keypress, then reboots. The keypress is read using BIOS interrupt 0x16, function 0x0. Again, this can only be done while we’re still in real mode. After the switch to protected mode, we’ll need to write our own functions to write to the screen, since we can’t use the BIOS anymore.
.func Reboot
Reboot:
lea si, rebootmsg # Load address of reboot message into si
call WriteString # print the string
xor ax, ax # subfuction 0
int 0x16 # call bios to wait for key
jmp 0xFFFF:0x0000 # far jump to the CPU's power-on entry point (reboot)
.endfunc
ReadSector
This last function reads a sector from the floppy disk and stores it in es:bx. The sector logical address is provided in register ax. This function uses BIOS interrupt 0x13, function 0x2 to do the reading. Note that floppy disks are not very reliable, so the code attempts to read the sector data four times before giving up. If it gives up, the boot process fails and the system reboots.
.func ReadSector
ReadSector:
xor cx, cx # Set try count = 0
readsect:
push ax # Store logical block
push cx # Store try number
push bx # Store data buffer offset
# Calculate cylinder, head and sector:
# Cylinder = (LBA / SectorsPerTrack) / NumHeads
# Sector = (LBA mod SectorsPerTrack) + 1
# Head = (LBA / SectorsPerTrack) mod NumHeads
mov bx, iTrackSect # Get sectors per track
xor dx, dx
div bx # Divide (dx:ax/bx to ax,dx)
# Quotient (ax) = LBA / SectorsPerTrack
# Remainder (dx) = LBA mod SectorsPerTrack
inc dx # Add 1 to remainder, since sector
mov cl, dl # Store result in cl for int 13h call.
mov bx, iHeadCnt # Get number of heads
xor dx, dx
div bx # Divide (dx:ax/bx to ax,dx)
# Quotient (ax) = Cylinder
# Remainder (dx) = head
mov ch, al # ch = cylinder
xchg dl, dh # dh = head number
# Call interrupt 0x13, subfunction 2 to actually
# read the sector.
# al = number of sectors
# ah = subfunction 2
# cx = sector number
# dh = head number
# dl = drive number
# es:bx = data buffer
# If it fails, the carry flag will be set.
mov ax, 0x0201 # Subfunction 2, read 1 sector
mov dl, iBootDrive # from this drive
pop bx # Restore data buffer offset.
int 0x13
jc readfail
# On success, return to caller.
pop cx # Discard try number
pop ax # Get logical block from stack
ret
# The read has failed.
# We will retry four times total, then jump to boot failure.
readfail:
pop cx # Get try number
inc cx # Next try
cmp cx, 4 # Stop at 4 tries
je bootFailure
# Reset the disk system:
xor ax, ax
int 0x13
# Get logical block from stack and retry.
pop ax
jmp readsect
.endfunc
Enabling the A20 line
The second-stage bootloader will need to enable the processors 21st address line (see this article for an in-depth explanation). The following functions and macros do just this.
CheckA20
This function checks whether the A20 line is currently enabled. It does this by testing whether the memory “wraps” at the one megabyte mark.
.func CheckA20
CheckA20:
pushf
push ds
push es
push di
push si
cli
xor ax, ax # Set es:di = 0000:0500
mov es, ax
mov di, 0x0500
not ax # Set ds:si = ffff:0510
mov ds, ax
mov si, 0x0510
mov al, byte ptr es:[di] # Save byte at es:di on stack.
push ax
mov al, byte ptr ds:[si] # Save byte at ds:si on stack.
push ax
mov byte ptr es:[di], 0x00 # [es:di] = 0x00
mov byte ptr ds:[si], 0xFF # [ds:si] = 0xff
cmp byte ptr es:[di], 0xFF # Did memory wrap around?
pop ax
mov byte ptr ds:[si], al # Restore byte at ds:si
pop ax
mov byte ptr es:[di], al # Restore byte at es:di
mov ax, 0
je check_a20__exit # If memory wrapped around, return 0.
mov ax, 1 # else return 1.
check_a20__exit:
pop si
pop di
pop es
pop ds
popf
ret
.endfunc
mSetA20BIOS
This macro uses BIOS interrupt 0x15, function 0x2401 to attempt to enable the A20 line.
.macro mSetA20BIOS
mov ax, 0x2401
int 0x15
.endm
Wait_8042_command
This function has the CPU wait until the 8042 keyboard controller is ready to accept a command byte.
.func Wait_8042_command
Wait_8042_command:
in al,0x64
test al,2
jnz Wait_8042_command
ret
.endfunc
Wait_8042_data
This function has the CPU wait until the 8042 keyboard controller is ready to accept a data byte.
.func Wait_8042_data
Wait_8042_data:
in al,0x64
test al,1
jz Wait_8042_data
ret
.endfunc
mSetA20Keyboard
This macro uses the 8042 keyboard controller chip to attempt to enable the A20 line.
.macro mSetA20Keyboard
cli # Disable interrupts
call Wait_8042_command # When controller ready for command
mov al,0xAD # Send command 0xad (disable keyboard).
out 0x64,al
call Wait_8042_command # When controller ready for command
mov al,0xD0 # Send command 0xd0 (read from input)
out 0x64,al
call Wait_8042_data # When controller has data ready
in al,0x60 # Read input from keyboard
push eax # ... and save it
call Wait_8042_command # When controller is ready for command
mov al,0xD1 # Set command 0xd1 (write to output)
out 0x64,al
call Wait_8042_command # When controller is ready for command
pop eax # Write input back, with bit #2 set
or al,2
out 0x60,al
call Wait_8042_command # When controller is ready for command
mov al,0xAE # Write command 0xae (enable keyboard)
out 0x64,al
call Wait_8042_command # Wait until controller is ready for command
sti # Enable interrupts
.endm
mSetA20FastGate
This macro attempts to enable the A20 line by using the “Fast A20” method, which is a special port that some chipsets have.
.macro mSetA20FastGate
in al, 0x92
test al, 2 # already enabled? then leave well alone
jnz 9f
or al, 2 # bit 1 is the A20 gate
and al, 0xFE # bit 0 is FAST RESET - never write it back as 1
out 0x92, al
9:
.endm
mEnableA20
This final macro combines all previous A20 macros to enable the A20 line. If none of the methods work, the macro gives up and reboots the system.
.macro mEnableA20
call CheckA20
cmp ax, 0
jne enable_A20__done\@
mSetA20BIOS
call CheckA20
cmp ax, 0
jne enable_A20__done\@
mSetA20Keyboard
call CheckA20
cmp ax, 0
jne enable_A20__done\@
mSetA20FastGate
call CheckA20
cmp ax, 0
jne enable_A20__done\@
mWriteString a20error
mReboot
enable_A20__done\@:
.endm
Macros for descriptor tables
The second-stage bootloader will need to set up a global descriptor table (GDT) and an empty interrupt descriptor table (IDT). The following macros take care of that:
mSetupGDT
This macro creates a GDT with three entries:
- A NULL-descriptor (required)
- A code segment of 4GB, starting at 0x0
- A data segment of 4GB, starting at 0x0
.macro mSetupGDT
# Point es:di at 0000:0800 and zero ax for the NULL descriptor.
# These used to be left over from mSetupIDT, which happens to finish with
# es = 0 and di = 2048 — exactly where the GDT belongs. That works, but it
# silently makes this macro depend on the other one having run first, and on
# the IDT being exactly 2048 bytes long. Set them here instead.
xor ax, ax
mov es, ax
mov di, 0x800
# NULL Descriptor:
mov cx, 4 # Write the NULL descriptor,
rep stosw # which is 4 zero-words.
# Code segment descriptor:
mov es:[di], word ptr 0xffff # limit = 0xffff (since granularity
# bit is set, this is 4 GB)
mov es:[di+2], word ptr 0x0000 # base = 0x0000
mov es:[di+4], byte ptr 0x0 # base
mov es:[di+5], byte ptr 0x9a # access = 0x9a (see above)
mov es:[di+6], byte ptr 0xcf # flags + limit = 0xcf (see above)
mov es:[di+7], byte ptr 0x00 # base
add di, 8
# Data segment descriptor:
mov es:[di], word ptr 0xffff # limit = 0xffff (since granularity
# bit is set, this is 4 GB)
mov es:[di+2], word ptr 0x0000 # base = 0x0000
mov es:[di+4], byte ptr 0x0 # base
mov es:[di+5], byte ptr 0x92 # access = 0x92 (see above)
mov es:[di+6], byte ptr 0xcf # flags + limit = 0xcf (see above)
mov es:[di+7], byte ptr 0x00 # base
.endm
mSetupIDT
This macro creates an interrupt descriptor table at 0000:0000. It has 256 entries of 8 bytes each, all filled with zeroes. The presence of an IDT is required to switch to protected mode, but we don’t need to define any interrupt handlers yet.
.macro mSetupIDT
xor ax, ax # al = 0 is the byte stosb will write
mov es, ax # Have es:di point to 0000:0000
mov di, 0
mov cx, 2048 # Write 2048 zeroes
rep stosb # since the table has 256 entries of 8 bytes.
.endm
mLoadDescriptorTables
This final macro tells the CPUs where the GDT and the IDT are:
.macro mLoadDescriptorTables
lgdt gdt
lidt idt
.endm
First-stage bootloader
Now that we’ve got all the required macros and functions out of the way, cobbling together the first-stage bootloader becomes simple. We basically turn the recipe presented at the start of this article into code, calling macros as necessary.
.code16
.intel_syntax noprefix
.text
.org 0x0
LOAD_SEGMENT = 0x1000 # 2nd stage loader will be loader into segment 1000h
FAT_SEGMENT = 0x0ee0 # The boot disk's FAT will be loaded into segment 0x0ee0
# (9*512 bytes under 2nd stage loader, because the FAT
# consists of 9 512-byte segments).
.global main
main:
jmp short start # jump to beginning of code
nop # Boot sector data starts 3 bytes from beginning, hence nop
.include "bootsector.s"
.include "macros.s"
start:
mInitSegments # Initialize memory segments used by this program
mResetDiskSystem # Reset the disk system
mWriteString loadmsg # Display "loading..."
mFindFile filename, LOAD_SEGMENT # Find the 2ndstage file in the root directory
mReadFAT FAT_SEGMENT # Load the FAT table into memory
mReadFile LOAD_SEGMENT, FAT_SEGMENT # Read the 2ndstage file into memory
mStartSecondStage # Execute the 2ndstage file.
#
# Booting has failed because of a disk error.
# Inform the user and reboot.
#
bootFailure:
mWriteString diskerror # Show "Disk error, press key to reboot"
mReboot # Reboot
.include "functions.s"
# PROGRAM DATA
filename: .asciz "2NDSTAGEBIN"
rebootmsg: .asciz "Press any key to reboot.\r\n"
diskerror: .asciz "Disk error. "
loadmsg: .asciz "Loading DevOS...\r\n"
root_strt: .word 0 # holds offset of root directory on disk
root_scts: .word 0 # holds # sectors in root directory
file_strt: .word 0 # holds first cluster of the file we're loading
.org 510, 0 # Pad with nulls up to offset 510
BootMagic: .word 0xAA55 # magic word for BIOS
As it stands, this code compiles to 485 bytes, which is just 25 bytes short of the maximum of 510 bytes. Whew!
Second-stage bootloader
Now we can finally write the complete second-stage bootloader.
.code16
.intel_syntax noprefix
.text
.org 0x0
LOAD_SEGMENT = 0x2000 # load the kernel to segment 0x2000
FAT_SEGMENT = 0x0ee0 # load FAT at segment 0x0ee0
STAGE2_BASE = 0x10000 # linear address we run at: stage 1 loaded us at 1000:0000
.global main
main:
jmp short start # jump to beginning of code
nop
# INCLUDES
.include "bootsector.s"
.include "functions.s"
.include "macros.s"
.include "a20.s"
.include "descriptors.s"
# PROGRAM DATA
filename: .asciz "KERNEL BIN"
rebootmsg: .asciz "Press any key to reboot.\r\n"
diskerror: .asciz "Disk error. "
a20error: .asciz "A20 unavailable. "
root_strt: .word 0 # holds offset of root directory on disk
root_scts: .word 0 # holds # sectors in root directory
file_strt: .word 0 # holds first cluster of the file we're loading
idt:
.word 2047 # Size of IDT: 256 entries of 8 bytes, minus one
.int 0x0 # Linear address of IDT
gdt:
.word 23 # Size of GDT: 3 entries of 8 bytes, minus one
.int 2048 # Linear address of GDT
start:
# Copy the boot sector to our code:
mCopyBootSector
# Find the kernel file:
mFindFile filename, LOAD_SEGMENT
# Load the kernel file into memory:
mReadFile LOAD_SEGMENT, FAT_SEGMENT
# Reset the disk system:
mResetDiskSystem
# Enable the A20 line:
mEnableA20
# From here on there is no going back: the IDT we are about to write lands
# on top of the real-mode interrupt vector table, and the descriptors in it
# are all marked "not present". An interrupt taken between here and the
# kernel would fault, then double-fault, then reset the machine.
cli
# Setup the interrupt descriptor table:
mSetupIDT
# Setup the global descriptor table:
mSetupGDT
# Actually load the IDT and GDT:
mLoadDescriptorTables
# Switch to protected mode (and far jump, to reload CS from the GDT):
mGoProtected STAGE2_BASE
# Point all data segments to GDT 2:
mSetup386Segments
# Jump to kernel code:
mJumpToKernel
# shouldn't get here...
#
# Booting has failed because of a disk error.
# Inform the user and reboot.
#
# mGoProtected left the assembler in .code32; this path is only ever
# reached from real mode, so switch back.
.code16
bootFailure:
mWriteString diskerror
mReboot
Summary
In this section of the “writing your own toy operating system” series, we’ve put together all the code we have written so far. The result is a first and second-stage bootloader that work together to load a kernel image, switch to protected mode, and run the kernel.
We haven’t actually gotten around yet to writing any kernel code, but in the source code that goes with this article there actually is a small kernel program that displays “Hello world”.
In the next section, we see about writing an actual kernel! Check back soon.
9 comments
Archived from the site's previous comment system. New comments are closed.
I have an error when I try to excute the makefile.
The error is: i386:x86-64 the input file architecture "kernel.o" is not compatible with the outpt i386.
What I should do?
I wish you will answer me.
Best regards, Alberto
I've succesfully run it until it goes to void start_kernel() but after that no matter what loop i try to write it keeps rebooting, is that an error?
Are you stepping through the code with bochs? Verify that the code looks like you think it does. If you're not in i386 mode, your kernel code may simply reboot. The CPU may fault and cause a reboot as well. At this level, the only way to get things right is to step through the code from beginning to end and see what it does, also looking closely at bochs console output. Since you've gotten this far, your BIOS sector reading code is probably fine, but you may have errors in the part where you move to protected mode.
Can you please continue the tutorial and show how to write the kernel?
I have the kernel set up, but it keeps rebooting after it says hello. It won't loop in the main function, in the video_init function, or in assembly. I have tried it with my version of the bootloader and the original, and they both end up producing the same effect.
I want to see how the kernel was made so I can fix this issue and add on to the kernel.
It is most curious that the tutorial seems to have concluded some time ago, maybe about two years ago? It is a shame that the readers that happen upon this novel and interesting work are left stranded and without closure! Maybe this was the intent from the beginning? Is it possible that the writer departed Independent Software?
Whatever the case, it would be charitable for Independent Software to complete the series remaining in this tutorial so that those of us reading it would be a able to feel that there was nothing else to learn and we could move on to other facets of learning because we were complete with the knowledge gained.
Please take a moment, or two, and give us the last chapters of this interesting tutorial.
I fully agree. This set of tutorials isn't finished. The problem is, it takes a huge amount of time to write new sections... They will come, eventually! It's a labour of love. Please stay tuned for updates in time.
it somehow gets stuck in the setup IDT section
Kernel print hello world and rebooting
stuck in bootloop
There is an error in macros.s. When readying 2nd-stage-bootloaders larger than 512 bytes, the program just read the first 512 bytes. I had to change lines 190 and 191 from:
test dx, 1 # See which way to shift
jz read_next_file_even
To:
test cx, 1
jnz read_next_file_even
In order to make it work.