2 Stage Boot Loader

Writing a Boot Sector v.0.2

To write a boot sector that loads a kernel, we can start by creating a simple assembly program that:

  1. Sets up the basic environment (like the stack).
  2. Loads the kernel from the disk into memory.
  3. Jumps to the kernel to start its execution.

Let’s break down this task into manageable steps. I’ll guide you through writing a minimal boot sector in x86 assembly, designed to boot in real mode (16-bit). The kernel will be loaded from the disk’s first sector (after the boot sector) into memory.

Step 1: Boot Sector Layout

A boot sector is exactly 512 bytes. The last two bytes must be 0x55 and 0xAA to mark it as bootable.

Step 2: Boot Sector Code

Here’s a simple boot sector that loads the kernel from the second sector (sector 1) of the disk into memory at 0x7E00 and then jumps to it:

[BITS 16]        ; Use 16-bit instructions (real mode)

ORG 0x7C00       ; Set the origin to where BIOS loads the boot sector (0x7C00)

start:
    ; Set up the segment registers
    xor ax, ax        ; Zero out AX
    mov ds, ax        ; Set DS (data segment) to 0
    mov es, ax        ; Set ES (extra segment) to 0
    mov ss, ax        ; Set SS (stack segment) to 0
    mov sp, 0x7C00    ; Set SP (stack pointer) to 0x7C00, so we use the same area for the stack

    ; Load the second sector (kernel) into memory at 0x7E00
    mov bx, 0x7E00    ; ES:BX is the destination in memory (0x7E00)
    mov ah, 0x02      ; BIOS read sector function
    mov al, 0x01      ; Number of sectors to read (1 sector = 512 bytes)
    mov ch, 0x00      ; Cylinder 0
    mov cl, 0x02      ; Sector 2 (sectors start at 1, so this is the second sector)
    mov dh, 0x00      ; Head 0
    mov dl, 0x80      ; Drive 0 (first hard drive)
    int 0x13          ; BIOS interrupt to read from disk

    jc error          ; If carry flag is set, an error occurred

    ; Jump to the loaded kernel
    jmp 0x0000:0x7E00 ; Jump to the start of the loaded kernel

error:
    ; Simple infinite loop in case of an error
    hlt
    jmp error

times 510-($-$$) db 0  ; Fill the rest of the boot sector with zeros
dw 0xAA55              ; Boot signature

Step 3: Compiling and Writing the Boot Sector

You need to assemble this code into a binary file and then write it to the first 512 bytes of your disk image. Hereโ€™s how you can do it using nasm and dd:

  1. Assemble the code: nasm -f bin bootsector.asm -o bootsector.bin
  2. Write the bootsector to a disk image: dd if=bootsector.bin of=disk.img bs=512 count=1 conv=notrunc
  3. Add your kernel to the disk image starting from the second sector. Assuming your kernel is also in binary form: dd if=kernel.bin of=disk.img bs=512 seek=1 conv=notrunc

Step 4: Testing

You can test this boot sector using an emulator like QEMU:

qemu-system-x86_64 -drive file=disk.img,format=raw

Next Steps

  • Extend the Bootloader: This basic boot sector only loads one sector of the kernel. If your kernel is larger, you’ll need to add code to load multiple sectors.
  • Protected Mode Transition: If your kernel is 32-bit or 64-bit, you’ll need to transition from real mode to protected mode or long mode.

Handling 64bit Kernels

Given that you have a 64-bit kernel, we’ll need to transition from the initial real mode (16-bit) environment provided by the BIOS to long mode (64-bit).

This requires a few additional steps:

  1. Load the Kernel into Memory.
  2. Switch to Protected Mode.
  3. Enable Paging and Switch to Long Mode.
  4. Jump to the 64-bit Kernel Entry Point.

Let’s walk through the steps to achieve this.

Step 1: Load the Kernel

We’ll begin by loading the kernel into memory as before, but we’ll also reserve additional memory space and set up for transitioning to 64-bit mode.

Step 2: Set Up Protected Mode

Protected Mode allows us to use 32-bit instructions and access more than 1 MB of memory. Here’s how we’ll enter protected mode:

  • Load the Global Descriptor Table (GDT).
  • Enable the PE (Protection Enable) bit in the Control Register CR0.

Step 3: Set Up Paging

To enter long mode, we must enable paging. This involves setting up page tables and then enabling the PG (Paging) bit in the CR0 register and the LME (Long Mode Enable) bit in the EFER MSR.

Step 4: Jump to 64-bit Kernel

Finally, we’ll jump to the 64-bit kernel entry point, which should now be in long mode.

Here’s an example of a bootloader that handles these steps:

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load the kernel (assume it's larger, so we load multiple sectors)
    mov bx, 0x1000          ; Load kernel to 0x1000:0x0000
    mov ah, 0x02            ; BIOS read sectors function
    mov al, 0x10            ; Load 16 sectors (8KB)
    mov ch, 0x00            ; Cylinder 0
    mov cl, 0x02            ; Sector 2
    mov dh, 0x00            ; Head 0
    mov dl, 0x80            ; Drive 0 (first hard disk)
    int 0x13                ; BIOS interrupt

    jc error                ; Jump to error if carry flag is set

    ; Load GDT
    lgdt [gdt_descriptor]

    ; Enter Protected Mode
    cli                     ; Clear interrupts
    mov eax, cr0
    or eax, 0x1             ; Set PE bit in CR0
    mov cr0, eax
    jmp CODE_SEG:protected_mode_entry ; Far jump to flush pipeline

[BITS 32]
protected_mode_entry:
    ; Set up segments for protected mode
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x7C00

    ; Enable A20 line (necessary for accessing memory above 1MB)
    call enable_a20

    ; Set up paging
    mov eax, page_directory
    mov cr3, eax            ; Load the page directory
    mov eax, cr4
    or eax, 0x10            ; Set PSE bit for 4MB pages
    mov cr4, eax
    mov eax, cr0
    or eax, 0x80000001      ; Enable paging and protection (set PG and PE bits)
    mov cr0, eax

    ; Enter long mode
    rdmsr
    or eax, 0x100           ; Set LME bit (Long Mode Enable)
    wrmsr
    jmp LONG_MODE_CODE_SEG:long_mode_entry

[BITS 64]
long_mode_entry:
    ; Long mode is now active
    ; Jump to the kernel entry point
    mov rax, 0x100000       ; Kernel entry point address
    jmp rax

error:
    ; Error handling code here
    hlt
    jmp error

; GDT for 64-bit mode transition
gdt_start:
    ; Null Descriptor
    dq 0x0000000000000000
    ; Code Segment Descriptor
    dq 0x00AF9A000000FFFF
    ; Data Segment Descriptor
    dq 0x00AF92000000FFFF
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; Size of GDT
    dd gdt_start                ; Address of GDT

CODE_SEG equ gdt_start + 0x08    ; Code segment offset in GDT
DATA_SEG equ gdt_start + 0x10    ; Data segment offset in GDT
LONG_MODE_CODE_SEG equ gdt_start + 0x18 ; Long mode code segment offset in GDT

align 4096
page_directory:
    dd 0x00000083 | (page_table >> 12)
    times 1023 dd 0x00000000
align 4096
page_table:
    times 1024 dd 0x00000083 ; Identity-mapped pages

enable_a20:
    ; Code to enable the A20 line
    ; A20 Gate BIOS routine
    in al, 0x64
wait1:
    test al, 0x2
    jnz wait1
    mov al, 0xD1
    out 0x64, al
wait2:
    in al, 0x64
    test al, 0x2
    jnz wait2
    mov al, 0xDF
    out 0x60, al
    ret

times 510-($-$$) db 0
dw 0xAA55

Explanation of the Code:

  1. Loading the Kernel:
    • The kernel is loaded from the disk to memory at 0x1000:0x0000.
  2. GDT Setup:
    • We define a Global Descriptor Table (GDT) with segments for 16-bit, 32-bit, and 64-bit modes.
  3. Protected Mode Transition:
    • The system is transitioned to Protected Mode by setting the PE bit in CR0.
  4. Paging Setup:
    • Paging is enabled, which is a prerequisite for entering long mode.
  5. Long Mode Entry:
    • The CPU is switched to long mode by setting the LME bit and then jumping to the 64-bit kernel’s entry point.

Step 5: Kernel Preparation

Ensure your 64-bit kernel is set up to receive control from this bootloader. The kernel should be linked to run from the physical address where it is loaded by the bootloader (in this case, 0x100000).

Selecting Multiple Kernels

To create a bootloader that allows the user to select between different kernel versions, you’ll need to add a simple text-based user interface to your bootloader. This interface can display a list of available kernel versions and allow the user to select one to boot.

This will involve adding a menu system, capturing user input, and loading the selected kernel.

High-Level Steps

  1. Store Kernel Information:
    • Store multiple kernels on the disk at different sectors.
    • Maintain a small metadata area in the bootloader to keep track of kernel names and their locations.
  2. Display a Menu:
    • Display a list of available kernel versions on the screen.
    • Allow the user to select a kernel using keyboard input.
  3. Load and Boot the Selected Kernel:
    • Load the selected kernel into memory.
    • Transition to the appropriate mode (64-bit in your case) and jump to the kernel’s entry point.

Example Bootloader with UI for Kernel Selection

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Display menu
    call display_menu

    ; Get user input and choose kernel
    call get_user_input
    call load_selected_kernel

    ; Jump to protected mode (as described earlier)
    call enter_protected_mode

    ; Jump to the 64-bit kernel entry point
    call long_mode_entry

    ; Halt on error
error:
    hlt
    jmp error

; Display a simple text-based menu to select the kernel version
display_menu:
    mov si, menu_text
    call print_string
    ret

; Print a string pointed to by SI
print_string:
    mov ah, 0x0E          ; BIOS teletype function
.next_char:
    lodsb                 ; Load the next byte from [SI] into AL
    cmp al, 0             ; Is this the null terminator?
    je .done              ; If yes, we're done
    int 0x10              ; Otherwise, print the character
    jmp .next_char        ; Repeat for the next character
.done:
    ret

; Get user input for selecting kernel
get_user_input:
    xor ax, ax
    int 0x16              ; BIOS keyboard input
    sub al, '1'           ; Convert keypress to an index (assuming '1', '2', '3' for menu items)
    mov bx, ax            ; Store the index in BX
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    ; Assume BX contains the selected kernel index
    mov cx, 0x1000         ; Load at memory location 0x1000
    mov dx, [kernel_sectors + bx*2]
    mov dh, [kernel_heads + bx]
    mov al, [kernel_sizes + bx] ; Number of sectors for this kernel

    ; BIOS interrupt to read from disk
    mov ah, 0x02          ; BIOS read sectors function
    int 0x13              ; BIOS interrupt

    jc error              ; Jump to error if carry flag is set

    ret

; Global Descriptor Table (GDT) setup, Protected Mode, Paging, and Long Mode
; ...

; Example kernel metadata (sectors, heads, sizes)
kernel_sectors:
    dw 2, 18, 34           ; Starting sector numbers for each kernel
kernel_heads:
    db 0, 0, 0             ; Head numbers (0 for all)
kernel_sizes:
    db 16, 16, 16          ; Number of sectors for each kernel

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
menu_text db '1. Kernel v1', 0xA, 0xD
menu_text db '2. Kernel v2', 0xA, 0xD
menu_text db '3. Kernel v3', 0xA, 0xD
menu_text db 0             ; Null terminator

;times 510-($-$$) db 0
dw 0xAA55

Explanation of the Code

  1. Menu Display:
    • The display_menu function prints a simple text menu listing the available kernels. This is done using BIOS interrupts to print each character on the screen.
  2. User Input:
    • The get_user_input function waits for a keypress using BIOS interrupt 0x16. The keypress is converted to an index to select the appropriate kernel.
  3. Kernel Loading:
    • The load_selected_kernel function uses the BIOS disk read interrupt (0x13) to load the selected kernel from the disk based on the userโ€™s choice.
  4. Kernel Metadata:
    • The kernel_sectors, kernel_heads, and kernel_sizes arrays store the disk locations and sizes of the kernels. Adjust these values based on the actual layout of your disk.

Disk Layout

You need to place your kernels at the specified sectors on the disk. For example, you can lay out your disk like this:

  • Boot Sector: First sector (0x0000 to 0x01FF).
  • Kernel v1: Starts at sector 2.
  • Kernel v2: Starts at sector 18.
  • Kernel v3: Starts at sector 34.

Use dd to place each kernel on the disk image:

dd if=kernel_v1.bin of=disk.img bs=512 seek=2 conv=notrunc
dd if=kernel_v2.bin of=disk.img bs=512 seek=18 conv=notrunc
dd if=kernel_v3.bin of=disk.img bs=512 seek=34 conv=notrunc

Filesystem to Store Kernels

Implementing a simple filesystem within your bootloader allows for more flexibility in managing multiple kernel versions. For this purpose, we can create a basic, custom filesystem that supports reading files from the disk.

The filesystem will include:

  1. Superblock: Contains metadata about the filesystem (e.g., number of files, size of the directory).
  2. Directory Table: Stores the names of the files and their locations on the disk.
  3. File Data Blocks: The actual content of the files (kernels in this case).

Filesystem Layout

We’ll define a simple filesystem layout on the disk as follows:

  1. Boot Sector: The first sector (512 bytes).
  2. Superblock: Contains information about the filesystem (e.g., number of files).
  3. Directory Table: Contains entries for each file (filename, start sector, size).
  4. File Data Blocks: Where the actual file data (kernels) is stored.

Filesystem Structures

  • Superblock: Contains the total number of files and a pointer to the directory table.
  • Directory Table: Contains entries for each file, including the filename, start sector, and size in sectors.
  • File Data Blocks: Store the actual content of each kernel.

Implementation

Hereโ€™s an implementation in assembly to create and interact with a simple filesystem:

1. Bootloader with Filesystem

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load filesystem metadata
    call load_superblock
    call load_directory_table

    ; Display menu
    call display_menu

    ; Get user input and choose kernel
    call get_user_input
    call load_selected_kernel

    ; Jump to protected mode and then to long mode (as described previously)
    call enter_protected_mode
    call long_mode_entry

error:
    hlt
    jmp error

; Load the superblock from disk
load_superblock:
    mov bx, superblock       ; Destination in memory
    mov ah, 0x02             ; BIOS read sectors function
    mov al, 0x01             ; Read 1 sector
    mov ch, 0x00             ; Cylinder 0
    mov cl, 0x02             ; Sector 2 (after boot sector)
    mov dh, 0x00             ; Head 0
    mov dl, 0x80             ; Drive 0 (first hard disk)
    int 0x13                 ; BIOS interrupt

    jc error                 ; Jump to error if carry flag is set

    ret

; Load the directory table from disk
load_directory_table:
    mov bx, directory_table  ; Destination in memory
    mov ax, [superblock]     ; Load directory table start sector
    mov ah, 0x02             ; BIOS read sectors function
    mov al, 0x01             ; Read 1 sector (you can adjust based on directory size)
    mov ch, 0x00             ; Cylinder 0
    mov cl, [ax+1]           ; Sector number from superblock
    mov dh, 0x00             ; Head 0
    mov dl, 0x80             ; Drive 0 (first hard disk)
    int 0x13                 ; BIOS interrupt

    jc error                 ; Jump to error if carry flag is set

    ret

; Display a simple text-based menu to select the kernel version
display_menu:
    mov si, menu_text
    call print_string

    ; Loop through directory entries and display filenames
    mov cx, [superblock]        ; Number of files
    mov bx, directory_table

.display_file:
    mov si, bx
    call print_string           ; Print the filename
    add bx, 16                  ; Move to the next directory entry (adjust size as necessary)
    loop .display_file

    ret

; Get user input for selecting kernel
get_user_input:
    xor ax, ax
    int 0x16                    ; BIOS keyboard input
    sub al, '1'                 ; Convert keypress to an index (assuming '1', '2', '3' for menu items)
    mov bx, ax                  ; Store the index in BX
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    ; Assume BX contains the selected kernel index
    mov si, directory_table
    add si, bx                  ; Point to the correct directory entry

    ; Load start sector and size from directory entry
    mov ax, [si+12]             ; Start sector (assuming little-endian)
    mov cx, [si+14]             ; Size in sectors
    mov bx, 0x1000              ; Load kernel into memory at 0x1000

.load_kernel_sector:
    mov ah, 0x02                ; BIOS read sectors function
    mov al, 0x01                ; Read 1 sector
    mov ch, 0x00                ; Cylinder 0
    mov cl, ax                  ; Sector number
    mov dh, 0x00                ; Head 0
    mov dl, 0x80                ; Drive 0 (first hard disk)
    int 0x13                    ; BIOS interrupt

    jc error                    ; Jump to error if carry flag is set

    add ax, 1                   ; Move to the next sector
    add bx, 512                 ; Move to the next memory block
    loop .load_kernel_sector

    ret

; Superblock data structure
superblock:
    dw 2                      ; Start sector of the directory table
    dw 3                      ; Number of files in the directory

; Directory table placeholder
directory_table:
    times 16 db 0             ; Placeholder, size depends on your needs

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
menu_text db 0                ; Null terminator

;times 510-($-$$) db 0
dw 0xAA55

Explanation

  1. Superblock:
    • The superblock contains metadata about the filesystem, such as the number of files and the start sector of the directory table.
  2. Directory Table:
    • The directory table holds entries for each file, including the filename, start sector, and size in sectors.
  3. Menu Display:
    • The display_menu function iterates over the directory table and displays each file (kernel) available for booting.
  4. Loading a Kernel:
    • The load_selected_kernel function reads the file’s start sector and size from the directory table and loads it into memory.

Filesystem Layout on Disk

To set up this simple filesystem, you’d structure your disk image like this:

  1. Boot Sector: First sector (0x0000 to 0x01FF).
  2. Superblock: Second sector (0x0200 to 0x02FF).
  3. Directory Table: Starting from the third sector.
  4. File Data Blocks: Kernels stored starting from subsequent sectors.

2. Preparing the Disk Image

  1. Write the bootloader: dd if=bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
  2. Prepare the superblock and directory table in a binary file (e.g., filesystem.bin).
  3. Write the filesystem metadata: dd if=filesystem.bin of=disk.img bs=512 seek=1 conv=notrunc
  4. Write the kernel files: dd if=kernel_v1.bin of=disk.img bs=512 seek=START_SECTOR_OF_KERNEL_V1 conv=notrunc dd if=kernel_v2.bin of=disk.img bs=512 seek=START_SECTOR_OF_KERNEL_V2 conv=notrunc

Where START_SECTOR_OF_KERNEL_V1 and START_SECTOR_OF_KERNEL_V2 correspond to the sectors specified in the directory table.

Adding a UI

Adding an advanced UI to your bootloader involves several enhancements over the basic text-based interface. These enhancements can include:

  1. Navigation with Arrow Keys: Allowing the user to navigate through the kernel options using the keyboard’s arrow keys.
  2. Highlighted Selection: Highlighting the currently selected kernel option.
  3. Countdown Timer: Automatically booting the default kernel after a countdown period if no user input is detected.
  4. Support for Simple Graphics: Optionally, we can move from text mode to a simple graphics mode to enhance the visual appearance of the menu.

Implementation

Hereโ€™s an implementation of an advanced UI in assembly:

1. Bootloader with Advanced UI

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load filesystem metadata
    call load_superblock
    call load_directory_table

    ; Display menu and wait for user input
    call display_menu_with_selection

    ; Load and boot the selected kernel
    call load_selected_kernel

    ; Jump to protected mode and long mode (as previously described)
    call enter_protected_mode
    call long_mode_entry

error:
    hlt
    jmp error

; Load the superblock from disk
load_superblock:
    mov bx, superblock
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Load the directory table from disk
load_directory_table:
    mov bx, directory_table
    mov ax, [superblock]
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, [ax+1]
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Display menu with selection and navigation
display_menu_with_selection:
    xor bx, bx                 ; Initially select the first item
    mov si, [superblock + 2]   ; Number of files

    ; Countdown loop (optional)
    mov cx, 5                  ; Countdown from 5 seconds
countdown_loop:
    push cx
    call display_menu
    call update_highlight      ; Highlight the selected option
    call countdown_timer       ; Display countdown
    pop cx
    dec cx
    jz countdown_expired       ; If countdown hits zero, boot selected kernel
    call check_keypress        ; Check for keypresses to navigate
    jmp countdown_loop

countdown_expired:
    ret                        ; Proceed to load the selected kernel

; Display the menu options
display_menu:
    mov si, menu_text
    call print_string

    ; Loop through directory entries and display filenames
    mov cx, [superblock + 2]
    mov bx, directory_table

display_file:
    mov si, bx
    call print_string          ; Print the filename
    add bx, 16                 ; Move to the next directory entry
    loop display_file
    ret

; Print a string pointed to by SI
print_string:
    mov ah, 0x0E
next_char:
    lodsb
    cmp al, 0
    je done
    int 0x10
    jmp next_char
done:
    ret

; Update the highlight for the selected kernel
update_highlight:
    ; Move cursor to the start of the selection
    mov cx, bx                 ; CX = selected index
    shl cx, 4                  ; Each entry is 16 bytes
    add cx, 20                 ; Offset to the start of options

    ; Set the video mode to display highlighted text (use BIOS interrupt)
    mov ah, 0x02
    int 0x10

    ; Highlight the selected option
    mov ah, 0x0E
    mov al, '>'
    int 0x10
    ret

; Handle navigation keypresses (arrow keys)
check_keypress:
    xor ax, ax
    int 0x16                    ; BIOS keyboard input
    cmp al, 0
    je no_keypress

    ; Check for arrow keys (scan codes for up/down)
    cmp al, 0x48                ; Up arrow key
    je move_up
    cmp al, 0x50                ; Down arrow key
    je move_down

    ; If Enter is pressed, load the selected kernel
    cmp al, 0x0D
    je countdown_expired

no_keypress:
    ret

move_up:
    dec bx                      ; Move selection up
    cmp bx, 0x00                ; Prevent moving above the first option
    jge update_highlight
    inc bx                      ; Restore BX if underflow
    ret

move_down:
    inc bx                      ; Move selection down
    cmp bx, [superblock + 2]    ; Prevent moving below the last option
    jl update_highlight
    dec bx                      ; Restore BX if overflow
    ret

; Simple countdown timer display
countdown_timer:
    ; Display countdown (assuming you want a timer at a specific screen location)
    ; You can implement this by setting cursor position and printing remaining time.
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    mov si, directory_table
    add si, bx                  ; Point to the correct directory entry

    ; Load start sector and size from directory entry
    mov ax, [si+12]             ; Start sector
    mov cx, [si+14]             ; Size in sectors
    mov bx, 0x1000              ; Load kernel into memory at 0x1000

load_kernel_sector:
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, ax
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error

    add ax, 1
    add bx, 512
    loop load_kernel_sector
    ret

; Superblock data structure
superblock:
    dw 2                      ; Start sector of the directory table
    dw 3                      ; Number of files in the directory

; Directory table placeholder
directory_table:
    times 16 db 0             ; Placeholder, size depends on your needs

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
menu_text db 0                ; Null terminator

;times 510-($-$$) db 0
dw 0xAA55

Key Features

  1. Arrow Key Navigation:
    • The check_keypress function handles arrow key input. The BIOS interrupt 0x16 is used to detect keypresses, with special handling for up and down arrow keys to navigate between menu options.
  2. Highlighted Selection:
    • The update_highlight function highlights the currently selected menu option using a simple method where a “>” symbol is displayed next to the selected option.
  3. Countdown Timer:
    • The countdown_timer function can display a countdown on the screen. This is currently a placeholder and can be further developed to display the countdown visually.
  4. Default Kernel Selection:
    • If no input is detected before the countdown expires, the default kernel (highlighted at the time) is automatically selected.
  5. Handling Input and Selection:
    • The user can press Enter to select a kernel immediately, or the default kernel is automatically selected after the countdown.

Potential Enhancements

  1. Graphics Mode:
    • Consider switching to a graphics mode for a more visually appealing interface. This requires setting up a VESA graphics mode and drawing the menu in a graphical context.
  2. More Complex Highlighting:
    • Instead of using a “>” symbol, you can change the text color or use inverse text (background and foreground colors swapped) to highlight the selection.
  3. Custom Fonts:
    • In graphics mode, you can load and display custom fonts to enhance the appearance of the text.
  4. Multilingual Support:
    • Extend the menu to support multiple languages by loading different strings based on user preferences.
  5. Dynamic Kernel Detection:
    • Automatically detect and list kernels based on files found in a directory or through scanning available sectors, rather than hardcoding the directory entries.

This advanced UI provides a more user-friendly and interactive way to select which kernel to boot, making the bootloader more versatile and visually appealing.

Dynamic Kernel Detection

Adding dynamic kernel detection to your bootloader involves scanning the disk to identify available kernel files rather than relying on hardcoded directory entries. The bootloader will then populate the menu dynamically based on the detected kernels.

Steps for Dynamic Kernel Detection

  1. File Naming Convention: Decide on a file naming convention for the kernels (e.g., KERNEL1.BIN, KERNEL2.BIN, etc.) so that the bootloader can recognize and list them.
  2. Scan the Disk: Implement a routine to scan the disk for these files.
  3. Store Detected Kernels: Store the details of detected kernels (e.g., filename, starting sector, size) in memory.
  4. Display the Detected Kernels in the Menu: Modify the menu display to list the dynamically detected kernels.

Implementation

Below is the modified bootloader code that includes dynamic kernel detection:

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load filesystem metadata
    call load_superblock
    call scan_for_kernels    ; Dynamically detect kernels

    ; Display menu and wait for user input
    call display_menu_with_selection

    ; Load and boot the selected kernel
    call load_selected_kernel

    ; Jump to protected mode and long mode (as previously described)
    call enter_protected_mode
    call long_mode_entry

error:
    hlt
    jmp error

; Load the superblock from disk
load_superblock:
    mov bx, superblock
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Dynamically scan the disk for kernel files
scan_for_kernels:
    mov cx, 0                 ; Kernel count
    mov di, directory_table   ; Start storing entries in the directory table

    ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc.
    mov si, 1                 ; Start with KERNEL1.BIN
scan_next_kernel:
    call generate_filename    ; Generate the filename (KERNELX.BIN)
    mov ax, 0x1301            ; BIOS interrupt for file read
    int 0x13
    jc no_more_kernels        ; Stop scanning if no more kernels found

    ; Store detected kernel in the directory table
    stosb                     ; Store filename in directory table
    stosw                     ; Store starting sector
    stosw                     ; Store size in sectors

    inc cx                    ; Increment kernel count
    add si, 1                 ; Move to the next potential kernel
    cmp cx, 16                ; Limit the number of kernels to 16
    jb scan_next_kernel

no_more_kernels:
    ; Store the number of detected kernels in the superblock
    mov [superblock + 2], cx
    ret

; Generate a filename like KERNELX.BIN based on the current index in SI
generate_filename:
    mov bx, si                 ; Store the current index in BX
    mov si, filename_template  ; Point to the filename template
    call replace_index_in_filename
    ret

; Replace 'X' in the filename template with the current index
replace_index_in_filename:
    mov cx, si                 ; Move index to CX for conversion
    add cl, '0'                ; Convert to ASCII
    mov [si + 6], cl           ; Replace 'X' with the index
    mov si, filename_template  ; Point SI back to the template
    ret

; Display menu with selection and navigation
display_menu_with_selection:
    xor bx, bx                 ; Initially select the first item
    mov si, [superblock + 2]   ; Number of files

    ; Countdown loop (optional)
    mov cx, 5                  ; Countdown from 5 seconds
countdown_loop:
    push cx
    call display_menu
    call update_highlight      ; Highlight the selected option
    call countdown_timer       ; Display countdown
    pop cx
    dec cx
    jz countdown_expired       ; If countdown hits zero, boot selected kernel
    call check_keypress        ; Check for keypresses to navigate
    jmp countdown_loop

countdown_expired:
    ret                        ; Proceed to load the selected kernel

; Display the menu options
display_menu:
    mov si, menu_text
    call print_string

    ; Loop through directory entries and display filenames
    mov cx, [superblock + 2]
    mov bx, directory_table

display_file:
    mov si, bx
    call print_string          ; Print the filename
    add bx, 16                 ; Move to the next directory entry
    loop display_file
    ret

; Print a string pointed to by SI
print_string:
    mov ah, 0x0E
next_char:
    lodsb
    cmp al, 0
    je done
    int 0x10
    jmp next_char
done:
    ret

; Update the highlight for the selected kernel
update_highlight:
    ; Move cursor to the start of the selection
    mov cx, bx                 ; CX = selected index
    shl cx, 4                  ; Each entry is 16 bytes
    add cx, 20                 ; Offset to the start of options

    ; Set the video mode to display highlighted text (use BIOS interrupt)
    mov ah, 0x02
    int 0x10

    ; Highlight the selected option
    mov ah, 0x0E
    mov al, '>'
    int 0x10
    ret

; Handle navigation keypresses (arrow keys)
check_keypress:
    xor ax, ax
    int 0x16                    ; BIOS keyboard input
    cmp al, 0
    je no_keypress

    ; Check for arrow keys (scan codes for up/down)
    cmp al, 0x48                ; Up arrow key
    je move_up
    cmp al, 0x50                ; Down arrow key
    je move_down

    ; If Enter is pressed, load the selected kernel
    cmp al, 0x0D
    je countdown_expired

no_keypress:
    ret

move_up:
    dec bx                      ; Move selection up
    cmp bx, 0x00                ; Prevent moving above the first option
    jge update_highlight
    inc bx                      ; Restore BX if underflow
    ret

move_down:
    inc bx                      ; Move selection down
    cmp bx, [superblock + 2]    ; Prevent moving below the last option
    jl update_highlight
    dec bx                      ; Restore BX if overflow
    ret

; Simple countdown timer display
countdown_timer:
    ; Display countdown (assuming you want a timer at a specific screen location)
    ; You can implement this by setting cursor position and printing remaining time.
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    mov si, directory_table
    add si, bx                  ; Point to the correct directory entry

    ; Load start sector and size from directory entry
    mov ax, [si+12]             ; Start sector
    mov cx, [si+14]             ; Size in sectors
    mov bx, 0x1000              ; Load kernel into memory at 0x1000

load_kernel_sector:
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, ax
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error

    add ax, 1
    add bx, 512
    loop load_kernel_sector
    ret

; Filename template for generating kernel names (e.g., KERNEL1.BIN)
filename_template db 'KERNELX.BIN', 0

; Superblock data structure
superblock:
    dw 2                      ; Start sector of the directory table
    dw 0                      ; Number of files (will be set dynamically)

; Directory table placeholder
directory_table:
    times 256 db 0            ; Space for 16 entries, each 16 bytes

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
menu_text db 0                ; Null terminator

times 510-($-$$) db 0
dw 0xAA55

Key Features and Modifications

  1. Dynamic Kernel Detection (scan_for_kernels):
    • This function scans the disk for files named KERNEL1.BIN, KERNEL2.BIN, etc. It stops when it no longer finds any files or reaches a predefined limit (e.g., 16 kernels).
    • Each detected kernelโ€™s information (filename, start sector, and size) is stored in a directory table in memory.
  2. Filename Generation (generate_filename and replace_index_in_filename):
    • These functions generate filenames like KERNEL1.BIN based on the current index during scanning.
  3. Menu Display with Detected Kernels:
    • The menu dynamically lists the kernels found during the scan, allowing the user to select one.

Hardening the Boot Sector

Improving the security and integrity of the bootloader and kernel selection process is critical, especially in environments where security is a concern. Below are several enhancements you can implement to enhance security, prevent unauthorized access, and ensure the integrity of the boot process.

1. Digital Signatures and Integrity Checks

a. Digital Signatures:

  • Implementation: Each kernel should be signed with a digital signature. The bootloader can then verify the signature before loading the kernel to ensure it hasn’t been tampered with. This requires embedding a public key in the bootloader and using it to verify signatures.
  • Process:
    • Each kernel file is signed with a private key during the build process.
    • The bootloader contains the corresponding public key.
    • Before loading a kernel, the bootloader verifies its signature using the public key.
    • If the signature verification fails, the bootloader should refuse to load the kernel and display an error message.

b. Checksums and Hashing:

  • Implementation: Use cryptographic hash functions (e.g., SHA-256) to generate a hash for each kernel file. The bootloader calculates the hash of the kernel before loading it and compares it with a known good hash.
  • Process:
    • Generate a hash of each kernel file after compilation.
    • Store the hash securely in the bootloader or a protected area on the disk.
    • During boot, the bootloader calculates the hash of the selected kernel and compares it with the stored hash.
    • If the hashes do not match, the bootloader should abort the boot process.

2. Secure Boot Implementation

  • Implementation: Integrate your bootloader with a Secure Boot mechanism. Secure Boot ensures that only trusted software is loaded by verifying the digital signatures of all components, including the bootloader, kernel, and any other binaries involved in the boot process.
  • Process:
    • The system’s firmware (e.g., UEFI) verifies the bootloader’s signature before handing control over to it.
    • The bootloader, in turn, verifies the kernel’s signature before loading it.
    • This prevents unauthorized or malicious modifications to the bootloader or kernels.

3. Role-Based Access Control (RBAC) for Bootloader Configuration

  • Implementation: Implement role-based access control for the bootloader’s configuration and kernel selection. For example, an administrator could be required to authenticate before changing the default kernel or altering the boot process.
  • Process:
    • Implement a simple password or passphrase check in the bootloader.
    • Certain actions (e.g., booting into a non-default kernel, altering bootloader settings) require authentication.
    • Store a hashed version of the password securely in the bootloader, and compare it to the user input at runtime.

4. Tamper Detection

  • Implementation: Include tamper detection mechanisms in the bootloader. These can include detecting changes to the bootloader code, the configuration, or the kernel files.
  • Process:
    • Implement a watchdog or audit log that detects changes in the bootloader binary or its configuration files.
    • Store a tamper-evident hash or checksum of critical bootloader components.
    • On each boot, the bootloader verifies its own integrity against this hash or checksum and refuses to proceed if tampering is detected.

5. Redundant Bootloader and Kernel Backup

  • Implementation: Maintain multiple copies of the bootloader and kernel files on the disk, and implement a fallback mechanism in case the primary bootloader or kernel is corrupted.
  • Process:
    • Store multiple copies of the bootloader and kernel files in different disk sectors.
    • The bootloader first attempts to load the primary copy; if it fails (due to corruption or any other reason), it automatically tries the backup.
    • The integrity of the primary and backup copies should be checked before they are used.

6. Encrypted Bootloader and Kernel Storage

  • Implementation: Encrypt the bootloader and kernel files on the disk to prevent unauthorized access or tampering.
  • Process:
    • Encrypt the bootloader and kernel files using a symmetric encryption algorithm (e.g., AES).
    • The bootloader is equipped with the decryption key (or a mechanism to derive it securely).
    • Upon boot, the bootloader decrypts itself and the selected kernel before loading it into memory.

7. Audit and Logging

  • Implementation: Implement logging mechanisms that record key events during the boot process (e.g., which kernel was selected, if any integrity checks failed, etc.). Logs can be stored in a secure, tamper-evident manner.
  • Process:
    • Create a secure area on the disk where boot logs are stored.
    • Record events such as successful boots, failed integrity checks, and unauthorized access attempts.
    • Implement a mechanism to review these logs post-boot (e.g., in the operating system) or display them on demand during the boot process.

8. Minimalist Approach to Reduce Attack Surface

  • Implementation: Keep the bootloader code as minimal and straightforward as possible to reduce the potential attack surface.
  • Process:
    • Avoid unnecessary features or code that could introduce vulnerabilities.
    • Perform a code audit to eliminate any redundant or potentially insecure code.
    • Regularly update the bootloader to address security vulnerabilities.

9. Periodic Integrity Verification

  • Implementation: Regularly verify the integrity of the bootloader and kernel files, even outside the boot process.
  • Process:
    • Use a scheduled task in the operating system to verify the integrity of the bootloader and kernel files periodically.
    • Alert the administrator if any discrepancies are found between the stored hash and the current file state.
    • Optionally, prevent the system from booting if the integrity check fails.

Conclusion

These enhancements would significantly improve the security and integrity of your bootloader and kernel loading process, protecting against unauthorized access, tampering, and failure.

By implementing these measures, you ensure that only trusted, verified software is executed, maintaining the integrity of the system’s boot process.

Example Hardening

Implementing tamper detection and a redundant bootloader in your system involves several key steps to ensure that the bootloader and kernel files have not been tampered with and that a backup bootloader is available in case of failure. Below is a detailed guide on how to implement these features.

1. Tamper Detection Implementation

Overview:

Tamper detection involves ensuring that the bootloader and kernel have not been modified. This can be achieved by calculating and verifying checksums or cryptographic hashes of the bootloader and kernel files.

Steps:

  1. Generate a Hash of the Bootloader and Kernel:
    • Use a cryptographic hash function like SHA-256 to generate a hash of the bootloader and each kernel file.
    • Store these hashes securely in a dedicated section of the disk (e.g., a “hash block” after the superblock).
  2. Verify Hashes During Boot:
    • Before executing any code, the bootloader reads the stored hash values and recalculates the hash of the bootloader and selected kernel.
    • If the recalculated hash does not match the stored hash, the bootloader detects tampering and halts the boot process or switches to a backup.
  3. Update Hashes After Modification:
    • Any legitimate updates to the bootloader or kernel should also update the corresponding hash values.

Code Example:

Hereโ€™s how you might implement a simple hash-based tamper detection mechanism in your bootloader.

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load hash block and verify bootloader integrity
    call load_hash_block
    call verify_bootloader_hash

    ; Load filesystem metadata and proceed as before
    call load_superblock
    call scan_for_kernels    ; Dynamically detect kernels
    call display_menu_with_selection
    call load_selected_kernel

    ; Jump to protected mode and long mode (as previously described)
    call enter_protected_mode
    call long_mode_entry

error:
    ; Error handling
    hlt
    jmp error

; Load the hash block from disk
load_hash_block:
    mov bx, hash_block
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, HASH_BLOCK_SECTOR  ; Sector where hash block is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Verify the bootloader's hash
verify_bootloader_hash:
    ; Compute hash of the bootloader
    call compute_bootloader_hash
    ; Compare with stored hash
    mov si, computed_bootloader_hash
    mov di, [hash_block + 0]  ; Bootloader hash starts at offset 0 in hash block
    call compare_hashes
    jc error                  ; Halt on mismatch
    ret

; Placeholder function to compute bootloader hash
compute_bootloader_hash:
    ; Implement your hash function (e.g., SHA-256) here
    ; Store the computed hash in 'computed_bootloader_hash'
    ret

; Compare two hash values (si: source, di: destination)
compare_hashes:
    mov cx, HASH_SIZE         ; Size of the hash (e.g., 32 bytes for SHA-256)
    repe cmpsb
    jne error                 ; If hashes don't match, trigger error
    ret

; Continue with other functions for scanning kernels, loading selected kernel, etc.

; Placeholder for hash block data
hash_block:
    times 512 db 0            ; 512-byte block for storing hashes

; Computed bootloader hash placeholder
computed_bootloader_hash:
    times 32 db 0             ; Assuming SHA-256, which is 32 bytes

; Constants
HASH_BLOCK_SECTOR equ 3       ; Example sector for hash block
HASH_SIZE equ 32              ; Size of SHA-256 hash

2. Redundant Bootloader Implementation

Overview:

A redundant bootloader ensures that if the primary bootloader fails or is detected as tampered with, a secondary (backup) bootloader is automatically used. This can be implemented by storing the backup bootloader in a different sector and having the primary bootloader check its own integrity before deciding to load the backup.

Steps:

  1. Store the Backup Bootloader:
    • Store a copy of the bootloader in another sector on the disk (e.g., the fourth sector).
  2. Primary Bootloader Integrity Check:
    • The primary bootloader checks its integrity during the boot process (as described above in the tamper detection section).
    • If the primary bootloader fails the integrity check, it jumps to the backup bootloader.
  3. Load and Execute the Backup Bootloader:
    • The backup bootloader is loaded into memory and executed if the primary fails.

Code Example:

Hereโ€™s how you might implement a redundant bootloader mechanism:

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00

    ; Load hash block and verify bootloader integrity
    call load_hash_block
    call verify_bootloader_hash

    ; Continue with regular boot process
    call load_superblock
    call scan_for_kernels
    call display_menu_with_selection
    call load_selected_kernel
    call enter_protected_mode
    call long_mode_entry

    jmp success

error:
    ; Load and execute backup bootloader on error
    call load_backup_bootloader
    jmp 0x0000:0x7C00  ; Jump to the start of the backup bootloader

success:
    ; If everything went fine, continue normally
    hlt

; Load the backup bootloader into memory
load_backup_bootloader:
    mov bx, 0x7C00
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, BACKUP_BOOTLOADER_SECTOR  ; Sector where the backup bootloader is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Placeholder for hash block data
hash_block:
    times 512 db 0            ; 512-byte block for storing hashes

; Computed bootloader hash placeholder
computed_bootloader_hash:
    times 32 db 0             ; Assuming SHA-256, which is 32 bytes

; Constants
HASH_BLOCK_SECTOR equ 3       ; Example sector for hash block
BACKUP_BOOTLOADER_SECTOR equ 4  ; Sector where the backup bootloader is stored
HASH_SIZE equ 32              ; Size of SHA-256 hash

3. Updating the Disk Image

To integrate the tamper detection and redundant bootloader:

  1. Assemble and Write the Primary Bootloader:
    • Assemble the primary bootloader as usual and write it to the first sector of the disk image: nasm -f bin primary_bootloader.asm -o primary_bootloader.bin dd if=primary_bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
  2. Prepare and Write the Backup Bootloader:
    • Assemble a copy of the bootloader as the backup and write it to the designated backup sector: nasm -f bin backup_bootloader.asm -o backup_bootloader.bin dd if=backup_bootloader.bin of=disk.img bs=512 seek=4 conv=notrunc
  3. Generate and Store Hashes:
    • Generate the hash of the primary bootloader and store it in the hash block sector: sha256sum primary_bootloader.bin > hash.txt dd if=hash.txt of=disk.img bs=512 seek=3 conv=notrunc

Conclusion

By implementing tamper detection and a redundant bootloader, you significantly enhance the security and reliability of your boot process. The system is now capable of detecting unauthorized modifications and automatically falling back to a safe, verified version of the bootloader in case of failure.

The full assembly code for a bootloader that includes dynamic kernel detection, a user interface with multiple selections, tamper detection using cryptographic hashes, and redundancy with a backup bootloader.

Full Bootloader Code

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7B00   ; Move stack below code to avoid overwriting

    ; Load hash block and verify bootloader integrity
    call load_hash_block
    call verify_bootloader_hash

    ; Load filesystem metadata and scan for kernels
    call load_superblock
    call scan_for_kernels

    ; Display menu and wait for user input
    call display_menu_with_selection

    ; Load and boot the selected kernel
    call load_selected_kernel

    ; Transition to protected mode
    call enter_protected_mode

    ; Jump to long mode (64-bit mode)
    jmp long_mode_entry

    jmp success

error:
    ; Load and execute backup bootloader on error
    call load_backup_bootloader
    jmp 0x0000:0x7C00  ; Jump to the start of the backup bootloader

success:
    ; If everything went fine, continue normally
    hlt

; Load the hash block from disk
load_hash_block:
    mov bx, hash_block
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, HASH_BLOCK_SECTOR  ; Sector where hash block is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Verify the bootloader's hash
verify_bootloader_hash:
    ; Compute hash of the bootloader
    call compute_bootloader_hash
    ; Compare with stored hash
    mov si, computed_bootloader_hash
    mov di, hash_block  ; Bootloader hash starts at offset 0 in hash block
    call compare_hashes
    jc error                  ; Halt on mismatch
    ret

; Placeholder function to compute bootloader hash
compute_bootloader_hash:
    ; Implement your hash function (e.g., SHA-256) here
    ; Store the computed hash in 'computed_bootloader_hash'
    ret

; Compare two hash values (si: source, di: destination)
compare_hashes:
    mov cx, HASH_SIZE         ; Size of the hash (e.g., 32 bytes for SHA-256)
    repe cmpsb
    jne error                 ; If hashes don't match, trigger error
    ret

; Load the backup bootloader into memory
load_backup_bootloader:
    mov bx, 0x7C00
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, BACKUP_BOOTLOADER_SECTOR  ; Sector where the backup bootloader is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Load the superblock from disk
load_superblock:
    mov bx, superblock
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, SUPERBLOCK_SECTOR  ; Sector where superblock is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Dynamically scan the disk for kernel files
scan_for_kernels:
    mov cx, 0                 ; Kernel count
    mov di, directory_table   ; Start storing entries in the directory table

    ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc.
    mov si, 1                 ; Start with KERNEL1.BIN
scan_next_kernel:
    call generate_filename    ; Generate the filename (KERNELX.BIN)
    ; BIOS interrupt to read file - implement file detection logic here
    ; If file is detected:
    ; Store detected kernel in the directory table
    ; stosb                     ; Store filename in directory table
    ; stosw                     ; Store starting sector
    ; stosw                     ; Store size in sectors

    ; Increment kernel count and continue scanning
    inc cx
    add si, 1                 ; Move to the next potential kernel
    cmp cx, 16                ; Limit the number of kernels to 16
    jb scan_next_kernel

no_more_kernels:
    ; Store the number of detected kernels in the superblock
    mov [superblock + 2], cx
    ret

; Generate a filename like KERNELX.BIN based on the current index
generate_filename:
    mov bx, si                 ; Store the current index in BX
    mov si, filename_template  ; Point to the filename template
    call replace_index_in_filename
    ret

; Replace 'X' in the filename template with the current index
replace_index_in_filename:
    mov cx, si                 ; Move index to CX for conversion
    add cl, '0'                ; Convert to ASCII
    mov [si + 6], cl           ; Replace 'X' with the index
    mov si, filename_template  ; Point SI back to the template
    ret

; Display menu with selection and navigation
display_menu_with_selection:
    xor bx, bx                 ; Initially select the first item
    mov si, [superblock + 2]   ; Number of files

    ; Countdown loop (optional)
    mov cx, 5                  ; Countdown from 5 seconds
countdown_loop:
    push cx
    call display_menu
    call update_highlight      ; Highlight the selected option
    call countdown_timer       ; Display countdown
    pop cx
    dec cx
    jz countdown_expired       ; If countdown hits zero, boot selected kernel
    call check_keypress        ; Check for keypresses to navigate
    jmp countdown_loop

countdown_expired:
    ret                        ; Proceed to load the selected kernel

; Display the menu options
display_menu:
    mov si, menu_text
    call print_string

    ; Loop through directory entries and display filenames
    mov cx, [superblock + 2]
    mov bx, directory_table

display_file:
    mov si, bx
    call print_string          ; Print the filename
    add bx, 16                 ; Move to the next directory entry
    loop display_file
    ret

; Print a string pointed to by SI
print_string:
    mov ah, 0x0E
next_char:
    lodsb
    cmp al, 0
    je done
    int 0x10
    jmp next_char
done:
    ret

; Update the highlight for the selected kernel
update_highlight:
    ; Move cursor to the start of the selection
    mov cx, bx                 ; CX = selected index
    shl cx, 4                  ; Each entry is 16 bytes
    add cx, 20                 ; Offset to the start of options

    ; Set the video mode to display highlighted text (use BIOS interrupt)
    mov ah, 0x02
    int 0x10

    ; Highlight the selected option
    mov ah, 0x0E
    mov al, '>'
    int 0x10
    ret

; Handle navigation keypresses (arrow keys)
check_keypress:
    xor ax, ax
    int 0x16                    ; BIOS keyboard input
    cmp al, 0
    je no_keypress

    ; Check for arrow keys (scan codes for up/down)
    cmp al, 0x48                ; Up arrow key
    je move_up
    cmp al, 0x50                ; Down arrow key
    je move_down

    ; If Enter is pressed, load the selected kernel
    cmp al, 0x0D
    je countdown_expired

no_keypress:
    ret

move_up:
    dec bx                      ; Move selection up
    cmp bx, 0x00                ; Prevent moving above the first option
    jge update_highlight
    inc bx                      ; Restore BX if underflow
    ret

move_down:
    inc bx                      ; Move selection down
    cmp bx, [superblock + 2]    ; Prevent moving below the last option
    jl update_highlight
    dec bx                      ; Restore BX if overflow
    ret

; Simple countdown timer display
countdown_timer:
    ; Display countdown (assuming you want a timer at a specific screen location)
    ; You can implement this by setting cursor position and printing remaining time.
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    mov si, directory_table
    add si, bx                  ; Point to the correct directory entry

    ; Load start sector and size from directory entry
    mov bx, si                ; Copy base address from si to bx
    mov ax, [bx + 12]         ; Load start sector (16-bit value) into ax
    mov cx, [bx + 14]         ; Load size in sectors (16-bit value) into cx
    mov bx, 0x1000            ; Load kernel into memory at 0x1000   

load_kernel_sector:
    push dx                   ; Save dx (to restore it later)
    mov dl, 0x80              ; Drive number (0x80 for the first hard drive)
    mov ah, 0x02              ; BIOS function: read sectors
    mov ch, 0x00              ; Cylinder number (0 initially)
    mov dh, 0x00              ; Head number (0 initially)

load_next_sector:
    mov cl, al                ; Set sector number from ax (1-based, so ax must start from 1)
    int 0x13                  ; Call BIOS to read sector
    jc error                  ; If carry flag is set, jump to error

    add ax, 1                 ; Increment sector number
    add bx, 512               ; Move to the next 512-byte block in memory
    dec cx                    ; Decrement sector count
    jnz load_next_sector      ; If cx != 0, load the next sector

    pop dx                    ; Restore dx
    ret

; Placeholder for entering protected mode
enter_protected_mode:
    ; Setup GDT, switch to protected mode, etc.
    ; This is a simplified example; the actual implementation depends on your kernel's requirements.
    cli                       ; Clear interrupts
    lgdt [gdt_descriptor]     ; Load GDT
    mov eax, cr0
    or eax, 1                 ; Set PE bit to enter protected mode
    mov cr0, eax
    jmp CODE_SEG:protected_mode_entry ; Far jump to flush prefetch queue and enter protected mode

[BITS 32]
protected_mode_entry:
    ; Setup data segments
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000          ; Set up stack pointer

    ; Enable A20 line
    in al, 0x92
    or al, 2
    out 0x92, al

    ; Continue to long mode entry
    ret

; Placeholder for entering long mode
long_mode_entry:
    ; Setup paging, enable long mode, etc.
    ; This is a simplified example; the actual implementation depends on your kernel's requirements.
    mov eax, cr4
    or eax, 0x20              ; Enable PAE
    mov cr4, eax

    mov eax, cr0
    or eax, 0x80000000        ; Enable paging
    mov cr0, eax

    mov ecx, 0xC0000080       ; Load EFER MSR
    rdmsr
    or eax, 0x100             ; Set LME bit to enable long mode
    wrmsr

    jmp long_mode_selector:long_mode_code ; Long jump to 64-bit mode

[BITS 64]
; Long mode code starts here
long_mode_code:
    ; Your 64-bit kernel entry point
    ; Set up 64-bit segments, etc.
    ret

; Placeholder GDT (Global Descriptor Table)
gdt_start:
    dq 0x0000000000000000      ; Null segment
    dq 0x00A09A000000FFFF      ; Code segment descriptor
    dq 0x00A092000000FFFF      ; Data segment descriptor
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1 ; GDT size
    dd gdt_start               ; GDT address

CODE_SEG equ 0x08   ; Code segment selector
DATA_SEG equ 0x10   ; Data segment selector
long_mode_selector equ 0x18 ; Long mode code segment selector

; Filename template for generating kernel names (e.g., KERNEL1.BIN)
filename_template db 'KERNELX.BIN', 0

; Superblock data structure
superblock:
    dw SUPERBLOCK_SECTOR       ; Start sector of the directory table
    dw 0                       ; Number of files (will be set dynamically)

; Directory table placeholder
directory_table:
    times 256 db 0            ; Space for 16 entries, each 16 bytes

; Placeholder for hash block data
hash_block:
    times 512 db 0            ; 512-byte block for storing hashes

; Computed bootloader hash placeholder
computed_bootloader_hash:
    times 32 db 0             ; Assuming SHA-256, which is 32 bytes

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
db 0                ; Null terminator

; Constants
SUPERBLOCK_SECTOR equ 2       ; Example sector for superblock
HASH_BLOCK_SECTOR equ 3       ; Example sector for hash block
BACKUP_BOOTLOADER_SECTOR equ 4  ; Sector where the backup bootloader is stored
HASH_SIZE equ 32              ; Size of SHA-256 hash

;times 510-($-$$) db 0  ; This commented out because code exceeds 512 bytes, 
; next iteration splits functions out
dw 0xAA55

Detailed Explanation

  1. Tamper Detection:
    • The bootloader calculates a cryptographic hash (e.g., SHA-256) of itself during the boot process. It compares this hash with a precomputed hash stored in a dedicated hash block on the disk.
    • If the hashes do not match, indicating tampering, the bootloader stops the boot process and switches to the backup bootloader.
  2. Redundant Bootloader:
    • A backup bootloader is stored on the disk in a different sector. If the primary bootloader fails its integrity check or encounters an error, the system will attempt to load the backup bootloader.
  3. Dynamic Kernel Detection:
    • The bootloader scans the disk for files matching a specific naming convention (e.g., KERNEL1.BIN, KERNEL2.BIN, etc.).
    • It dynamically populates the boot menu with the available kernels.
  4. User Interface with Multiple Selections:
    • The bootloader provides a simple text-based UI allowing the user to select which kernel to boot. The selection can be made using arrow keys, and the bootloader highlights the selected option.
    • A countdown timer is also included, automatically selecting the default kernel if no input is provided.
  5. Hash and Checksum Handling:
    • The bootloader uses placeholders for cryptographic operations, such as calculating and comparing hashes. In a real-world implementation, you would need to replace these placeholders with actual cryptographic functions, such as a SHA-256 hash function.

Preparing the Disk Image

  1. Assemble and Write the Primary Bootloader: nasm -f bin primary_bootloader.asm -o primary_bootloader.bin dd if=primary_bootloader.bin of=disk.img bs=512 count=1 conv=notrunc
  2. Prepare and Write the Backup Bootloader: nasm -f bin backup_bootloader.asm -o backup_bootloader.bin dd if=backup_bootloader.bin of=disk.img bs=512 seek=4 conv=notrunc
  3. Generate and Store Hashes:
    • After compiling the bootloader, generate the hash:
    sha256sum primary_bootloader.bin > hash.txt dd if=hash.txt of=disk.img bs=512 seek=3 conv=notrunc
  4. Write the Kernel Files:
    • Place kernel binaries in the appropriate sectors on the disk image as detected by the bootloader.

Conclusion

This implementation provides a robust and secure bootloader with tamper detection, redundancy, dynamic kernel detection, and a user-friendly interface. This foundation can be extended further by adding more advanced features such as encryption, more sophisticated error handling, or even graphical elements in the UI.

Fixing Size

Below is a version of the code split into two parts: one for the bootloader and the other for kernel detection, verification, and selection.

Part 1: Bootloader (Primary Bootloader)

This part handles the initial boot process, setting up the stack, loading the secondary stage (kernel detection, verification, and selection), and transitioning to protected mode.

[BITS 16]
ORG 0x7C00

start:
    ; Set up stack and data segments
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7B00   ; Move stack below code to avoid overwriting

    ; Load the secondary stage (kernel detection, verification, and selection)
    call load_secondary_stage

    ; Transition to protected mode
    call enter_protected_mode

    ; Jump to long mode (64-bit mode)
    jmp long_mode_entry

    jmp success

error:
    hlt                         ; Halt the system on error

success:
    hlt                         ; Halt the system if successful (this should be replaced by a jump to the loaded kernel)

; Load the secondary stage into memory
load_secondary_stage:
    mov ax, SECOND_STAGE_SECTOR ; Sector where the secondary stage starts
    mov cx, SECOND_STAGE_SIZE   ; Number of sectors to load
    mov bx, 0x2000              ; Load secondary stage into memory at 0x2000

load_secondary_sector:
    push dx                     ; Save DX (important to preserve registers)
    mov dl, 0x80                ; Set the drive number (0x80 for the first hard drive)
    mov ah, 0x02                ; BIOS function: read sectors into memory
    mov ch, 0x00                ; Cylinder number (0 initially)
    mov dh, 0x00                ; Head number (0 initially)
    
load_next_secondary_sector:
    mov cl, al                  ; Load sector number into CL
    int 0x13                    ; Call BIOS interrupt to read sector
    jc error                    ; If carry flag is set, jump to error handling

    add ax, 1                   ; Increment sector number
    add bx, 512                 ; Move to the next 512-byte block in memory
    dec cx                      ; Decrement sector count
    jnz load_next_secondary_sector ; If CX != 0, load the next sector

    pop dx                      ; Restore DX
    jmp 0x2000                  ; Jump to the secondary stage in memory

    ret

; Placeholder for entering protected mode
enter_protected_mode:
    cli                         ; Clear interrupts
    lgdt [gdt_descriptor]       ; Load GDT
    mov eax, cr0
    or eax, 1                   ; Set PE bit to enter protected mode
    mov cr0, eax
    jmp CODE_SEG:protected_mode_entry ; Far jump to flush prefetch queue and enter protected mode

[BITS 32]
protected_mode_entry:
    ; Setup data segments
    mov ax, DATA_SEG
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax
    mov esp, 0x90000            ; Set up stack pointer

    ; Enable A20 line
    in al, 0x92
    or al, 2
    out 0x92, al

    ; Continue to long mode entry
    ret

; Placeholder for entering long mode
long_mode_entry:
    mov eax, cr4
    or eax, 0x20                ; Enable PAE
    mov cr4, eax

    mov eax, cr0
    or eax, 0x80000000          ; Enable paging
    mov cr0, eax

    mov ecx, 0xC0000080         ; Load EFER MSR
    rdmsr
    or eax, 0x100               ; Set LME bit to enable long mode
    wrmsr

    jmp long_mode_selector:long_mode_code ; Long jump to 64-bit mode

[BITS 64]
; Long mode code starts here
long_mode_code:
    ; Your 64-bit kernel entry point
    ret

; Placeholder GDT (Global Descriptor Table)
gdt_start:
    dq 0x0000000000000000       ; Null segment
    dq 0x00A09A000000FFFF       ; Code segment descriptor
    dq 0x00A092000000FFFF       ; Data segment descriptor
gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1  ; GDT size
    dd gdt_start                ; GDT address

CODE_SEG equ 0x08               ; Code segment selector
DATA_SEG equ 0x10               ; Data segment selector
long_mode_selector equ 0x18     ; Long mode code segment selector

; Constants
SECOND_STAGE_SECTOR equ 5       ; Sector where the secondary stage starts
SECOND_STAGE_SIZE equ 2         ; Example size of the secondary stage in sectors

times 510-($-$$) db 0
dw 0xAA55

Part 2: Kernel Detection, Verification, and Selection (Secondary Stage)

This secondary stage will be loaded by the primary bootloader. It handles kernel detection, verification, and selection.

[BITS 16]
ORG 0x2000

start_secondary_stage:
    ; Load filesystem metadata and scan for kernels
    call load_superblock
    call scan_for_kernels

    ; Display menu and wait for user input
    call display_menu_with_selection

    ; Load and boot the selected kernel
    call load_selected_kernel

    jmp 0x0000:0x7C00  ; Jump back to the primary bootloader or directly to the kernel

error:
    hlt                         ; Halt the system on error

; Load the superblock from disk
load_superblock:
    mov bx, superblock
    mov ah, 0x02
    mov al, 0x01
    mov ch, 0x00
    mov cl, SUPERBLOCK_SECTOR   ; Sector where superblock is stored
    mov dh, 0x00
    mov dl, 0x80
    int 0x13
    jc error
    ret

; Dynamically scan the disk for kernel files
scan_for_kernels:
    mov cx, 0                   ; Kernel count
    mov di, directory_table     ; Start storing entries in the directory table

    ; Scan for files named KERNEL1.BIN, KERNEL2.BIN, etc.
    mov si, 1                   ; Start with KERNEL1.BIN
scan_next_kernel:
    call generate_filename      ; Generate the filename (KERNELX.BIN)
    ; BIOS interrupt to read file - implement file detection logic here
    ; If file is detected:
    ; Store detected kernel in the directory table
    ; stosb                     ; Store filename in directory table
    ; stosw                     ; Store starting sector
    ; stosw                     ; Store size in sectors

    ; Increment kernel count and continue scanning
    inc cx
    add si, 1                   ; Move to the next potential kernel
    cmp cx, 16                  ; Limit the number of kernels to 16
    jb scan_next_kernel

no_more_kernels:
    ; Store the number of detected kernels in the superblock
    mov [superblock + 2], cx
    ret

; Generate a filename like KERNELX.BIN based on the current index
generate_filename:
    mov bx, si                   ; Store the current index in BX
    mov si, filename_template    ; Point to the filename template
    call replace_index_in_filename
    ret

; Replace 'X' in the filename template with the current index
replace_index_in_filename:
    mov cx, si                   ; Move index to CX for conversion
    add cl, '0'                  ; Convert to ASCII
    mov [si + 6], cl             ; Replace 'X' with the index
    mov si, filename_template    ; Point SI back to the template
    ret

; Display menu with selection and navigation
display_menu_with_selection:
    xor bx, bx                   ; Initially select the first item
    mov si, [superblock + 2]     ; Number of files

    ; Countdown loop (optional)
    mov cx, 5                    ; Countdown from 5 seconds
countdown_loop:
    push cx
    call display_menu
    call update_highlight        ; Highlight the selected option
    call countdown_timer         ; Display countdown
    pop cx
    dec cx
    jz countdown_expired         ; If countdown hits zero, boot selected kernel
    call check_keypress          ; Check for keypresses to navigate
    jmp countdown_loop

countdown_expired:
    ret                          ; Proceed to load the selected kernel

; Display the menu options
display_menu:
    mov si, menu_text
    call print_string

    ; Loop through directory entries and display filenames
    mov cx, [superblock + 2]
    mov bx, directory_table

display_file:
    mov si, bx
    call print_string            ; Print the filename
    add bx, 16                   ; Move to the next directory entry
    loop display_file
    ret

; Print a string pointed to by SI
print_string:
    mov ah, 0x0E
next_char:
    lodsb
    cmp al, 0
    je done
    int 0x10
    jmp next_char
done:
    ret

; Update the

 highlight for the selected kernel
update_highlight:
    ; Move cursor to the start of the selection
    mov cx, bx                   ; CX = selected index
    shl cx, 4                    ; Each entry is 16 bytes
    add cx, 20                   ; Offset to the start of options

    ; Set the video mode to display highlighted text (use BIOS interrupt)
    mov ah, 0x02
    int 0x10

    ; Highlight the selected option
    mov ah, 0x0E
    mov al, '>'
    int 0x10
    ret

; Handle navigation keypresses (arrow keys)
check_keypress:
    xor ax, ax
    int 0x16                      ; BIOS keyboard input
    cmp al, 0
    je no_keypress

    ; Check for arrow keys (scan codes for up/down)
    cmp al, 0x48                  ; Up arrow key
    je move_up
    cmp al, 0x50                  ; Down arrow key
    je move_down

    ; If Enter is pressed, load the selected kernel
    cmp al, 0x0D
    je countdown_expired

no_keypress:
    ret

move_up:
    dec bx                        ; Move selection up
    cmp bx, 0x00                  ; Prevent moving above the first option
    jge update_highlight
    inc bx                        ; Restore BX if underflow
    ret

move_down:
    inc bx                        ; Move selection down
    cmp bx, [superblock + 2]      ; Prevent moving below the last option
    jl update_highlight
    dec bx                        ; Restore BX if overflow
    ret

; Simple countdown timer display
countdown_timer:
    ret

; Load the selected kernel into memory based on user input
load_selected_kernel:
    mov si, directory_table
    add si, bx                    ; Point to the correct directory entry

    ; Load start sector and size from directory entry
    mov bx, si                    ; Copy base address from si to bx
    mov ax, [bx + 12]             ; Load start sector (16-bit value) into ax
    mov cx, [bx + 14]             ; Load size in sectors (16-bit value) into cx
    mov bx, 0x1000                ; Load kernel into memory at 0x1000   

load_kernel_sector:
    push dx                       ; Save dx (to restore it later)
    mov dl, 0x80                  ; Drive number (0x80 for the first hard drive)
    mov ah, 0x02                  ; BIOS function: read sectors
    mov ch, 0x00                  ; Cylinder number (0 initially)
    mov dh, 0x00                  ; Head number (0 initially)

load_next_sector:
    mov cl, al                    ; Set sector number from ax (1-based, so ax must start from 1)
    int 0x13                      ; Call BIOS to read sector
    jc error                      ; If carry flag is set, jump to error

    add ax, 1                     ; Increment sector number
    add bx, 512                   ; Move to the next 512-byte block in memory
    dec cx                        ; Decrement sector count
    jnz load_next_sector          ; If cx != 0, load the next sector

    pop dx                        ; Restore dx
    ret

; Filename template for generating kernel names (e.g., KERNEL1.BIN)
filename_template db 'KERNELX.BIN', 0

; Superblock data structure
superblock:
    dw SUPERBLOCK_SECTOR          ; Start sector of the directory table
    dw 0                          ; Number of files (will be set dynamically)

; Directory table placeholder
directory_table:
    times 256 db 0                ; Space for 16 entries, each 16 bytes

; Menu text
menu_text db 'Select a Kernel Version:', 0xA, 0xD
db 0                               ; Null terminator

; Constants
SUPERBLOCK_SECTOR equ 2            ; Example sector for superblock

;times 510-($-$$) db 0
dw 0xAA55

Summary:

  • Primary Bootloader: Handles basic bootloader tasks, sets up the environment, and loads the secondary stage, which is responsible for more complex tasks like kernel detection, verification, and selection.
  • Secondary Stage: Once loaded by the primary bootloader, this stage performs kernel scanning, displays a menu for kernel selection, and then loads the selected kernel.

You can assemble and write each part to separate sectors on the disk. The primary bootloader will start at 0x7C00, and the secondary stage will be loaded into a separate memory location (e.g., 0x2000 in this example).

To compile and write the bootloader and secondary stage to a disk, you’ll need to follow these steps:

Tools Required:

  • NASM (Netwide Assembler): For assembling the bootloader and secondary stage source files.
  • dd (Data Duplicator): A command-line tool used to write raw data to disk images (on Linux or MacOS).
  • Disk Image: This can be a physical disk, a USB drive, or a virtual disk image (for use in emulators like QEMU or VirtualBox).

Step 1: Assemble the Bootloader and Secondary Stage

  1. Assemble the Primary Bootloader:
    • Save the bootloader code (Part 1) in a file called primary_bootloader.asm.
    • Use NASM to assemble this file into a binary file:
    nasm -f bin primary_bootloader.asm -o primary_bootloader.bin This will create a binary file named primary_bootloader.bin.
  2. Assemble the Secondary Stage:
    • Save the secondary stage code (Part 2) in a file called secondary_stage.asm.
    • Use NASM to assemble this file into a binary file:
    nasm -f bin secondary_stage.asm -o secondary_stage.bin This will create a binary file named secondary_stage.bin.

Step 2: Create a Disk Image (Optional)

If you want to write the bootloader to a disk image rather than a physical disk, you can create a blank disk image first:

dd if=/dev/zero of=bootdisk.img bs=512 count=2880

This creates a 1.44 MB floppy disk image filled with zeros. The size and type can be adjusted depending on your needs.

Step 3: Write the Bootloader and Secondary Stage to the Disk

  1. Write the Bootloader to the Disk:
    • The bootloader is written to the first sector of the disk, which is where the BIOS looks for the bootloader.
    dd if=primary_bootloader.bin of=/dev/sdX bs=512 count=1 Replace /dev/sdX with the correct device for your disk. If you’re using a disk image (like bootdisk.img), use that as the output file: dd if=primary_bootloader.bin of=bootdisk.img bs=512 count=1 conv=notrunc conv=notrunc is important when writing to disk images to prevent truncating the file.
  2. Write the Secondary Stage to the Disk:
    • The secondary stage should be written to a specific sector(s) on the disk. The SECOND_STAGE_SECTOR constant in the primary bootloader indicates where to load this stage from. For example, if SECOND_STAGE_SECTOR equ 5, you would write the secondary stage starting at sector 5:
    dd if=secondary_stage.bin of=/dev/sdX bs=512 seek=5 Or if using a disk image: dd if=secondary_stage.bin of=bootdisk.img bs=512 seek=5 conv=notrunc

Step 4: Boot the Disk or Disk Image

If you wrote the bootloader and secondary stage to a physical disk (like a USB drive), you can now boot a computer from this drive.

If you used a disk image, you can boot it in an emulator like QEMU:

qemu-system-x86_64 -drive format=raw,file=bootdisk.img

Or in VirtualBox by attaching the disk image as a virtual hard drive.

Summary:

  • NASM assembles the bootloader and secondary stage source files into binary files.
  • dd writes these binaries to specific sectors on a disk or disk image.
  • The bootloader resides in the first sector of the disk, and it loads the secondary stage from a predefined location (e.g., sector 5).
  • Boot the disk or disk image using a physical machine or an emulator.

This setup allows the bootloader to initiate the boot process, then load and execute the secondary stage, which handles more complex tasks like kernel detection and selection.

SHA-256 Code

To generate a SHA-256 hash of a file like primary_bootloader.bin and save the output to a text file hash.txt, you can use the following code in C. This code mimics the functionality of the sha256sum command in Linux, computing the SHA-256 hash and outputting it in hexadecimal format.

Here’s a C program to do this:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/sha.h>

void compute_sha256(const char* path, unsigned char output[SHA256_DIGEST_LENGTH]) {
    FILE* file = fopen(path, "rb");
    if (!file) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    unsigned char buffer[1024];
    size_t bytes_read = 0;

    while ((bytes_read = fread(buffer, 1, sizeof(buffer), file)) > 0) {
        SHA256_Update(&sha256, buffer, bytes_read);
    }

    fclose(file);
    SHA256_Final(output, &sha256);
}

void print_sha256(unsigned char hash[SHA256_DIGEST_LENGTH]) {
    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        printf("%02x", hash[i]);
    }
    printf("\n");
}

void save_sha256_to_file(unsigned char hash[SHA256_DIGEST_LENGTH], const char* output_path) {
    FILE* output_file = fopen(output_path, "w");
    if (!output_file) {
        perror("fopen");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < SHA256_DIGEST_LENGTH; i++) {
        fprintf(output_file, "%02x", hash[i]);
    }
    fprintf(output_file, "\n");

    fclose(output_file);
}

int main(int argc, char *argv[]) {
    if (argc != 3) {
        fprintf(stderr, "Usage: %s <input_file> <output_file>\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    const char* input_file = argv[1];
    const char* output_file = argv[2];

    unsigned char hash[SHA256_DIGEST_LENGTH];
    compute_sha256(input_file, hash);

    // Print the hash to the console (optional)
    print_sha256(hash);

    // Save the hash to the output file
    save_sha256_to_file(hash, output_file);

    return 0;
}

Compilation and Usage

  1. Save the code to a file, for example, sha256sum.c.
  2. Compile the code using GCC: gcc -o sha256sum sha256sum.c -lssl -lcrypto This links the OpenSSL library, which provides the SHA256 functions.
  3. Run the program: ./sha256sum primary_bootloader.bin hash.txt This command computes the SHA-256 hash of primary_bootloader.bin and writes the result to hash.txt.

How It Works

  • compute_sha256: This function reads the file in chunks, updates the SHA-256 context with each chunk, and finally computes the hash.
  • print_sha256: This function prints the computed SHA-256 hash to the console in hexadecimal format. It’s useful for debugging or verifying the hash manually.
  • save_sha256_to_file: This function saves the computed hash to a file, hash.txt, in hexadecimal format, similar to what sha256sum does in Linux.

Example Output in hash.txt

After running the program, hash.txt will contain something like:

e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

This is the SHA-256 hash of primary_bootloader.bin, represented in hexadecimal.

Conclusion

This C program provides the same functionality as the sha256sum command, allowing you to generate a SHA-256 hash for your bootloader binary and save it to a text file. You can then use this hash in your bootloader for tamper detection.

Test Harness

Creating a test harness to check the functions of the bootloader in an environment where we canโ€™t execute BIOS interrupts directly or interact with real hardware can be challenging.

However, we can simulate certain aspects of the bootloader in a controlled environment using a simple test framework in C or Assembly that runs on a standard operating system.

For the sake of simplicity and to allow you to run these tests on a typical development environment, we will create a test harness in C that simulates some of the bootloaderโ€™s functions, particularly those related to string handling, hashing, and integrity checks.

Test Harness in C

Here’s a C program that simulates and tests key aspects of the bootloader:

#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <openssl/sha.h> // You will need to link with the OpenSSL library

#define HASH_SIZE 32 // Size of SHA-256 hash
#define MAX_KERNELS 16
#define KERNEL_NAME_TEMPLATE "KERNEL%d.BIN"

// Simulated directory table
typedef struct {
    char filename[12];
    uint16_t start_sector;
    uint16_t size;
} DirectoryEntry;

DirectoryEntry directory_table[MAX_KERNELS];

// Simulated hash block
uint8_t hash_block[HASH_SIZE];

// Function prototypes
void generate_filename(int index, char* output);
void replace_index_in_filename(int index, char* filename);
void compute_hash(const uint8_t* data, size_t length, uint8_t* output);
int compare_hashes(const uint8_t* hash1, const uint8_t* hash2);
void load_superblock(void);
void scan_for_kernels(void);
void print_directory_table(void);

int main() {
    // Simulate loading the superblock and scanning for kernels
    load_superblock();
    scan_for_kernels();

    // Print the detected kernels
    print_directory_table();

    // Simulate computing and verifying the bootloader hash
    uint8_t computed_hash[HASH_SIZE];
    const char* bootloader_data = "Simulated bootloader data";
    compute_hash((const uint8_t*)bootloader_data, strlen(bootloader_data), computed_hash);

    // Assuming the hash_block was set up previously
    memcpy(hash_block, computed_hash, HASH_SIZE); // Simulate a correct hash
    if (compare_hashes(computed_hash, hash_block) == 0) {
        printf("Bootloader integrity check passed.\n");
    } else {
        printf("Bootloader integrity check failed.\n");
    }

    return 0;
}

// Generate a filename like KERNELX.BIN based on the current index
void generate_filename(int index, char* output) {
    sprintf(output, KERNEL_NAME_TEMPLATE, index);
}

// Simulate replacing 'X' in the filename template with the current index
void replace_index_in_filename(int index, char* filename) {
    sprintf(filename, "KERNEL%d.BIN", index);
}

// Compute a SHA-256 hash of the given data
void compute_hash(const uint8_t* data, size_t length, uint8_t* output) {
    SHA256_CTX sha256;
    SHA256_Init(&sha256);
    SHA256_Update(&sha256, data, length);
    SHA256_Final(output, &sha256);
}

// Compare two hashes for equality
int compare_hashes(const uint8_t* hash1, const uint8_t* hash2) {
    return memcmp(hash1, hash2, HASH_SIZE);
}

// Simulate loading the superblock (just initializing in this case)
void load_superblock(void) {
    // Normally you would load this from disk, here we just simulate it
    printf("Superblock loaded.\n");
}

// Simulate scanning for kernels (just adding dummy entries)
void scan_for_kernels(void) {
    for (int i = 0; i < MAX_KERNELS; i++) {
        generate_filename(i + 1, directory_table[i].filename);
        directory_table[i].start_sector = 2 + i * 16; // Dummy sector
        directory_table[i].size = 16; // Dummy size
    }
    printf("Scanned and detected %d kernels.\n", MAX_KERNELS);
}

// Print the simulated directory table
void print_directory_table(void) {
    printf("Detected Kernels:\n");
    for (int i = 0; i < MAX_KERNELS; i++) {
        printf("Filename: %s, Start Sector: %d, Size: %d\n",
               directory_table[i].filename,
               directory_table[i].start_sector,
               directory_table[i].size);
    }
}

How This Works:

  1. Simulated Functions:
    • generate_filename and replace_index_in_filename: These functions simulate generating and modifying kernel filenames based on an index. This mirrors the logic in the assembly code for generating file names like KERNEL1.BIN, KERNEL2.BIN, etc.
    • compute_hash: This function computes a SHA-256 hash of the bootloader data using OpenSSL. In a real bootloader, you would compute the hash over the actual bootloader binary.
    • compare_hashes: This function compares two hash values to check if they match, simulating the tamper detection logic.
    • load_superblock and scan_for_kernels: These functions simulate loading the superblock and scanning for kernel files. The actual bootloader would load these from disk.
  2. Hash Verification:
    • The test harness computes a hash for simulated bootloader data and compares it to a pre-stored hash (hash_block). If they match, it prints that the integrity check passed; otherwise, it prints a failure message.
  3. Simulated Directory Table:
    • The directory_table stores information about detected kernels. The test harness populates this with dummy data, simulating what the bootloader would do after scanning the disk for kernel files.

Running the Test Harness

  1. Compile and Run:
    • Ensure you have OpenSSL installed, as itโ€™s used to calculate the SHA-256 hash. Compile and run the test harness like this:
    gcc -o test_harness test_harness.c -lssl -lcrypto ./test_harness
  2. Review the Output:
    • The output will show the simulated scanning of kernel files, display the directory table, and perform the integrity check on the bootloader.

Extending the Test Harness

  • Add More Tests:
    • You can extend this test harness by adding additional tests for specific functions, like checking the user interface logic or handling simulated user input.
  • Simulate Disk I/O:
    • For a more realistic test, simulate disk I/O operations where the functions load data from a binary file representing the disk image instead of hardcoded data.
  • Automate Testing:
    • Consider automating the test process to verify bootloader integrity and functionality across different scenarios, such as when tampering is detected or when the backup bootloader is used.

Conclusion

This C-based test harness provides a simplified environment to test some of the key functions of your bootloader, such as generating filenames, computing hashes, and verifying integrity. This approach helps you validate logic before deploying the actual bootloader on a disk.

Architectural Summary

Bootloader System with Kernel Detection, Verification, and Selection

1. Overview

The system architecture described here is a two-stage bootloader designed to initialize and load an operating system kernel in a flexible and modular manner. The architecture separates the responsibilities of basic system initialization and complex kernel selection into two distinct stages:

  1. Primary Bootloader (Stage 1): This is the initial piece of code executed by the system upon boot. It is responsible for basic hardware setup, loading the secondary stage from disk, and transitioning the system into a more advanced mode (Protected Mode or Long Mode).
  2. Secondary Stage (Stage 2): Once loaded by the primary bootloader, this stage is responsible for detecting available kernel images on the disk, verifying their integrity, presenting a selection menu to the user, and finally loading and executing the selected kernel.

2. Architectural Components

A. Primary Bootloader (Stage 1)
  • Purpose:
    • The primary bootloader is the first code executed when the system boots. It is small, compact, and fits within the first 512 bytes of the disk (typically the Master Boot Record (MBR) or a dedicated boot partition).
  • Key Responsibilities:
    • Hardware Initialization: The bootloader sets up the stack and initializes the data segments.
    • Secondary Stage Loading: The primary bootloader loads the secondary stage (which contains more complex boot logic) from a predefined location on the disk.
    • Transition to Protected Mode: If the operating system requires it, the bootloader transitions the CPU from Real Mode to Protected Mode (and optionally to Long Mode for 64-bit operation).
  • Key Operations:
    • Disk I/O: The bootloader uses BIOS interrupts (e.g., int 0x13) to read sectors from the disk into memory.
    • Segment and Stack Setup: Properly initializes data segments and stack pointer for consistent operation.
    • Mode Transition: Prepares and transitions the system into Protected or Long Mode, setting up the Global Descriptor Table (GDT) and enabling paging if necessary.
  • Constraints:
    • The primary bootloader is constrained by the 512-byte size limit of the boot sector.
    • It must be simple and robust, with minimal dependencies, to ensure reliability across different hardware.
B. Secondary Stage (Stage 2)
  • Purpose:
    • The secondary stage is loaded into memory by the primary bootloader and is responsible for the more complex task of detecting, verifying, and selecting an operating system kernel.
  • Key Responsibilities:
    • Filesystem Metadata Handling: Reads filesystem metadata, such as the superblock, to identify where kernel files are located on the disk.
    • Kernel Detection: Scans the disk for available kernel files, typically named in a sequential manner (e.g., KERNEL1.BIN, KERNEL2.BIN).
    • User Interaction: Displays a menu for the user to select which kernel to boot.
    • Kernel Loading: Loads the selected kernel into memory and passes control to it.
  • Key Operations:
    • File Detection and Management: Uses a basic method to locate and verify kernel files on the disk.
    • Menu Display and Selection: Provides a simple user interface (usually text-based) to allow kernel selection.
    • Memory Management: Ensures that the selected kernel is loaded into the correct memory location for execution.
  • Constraints:
    • The secondary stage is more complex and can be larger than the primary bootloader, but it still needs to be efficient in terms of memory and processing time.
    • It must handle errors gracefully, providing feedback to the user and fallback options if necessary.

3. Execution Flow

  1. System Boot:
    • The BIOS or UEFI firmware loads the primary bootloader from the first sector of the boot disk (typically 0x7C00).
  2. Primary Bootloader Execution:
    • The primary bootloader sets up the CPUโ€™s stack and data segments.
    • It loads the secondary stage from a predetermined sector on the disk into memory.
    • Once the secondary stage is loaded, the primary bootloader transitions the system into Protected Mode if necessary, and jumps to the secondary stage.
  3. Secondary Stage Execution:
    • The secondary stage reads the superblock from the disk to understand the layout and locate kernel files.
    • It scans the disk for kernel files, validates them, and builds a list of available kernels.
    • The secondary stage displays a selection menu to the user.
    • Upon user selection (or after a timeout), the secondary stage loads the selected kernel into memory.
    • The secondary stage then jumps to the kernel’s entry point, handing over control to the operating system.

4. Design Considerations

  • Modularity:
    • The split between primary and secondary stages allows for a clean separation of concerns. The primary stage is minimal and focused on getting the system ready, while the secondary stage handles more complex tasks that can vary between systems.
  • Flexibility:
    • By isolating the kernel detection and selection logic into a secondary stage, the system can support multiple kernels and configurations without requiring changes to the primary bootloader.
  • Scalability:
    • The architecture can be extended by adding more stages or integrating more sophisticated file systems and kernel management features in the secondary stage.
  • Compatibility:
    • The primary bootloader adheres to the BIOS/MBR standards, ensuring it can boot on a wide range of legacy and modern hardware. The secondary stage can be designed to work with different filesystems or kernel types.

5. Conclusion

This two-stage bootloader architecture provides a robust, flexible, and modular approach to booting an operating system. The primary bootloader handles critical early-stage tasks with minimal code, ensuring reliability, while the secondary stage offers powerful kernel management capabilities, allowing for dynamic kernel selection and loading. This design makes it easier to maintain and extend the boot process, offering both simplicity and flexibility in system initialization.