Пишем 16-битное ядро. Урок первый.

Всем привет!

Сегодня мы с нуля напишем нашу собственную ОС для ПК и ноутбуков. Она будет работать в 16-битном режиме (real mode).

В данном уроке мы с вами напишем загрузчик ОС на языке программирования NASM. Также мы выведем на экран приветствие "Hello, world!" используя функции BIOS (прерывания).

Для начала я порекомендовал бы ознакомиться с BIOS прерываниями INT 10h. Сделать это вы можете на этом сайте в статье BIOS прерывания.

Ну, теперь можно начать.


Структура проекта

Создадим 2 файла (нам этого будет достаточно):


Работаем мы как всегда либо из-под Линукса, либо из-под 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
                
  • Компилируем загрузчик
  • Создаём путой образ диска
  • Запиываем на первый сектор наш загрузчик
  • Запускаем (например через мой любимый qemu)
  • Не забудьте после создания скрипта компиляции прописать:

    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
                
  • lodsb загружает байт из адреса, на который указывает регистр SI (Source Index), в регистр AL
  • Мы можем использовать эту функцию вот так:

    
    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                  ; Загрузочная сигнатура
                

    Теперь в любом месте кода мы таким образом сможем выводить на экран строку.

    После компиляции и запуска мы увидим это:


    Фраза hello,world

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

    В следующем уроке мы допишем наш загрузчик и будем писать код нашего простого ядра