multiple changes: BL1/BL2/kernel separation (build system, etc.) BL2 implementation. BL documentation

This commit is contained in:
vmttmv
2025-11-13 23:35:54 +02:00
parent f1b0670a15
commit 62fe09d80d
8 changed files with 348 additions and 284 deletions

View File

@@ -1,64 +1,58 @@
AS = nasm AS = nasm
ASFLAGS = -f elf32 -g -F dwarf
CC = gcc CC = gcc
CFLAGS = -std=c11 -m32 -ffreestanding -c -fno-stack-protector -fno-pie
LD = ld LD = ld
QEMU = qemu-system-i386 QEMU= qemu-system-i386
IMG_SIZE = 1440k
BUILD_DIR = build BUILD_DIR = build
DISK_IMG = $(BUILD_DIR)/disk.img
BOOT_SRC = bootloader/boot.asm STAGE2_SIZE = 2048
BOOT_OBJ = $(BUILD_DIR)/boot.o
BOOT_ELF = $(BUILD_DIR)/boot.elf
BOOT_IMG = $(BUILD_DIR)/boot.img
KERNEL_C_SRC = $(wildcard kernel/*.c) KERNEL_C_SRC = $(wildcard kernel/*.c)
KERNEL_ASM_SRC = $(wildcard kernel/*.asm) KERNEL_ASM_SRC = $(wildcard kernel/*.asm)
KERNEL_OBJ = $(patsubst kernel/%.c, $(BUILD_DIR)/%.o, $(KERNEL_C_SRC)) KERNEL_OBJ = $(patsubst kernel/%.c, $(BUILD_DIR)/%.o, $(KERNEL_C_SRC))
KERNEL_OBJ += $(patsubst kernel/%.asm, $(BUILD_DIR)/asm_%.o, $(KERNEL_ASM_SRC)) KERNEL_OBJ += $(patsubst kernel/%.asm, $(BUILD_DIR)/asm_%.o, $(KERNEL_ASM_SRC))
KERNEL_OBJ += $(BUILD_DIR)/boot1.o
KERNEL_ELF = $(BUILD_DIR)/kernel.elf
KERNEL_BIN = $(BUILD_DIR)/kernel.bin
DISK_IMG = $(BUILD_DIR)/disk.img all: $(DISK_IMG)
all: $(BOOT_IMG) $(KERNEL_BIN) $(DISK_IMG) .PHONY: stage1 stage2 kernel run gdb clean
stage1: $(BUILD_DIR)
$(AS) $(ASFLAGS) -o $(BUILD_DIR)/$@.o bootloader/$@.asm
$(LD) -Ttext=0x7c00 -melf_i386 -o $(BUILD_DIR)/$@.elf $(BUILD_DIR)/$@.o
objcopy -O binary $(BUILD_DIR)/$@.elf $(BUILD_DIR)/$@.bin
# NOTE: Stage2 final size should be checked against `$(STAGE2_SIZE)` by the build system to avoid an overflow.
# Alternatively, convey the final stage2 size through other means to stage1.
stage2: $(BUILD_DIR)
$(AS) $(ASFLAGS) -o $(BUILD_DIR)/stage2.o bootloader/stage2.asm
$(CC) -std=c11 -ffreestanding -nostdlib -fno-stack-protector -m32 -g -c -o $(BUILD_DIR)/stage2_load.o bootloader/stage2_load.c
$(LD) -Tbootloader/stage2.ld -melf_i386 -o $(BUILD_DIR)/$@.elf $(BUILD_DIR)/stage2.o $(BUILD_DIR)/stage2_load.o
objcopy -O binary $(BUILD_DIR)/$@.elf $(BUILD_DIR)/$@.bin
truncate -s $(STAGE2_SIZE) $(BUILD_DIR)/$@.bin
$(BUILD_DIR)/asm_%.o: kernel/%.asm
$(AS) $(ASFLAGS) -o $@ $<
$(BUILD_DIR)/%.o: kernel/%.c
$(CC) -std=c11 -ffreestanding -nostdlib -fno-stack-protector -m32 -g -c -o $@ $<
kernel: $(KERNEL_OBJ) | $(BUILD_DIR)
$(LD) -melf_i386 -Tbootloader/linker.ld -o $(BUILD_DIR)/kernel.elf $(KERNEL_OBJ)
$(DISK_IMG): stage1 stage2 kernel
dd if=$(BUILD_DIR)/stage1.bin of=$@
dd if=$(BUILD_DIR)/stage2.bin of=$@ oflag=append conv=notrunc
dd if=$(BUILD_DIR)/kernel.elf of=$@ oflag=append conv=notrunc
truncate -s 1M $@
$(BUILD_DIR): $(BUILD_DIR):
mkdir -p $@ mkdir -p $@
$(BOOT_OBJ): $(BOOT_SRC) | $(BUILD_DIR) run:
$(AS) -f elf32 -g -F dwarf -o $@ $< qemu-system-i386 -s -S $(DISK_IMG)
$(BOOT_ELF): $(BOOT_OBJ) gdb:
$(LD) -Ttext=0x7c00 -melf_i386 -o $@ $< gdb -x gdb.txt
$(BOOT_IMG): $(BOOT_ELF)
objcopy -O binary $< $@
truncate -s $(IMG_SIZE) $@
$(BUILD_DIR)/boot1.o: bootloader/boot1.asm
$(AS) -f elf32 -o $@ $<
$(BUILD_DIR)/asm_%.o: kernel/%.asm
$(AS) -f elf32 -o $@ $<
$(BUILD_DIR)/%.o: kernel/%.c
$(CC) $(CFLAGS) $< -o $@
$(KERNEL_BIN): $(KERNEL_OBJ) | $(BUILD_DIR)
$(LD) -melf_i386 --oformat binary -T bootloader/linker.ld -o $@ $(KERNEL_OBJ)
$(DISK_IMG): $(BOOT_IMG) $(KERNEL_BIN)
dd if=$(BOOT_IMG) of=$@ bs=512 seek=4
dd if=$(KERNEL_BIN) of=$@ bs=512 seek=200
run: $(DISK_IMG)
$(QEMU) -drive file=$<,format=raw,if=floppy
.PHONY: stage1 clean
stage1: $(BOOT_IMG)
clean: clean:
rm -rf $(BUILD_DIR) rm -rf $(BUILD_DIR)

27
bootloader/README.md Normal file
View File

@@ -0,0 +1,27 @@
# ClassicOS 2-stage bootloader
Bootloader documentation for ClassicOS
## Disk image organization:
```
[ 512 B ] [ 2048 B ] [ Unspecified ]
Stage 1 Stage 2 Kernel
```
## Stage 1 (`stage1.asm`)
Responsible for loading the second stage using BIOS routines, and switching to protected mode.
- Queries CHS parameters from BIOS
- Loads the second stage bootloader (2048 B) to `07c00h`
- Sets up a GDT with descriptor entries for code and data both covering the whole 32-bit address space
- Enables A20
- Set CR0.PE (enable protected mode) and jump to stage 2
## Stage 2 (`stage2.asm, stage2_load.c`)
- Set up segment registers
- Load the kernel ELF header
- Parse the program headers, and load all `PT_LOAD` segments from disk
- Jump to the kernel entry

View File

@@ -1,194 +0,0 @@
; ==============================================================================
; boot1.asm - Second Stage Bootloader (Fixed Real Mode Transition)
; ==============================================================================
[BITS 32]
global _start
extern kmain
_start:
; Set up segments
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Stack (must be identity-mapped)
mov esp, 0x90000
; CPU Feature Detection: check CPUID support
pushfd ; Save flags
pop eax
mov ecx, eax
xor eax, 1 << 21 ; Flip ID bit
push eax
popfd
pushfd
pop eax
xor eax, ecx
jz .no_cpuid ; CPUID unsupported if no change
; CPUID supported, verify features
mov eax, 1
cpuid
; Check for paging support (bit 31 of edx)
test edx, 1 << 31
jz .no_paging_support
; Additional CPU feature checks could be added here
jmp .cpuid_check_done
.no_cpuid:
mov si, no_cpuid_msg
call print_string_16
jmp halt
.no_paging_support:
mov si, no_paging_msg
call print_string_16
jmp halt
.cpuid_check_done:
; Temporarily switch back to real mode
cli
mov eax, cr0
and eax, 0x7FFFFFFE ; Clear PE & PG bits
mov cr0, eax
jmp 0x18:real_mode_entry
; ----------------------------------------------------------------
[BITS 16]
real_mode_entry:
; Real mode for BIOS access (E820, VESA)
xor ax, ax
mov es, ax
; VESA call
mov di, VbeControllerInfo
mov ax, 0x4F00
int 0x10
jc vesa_error
; E820 memory map
xor ebx, ebx
mov edx, 0x534D4150
mov di, MemoryMapBuffer
mov [MemoryMapEntries], dword 0
.e820_loop:
mov eax, 0xE820
mov ecx, 24
int 0x15
jc e820_error
add di, 24
inc dword [MemoryMapEntries]
test ebx, ebx
jnz .e820_loop
jmp e820_done
e820_error:
mov si, e820_error_msg
call print_string_16
jmp halt
vesa_error:
mov si, vesa_error_msg
call print_string_16
; Fallback: set VGA text mode 3 and continue
mov ah, 0x00 ; BIOS Set Video Mode function
mov al, 0x03 ; VGA 80x25 text mode
int 0x10
; Clear screen
mov ah, 0x06 ; Scroll up function
mov al, 0 ; Clear entire screen
mov bh, 0x07 ; Text attribute (gray on black)
mov cx, 0 ; Upper-left corner
mov dx, 0x184F ; Lower-right corner
int 0x10
jmp e820_done ; Continue booting without VESA graphics
e820_done:
; Back to protected mode
cli
mov eax, cr0
or eax, 1
mov cr0, eax
jmp 0x08:protected_entry
; ----------------------------------------------------------------
; removed duplicate print from here
; suggested calling it from boot.asm
e820_error_msg db "E820 Failed!", 0
vesa_error_msg db "VESA Failed!", 0
no_cpuid_msg db "No CPUID support detected!", 0
no_paging_msg db "CPU lacks paging support!", 0
; ----------------------------------------------------------------
[BITS 32]
protected_entry:
; Paging setup
xor eax, eax
mov edi, page_directory
mov ecx, 1024
rep stosd
mov edi, page_table
rep stosd
mov eax, page_table
or eax, 0x3
mov [page_directory], eax
mov ecx, 1024
mov edi, page_table
mov eax, 0x00000003
.fill_pages:
mov [edi], eax
add eax, 0x1000
add edi, 4
loop .fill_pages
mov eax, page_directory
mov cr3, eax
mov eax, cr0
or eax, 0x80000000
mov cr0, eax
jmp kmain
halt:
cli
.hang:
hlt
jmp .hang
; ----------------------------------------------------------------
; Data buffers and variables must be appropriately defined in your data section
MemoryMapBuffer times 128 db 0 ; 128*24 bytes reserved for E820 memory map (adjust size as needed)
MemoryMapEntries dd 0
VbeControllerInfo times 512 db 0 ; Buffer for VESA controller info (adjust size as needed)
; Define page directory and page table aligned as needed (in your data section)
align 4096
page_directory times 1024 dd 0
align 4096
page_table times 1024 dd 0
%assign pad_size 4096
%ifdef __SIZE__
%define size_current __SIZE__
%else
%define size_current ($ - $$)
%endif
%if size_current < pad_size
times pad_size - size_current db 0
%endif
checksum_byte db 0

View File

@@ -2,10 +2,16 @@
; boot.asm - First Stage Bootloader (CHS Based) ; boot.asm - First Stage Bootloader (CHS Based)
; ============================================================================== ; ==============================================================================
[BITS 16] ; Params for stage2
; [ORG 0x7C00] %define s2_addr 1 ; stage2 disk offset, in sectors
%define s2_laddr 0x7e00 ; stage2 load address
%define s2_size 2048 ; stage2 size
%define s2_nsect s2_size / 512 ; stage2 size in sectors
start: [BITS 16]
global _start
_start:
cli ; Disable interrupts cli ; Disable interrupts
mov [bootdev], dl ; Save boot device number (from BIOS in DL) mov [bootdev], dl ; Save boot device number (from BIOS in DL)
@@ -20,29 +26,16 @@ start:
mov ds, ax mov ds, ax
mov es, ax mov es, ax
; Query bios for disk parameters
call get_disk_params
; Load second-stage bootloader (boot1.asm) to 0x7E00 ; Load second-stage bootloader (boot1.asm) to 0x7E00
mov ax, 1 ; LBA of boot1.asm (starts at sector 1) mov ax, 1 ; LBA of boot1.asm (starts at sector 1)
call lba_to_chs call lba_to_chs
mov al, 4 ; Number of sectors to read mov al, s2_nsect ; Number of sectors to read
mov bx, 0x7E00 ; Destination address offset ES = 0 (0x0000:0x7E00) mov bx, 0x7E00 ; Destination address offset ES = 0 (0x0000:0x7E00)
call read_chs call read_chs
; Load kernel to 0x100000 (1 MB)
mov ax, 0xffff
mov es, ax ; Set ES for destination address (0xffff << 4 == 0xffff0)
mov ax, 5 ; LBA of kernel start (boot1 is 4 sectors: LBA 14 → kernel at LBA 5)
call lba_to_chs
mov al, 16 ; Number of sectors for kernel
mov bx, 0x10 ; Destination address offset (0xffff:0x10 = 0xffff << 4 + 0x10 = 0x100000)
call read_chs
jc disk_error
; Memory Validation: Verify checksum of second stage bootloader
mov si, 0x7E00 ; Start of second stage
mov cx, 512 * 4 ; Size in bytes (adjust if more sectors loaded)
call verify_checksum
jc disk_error ; Jump if checksum fails
; Enable A20 line ; Enable A20 line
call enable_a20 call enable_a20
jc a20_error ; Jump if A20 enable fails jc a20_error ; Jump if A20 enable fails
@@ -95,17 +88,32 @@ verify_checksum:
pop ax pop ax
ret ret
get_disk_params:
mov ah, 08h ; BIOS: Get Drive Parameters
int 13h
; TODO: error checking
; CL bits 05 contain sectors per track
mov al, cl
and al, 3Fh ; mask bits 05
mov ah, 0
mov [sectors_per_track], ax
; DH = maximum head number (0-based)
mov al, dh
inc ax ; convert to count (heads = maxhead + 1)
mov [heads_per_cylinder], ax
ret
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
; CHS Disk Read Routine ; CHS Disk Read Routine
; AL = number of sectors ; AL = number of sectors
; CL = starting sector (1-based) ; CL = starting sector (1-based)
; SI = destination offset (Segment:ES already set)
; Inputs: ; Inputs:
; AL = sector count ; AL = sector count
; CH = cylinder ; CH = cylinder
; DH = head ; DH = head
; CL = sector (163, with top 2 bits as high cylinder bits) ; CL = sector (163, with top 2 bits as high cylinder bits)
; SI = destination offset (segment ES must be set)
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
; Convert LBA to CHS ; Convert LBA to CHS
@@ -117,36 +125,29 @@ verify_checksum:
; CL = sector (1-63, top 2 bits are upper cylinder bits) ; CL = sector (1-63, top 2 bits are upper cylinder bits)
lba_to_chs: lba_to_chs:
pusha ; Sector
xor dx, dx xor dx, dx
mov bx, [sectors_per_track] mov bx, ax
div bx ; AX = LBA / sectors_per_track, DX = remainder (sector number) div word [sectors_per_track] ; divide lba with max sectors
mov si, ax ; SI = temp quotient (track index) add dl, 1 ; take the remainder, sectors start at 1
mov cx, [heads_per_cylinder] mov cl, dl ; sector is in cl
xor dx, dx
div cx ; AX = cylinder, DX = head
mov ch, al ; CH = cylinder low byte
mov dh, dl ; DH = head
; Now take sector number from earlier remainder ; Head
mov cx, si ; Copy track index to CX to access CL mov ax, bx
and cl, 0x3F ; Mask to 6 bits (sector number) mov dx, 0
inc cl ; Sector numbers are 1-based div word [sectors_per_track] ; divide lba with max sectors
mov dx, 0
div word [heads_per_cylinder] ; divide quotient with heads
mov dh, dl ; take the remainder, head is in dh
; Insert upper 2 bits of cylinder into CL ; Cylinder
mov ah, al ; AH = cylinder again mov ch, al ; take the quotient, cylinder is in ch
and ah, 0xC0 ; Get top 2 bits of cylinder
or cl, ah ; OR them into sector byte
popa
ret ret
read_chs: read_chs:
pusha pusha
push dx push dx
mov cx, 5
.retry: .retry:
mov ah, 0x02 ; BIOS: Read sectors mov ah, 0x02 ; BIOS: Read sectors
mov dl, [bootdev] ; Boot device mov dl, [bootdev] ; Boot device
@@ -262,7 +263,7 @@ switch_to_pm:
mov eax, cr0 mov eax, cr0
or eax, 1 or eax, 1
mov cr0, eax mov cr0, eax
jmp 0x08:0x7E00 jmp 0x08:0x7E00 ; jump to S2
; ---------------------------------------------------------------- ; ----------------------------------------------------------------
print_string_16: print_string_16:
@@ -284,8 +285,8 @@ halt:
hlt hlt
bootdev db 0 bootdev db 0
sectors_per_track dw 63 sectors_per_track dw 0
heads_per_cylinder dw 255 heads_per_cylinder dw 0
times 510 - ($ - $$) db 0 times 510 - ($ - $$) db 0
dw 0xAA55 dw 0xAA55

98
bootloader/stage2.asm Normal file
View File

@@ -0,0 +1,98 @@
[BITS 32]
global _start
global ata_lba_read
extern load_kernel
_start:
; Set up segments
; Data segments
mov ax, 0x10
mov ds, ax
mov es, ax
mov fs, ax
mov gs, ax
mov ss, ax
; Code segment
mov ax, 0x08
mov cs, ax
; Stack (must be identity-mapped)
mov esp, 0x90000
call load_kernel
jmp eax
; ----------------------------------------------------------------------------
; ATA read sectors (LBA mode)
;
; sysv32 abi signature:
; void ata_lba_read(uint32_t lba, uint8_t nsect, void *addr);
; ----------------------------------------------------------------------------
ata_lba_read:
push ebp
mov ebp, esp
push ebx
push ecx
push edx
push edi
mov eax, [ebp+8] ; arg #1 = LBA
mov cl, [ebp+12] ; arg #2 = # of sectors
mov edi, [ebp+16] ; arg #3 = buffer address
and eax, 0x0FFFFFFF
mov ebx, eax ; Save LBA in RBX
mov edx, 0x01F6 ; Port to send drive and bit 24 - 27 of LBA
shr eax, 24 ; Get bit 24 - 27 in al
or al, 11100000b ; Set bit 6 in al for LBA mode
out dx, al
mov edx, 0x01F2 ; Port to send number of sectors
mov al, cl ; Get number of sectors from CL
out dx, al
mov edx, 0x1F3 ; Port to send bit 0 - 7 of LBA
mov eax, ebx ; Get LBA from EBX
out dx, al
mov edx, 0x1F4 ; Port to send bit 8 - 15 of LBA
mov eax, ebx ; Get LBA from EBX
shr eax, 8 ; Get bit 8 - 15 in AL
out dx, al
mov edx, 0x1F5 ; Port to send bit 16 - 23 of LBA
mov eax, ebx ; Get LBA from EBX
shr eax, 16 ; Get bit 16 - 23 in AL
out dx, al
mov edx, 0x1F7 ; Command port
mov al, 0x20 ; Read with retry.
out dx, al
mov bl, cl ; Save # of sectors in BL
.wait_drq:
mov edx, 0x1F7
.do_wait_drq:
in al, dx
test al, 8 ; the sector buffer requires servicing.
jz .do_wait_drq ; keep polling until the sector buffer is ready.
mov edx, 0x1F0 ; Data port, in and out
mov ecx, 256
rep insw ; in to [RDI]
dec bl ; are we...
jnz .wait_drq ; ...done?
pop edi
pop edx
pop ecx
pop ebx
pop ebp
ret

14
bootloader/stage2.ld Normal file
View File

@@ -0,0 +1,14 @@
SECTIONS {
. = 0x7e00;
.text : { *(.text*) }
.rodata : { *(.rodata*) }
.data : { *(.data*) }
.bss : {
*(.bss*)
*(COMMON)
}
read_buf = .;
}

118
bootloader/stage2_load.c Normal file
View File

@@ -0,0 +1,118 @@
#include <stdint.h>
// ELF Ident indexes
#define EI_NIDENT 16
// Program header types
#define PT_NULL 0
#define PT_LOAD 1
// ELF Header (32-bit)
typedef struct {
uint8_t e_ident[EI_NIDENT];
uint16_t e_type;
uint16_t e_machine;
uint32_t e_version;
uint32_t e_entry; // Entry point
uint32_t e_phoff; // Program header table offset
uint32_t e_shoff; // Section header table offset
uint32_t e_flags;
uint16_t e_ehsize;
uint16_t e_phentsize;
uint16_t e_phnum;
uint16_t e_shentsize;
uint16_t e_shnum;
uint16_t e_shstrndx;
} __attribute__((packed)) Elf32_Ehdr;
// Program Header (32-bit)
typedef struct {
uint32_t p_type;
uint32_t p_offset;
uint32_t p_vaddr;
uint32_t p_paddr;
uint32_t p_filesz;
uint32_t p_memsz;
uint32_t p_flags;
uint32_t p_align;
} __attribute__((packed)) Elf32_Phdr;
// Load an ELF executable into memory.
static int elf_load(const void* data, void (*load_segment)(uint8_t *vaddr, uint32_t src, uint32_t size)) {
const Elf32_Ehdr* header = (const Elf32_Ehdr*)data;
const Elf32_Phdr* ph = (const Elf32_Phdr*)((uint8_t*)data + header->e_phoff);
for (int i = 0; i < header->e_phnum; i++) {
if (ph[i].p_type != PT_LOAD)
continue;
uint32_t offset = ph[i].p_offset;
uint32_t vaddr = ph[i].p_vaddr;
uint32_t filesz = ph[i].p_filesz;
uint32_t memsz = ph[i].p_memsz;
// Copy data segment
//load_segment((uint8_t *)vaddr, offset, filesz);
load_segment((uint8_t *)vaddr, offset, filesz);
// Zero remaining BSS (if any)
if (memsz > filesz) {
uint8_t* bss_start = (uint8_t*)(vaddr + filesz);
for (uint32_t j = 0; j < memsz - filesz; j++) {
bss_start[j] = 0;
}
}
}
return header->e_entry;
}
#define KERN_START_SECT 5
#define MAX(a, b) ((a)>(b) ? (a) : (b))
extern void ata_lba_read(uint32_t lba, uint8_t nsect, void *addr);
extern uint8_t read_buf[];
static uint32_t
total_header_size(const Elf32_Ehdr *header) {
uint32_t phend = header->e_phoff + header->e_phentsize*header->e_phnum;
// Align to 512
return (phend + 511) & ~511;
}
static void read_sectors(uint8_t *vaddr, uint32_t offset, uint32_t size) {
// # of sectors to read
uint32_t rem_nsect = ((size + 511) & ~511) / 512;
// Current lba address, offset by the first sector already read
uint32_t lba = KERN_START_SECT + offset / 512;
// Max 255 sectors at a time
while (rem_nsect) {
uint8_t nsect = rem_nsect > 255 ? 255 : rem_nsect;
ata_lba_read(lba, nsect, vaddr);
vaddr += nsect * 512;
rem_nsect -= nsect;
lba += nsect;
}
}
void *load_kernel(void) {
// Read the first sector
ata_lba_read(KERN_START_SECT, 1, read_buf);
const Elf32_Ehdr* header = (const Elf32_Ehdr*)read_buf;
// Remaining data size, subtract the first 512B already read
uint32_t rem = total_header_size(header) - 512;
// Read the rest if necessary
if (rem)
read_sectors(read_buf+512, 512, rem);
elf_load(read_buf, read_sectors);
return (void *)header->e_entry;
}

6
gdb.txt Normal file
View File

@@ -0,0 +1,6 @@
target remote :1234
add-symbol-file build/stage1.elf
add-symbol-file build/stage2.elf
add-symbol-file build/kernel.elf
hbreak *0x7c00
c