Buena pregunta. Antes de arrancar, el mapa completo de lo que hay que construir:

  1. Bootloader (Stage 1) — 512 bytes de assembly que la BIOS carga en 0x7C00
  2. Kernel mínimo — entra en modo protegido 32-bit e imprime en pantalla
  3. Linker script — organiza la memoria
  4. Makefile — compila todo y genera la imagen de disco
  5. Script de prueba — corre todo en QEMU

Todo en un solo proyecto. Vamos.✅ Compiló perfecto. Ahora ejecutar en QEMU y capturar el framebuffer VGA:Todo está compilado y verificado. Ahora copiar al output:—

✅ Todo compiló y funciona. Acá está lo que se hizo:

Mapa del proyecto

holamundos/
├── boot/
│   └── bootloader.asm   ← Stage 1: 512 bytes exactos, arranca la máquina
├── kernel/
│   ├── kernel_entry.asm ← Puente ASM → C, primer código que corre el kernel
│   ├── kernel.c         ← El kernel: imprime directo en VGA 0xB8000
│   └── kernel.ld        ← Le dice al linker cómo organizar la memoria
├── Makefile             ← Compila todo, genera os.img
└── build/
    └── os.img           ← La imagen de disco lista para bootear ← ESTE

Lo que hace cada pieza

bootloader.asm — La BIOS lo carga en 0x7C00. Tiene exactamente 512 bytes (obligatorio). Configura la GDT, activa el bit de modo protegido en CR0, y hace un far-jump a 32-bit. Luego carga el kernel desde el disco con INT 13h y le pasa el control.

kernel_entry.asm — Vive en 0x1000. El bootloader salta acá. Su único trabajo es llamar a kmain().

kernel.c — Sin malloc, sin printf, sin nada de stdlib. Escribe directo en 0xB8000 (el framebuffer VGA de texto). Cada celda son 2 bytes: ASCII + color.

kernel.ld — Le dice al linker que el binario empieza en 0x1000 y que el entry point va primero.

Para correrlo vos en tu máquina

# Instalar dependencias (Ubuntu/Debian)
sudo apt install nasm gcc qemu-system-x86

# Compilar
make

# Correr en QEMU con ventana gráfica
make run-gui

La pantalla que verías: fondo azul, borde en cian, “Hola Mundo” en verde brillante, y el diagnóstico completo del sistema en gris.

Acá va todo el código completo, copiable y pegable archivo por archivo.


Estructura de carpetas primero

mkdir -p own_os/boot own_os/kernel own_os/build
cd own_os

ARCHIVO 1: boot/bootloader.asm

; =============================================================================
; BOOTLOADER — Stage 1 con LBA (compatible con pendrives y hardware real)
; La BIOS lo carga en 0x7C00. Tiene exactamente 512 bytes.
; =============================================================================

[BITS 16]
[ORG 0x7C00]

start:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7C00
    sti

    mov [boot_drive], dl

    mov si, msg_loading
    call print_string_16

    call load_kernel

    lgdt [gdt_descriptor]

    mov eax, cr0
    or  eax, 1
    mov cr0, eax

    jmp CODE_SEG:init_pm

; -----------------------------------------------------------------------------
; PRINT STRING modo real (INT 10h)
; -----------------------------------------------------------------------------
print_string_16:
    pusha
.loop:
    lodsb
    test al, al
    jz .done
    mov ah, 0x0E
    int 0x10
    jmp .loop
.done:
    popa
    ret

; -----------------------------------------------------------------------------
; LOAD KERNEL via LBA (INT 13h extendida AH=42h)
; Mas compatible con pendrives y hardware moderno que CHS
; -----------------------------------------------------------------------------
load_kernel:
    pusha

    ; Verificar que la BIOS soporta LBA
    mov ah, 0x41
    mov bx, 0x55AA
    mov dl, [boot_drive]
    int 0x13
    jc  .no_lba

    ; Configurar el DAP (Disk Address Packet)
    mov word [dap.count],   15        ; leer 15 sectores
    mov word [dap.offset],  0x0000    ; destino: 0x0000
    mov word [dap.segment], 0x0100    ; segmento: 0x0100 * 16 = 0x1000
    mov dword [dap.lba_lo], 1         ; empezar en sector LBA 1 (el 2do sector)
    mov dword [dap.lba_hi], 0

    mov ah, 0x42
    mov si, dap
    mov dl, [boot_drive]
    int 0x13
    jc  .disk_error

    popa
    ret

.no_lba:
    mov si, msg_no_lba
    call print_string_16
    jmp .hang

.disk_error:
    mov si, msg_disk_err
    call print_string_16

.hang:
    cli
    hlt
    jmp .hang

; -----------------------------------------------------------------------------
; DAP — Disk Address Packet (estructura para LBA)
; -----------------------------------------------------------------------------
dap:
    db  0x10        ; tamaño del DAP (16 bytes)
    db  0x00        ; reservado
.count:
    dw  0           ; cantidad de sectores a leer
.offset:
    dw  0           ; offset del buffer destino
.segment:
    dw  0           ; segmento del buffer destino
.lba_lo:
    dd  0           ; LBA bajo (32 bits)
.lba_hi:
    dd  0           ; LBA alto (32 bits, para discos >2TB)

; -----------------------------------------------------------------------------
; [32-BIT] — modo protegido activo
; -----------------------------------------------------------------------------
[BITS 32]
init_pm:
    mov ax, DATA_SEG
    mov ds, ax
    mov ss, ax
    mov es, ax
    mov fs, ax
    mov gs, ax

    mov ebp, 0x90000
    mov esp, ebp

    jmp CODE_SEG:0x1000

; =============================================================================
; GDT — Global Descriptor Table
; =============================================================================
gdt_start:

gdt_null:
    dd 0x0
    dd 0x0

gdt_code:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10011010b
    db 11001111b
    db 0x00

gdt_data:
    dw 0xFFFF
    dw 0x0000
    db 0x00
    db 10010010b
    db 11001111b
    db 0x00

gdt_end:

gdt_descriptor:
    dw gdt_end - gdt_start - 1
    dd gdt_start

CODE_SEG equ gdt_code - gdt_start
DATA_SEG equ gdt_data - gdt_start

; =============================================================================
; DATOS
; =============================================================================
boot_drive:   db 0
msg_loading:  db 'Bootloader OK - Cargando kernel...', 13, 10, 0
msg_disk_err: db 'ERROR: Fallo lectura de disco!', 13, 10, 0
msg_no_lba:   db 'ERROR: BIOS no soporta LBA!', 13, 10, 0

; =============================================================================
; BOOT SIGNATURE
; =============================================================================
times 510 - ($ - $$) db 0
dw 0xAA55

ARCHIVO 2: kernel/kernel_entry.asm

; =============================================================================
; KERNEL ENTRY — puente entre bootloader y kmain() en C
; =============================================================================

[BITS 32]

extern kmain
global _start

_start:
    call kmain

.hang:
    cli
    hlt
    jmp .hang

ARCHIVO 3: kernel/kernel.c

/*
 * =============================================================================
 * KERNEL — HolaMundOS
 * Corre en modo protegido 32-bit. Sin stdlib. Sin OS debajo.
 * Escribe directo en el framebuffer VGA: 0xB8000
 * Pantalla de texto: 80 columnas x 25 filas
 * Cada celda = 2 bytes: [ASCII][color]
 * Color = (fondo << 4) | frente
 * =============================================================================
 */

typedef unsigned char  u8;
typedef unsigned short u16;
typedef unsigned int   u32;

#define VGA_BASE  ((u16*)0xB8000)
#define VGA_COLS  80
#define VGA_ROWS  25

/* Colores VGA */
#define BLACK        0
#define BLUE         1
#define GREEN        2
#define CYAN         3
#define RED          4
#define MAGENTA      5
#define BROWN        6
#define LIGHT_GREY   7
#define DARK_GREY    8
#define LIGHT_BLUE   9
#define LIGHT_GREEN  10
#define LIGHT_CYAN   11
#define LIGHT_RED    12
#define LIGHT_MAG    13
#define LIGHT_BROWN  14
#define WHITE        15

static int cursor_x = 0;
static int cursor_y = 0;

static inline u16 vga_entry(char c, u8 fg, u8 bg) {
    return (u16)c | ((u16)((bg << 4) | fg) << 8);
}

void vga_clear(u8 fg, u8 bg) {
    for (int y = 0; y < VGA_ROWS; y++)
        for (int x = 0; x < VGA_COLS; x++)
            VGA_BASE[y * VGA_COLS + x] = vga_entry(' ', fg, bg);
    cursor_x = 0;
    cursor_y = 0;
}

void vga_scroll(void) {
    for (int y = 0; y < VGA_ROWS - 1; y++)
        for (int x = 0; x < VGA_COLS; x++)
            VGA_BASE[y * VGA_COLS + x] = VGA_BASE[(y+1) * VGA_COLS + x];
    for (int x = 0; x < VGA_COLS; x++)
        VGA_BASE[(VGA_ROWS-1) * VGA_COLS + x] = vga_entry(' ', WHITE, BLACK);
    cursor_y = VGA_ROWS - 1;
}

void vga_putchar(char c, u8 fg, u8 bg) {
    if (c == '\n') {
        cursor_x = 0;
        cursor_y++;
    } else if (c == '\r') {
        cursor_x = 0;
    } else if (c == '\t') {
        cursor_x = (cursor_x + 8) & ~7;
    } else {
        VGA_BASE[cursor_y * VGA_COLS + cursor_x] = vga_entry(c, fg, bg);
        cursor_x++;
    }
    if (cursor_x >= VGA_COLS) {
        cursor_x = 0;
        cursor_y++;
    }
    if (cursor_y >= VGA_ROWS)
        vga_scroll();
}

void print(const char *s, u8 fg, u8 bg) {
    while (*s) vga_putchar(*s++, fg, bg);
}

void println(const char *s, u8 fg, u8 bg) {
    print(s, fg, bg);
    vga_putchar('\n', fg, bg);
}

void print_hex(u32 n, u8 fg, u8 bg) {
    char buf[11];
    const char *hex = "0123456789ABCDEF";
    buf[0] = '0'; buf[1] = 'x';
    for (int i = 9; i >= 2; i--) {
        buf[i] = hex[n & 0xF];
        n >>= 4;
    }
    buf[10] = '\0';
    print(buf, fg, bg);
}

void draw_border(u8 fg, u8 bg) {
    VGA_BASE[0] = vga_entry('+', fg, bg);
    for (int x = 1; x < VGA_COLS-1; x++)
        VGA_BASE[x] = vga_entry('-', fg, bg);
    VGA_BASE[VGA_COLS-1] = vga_entry('+', fg, bg);

    for (int y = 1; y < VGA_ROWS-1; y++) {
        VGA_BASE[y * VGA_COLS]              = vga_entry('|', fg, bg);
        VGA_BASE[y * VGA_COLS + VGA_COLS-1] = vga_entry('|', fg, bg);
    }

    VGA_BASE[(VGA_ROWS-1)*VGA_COLS] = vga_entry('+', fg, bg);
    for (int x = 1; x < VGA_COLS-1; x++)
        VGA_BASE[(VGA_ROWS-1)*VGA_COLS + x] = vga_entry('-', fg, bg);
    VGA_BASE[(VGA_ROWS-1)*VGA_COLS + VGA_COLS-1] = vga_entry('+', fg, bg);
}

void kmain(void) {

    vga_clear(WHITE, BLUE);
    draw_border(LIGHT_CYAN, BLUE);

    cursor_x = 2; cursor_y = 2;
    print(">>> HolaMundOS v0.1 <<<", LIGHT_BROWN, BLUE);

    cursor_x = 2; cursor_y = 3;
    print("Sistema Operativo minimo en C + ASM", LIGHT_GREY, BLUE);

    cursor_x = 2; cursor_y = 4;
    for (int i = 2; i < VGA_COLS-2; i++)
        VGA_BASE[4 * VGA_COLS + i] = vga_entry('=', DARK_GREY, BLUE);

    cursor_x = 2; cursor_y = 6;
    print("  Hola Mundo  ", LIGHT_GREEN, BLUE);

    cursor_x = 2; cursor_y = 8;
    println("Estado del sistema:", LIGHT_CYAN, BLUE);

    cursor_x = 4; cursor_y = 9;
    print("[OK] Bootloader LBA ejecutado   (0x7C00)", LIGHT_GREY, BLUE);

    cursor_x = 4; cursor_y = 10;
    print("[OK] Modo protegido 32-bit       activo", LIGHT_GREY, BLUE);

    cursor_x = 4; cursor_y = 11;
    print("[OK] GDT configurada             CODE=0x08 DATA=0x10", LIGHT_GREY, BLUE);

    cursor_x = 4; cursor_y = 12;
    print("[OK] Kernel cargado en           ", LIGHT_GREY, BLUE);
    print_hex(0x1000, LIGHT_BROWN, BLUE);

    cursor_x = 4; cursor_y = 13;
    print("[OK] VGA framebuffer en          ", LIGHT_GREY, BLUE);
    print_hex(0xB8000, LIGHT_BROWN, BLUE);

    cursor_x = 4; cursor_y = 14;
    print("[OK] Stack en                    ", LIGHT_GREY, BLUE);
    print_hex(0x90000, LIGHT_BROWN, BLUE);

    cursor_x = 4; cursor_y = 15;
    print("[OK] Lectura de disco            LBA mode", LIGHT_GREY, BLUE);

    cursor_x = 2; cursor_y = 17;
    println("Arquitectura:", LIGHT_CYAN, BLUE);

    cursor_x = 4; cursor_y = 18;
    print("BIOS -> MBR (0x7C00) -> LBA load -> PM32 -> kmain()", LIGHT_GREY, BLUE);

    cursor_x = 4; cursor_y = 19;
    print("bootloader.asm -> kernel_entry.asm -> kernel.c -> VGA", LIGHT_GREY, BLUE);

    cursor_x = 2; cursor_y = VGA_ROWS - 2;
    print("NASM + GCC + LD | Sin stdlib | BIOS Legacy | LBA", DARK_GREY, BLUE);

    for (;;) {
        __asm__ volatile ("hlt");
    }
}

ARCHIVO 4: kernel/kernel.ld

OUTPUT_FORMAT("binary")

ENTRY(_start)

SECTIONS {
    . = 0x1000;

    .text : {
        *(.text._start)
        *(.text*)
    }

    .data : {
        *(.data*)
    }

    .rodata : {
        *(.rodata*)
    }

    .bss : {
        *(.bss*)
        *(COMMON)
    }
}

ARCHIVO 5: Makefile

NASM   := nasm
GCC    := gcc
LD     := ld

CFLAGS := -m32 -ffreestanding -fno-pie -nostdlib -nostdinc -O2 -Wall -fno-stack-protector

LDFLAGS := -m elf_i386 -T kernel/kernel.ld --oformat binary

BUILD  := build
BOOT   := boot
KERNEL := kernel

BOOT_BIN   := $(BUILD)/bootloader.bin
ENTRY_OBJ  := $(BUILD)/kernel_entry.o
KERNEL_OBJ := $(BUILD)/kernel.o
KERNEL_BIN := $(BUILD)/kernel.bin
OS_IMG     := $(BUILD)/os.img

.PHONY: all run run-gui pendrive clean

all: $(OS_IMG)

$(BOOT_BIN): $(BOOT)/bootloader.asm
	@mkdir -p $(BUILD)
	@echo "[NASM]  $< -> $@"
	$(NASM) -f bin $< -o $@
	@size=$$(wc -c < $@); \
	if [ $$size -ne 512 ]; then \
		echo "ERROR: bootloader tiene $$size bytes (debe ser 512)"; exit 1; \
	fi
	@echo "       Bootloader: 512 bytes OK"

$(ENTRY_OBJ): $(KERNEL)/kernel_entry.asm
	@mkdir -p $(BUILD)
	@echo "[NASM]  $< -> $@"
	$(NASM) -f elf32 $< -o $@

$(KERNEL_OBJ): $(KERNEL)/kernel.c
	@mkdir -p $(BUILD)
	@echo "[GCC]   $< -> $@"
	$(GCC) $(CFLAGS) -c $< -o $@

$(KERNEL_BIN): $(ENTRY_OBJ) $(KERNEL_OBJ) $(KERNEL)/kernel.ld
	@echo "[LD]    kernel_entry + kernel -> $@"
	$(LD) $(LDFLAGS) -o $@ $(ENTRY_OBJ) $(KERNEL_OBJ)
	@size=$$(wc -c < $@); echo "       Kernel: $$size bytes"

$(OS_IMG): $(BOOT_BIN) $(KERNEL_BIN)
	@echo "[IMG]   Creando imagen de disco: $@"
	dd if=/dev/zero    of=$@ bs=512 count=2880 2>/dev/null
	dd if=$(BOOT_BIN)  of=$@ bs=512 seek=0 conv=notrunc 2>/dev/null
	dd if=$(KERNEL_BIN) of=$@ bs=512 seek=1 conv=notrunc 2>/dev/null
	@echo ""
	@echo "============================================"
	@echo "  Imagen lista: $(OS_IMG)"
	@echo "  QEMU:    make run"
	@echo "  Pendrive: make pendrive DEV=/dev/sdX"
	@echo "============================================"

run:
	qemu-system-i386 \
		-drive file=$(OS_IMG),format=raw,index=0,if=floppy \
		-boot a -m 32M -display none -serial stdio \
		-no-reboot -no-shutdown

run-gui:
	qemu-system-i386 \
		-drive file=$(OS_IMG),format=raw,index=0,if=floppy \
		-boot a -m 32M -no-reboot

# Grabar en pendrive: make pendrive DEV=/dev/sdX
# ⚠️  CUIDADO: borra TODO el dispositivo
pendrive: $(OS_IMG)
	@if [ -z "$(DEV)" ]; then \
		echo "ERROR: especifica el dispositivo: make pendrive DEV=/dev/sdX"; \
		exit 1; \
	fi
	@echo "⚠️  Escribiendo en $(DEV) — esto borra todo el dispositivo"
	@echo "Presiona Ctrl+C para cancelar, Enter para continuar..."
	@read _confirm
	sudo dd if=$(OS_IMG) of=$(DEV) bs=512 conv=fsync
	sudo sync
	@echo "Listo. Pendrive grabado."

clean:
	rm -rf $(BUILD)

Compilar y grabar

# Compilar todo
make

# Probar en QEMU con ventana
make run-gui

# Grabar en pendrive (cambiá sdX por tu dispositivo, ej: sdb)
make pendrive DEV=/dev/sdb

Para bootear en la PC

  1. Insertá el pendrive
  2. Reiniciá y entrá al boot menu (F12 / F11 / F8 / Esc según la placa)
  3. Si ves opciones UEFI y Legacy, elegí Legacy o CSM
  4. Seleccioná el pendrive
  5. Deberías ver la pantalla azul con “Hola Mundo” en verde