; bootler ; legacy bootloader in nasm-flavoured asm ; 2026 Andromeda ; GPL licensed LOAD_ADDR equ 0x7C00 KERNEL_START equ 2 ; first sector on disk to load kernel from; 1 indexed KERNEL_SIZE equ 1 ; length of kernel in sectors KERNEL_LOAD_ADDR_ES equ 0x1000 ; kernel to be loaded at es * 0x10 + 0x0000 PAGE_TABLE_LOAD_ADDR equ 0x1000 ; start of page table; 4 * pt size PAGE_TABLE_SIZE equ 0x1000 ; size of page table in bytes ; 64 bit IDT stuff IDT_ADDR equ 0x8000 ; bottom of IDT IDT_SIZE equ 0x1000 ; size of IDT IDT_SEGMENT equ 0x0008 ; gdt code segment to jump to for idt calls ; magic numbers and things A20_LINE_ENABLE equ 0x2401 ; magic number in ax to enable a20 line with 0x15 BIOS_INT_DISK_OP equ 0x13 BIOS_INT_MEMORY_OP equ 0x15 CR0_PE equ 1 << 0 ; Protected Mode Enabled CR0_PG equ 1 << 31 ; Paging CR4_PAE equ 1 << 5 ; Physical Address Extension CR4_PGE equ 1 << 7 ; Page Global Enabled MSR_IA32_EFER equ 0xC0000080 ; Extended Feature Enable model-specific Register [bits 16] [org LOAD_ADDR] jmp 0x0000:.cs_reset ; in case it loads us at 0x7C00:0x0000 .cs_reset: xor ax, ax mov ss, ax mov sp, LOAD_ADDR ; stack builds down, loader faces up mov ds, ax mov es, ax mov fs, ax mov gs, ax cld ; activate a20 mov ax, A20_LINE_ENABLE int BIOS_INT_MEMORY_OP jc error mov si, msg_a20_line call print ; prep to read disk mov al, KERNEL_SIZE ; size in sectors, min. 1 mov ch, 0 ; cylinder mov cl, KERNEL_START ; start sector, 1-indexed mov dh, 0 ; head ; dl set by bios mov bx, KERNEL_LOAD_ADDR_ES ; load addr es:bx mov es, bx xor bx, bx ; read disk mov ah, 0x02 int BIOS_INT_DISK_OP jc error mov si, msg_disk_read call print ; zero es push ax xor ax, ax mov es, ax pop ax mov edi, PAGE_TABLE_LOAD_ADDR jmp lm_load ; expects: ; es:edi -> 16kb for page table ; ss:esp -> stack ; heavily based on https://wiki.osdev.org/Entering_Long_Mode_Directly lm_load: ; clear page table push di mov ecx, PAGE_TABLE_SIZE xor eax, eax cld rep stosd ; clear space for all 4 tables pop di ; PML4TE0 lea eax, [es:di + PAGE_TABLE_SIZE] or eax, 11b mov [es:di], eax ; PDPT0E0 lea eax, [es:di + 2 * PAGE_TABLE_SIZE] or eax, 11b mov [es:di + PAGE_TABLE_SIZE], eax ; PDT0E0 lea eax, [es:di + 3 * PAGE_TABLE_SIZE] or eax, 11b mov [es:di + 2 * PAGE_TABLE_SIZE], eax ; PT0 push di lea di, [di + 3 * PAGE_TABLE_SIZE] mov eax, 0x11 .set_pt_entry mov [es:di], eax add eax, 0x1000 add di, 8 cmp eax, 0x200000 ; size of page jb .set_pt_entry pop di ; disable interrupts from PIC by masking all interrupts mov al, 0xFF ; mask out 0xA1, al out 0x21, al ; PAE, PGE mov eax, CR4_PAE | CR4_PGE mov cr4, eax ; cr3 -> PML4TE0 mov edx, edi mov cr3, edx ; highs LME bit in EFER mov ecx, MSR_IA32_EFER rdmsr or eax, 0x00000100 wrmsr ; activate long mode mov ebx, cr0 or ebx, CR0_PG | CR0_PE mov cr0, ebx cli lgdt [gdt_descriptor] jmp 0x08:lm_start error: mov si, .msg call print .msg db "err ", 0x00 ret done: hlt jmp done print: pushad .loop lodsb test al, al je .done mov ah, 0x0E int 0x10 jmp .loop .done popad ret ; TODO make readable (yoinked from https://wiki.osdev.org/Entering_Long_Mode_Directly) gdt_data: dq 0x0000000000000000 dq 0x00209A0000000000 dq 0x0000920000000000 gdt_descriptor: dw $ - gdt_data - 1 ; size dd gdt_data ; start [bits 64] ; long mode entry lm_start: mov ax, 0x10 mov ds, ax mov es, ax mov fs, ax mov gs, ax mov ss, ax ; set up serial device ; TODO poll and whatnot (in rust??) mov dx, 0x3F8 ; qemu -no-graphic overlaps serial and bios output without this mov al, 0x0D out dx, al mov al, 0x0A out dx, al ; initialize IDT at IDT_ADDR call idt_init int3 ; silly test jmp KERNEL_LOAD_ADDR_ES * 0x10 idt_init: ; clear space for idt mov rdi, IDT_ADDR push di mov rcx, IDT_SIZE / 4 xor rax, rax cld rep stosd ; clear double word for each rcx pop di push rdx ; dx contains the output device ; nothing between here and the `pop rdx` will be able to print ; breakpoint mov rdi, 0x8E ; present, no dpl, interrupt mov rsi, 0x03 ; breakpoint interrupt lea rdx, [int_hdl_dummy] call mk_idt_entry pop rdx ; get back dx output device lidt [idt_descriptor] ret int_hdl_dummy: mov rsi, .msg call lm_print iretq .msg db "INTdummy", 0x0D, 0x0A, 0x00 ; rdi = flags ; rsi = entry number ; rdx -> handler mk_idt_entry: mov rax, rdx mov rcx, rsi shl rcx, 4 ; offset = entry number * 16 add rcx, IDT_ADDR mov word [rcx + 0], ax ; offset mov word [rcx + 2], IDT_SEGMENT ; code segment mov word [rcx + 5], di mov byte [rcx + 4], 0 ; IST (0 = none) shr rax, 16 mov word [rcx + 6], ax ; offset shr rax, 16 mov dword [rcx + 8], eax ; offset mov dword [rcx + 12], 0 ; reserved ret lm_print: .loop mov al, [rsi] test al, al jz .done out dx, al inc si jmp .loop .done: ret idt_descriptor: dw IDT_SIZE - 1 ; size dq IDT_ADDR ; start msg_a20_line db "a20 line", 0x0D, 0x0A, 0x00 msg_disk_read db "disk", 0x0D, 0x0A, 0x00 times 510-($-$$) db 0 ; 2 bytes less now db 0x55 db 0xAA