Merge pull request #1 from Plexi09/boot-to-c

Makes the main bootloader functional. Instead of a real mode "hello world" sector, the boot chain now loads the kernel from disk, switches through 32-bi protected mode into 64-bit long mode, and runs C code (kmain) that prints to the VGA console and halts. Old root boot.asm removed and replaced by kernel/arch/x86_64/boot.asm.
This commit is contained in:
Pierre-Emmanuel Gob. 2026-09-17 22:25:08 +02:00 • committed by GitHub
commit 8c46ac48d9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 672 additions and 46 deletions

View file

@ -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 "nadir.img"
NEEDLE = b"64-bit"
BOOT_WAIT_S = 8

View file

@ -1,5 +1,7 @@
# Nadir Kernel CI. Runs on every pull request and every push to main.
#
# 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
@ -20,7 +22,27 @@ 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
- name: Build image
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
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 nadir.img

6
.gitignore vendored
View file

@ -23,3 +23,9 @@ qemu*.log
AGENTS.md
.omo
.codegraph
# Local scripts
*.sh
# Python bytecode (CI boot check)
__pycache__/

View file

@ -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 nadir.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`).

60
Makefile Normal file
View file

@ -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

View file

@ -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 nadir.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 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
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.

View file

@ -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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.9 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Before After
Before After

27
include/console.h Normal file
View file

@ -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 <stdint.h>
/* 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 */

243
kernel/arch/x86_64/boot.asm Normal file
View file

@ -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

View file

@ -0,0 +1,76 @@
/* Nadir console: VGA text-mode driver (see console.h). */
#include <stdint.h>
#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]);
}
}

View file

@ -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

View file

@ -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*)
}
}

28
kernel/core/kmain.c Normal file
View file

@ -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");
}
}