From 7d736cd76d69aa7c224f53e6bfb56780d43b29c9 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Thu, 17 Sep 2026 22:07:47 +0200 Subject: [PATCH 1/2] Boot to C in 64-bit long mode --- .github/scripts/check_boot.py | 4 +- .github/workflows/ci.yml | 22 ++- .gitignore | 6 + CONTRIBUTING.md | 6 +- Makefile | 60 +++++++++ README.md | 44 +++--- boot.asm | 22 --- docs/assets/boot.png | Bin 10097 -> 1285 bytes include/console.h | 27 ++++ kernel/arch/x86_64/boot.asm | 243 ++++++++++++++++++++++++++++++++++ kernel/arch/x86_64/console.c | 76 +++++++++++ kernel/arch/x86_64/entry.asm | 128 ++++++++++++++++++ kernel/arch/x86_64/link.ld | 48 +++++++ kernel/core/kmain.c | 28 ++++ 14 files changed, 668 insertions(+), 46 deletions(-) create mode 100644 Makefile delete mode 100644 boot.asm create mode 100644 include/console.h create mode 100644 kernel/arch/x86_64/boot.asm create mode 100644 kernel/arch/x86_64/console.c create mode 100644 kernel/arch/x86_64/entry.asm create mode 100644 kernel/arch/x86_64/link.ld create mode 100644 kernel/core/kmain.c diff --git a/.github/scripts/check_boot.py b/.github/scripts/check_boot.py index e7302d9..d6e7122 100644 --- a/.github/scripts/check_boot.py +++ b/.github/scripts/check_boot.py @@ -18,8 +18,8 @@ import sys import termios import time -IMAGE = "boot.bin" -NEEDLE = b"Nadir" +IMAGE = sys.argv[1] if len(sys.argv) > 1 else "os.img" +NEEDLE = b"64-bit" BOOT_WAIT_S = 8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a37aafe..079be16 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,7 @@ # Nadir Kernel CI. Runs on every pull request and every push to main. # +# Builds os.img with make, verifies the image, then boots it headless in +# QEMU and asserts the kernel's welcome message reaches the screen. name: CI @@ -20,7 +22,23 @@ jobs: uses: actions/checkout@v4 - name: Install toolchain + # gcc, make and binutils ship with the runner; nasm and qemu do not. run: sudo apt-get update && sudo apt-get install -y nasm qemu-system-x86 - - name: Build boot sector - run: nasm -f bin boot.asm -o boot.bin \ No newline at end of file + - name: Build image + run: make + + - name: Verify image + run: | + set -euo pipefail + test -f os.img + size=$(stat -c %s os.img) + test "$size" -ge 1024 + test $(( (size - 512) % 512 )) -eq 0 + sig=$(dd if=os.img bs=1 skip=510 count=2 2>/dev/null | od -A n -t x1 | tr -d ' \n') + test "$sig" = "55aa" || { echo "missing 0xAA55 boot signature"; exit 1; } + grep -q "Nadir" os.img || { echo "kernel payload missing from os.img"; exit 1; } + echo "os.img: $size bytes, boot signature and payload present" + + - name: Boot and check welcome message in QEMU + run: python3 .github/scripts/check_boot.py os.img diff --git a/.gitignore b/.gitignore index b795cde..c34569c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,9 @@ qemu*.log AGENTS.md .omo .codegraph + +# Local scripts +*.sh + +# Python bytecode (CI boot check) +__pycache__/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 567f0f7..a85bb64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,11 +33,11 @@ Please stick to the following rules if you plan to use AI in your contributions: ## Building and running -The current toolchain is two commands from the repository root. See the [README](README.md) for prerequisites and troubleshooting: +Build from the repository root. See the [README](README.md) for prerequisites and troubleshooting: ``` -nasm -f bin boot.asm -o boot.bin -qemu-system-x86_64 -fda boot.bin +make # builds os.img (boot sector + kernel) +make run # boots it in QEMU ``` The toolchain is not yet finalized (candidates: `gcc` cross + `ld`, or `clang` + `lld` + `nasm`; target `x86-64-elf`). diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..f7efd43 --- /dev/null +++ b/Makefile @@ -0,0 +1,60 @@ +# Nadir Kernel build. +# +# Toolchain: host gcc/binutils (x86-64) with strict freestanding flags. +# A dedicated x86-64-elf cross-compiler is the planned next step; until +# then CC/LD/OBJCOPY stay overridable so it drops in without edits. +# Requires GNU coreutils (truncate -s %N). + +NASM ?= nasm +CC ?= gcc +LD ?= ld +OBJCOPY ?= objcopy +QEMU ?= qemu-system-x86_64 + +ARCH_DIR := kernel/arch/x86_64 +BUILD := build +IMG := nadir.img + +CFLAGS := -std=c17 -ffreestanding -nostdlib -fno-builtin \ + -m64 -mno-red-zone -mno-mmx -mno-sse \ + -fno-pic -fno-pie -fno-stack-protector \ + -Wall -Wextra -Werror -O2 -Iinclude + +OBJS := $(BUILD)/entry.o $(BUILD)/kmain.o $(BUILD)/console.o + +all: $(IMG) + +$(BUILD): + mkdir -p $(BUILD) + +$(BUILD)/entry.o: $(ARCH_DIR)/entry.asm | $(BUILD) + $(NASM) -f elf64 $< -o $@ + +$(BUILD)/kmain.o: kernel/core/kmain.c include/console.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/console.o: $(ARCH_DIR)/console.c include/console.h | $(BUILD) + $(CC) $(CFLAGS) -c $< -o $@ + +$(BUILD)/kernel.elf: $(OBJS) $(ARCH_DIR)/link.ld + $(LD) -m elf_x86_64 -T $(ARCH_DIR)/link.ld -o $@ $(OBJS) + +$(BUILD)/kernel.bin: $(BUILD)/kernel.elf + $(OBJCOPY) -O binary $< $@ + truncate -s %512 $@ + +# The boot sector needs the kernel size up front, so it builds last. +$(BUILD)/boot.bin: $(ARCH_DIR)/boot.asm $(BUILD)/kernel.bin | $(BUILD) + SECTORS=$$(( ($$(stat -c%s $(BUILD)/kernel.bin) + 511) / 512 )); \ + $(NASM) -f bin -D KERNEL_SECTORS=$$SECTORS $< -o $@ + +$(IMG): $(BUILD)/boot.bin $(BUILD)/kernel.bin + cat $^ > $@ + +run: $(IMG) + $(QEMU) -fda $(IMG) + +clean: + rm -rf $(BUILD) $(IMG) + +.PHONY: all run clean diff --git a/README.md b/README.md index f48f2e7..b063a60 100644 --- a/README.md +++ b/README.md @@ -5,37 +5,38 @@ The name Nadir comes from the Arabic word *naẓīr*. In astronomy it means the ## Status of the project -The project is in its foundation phase and is not yet usable. Only the bootloader is currently being written. +The project is in its foundation phase and is not yet usable. The boot chain works: a BIOS boot sector loads the kernel, switches through 32-bit protected mode into 64-bit long mode, and runs C code (`kmain`) that prints to the screen. Next: interrupts (IDT), then memory management. ## Getting started ### Prerequisites -You need two tools: +You need an assembler, a C toolchain, and an emulator: -- **nasm** to assemble `boot.asm` into a bootable image. -- **qemu-system-x86_64**, the emulator used to run it. +- **nasm** assembles the boot sector and the long-mode entry stub. +- **gcc**, **binutils** (`ld`, `objcopy`) and **make** build the C kernel (`gcc` pulls in `binutils` on most distros). +- **qemu-system-x86_64** runs the resulting image. -Install both from your package manager: +Install everything from your package manager: **Debian / Ubuntu** ```bash -sudo apt install nasm qemu-system-x86 +sudo apt install nasm qemu-system-x86 gcc make ``` **Fedora** ```bash -sudo dnf install nasm qemu-system-x86 +sudo dnf install nasm qemu-system-x86 gcc make ``` **Arch Linux** ```bash -sudo pacman -S nasm qemu-desktop +sudo pacman -S nasm qemu-desktop gcc make ``` **macOS (Homebrew)** ```bash -brew install nasm qemu +brew install nasm qemu gcc make ``` **Windows** @@ -47,8 +48,8 @@ I can only recommend using Linux distribution, but the Windows Subsystem for Lin From the repository root: ```bash -nasm -f bin boot.asm -o boot.bin -qemu-system-x86_64 -fda boot.bin +make # builds os.img +make run # boots it in QEMU ``` A QEMU window opens and you should see this: @@ -57,12 +58,12 @@ A QEMU window opens and you should see this: Notes: -- `-fda` boots the image as a BIOS floppy. QEMU's default firmware loads it at address `0x7C00` and executes it in 16-bit real mode. -- `boot.bin` is a generated artifact and is ignored by git, so you need to build it first. `rm boot.bin` cleans up and it will regenerates on the next `nasm` run. -- There is no build script or Makefile yet. Those two commands are the whole toolchain. -- Running over SSH or in a bare terminal? Add `-display curses` to render the boot screen as text instead of a window: +- `nadir.img` is a raw floppy image: the 512-byte boot sector first, then the kernel. `-fda` boots it as a BIOS floppy; QEMU's default firmware loads the sector at address `0x7C00` and executes it in 16-bit real mode. +- `build/` and `nadir.img` are generated artifacts ignored by git. `make clean` removes them. +- The C kernel builds with plain host `gcc` and freestanding flags for now (see `Makefile`); a dedicated `x86-64-elf` cross-compiler is the planned next step. +- Running over SSH or in a bare terminal? `make run` needs a window; boot headless instead: ```bash - qemu-system-x86_64 -fda boot.bin -display curses + qemu-system-x86_64 -fda nadir.img -display curses ``` Quit with `Ctrl-A` then `X`. @@ -70,10 +71,19 @@ Notes: | Symptom | Cause | |---|---| -| `Boot failed: not a bootable disk` | `boot.bin` is not a valid boot sector: it must be exactly 512 bytes and end with the `0xAA55` boot signature. Re-run the `nasm` step and check it completes without errors. | +| `Boot failed: not a bootable disk` | The first 512 bytes of `nadir.img` are not a valid boot sector (they must end with the `0xAA55` signature). Re-run `make` and check it completes without errors. | | Black screen / blinking cursor | The boot sector only speaks legacy BIOS (`int 0x10` text mode). This works out of the box with QEMU's default SeaBIOS firmware but will not boot under UEFI-only firmware (or UEFI-only real hardware) unless legacy/CSM boot is enabled. | +| QEMU reboots in a loop after `entering protected mode` (or before any C output) | A triple fault in the boot chain: the CPU crashed with no IDT installed and reset itself. Boot with `qemu-system-x86_64 -fda os.img -d int -no-reboot` and read the fault (`v=...`) plus registers from the log. | | No window appears under WSL2 | Use the `-display curses` variant above. It should work in any terminal. | +## How booting works + +1. The BIOS loads the 512-byte sector (`kernel/arch/x86_64/boot.asm`) at `0x7C00`: it enables A20, loads the kernel to `0x10000`, installs a flat GDT, and enters 32-bit protected mode. +2. The stage-2 entry (`kernel/arch/x86_64/entry.asm`) checks for long mode, identity-maps the first 1 GB with 2 MB pages, enables paging, jumps to 64-bit code, zeroes BSS, and calls `kmain`. +3. `kmain` (`kernel/core/kmain.c`) prints through the VGA text console (`kernel/arch/x86_64/console.c`, `include/console.h`) and halts. + +The full memory map lives in the header comment of `kernel/arch/x86_64/boot.asm`. + ## Contributing Every contribution is welcome. Please fork and send a pull request if you wish to contribute. You will be credited at the end of this very file. Please see the [CONTRIBUTING](CONTRIBUTING.md) file for more information. diff --git a/boot.asm b/boot.asm deleted file mode 100644 index 393e19f..0000000 --- a/boot.asm +++ /dev/null @@ -1,22 +0,0 @@ -bits 16 ; 16 bit mode -org 0x7c00 - -boot: - mov si, message ; Point SI register to message - mov ah, 0x0e ; Set higher bits to the display character command - -.loop: - lodsb ; Load the character within the AL register, and increment SI - cmp al, 0 ; Is the AL register a null byte? - je halt ; Jump to halt - int 0x10 ; Trigger video service interrupt - jmp .loop ; Loop again - -halt: - hlt ; stop - -message: - db "Hello World from Nadir Kernel !", 0x0a, 0x0d, 0x00 - -times 510-($-$$) db 0 ; 510 bytes of zeros -dw 0xAA55 ; boot signature \ No newline at end of file diff --git a/docs/assets/boot.png b/docs/assets/boot.png index 8a94de6d4f9170cf509834ab8ee9da658d8e6732..dcc1c63874a8dea13a5943c45342c7857fdb6f1e 100644 GIT binary patch literal 1285 zcmeAS@N?(olHy`uVBq!ia0y~yV7kD-z&L>sD8R6OPSyb+rI73q?NMQuI#Tk1%)j% z7JWJS94Mq(;u=wsl30>zm0Xkxq!^40jLdWmEOiadLkumgOwFu}4YUmmtPBi{bQe8D z(U6;;l9^VCTZ40fR4h<~2Hb{{%-q!ClEmBsOg$!0OXL<9&1PU=e(CAr7*cWT?QO^G z+i5b#KhFPlVDah4E8Mx4Ut5s>z_7_|!h>&1H8}QkZ)Rmv+_2#U$D{6RF?Ub@60_o3 z=)RNfsnQM8b{QF_+o|kNpEJjHX6G$=EyqD4 z;ci>UnUw;I#1Ec2yRkz+XWq1=X<HO1fbLC5xYBKJF@^y`|HZkMnhPED(g zSXMr%?~`Kd`*{zv&%Zq?b@WPky#7?aXD=7{aDQi6`2Jhc1D&hy4n*1gefa)m&AhmX zuyePnzVg5H(YJiPE%|$Jwt?oXe&yruV&^K<{3$Atiu`$SU$xBg?&Y0!Z>zTLVT^vJ zz-qD~&DE?)f1_RfS*rxuZf{KD*s9{Oqlaf{_U4yLT;)!gpBdw;06 ze94^@iQiJ1QO!D+FUZ=ID+|tO+%m~VRDMz(N9OXw;mmJBJj?BumD<|t`t17p>$&&e z^eXj}{EDexOP>2#InR9O`pD19V6)lFuFdn7-(RnH*D-s-`7~Q^pdRPq&oj5=@B8H%>SdSJM4sK&H}&_{&<#;+ z4G#ZscggG5D%GD>6#deaxhNm~{@m?(O(n;z^1W9q=UqBAqEu%ub8#(-GXbc zT~B;Jbgh3v>Dtc%EB1cfz3bkWy^l+F?>KiWYVNVjt0iSWY$RuusOP9o$Xoc?dBL~K zso~ilZB8`@#`=oye&ypd6__Z@x13{kcscP>qri#nk9TRY2i3;znk}}^(|g6WrI%i= zEtzB+Y4A5~-Zs6nYgAKiNWIVzDV@A7{ps0o%i0GAIxiclx!%rZI=uPsUNePFRfV>> zw~zl#T(7a|lk(;d@0(A(|NmdH_I;Btzxv@@;-~+-`;!uNN_1Jc=>*-EQ-8m1H=i05 zm3Dr6)9O569Q};BoL0ZLschTJj*2TS7QQZc e!I%5LnB~hmW`FsR3M?lW7(8A5T-G@yGywqEH9;T% literal 10097 zcmeHtXHZl7*Y3uKiU;I~f>ecbM0ycKX+fnZDjY!w2?mrZB_N#uAy|-34hQKHPz0nU zh7xMPLXjF;LWe^y0YYyf;cm{opZ@oq|DAW{-goYYH#3>p^P9EzudVg0XO)OQj0}D` zEqEFLfM4$2)inWtBb@-i(R}g*I8x}q4GsXOUfhLw0RVUX;h!TxhFb_6Jmalr;qB_~ z=w$oM)dIMwdJ`Kw^Y&lHVSvgt#RQ!@2H^PFLwPSB8v}cHq`kK%>VcD&H*iqZoCW}w zfP1>P9{ayv8bKw0GD9n@SZ^xdr*xcQik}ov`Bkwv+~Lv75Sj15^~T{qiEyR9rj@f# z{&I2q81YErY;T&ss&>T0?I`XuQJ2no>cQ+Dg=NaNOz1pKeu+z;Pnn;`GdaCY5vHwy z`J?&x<=r6BRL81?-kYs7&dmDP$ zf%5)57r%TsYJJwg*qssGe-M&X|28JgW~6E}egpylb5|R>Y+Nr2Mivc`8onQ2&vGgM z{k{#8wDkQN0Q4%Vs|(U;?&0`QF8+qbhiQ`4;47W0kqtH*ZKFzTw$@g?KSjAzpWa9^qUI48*;H-u-y8pUxX(1==iACy7dwNfX!=4 zjvd2u3(IcwR{)@+vC$jg*%Ui&QRk}|dvoUV&fmwCN{6D<5!jW7& zkuiayq76y=Zo-)TRU&gauwIYfn$Lx%Da3VQ>4KIPdCBru0C@g-8M;3+j#+QJMp@pd zpPRc|XsIDkEeAjAn1S|Pq?Rdh?+!o=7@beO-G>CoY32=~YT5z~36L1FgDcidD^P!I zKa|)c8d%TcUirK48J?y>SV>kVRaJ@$)Zn5&R7bSSQC@|XOjlu#dR`^CY$JP{?b0R? zp!6K8e>rxzNPw>97CRzFRyvIxzTkqhI=?=v5PpY8>o*#~TF8>5*@vG7fDqE>8=FyXLvT&zeZco`?6A7Z=2ODM1qO?M zCm}_ggQPfeKG)XPR{Tw&L@})WeDBr~@!GX%g+{c@a>N~;n|8T-#u}UND%=~iJVrCnm&C;{$iW4+iz*}&Wm7B;{`x&? z5`4A32DJ{assV~#EGii^5u1e5Ez#E;JEAGpo*oVASgo14o&DeWn?ReQ(@Z^%1+3@9 z@bW4ttN&`Oy=$$68BYu^t`fayB~XqWQdm}46f!=~-*DGkuT8f8?slyI^UaY*(c!oG zY3DH_x!x9}@vx_^lzB<9&x^)E6 zsV8T3jiE^&S?yH7sIr+qe?}d>zyn#k``&j!YBhyorSc&rJ2zdhZlC%C2^SC9%PO}m zj-8-+9c~0#yWbEVeq(lcyD8moWaKfLmltB`;r?W09NQRrurgoq_YW}2qwyx&>$`7T|XCrTB`_53pDMbPIf{@g|Dgvo5G

r{rBdns~h=dtNpgv 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 Ud@o=HBv8OTJtJNG?WeE)2bbYrY5)KL diff --git a/include/console.h b/include/console.h new file mode 100644 index 0000000..0a8335e --- /dev/null +++ b/include/console.h @@ -0,0 +1,27 @@ +/* Nadir early console: VGA text-mode driver. + * + * Writes straight to the VGA text buffer at 0xB8000, which the stage-2 + * page tables identity-map. Runs in 64-bit long mode at Ring 0 (CPL 0); + * no faults are possible beyond a programming bug. There is no IDT yet, + * so a wild pointer here triple-faults the machine. + */ + +#ifndef NADIR_CONSOLE_H +#define NADIR_CONSOLE_H + +#include + +/* Clear the screen and park the cursor at the top left. */ +void console_clear(void); + +/* Print one character. '\n' moves to the start of the next line; the + * screen scrolls up when the cursor runs past the last line. */ +void console_putchar(char c); + +/* Print a NUL-terminated string. */ +void console_puts(const char *s); + +/* Print a 64-bit value as 0x-prefixed, zero-padded hexadecimal. */ +void console_puthex64(uint64_t value); + +#endif /* NADIR_CONSOLE_H */ diff --git a/kernel/arch/x86_64/boot.asm b/kernel/arch/x86_64/boot.asm new file mode 100644 index 0000000..3d7797b --- /dev/null +++ b/kernel/arch/x86_64/boot.asm @@ -0,0 +1,243 @@ +; Nadir stage-1 boot sector. +; +; Runs in 16-bit real mode at 0x7C00 (Ring 0, CPL 0, BIOS hands us full +; privilege; privileged instructions used: cli, lgdt, mov cr0). +; Fault behavior: none possible here beyond a failed disk read, which prints +; an error and halts. There is no IDT yet, so any CPU exception would +; triple-fault and reset the machine. +; +; Job: enable A20, load the kernel image (LBA 1..N) to 0x10000, install a +; flat GDT, enter 32-bit protected mode and jump to the stage-2 entry. +; +; Memory map used by the whole boot chain: +; 0x007C00 this sector (512 bytes) +; 0x010000 kernel image (loaded here, linked here) +; 0x70000 PML4 | 0x71000 PDPT | 0x72000 PD (built by stage 2) +; 0x90000 stack top (real-mode stack grows down from 0x7C00 instead) +; 0xB8000 VGA text buffer + +bits 16 +org 0x7C00 + +%ifndef KERNEL_SECTORS +%error "KERNEL_SECTORS not defined - build with make" +%endif + +KERNEL_LOAD_SEG equ 0x1000 ; ES for load target 0x1000:0x0000 = 0x10000 +STACK_TOP equ 0x7C00 ; real-mode stack grows down from here + +start: + cli + xor ax, ax + mov ds, ax + mov es, ax + mov ss, ax + mov sp, STACK_TOP + sti + mov [boot_drive], dl ; BIOS passes the boot drive in DL + + mov si, msg_loading + call puts + + call enable_a20 + call read_drive_params + call load_kernel + + mov si, msg_entering + call puts + + cli + lgdt [gdt_ptr] + mov eax, cr0 + or eax, 1 + mov cr0, eax + jmp 0x08:prot32 + +; --------------------------------------------------------------------------- +; puts: print NUL-terminated string at DS:SI via BIOS teletype. +; Clobbers AX, BX, SI. +puts: + push ax + push bx + mov ah, 0x0E + mov bh, 0 +.loop: + lodsb + test al, al + jz .done + int 0x10 + jmp .loop +.done: + pop bx + pop ax + ret + +; --------------------------------------------------------------------------- +; enable_a20: try BIOS, keyboard-controller and fast-A20 methods in turn. +; Each is harmless if A20 is already on. +enable_a20: + mov ax, 0x2401 ; BIOS: enable A20 + int 0x15 + call kbd_wait ; keyboard controller: pulse output port + mov al, 0xD1 + out 0x64, al + call kbd_wait + mov al, 0xDF + out 0x60, al + call kbd_wait + in al, 0x92 ; fast A20 (skip if already set or absent) + test al, 0x02 + jnz .done + or al, 0x02 + out 0x92, al +.done: + ret + +kbd_wait: + in al, 0x64 + test al, 0x02 + jnz kbd_wait + ret + +; --------------------------------------------------------------------------- +; read_drive_params: query sectors/track and head count via int 0x13 AH=08. +; Falls back to floppy defaults (18 spt, 2 heads) on failure or nonsense. +read_drive_params: + xor ax, ax + mov ds, ax ; re-establish DS (BIOS may clobber it) + mov ah, 0x08 + mov dl, [boot_drive] + int 0x13 + jc .defaults + and cx, 0x3F ; CL[5:0] = max sector = sectors per track + jz .defaults + mov [spt], cl + inc dh ; DH = max head -> head count + jz .defaults + mov [heads], dh + ret +.defaults: + mov byte [spt], 18 + mov byte [heads], 2 + ret + +; --------------------------------------------------------------------------- +; lba_to_chs: convert LBA in AX to int 0x13 CHS in CH/CL/DH. +; Clobbers AX, BX, CX, DX. +lba_to_chs: + xor dx, dx + div word [spt] ; AX = LBA/spt, DX = sector offset + inc dx ; sectors are 1-based + push dx ; save sector number + xor dx, dx + div word [heads] ; AX = cylinder, DX = head + mov dh, dl + mov bx, ax ; BX = cylinder + pop ax ; AL = sector + mov cl, al + mov ch, bl ; CH = cylinder bits 0-7 + shr bx, 2 + and bl, 0xC0 + or cl, bl ; CL[7:6] = cylinder bits 8-9 + ret + +; --------------------------------------------------------------------------- +; load_kernel: read KERNEL_SECTORS sectors starting at LBA 1 to 0x10000. +; Halts with an error message after 3 failed retries per sector. +; +; NOTE: no register value is trusted across int 0x13 — SeaBIOS/real BIOSes +; may clobber general registers (observed: BX zeroed) and disturb the +; stack, so all loop state lives in memory and ES:BX/DS/DL are reloaded +; from memory before every disk call. +load_kernel: + xor ax, ax + mov ds, ax ; re-establish DS (BIOS may clobber it) + mov ax, KERNEL_LOAD_SEG + mov [buf_seg], ax + mov word [buf_off], 0 + mov word [lba], 1 + mov ax, KERNEL_SECTORS + mov [sectors_left], ax +.next: + mov ax, [lba] + call lba_to_chs + mov byte [tries], 3 +.attempt: + mov ax, [buf_seg] ; reload buffer: ES:BX must not survive + mov es, ax ; a BIOS call + mov bx, [buf_off] + mov dl, [boot_drive] + mov ah, 0x02 ; read 1 sector to ES:BX + mov al, 1 + int 0x13 + xor ax, ax + mov ds, ax ; DS may not have survived either + jnc .ok + mov ah, 0x00 ; reset drive, then retry + mov dl, [boot_drive] + int 0x13 + xor ax, ax + mov ds, ax + dec byte [tries] + jnz .attempt + mov si, msg_disk_err + call puts + jmp halt +.ok: + add word [buf_off], 512 ; advance buffer past the sector + jnc .no_carry + add word [buf_seg], 0x1000 ; +64K on segment wrap +.no_carry: + inc word [lba] + dec word [sectors_left] + jnz .next + ret + +halt: + cli +.hang: + hlt + jmp .hang + +; --------------------------------------------------------------------------- +bits 32 +prot32: + mov ax, 0x10 ; flat data segment + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + mov ss, ax + mov esp, 0x90000 + mov eax, 0x10000 ; stage-2 entry at start of kernel image + jmp eax + +; --------------------------------------------------------------------------- +bits 16 + +; Flat GDT: null, 32-bit code, 32-bit data. (64-bit descriptors live in the +; stage-2 image, which brings its own GDT.) +gdt: + dq 0x0000000000000000 + dq 0x00CF9A000000FFFF ; code32: base 0, 4GB, exec/read + dq 0x00CF92000000FFFF ; data32: base 0, 4GB, read/write +gdt_end: +gdt_ptr: + dw gdt_end - gdt - 1 + dd gdt + +msg_loading db "Nadir: loading kernel...", 0x0D, 0x0A, 0 +msg_entering db "Nadir: entering protected mode", 0x0D, 0x0A, 0 +msg_disk_err db "Nadir: disk read error", 0x0D, 0x0A, 0 + +boot_drive db 0 +spt dw 18 +heads dw 2 +lba dw 0 +buf_seg dw 0 ; load buffer segment (starts 0x1000) +buf_off dw 0 ; load buffer offset within segment +sectors_left dw 0 ; sectors still to load +tries db 0 ; retries left for current sector + +times 510-($-$$) db 0 +dw 0xAA55 diff --git a/kernel/arch/x86_64/console.c b/kernel/arch/x86_64/console.c new file mode 100644 index 0000000..054da5a --- /dev/null +++ b/kernel/arch/x86_64/console.c @@ -0,0 +1,76 @@ +/* Nadir console: VGA text-mode driver (see console.h). */ + +#include + +#include "console.h" + +#define VGA_WIDTH 80 +#define VGA_HEIGHT 25 +#define VGA_ADDR 0xB8000 +#define COLOR_DEFAULT 0x0FU /* white on black */ + +/* Cursor position. Lives in BSS, zeroed by stage 2 before kmain runs. */ +static uint8_t cursor_row; +static uint8_t cursor_col; + +static volatile uint16_t *const vga = (uint16_t *)VGA_ADDR; + +static void scroll(void) +{ + for (uint8_t row = 1; row < VGA_HEIGHT; row++) { + for (uint8_t col = 0; col < VGA_WIDTH; col++) { + vga[(row - 1) * VGA_WIDTH + col] = vga[row * VGA_WIDTH + col]; + } + } + for (uint8_t col = 0; col < VGA_WIDTH; col++) { + vga[(VGA_HEIGHT - 1) * VGA_WIDTH + col] = + (uint16_t)((COLOR_DEFAULT << 8) | ' '); + } +} + +void console_clear(void) +{ + for (uint16_t i = 0; i < VGA_WIDTH * VGA_HEIGHT; i++) { + vga[i] = (uint16_t)((COLOR_DEFAULT << 8) | ' '); + } + cursor_row = 0; + cursor_col = 0; +} + +void console_putchar(char c) +{ + if (c == '\n') { + cursor_row++; + cursor_col = 0; + } else { + vga[cursor_row * VGA_WIDTH + cursor_col] = + (uint16_t)((COLOR_DEFAULT << 8) | c); + cursor_col++; + if (cursor_col >= VGA_WIDTH) { + cursor_col = 0; + cursor_row++; + } + } + if (cursor_row >= VGA_HEIGHT) { + scroll(); + cursor_row = VGA_HEIGHT - 1; + } +} + +void console_puts(const char *s) +{ + while (*s != '\0') { + console_putchar(*s); + s++; + } +} + +void console_puthex64(uint64_t value) +{ + static const char digits[] = "0123456789ABCDEF"; + + console_puts("0x"); + for (int shift = 60; shift >= 0; shift -= 4) { + console_putchar(digits[(value >> shift) & 0xFU]); + } +} diff --git a/kernel/arch/x86_64/entry.asm b/kernel/arch/x86_64/entry.asm new file mode 100644 index 0000000..34e73db --- /dev/null +++ b/kernel/arch/x86_64/entry.asm @@ -0,0 +1,128 @@ +; Nadir stage-2 entry: 32-bit protected mode -> 64-bit long mode -> C. +; +; Entered via a near jump to 0x10000 with a flat code segment (Ring 0, +; CPL 0 —still the privilege BIOS gave us; no userspace exists yet). +; Privileged instructions used: cpuid, mov cr4/cr0, rdmsr/wrmsr, lgdt. +; Fault behavior: no IDT is installed, so any exception here triple-faults +; and resets the machine. The only graceful failure is a CPU without long +; mode, which prints an error straight to VGA and halts. +; +; Steps: verify long mode exists, identity-map the first 1GB with 2MB +; pages, enable PAE + LME + paging, reload a GDT with a 64-bit code +; segment, zero BSS, then call the C entry point kmain (System V ABI). +; Interrupts stay disabled for the whole sequence. + +bits 32 +section .boot32 +global boot32_entry +extern kmain +extern __bss_start +extern __bss_end + +PML4 equ 0x70000 +PDPT equ 0x71000 +PD equ 0x72000 +STACK64_TOP equ 0x90000 + +boot32_entry: + ; --- CPUID: does this CPU do 64-bit long mode? --- + mov eax, 0x80000000 + cpuid + cmp eax, 0x80000001 + jb .no_longmode + mov eax, 0x80000001 + cpuid + test edx, 1 << 29 ; LM bit + jz .no_longmode + + ; --- page tables: PML4 -> PDPT -> PD, 512 x 2MB identity pages --- + mov edi, PML4 + xor eax, eax + mov ecx, 0x1000 ; clear 16KB (tables + slack) + rep stosd + mov eax, PDPT + or eax, 0x03 ; present + writable + mov [PML4], eax + mov eax, PD + or eax, 0x03 + mov [PDPT], eax + mov ecx, 512 + mov edi, PD + mov eax, 0x83 ; present + writable + 2MB page +.fill_pd: + mov [edi], eax + add eax, 0x200000 + add edi, 8 + loop .fill_pd + + ; --- enable long mode --- + mov eax, PML4 ; page-walk root (CR3 was 0 until now) + mov cr3, eax + mov eax, cr4 + or eax, 1 << 5 ; PAE + mov cr4, eax + mov ecx, 0xC0000080 ; IA32_EFER + rdmsr + or eax, 1 << 8 ; LME + wrmsr + mov eax, cr0 + or eax, 1 << 31 ; PG: paging on, long mode active + mov cr0, eax + + lgdt [gdt64_ptr] + jmp 0x18:long_entry ; far jump loads the 64-bit code segment + +.no_longmode: + mov edi, 0xB8000 ; VGA text buffer, line 0 + mov esi, err_nolm + mov ah, 0x4F ; white on red +.print: + lodsb + test al, al + jz .die + stosw + jmp .print +.die: + cli +.hang: + hlt + jmp .hang + +err_nolm db "ERR: CPU has no 64-bit long mode", 0 + +; Own GDT: the stage-1 GDT is behind us, so bring null/code32/data32 plus +; the 64-bit code+data segments this stage needs. +align 8 +gdt64: + dq 0x0000000000000000 + dq 0x00CF9A000000FFFF ; code32 (kept for completeness) + dq 0x00CF92000000FFFF ; data32 + dq 0x00209A0000000000 ; code64: L=1, exec/read + dq 0x0000920000000000 ; data64: read/write +gdt64_end: +gdt64_ptr: + dw gdt64_end - gdt64 - 1 + dd gdt64 + +; --------------------------------------------------------------------------- +bits 64 +long_entry: + mov ax, 0x20 ; 64-bit data segment + mov ds, ax + mov es, ax + mov fs, ax + mov gs, ax + mov ss, ax + mov rsp, STACK64_TOP + + mov rdi, __bss_start ; zero BSS (uninitialized C statics) + mov rcx, __bss_end + sub rcx, rdi + xor eax, eax + rep stosb + + call kmain ; first C code — does not return + cli +.hang: + hlt + jmp .hang diff --git a/kernel/arch/x86_64/link.ld b/kernel/arch/x86_64/link.ld new file mode 100644 index 0000000..f282575 --- /dev/null +++ b/kernel/arch/x86_64/link.ld @@ -0,0 +1,48 @@ +/* Nadir kernel linker script. + * + * Links the whole kernel (stage-2 entry + C code) as one flat binary loaded + * by the boot sector at 0x10000. Virtual addresses equal physical ones: the + * stage-2 page tables identity-map the first 1GB, so linked addresses work + * as-is the moment long mode is on. + * + * Layout rules: the stage-2 entry (.boot32) must be the very first bytes of + * the image, BSS exposes __bss_start/__bss_end for runtime zeroing, and + * toolchain metadata sections are discarded so the binary is pure code+data. + */ + +ENTRY(boot32_entry) +OUTPUT_ARCH(i386:x86-64) + +SECTIONS +{ + . = 0x10000; + + .boot : { + *(.boot32) + } + + .text : { + *(.text*) + } + + .rodata : { + *(.rodata*) + } + + .data : { + *(.data*) + } + + .bss : { + __bss_start = .; + *(.bss*) + *(COMMON) + __bss_end = .; + } + + /DISCARD/ : { + *(.note*) + *(.comment*) + *(.eh_frame*) + } +} diff --git a/kernel/core/kmain.c b/kernel/core/kmain.c new file mode 100644 index 0000000..090ae40 --- /dev/null +++ b/kernel/core/kmain.c @@ -0,0 +1,28 @@ +/* Nadir kernel entry point: first C code to run. + * + * Called once from the stage-2 entry stub after the CPU is in 64-bit long + * mode with a valid stack, zeroed BSS and identity-mapped memory. Runs at + * Ring 0 (CPL 0), interrupts disabled, no IDT yet: any fault triple-faults, + * so this function proves the whole boot chain simply by running. + * It never returns; on return the caller halts the CPU. + */ + +#include "console.h" + +/* A value that only comes out right if the CPU truly executes 64-bit + * arithmetic: every nibble survives the shifts above 32 bits. */ +#define PROOF_VALUE 0x0123456789ABCDEFULL + +void kmain(void) +{ + console_clear(); + console_puts("Hello World from Nadir Kernel !\n"); + console_puts("Running C code in 64-bit long mode (Ring 0).\n"); + console_puts("64-bit proof value: "); + console_puthex64(PROOF_VALUE); + console_puts("\nHalting.\n"); + + for (;;) { + __asm__ volatile("hlt"); + } +} From 2b8174792241ffce065ac2fd8f886fd6459f5056 Mon Sep 17 00:00:00 2001 From: "Pierre-Emmanuel Gob." Date: Thu, 17 Sep 2026 22:20:34 +0200 Subject: [PATCH 2/2] Align CI and docs with nadir.img artifact name --- .github/scripts/check_boot.py | 2 +- .github/workflows/ci.yml | 24 ++++++++++++++---------- CONTRIBUTING.md | 2 +- README.md | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/.github/scripts/check_boot.py b/.github/scripts/check_boot.py index d6e7122..162094a 100644 --- a/.github/scripts/check_boot.py +++ b/.github/scripts/check_boot.py @@ -18,7 +18,7 @@ import sys import termios import time -IMAGE = sys.argv[1] if len(sys.argv) > 1 else "os.img" +IMAGE = sys.argv[1] if len(sys.argv) > 1 else "nadir.img" NEEDLE = b"64-bit" BOOT_WAIT_S = 8 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 079be16..6788b3d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,6 @@ # Nadir Kernel CI. Runs on every pull request and every push to main. # -# Builds os.img with make, verifies the image, then boots it headless in +# Builds nadir.img with make, verifies the image, then boots it headless in # QEMU and asserts the kernel's welcome message reaches the screen. name: CI @@ -29,16 +29,20 @@ jobs: run: make - name: Verify image + # Every check prints what it saw: a silent bare `test` once hid a + # failure here, so each assertion reports and names its own failure. run: | set -euo pipefail - test -f os.img - size=$(stat -c %s os.img) - test "$size" -ge 1024 - test $(( (size - 512) % 512 )) -eq 0 - sig=$(dd if=os.img bs=1 skip=510 count=2 2>/dev/null | od -A n -t x1 | tr -d ' \n') - test "$sig" = "55aa" || { echo "missing 0xAA55 boot signature"; exit 1; } - grep -q "Nadir" os.img || { echo "kernel payload missing from os.img"; exit 1; } - echo "os.img: $size bytes, boot signature and payload present" + ls -l nadir.img + size=$(stat -c %s nadir.img) + echo "image size: $size bytes" + test "$size" -ge 1024 || { echo "FAIL: image smaller than sector+kernel minimum"; exit 1; } + test $(( (size - 512) % 512 )) -eq 0 || { echo "FAIL: kernel part is not sector-aligned"; exit 1; } + sig=$(dd if=nadir.img bs=1 skip=510 count=2 2>/dev/null | od -A n -t x1 | tr -d ' \n') + echo "boot signature: $sig" + test "$sig" = "55aa" || { echo "FAIL: missing 0xAA55 boot signature"; exit 1; } + grep -q "Nadir" nadir.img || { echo "FAIL: kernel payload missing from image"; exit 1; } + echo "OK: nadir.img valid, boot signature and payload present" - name: Boot and check welcome message in QEMU - run: python3 .github/scripts/check_boot.py os.img + run: python3 .github/scripts/check_boot.py nadir.img diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a85bb64..f8451b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -36,7 +36,7 @@ Please stick to the following rules if you plan to use AI in your contributions: Build from the repository root. See the [README](README.md) for prerequisites and troubleshooting: ``` -make # builds os.img (boot sector + kernel) +make # builds nadir.img (boot sector + kernel) make run # boots it in QEMU ``` diff --git a/README.md b/README.md index b063a60..da1e34f 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ I can only recommend using Linux distribution, but the Windows Subsystem for Lin From the repository root: ```bash -make # builds os.img +make # builds nadir.img make run # boots it in QEMU ``` @@ -73,7 +73,7 @@ Notes: |---|---| | `Boot failed: not a bootable disk` | The first 512 bytes of `nadir.img` are not a valid boot sector (they must end with the `0xAA55` signature). Re-run `make` and check it completes without errors. | | Black screen / blinking cursor | The boot sector only speaks legacy BIOS (`int 0x10` text mode). This works out of the box with QEMU's default SeaBIOS firmware but will not boot under UEFI-only firmware (or UEFI-only real hardware) unless legacy/CSM boot is enabled. | -| QEMU reboots in a loop after `entering protected mode` (or before any C output) | A triple fault in the boot chain: the CPU crashed with no IDT installed and reset itself. Boot with `qemu-system-x86_64 -fda os.img -d int -no-reboot` and read the fault (`v=...`) plus registers from the log. | +| QEMU reboots in a loop after `entering protected mode` (or before any C output) | A triple fault in the boot chain: the CPU crashed with no IDT installed and reset itself. Boot with `qemu-system-x86_64 -fda nadir.img -d int -no-reboot` and read the fault (`v=...`) plus registers from the log. | | No window appears under WSL2 | Use the `-display curses` variant above. It should work in any terminal. | ## How booting works