Пишем 16-битное ядро. Урок первый.
Всем привет!
Сегодня мы с нуля напишем нашу собственную ОС для ПК и ноутбуков. Она будет работать в 16-битном режиме (real mode).
В данном уроке мы с вами напишем загрузчик ОС на языке программирования NASM. Также мы выведем на экран приветствие "Hello, world!" используя функции BIOS (прерывания).
Для начала я порекомендовал бы ознакомиться с BIOS прерываниями INT 10h. Сделать это вы можете на этом сайте в статье BIOS прерывания.
Ну, теперь можно начать.
Структура проекта
Создадим 2 файла (нам этого будет достаточно):
- boot.asm - загрузчик ОС
- build.sh - файл, который поможет нам не вводить кучу команд для сборки
Работаем мы как всегда либо из-под Линукса, либо из-под WSL для Windows.
Установка необходимых пакетов
Нам понадобится только NASM
sudo apt install nasm
Как вы уже поняли мы пишем шестнадцатибитную ОС, а это значит, что не будет простого Си. Только NASM.
Давайте для начала напишем код загрузчика, который выводит только 1 символ.
boot.asm:
[BITS 16]
[ORG 7C00h]
start:
cli ; Отключаем прерывания
xor ax, ax ; Обнуляем регистр AX
mov ds, ax ; Устанавливаем сегмент данных
mov es, ax ; Устанавливаем сегмент дополнительной памяти
; Задаём видеорежим
mov ax, 0x03 ; Видеорежим 80x25 текстовый
int 0x10 ; Вызов BIOS для установки видеорежима
; Выводим символ на экран
mov ah, 0x0E ; Функция вывода символа
mov al, 'A' ; Символ для вывода
int 0x10 ; Вызов BIOS для вывода символа
; Бесконечный цикл
hang:
jmp hang ; Зацикливаем выполнение
times 510 - ($ - $$) db 0
dw 0xAA55 ; Загрузочная сигнатура
build.sh:
nasm -f bin boot.asm -o boot.bin
dd if=/dev/zero of=OS.img bs=512 count=16
dd if=boot.bin of=OS.img conv=notrunc
qemu-system-i386 -hda OS.img
Не забудьте после создания скрипта компиляции прописать:
chmod +x build.sh
Ну, а потом просто:
./build.sh
Готово!
После компиляции и запуска мы увидим это:

Выводим строку
С выводом символа на экран мы разобрались, но есть проблема:
вывод таким методом фразы "Hello, world!" будеет выглядеть так:
[BITS 16]
[ORG 7C00h]
start:
cli ; Отключаем прерывания
xor ax, ax ; Обнуляем регистр AX
mov ds, ax ; Устанавливаем сегмент данных
mov es, ax ; Устанавливаем сегмент дополнительной памяти
; Задаём видеорежим
mov ax, 0x03 ; Видеорежим 80x25 текстовый
int 0x10 ; Вызов BIOS для установки видеорежима
; Выводим hello, world на экран
mov ah, 0x0E
mov al, 'H'
int 0x10
mov ah, 0x0E
mov al, 'e'
int 0x10
mov ah, 0x0E
mov al, 'l'
int 0x10
mov ah, 0x0E
mov al, 'l'
int 0x10
mov ah, 0x0E
mov al, 'o'
int 0x10
mov ah, 0x0E
mov al, ','
int 0x10
mov ah, 0x0E
mov al, ' '
int 0x10
mov ah, 0x0E
mov al, 'w'
int 0x10
mov ah, 0x0E
mov al, 'o'
int 0x10
mov ah, 0x0E
mov al, 'r'
int 0x10
mov ah, 0x0E
mov al, 'l'
int 0x10
mov ah, 0x0E
mov al, 'd'
int 0x10
mov ah, 0x0E
mov al, '!'
int 0x10
; Бесконечный цикл
hang:
jmp hang ; Зацикливаем выполнение
times 510 - ($ - $$) db 0
dw 0xAA55 ; Загрузочная сигнатура
Конечно, какждый раз для вывода строки писать это очень долго и не удобно. Давайте создадим нормальную функцию для отображения строки:
print_string:
mov ah, 0x0E
.print_char:
lodsb
cmp al, 0
je .done
int 0x10
jmp .print_char
.done:
ret
Мы можем использовать эту функцию вот так:
mov si, msg ; msg - строка для вывода
call print_string ; Вызываем функцию
msg db 'Hello, world!', 0 ; Обьявление строки
Код обновлённого загрузчика:
[BITS 16]
[ORG 7C00h]
start:
cli ; Отключаем прерывания
xor ax, ax ; Обнуляем регистр AX
mov ds, ax ; Устанавливаем сегмент данных
mov es, ax ; Устанавливаем сегмент дополнительной памяти
; Задаём видеорежим
mov ax, 0x03 ; Видеорежим 80x25 текстовый
int 0x10 ; Вызов BIOS для установки видеорежима
; Выводим hello, world на экран
mov si, hello_msg
call print_string
;Бесконечный цикл
call hang
; Функция вывода строки на экран
print_string:
mov ah, 0x0E
.print_char:
lodsb
cmp al, 0
je .done
int 0x10
jmp .print_char
.done:
ret
; Бесконечный цикл
hang:
jmp hang ; Зацикливаем выполнение
hello_msg db 'Hello, world!', 0 ; Обьявление строки
times 510 - ($ - $$) db 0
dw 0xAA55 ; Загрузочная сигнатура
Теперь в любом месте кода мы таким образом сможем выводить на экран строку.
После компиляции и запуска мы увидим это:

С некой базой мы разобрались. Но есть нюанс: писать всю ОС на загрузочном секторе диска (512 байт) невозможно. Ведь рано или поздно мы привысим данный объём и не сможем добавлять больший функционал. По этому нам понадобится загружать ядро. И уже в загруженом ядре у нас не будет такого жёсткого ограничения.
В следующем уроке мы допишем наш загрузчик и будем писать код нашего простого ядра