---
title: "Bootloader x86: grafika 2D w trybie chronionym i 3D w 512 bajtach"
url: https://michalasobczak.pl/pl/posts/x86-bootloader-2d-and-3d-graphics-in-nasm.html
author: "Michał Sobczak"
description: "Dwa programy bare-metal na x86, które startują z obrazu dyskietki bez żadnego systemu operacyjnego: animowane demo 2D w 32-bitowym trybie chronionym z MMX oraz projekcja 3D w trybie rzeczywistym, która musi zmieścić się w jednym 512-bajtowym sektorze rozruchowym."
published: 2026-10-25T09:00:00.000Z
updated: 2026-10-25T09:00:00.000Z
language: pl
categories: ["Hobby"]
tags: ["Programming", "Vintage Computing", "3D"]
license: CC BY-NC-ND 4.0 (https://creativecommons.org/licenses/by-nc-nd/4.0/)
---
# Bootloader x86: grafika 2D w trybie chronionym i 3D w 512 bajtach

> **Zastrzeżenie:** opisane tu projekty są wynikiem mojej własnej, samodzielnej pracy, wykonanej bez użycia AI. Sam artykuł powstał z pomocą AI.

Katalog [x86-bootloader](https://github.com/michalasobczak/retro-lab/tree/master/x86-bootloader) w moim repozytorium [retro-lab](https://github.com/michalasobczak/retro-lab) zawiera dwa programy, które działają bezpośrednio na procesorze po tym, jak BIOS przekaże im sterowanie. Nie ma systemu operacyjnego, libc ani sterowników: tylko asembler NASM, przerwania BIOS-u i pamięć wideo pod `0xA0000`. Oba działają w emulatorze Bochs lub na prawdziwym sprzęcie uruchomionym z dyskietki.

| Część | Tryb | Rozmiar | Co robi |
| --- | --- | --- | --- |
| `protected-mode-2d` | 32-bitowy tryb chroniony | 4555 bajtów (512 B sektora rozruchowego + 4043 B programu) | animowane kształty 2D, linie, logo, dźwięk z PC speakera, kopiowanie bufora z MMX |
| `real-mode-3d` | 16-bitowy tryb rzeczywisty | 512 bajtów | linie Bresenhama i projekcja 3D na 2D, wszystko wewnątrz sektora rozruchowego |

## protected-mode-2d: kod z 2005 roku

Ta część jest starsza niż kod na GitHubie. Napisałem ją około 2005 roku, a jej 11. wersję zacommitowałem w 2018. Jest podzielona na małe pliki: `bootsector.asm`, `main.asm`, `point.asm`, `line.asm`, `rect.asm`, `logo.asm` i `sound.asm`, asemblowane osobno i łączone w jeden obraz dyskietki.

![Animowana grafika 2D rysowana przez bootloader w trybie chronionym w Bochs](https://michalasobczak.pl/media/projects/x86-protected-mode-2d.png)

### Z trybu rzeczywistego do trybu chronionego

Sektor rozruchowy jest ładowany przez BIOS pod adres `0x7C00` i nadal działa w 16-bitowym trybie rzeczywistym. Musi zrobić wszystko, co później, bez BIOS-u, stanie się niemożliwe:

1.  zresetować napęd i wczytać kolejne 10 sektorów (właściwy program) pod `0000:1000` za pomocą `int 13h`,
2.  przełączyć się w tryb graficzny 13h (320×200, 256 kolorów) za pomocą `int 10h`,
3.  wyłączyć przerwania i załadować globalną tablicę deskryptorów (GDT) instrukcją `lgdt`,
4.  ustawić bit 0 rejestru `CR0` i wykonać daleki skok, aby opróżnić kolejkę prefetch,
5.  w kodzie 32-bitowym: ustawić segmenty danych i stosu oraz skoczyć do programu pod `08h:01000h`.

```x86asm
  cli                   ; Disable interrupts, we want to be alone
  xor  ax, ax
  mov  ds, ax           ; Set DS-register to 0 - used by lgdt
  lgdt  [gdt_desc]      ; Load the GDT descriptor
  mov  eax, cr0
  or   eax, 1           ; Set bit 0
  mov  cr0, eax
  jmp  08h:clear_pipe   ; Jump to code segment, offset clear_pipe

[BITS 32]
clear_pipe:
  mov  ax, 10h          ; Data segment selector
  mov  ds, ax
  mov  ss, ax
  mov  esp, 090000h
  jmp  08h:01000h       ; Jump to the program loaded from disk
```

GDT to absolutne minimum, które działa: deskryptor zerowy, jeden segment kodu (odczyt/wykonanie) i jeden segment danych (odczyt/zapis), oba płaskie na całą 4-gigabajtową przestrzeń adresową.

### Podwójne buforowanie z MMX

Rysowanie bezpośrednio pod `0xA0000` powoduje migotanie. Program rysuje każdą klatkę do bufora pod `0x10000`, a następnie kopiuje 64 000 bajtów do pamięci wideo. Kopiowanie wykorzystuje osiem 64-bitowych rejestrów MMX, więc jedna iteracja pętli przenosi 64 bajty, a 1000 iteracji przenosi cały ekran:

```x86asm
%macro draw_screen 0
  mov     esi, 010000h
  mov     edi, 0A0000h
  mov     cx, 1000
  %%copy_loop:
    movq  mm0, [esi]
    movq  mm1, [esi+8]
    ; ... mm2 - mm7
    movq  [edi],    mm0
    movq  [edi+8],  mm1
    ; ... mm2 - mm7
    add  esi, 64
    add  edi, 64
    loop    %%copy_loop
%endmacro
```

MMX pojawiło się w 1996 roku, więc dla hobbystycznego projektu z 2005 roku było to najtańsze „SIMD”, jakie mogłem dostać na dowolnym PC.

## real-mode-3d: wszystko w jednym sektorze

Drugi program, napisany w 2018 roku, stawia ostrzejszy limit: całość musi zmieścić się w 512-bajtowym sektorze rozruchowym, który ładuje BIOS. Bez drugiego etapu, bez dodatkowych sektorów.

![Projekcja 3D w trybie rzeczywistym renderowana z 512-bajtowego sektora rozruchowego](https://michalasobczak.pl/media/projects/x86-real-mode-3d.png)

Program pozostaje w trybie rzeczywistym, wchodzi w tryb 13h przez `int 10h`, ustawia `ES` na segment `0xA000` i zapisuje piksele przez `[es:di]`. Rysowanie pikseli i linii to makra NASM, a nie procedury, co oszczędza narzut `call`/`ret` i pozwala asemblerowi pominąć to, co nie jest używane:

```x86asm
%macro PutPixelAt 3
  mov bx, %2
  imul bx,320
  add bx, %1
  mov di, bx
  mov al, %3
  mov [es:di], al
%endmacro
```

### Bresenham najpierw w Rubym, potem w asemblerze

Linie korzystają z algorytmu Bresenhama, operującego wyłącznie na liczbach całkowitych. Najpierw napisałem go w Rubym (`bresenham_integer.rb`), żeby mieć czytelną implementację referencyjną, a potem przetłumaczyłem linia po linii na makro `LineFromTo`. Komentarze w asemblerze to kod w Rubym:

```x86asm
  mov ax, [vdy]                       ; d = 2*dy - dx
  imul ax, 2
  sub ax, [vdx]
  mov [vd], ax
  ...
  cmp word [vd], 0                    ; if d > 0
  jg  %%.True
```

Część 3D to ta sama uproszczona projekcja, której użyłem na Commodore 64 (zob. [3D na Commodore 64](https://michalasobczak.pl/posts/commodore-64-restoration-and-3d-in-basic-c-and-assembly.html)): głębokość jest dzielona przez stałe i dodawana do x i y, co daje widok ukośny, niemal ortograficzny, przy użyciu wyłącznie całkowitoliczbowego `div`. Bez liczb zmiennoprzecinkowych, bez FPU, bez tablic lookup.

## Budowanie i uruchamianie

```bash
nasm main.asm -f bin -o boot.bin
dd if=/dev/zero of=disk.img bs=1024 count=1440
dd if=boot.bin of=disk.img conv=notrunc
```

Bochs uruchamia to z minimalnym plikiem `bochsrc.bxrc`:

```text
megs: 32
floppya: 1_44=disk.img, status=inserted
boot: a
log: bochsout.txt
cpu: ips=15000000
```

## Czego się z tego nauczyłem

-   Przejście z trybu rzeczywistego do chronionego to około dwudziestu instrukcji, ale każda z nich musi być we właściwej kolejności. Log Bochsa (`bochsout.txt`) to jedyny debugger, jaki masz, i to wystarcza.
-   512 bajtów to bardzo dobry nauczyciel. Makra kontra procedury, ponowne wykorzystanie rejestrów i rozmiar instrukcji nagle zaczynają mieć znaczenie.
-   Napisanie algorytmu najpierw w języku wysokiego poziomu (tutaj w Rubym), a potem ręczne przetłumaczenie go, to najszybszy znany mi sposób na uzyskanie poprawnego kodu w asemblerze.

Kod źródłowy: [retro-lab/x86-bootloader](https://github.com/michalasobczak/retro-lab/tree/master/x86-bootloader). Zobacz też [przegląd moich praktycznych projektów](https://michalasobczak.pl/posts/hands-on-projects-on-github.html).
