Buena pregunta. Antes de arrancar, el mapa completo de lo que hay que construir:
0x7C00Todo 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:—
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
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.
# 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.
mkdir -p own_os/boot own_os/kernel own_os/build
cd own_os
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
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
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");
}
}
kernel/kernel.ldOUTPUT_FORMAT("binary")
ENTRY(_start)
SECTIONS {
. = 0x1000;
.text : {
*(.text._start)
*(.text*)
}
.data : {
*(.data*)
}
.rodata : {
*(.rodata*)
}
.bss : {
*(.bss*)
*(COMMON)
}
}
MakefileNASM := 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 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