Предисловие

Здравствуйте. Это моя первая статья, и она будет о разработке собственной операционной системы. Если в процессе чтения заметите какие либо ошибки или у вас более глубокие познания в этой области, пишите комментарии и я внесу правки. Автор ещё учится этому непростому ремеслу. Полный исходный код ОС будет приведён в конце статьи. ОС написана полностью на NASM, для архитектуры процессоров Intel x86-64.

Вначале было слово...

Для более полного понимания процесса запуска компьютера советую прочитать статью.

После нажатия на кнопку "Power", системный контроллер расположенный на материнской плате, подаёт сигнал на включение процессору. Затем процессор загружает и исполняет код BIOS.

Код BIOS содержит алгоритм проверки комплектующих, эта проверка называется POST. После проверки, начинается процесс считывания первых 512 байт (1 сектор) с жёсткого диска, указанного в настройках BIOS, во вкладке Boot -> Boot Device Priority (Может отличаться в зависимости от версии BIOS). Процессор скажем так, "ищет" загрузочный сектор, он известен нам как MBR.

Постановка приоритетов загрузки ОС
Постановка приоритетов загрузки ОС

И слово было 0xaa55

Как я выразился выше, процессор "ищет" загрузочный сектор. В конце сектора должно быть магическое число 0xaa55. Благодаря этому магическому числу, процессор "понимает", что это загрузочный сектор, затем сгружает его в ОЗУ по адресу 0x7c00 и исполняет его в реальном режиме. В случае если жёсткий диск не содержит загрузочный сектор, вызывается прерывание 0x19 и процессор переходит к следующему диску. Если устройств больше нет, то выведет ошибку по типу "No bootable devices found.".

Процессор не нашёл загрузочных устройств
Процессор не нашёл загрузочных устройств

Регистры

В реальном режиме работы процессора "Real Mode", пользователю доступны 16-битные регистры: ax, bx, cx, dx, sp, si, di, bp. Также пользователь имеет доступ и к 8-битным регистрам. 8-битные регистры формируют 16-битный регистр.

Старший 8-битный регистр

Младший 8-битный регистр

16-битный регистр

AH

AL

AX

BH

BL

BX

CH

CL

CX

DH

DL

DX

Прерывания

BIOS предоставляет разработчикам целый инструментарий для работы с периферией, графикой, жёсткими дисками и так далее (256 прерываний). Я приведу таблицу прерываний, используемых в процессе написания ОС.

Номер прерывания

Назначение прерывания

0x13

Работа с жёстким диском

0x10

Видео сервис. Понадобится для вывода текста на экран

0x16

Работа с клавиатурой

0x15

Работа с памятью. Понадобится для задержки выполнения кода

0x19

Перезагрузка системы с помощью «теплой» перезагрузки без очистки памяти или восстановления таблицы векторов прерываний (IVT)

Режимы работы процессора x86-64

Процессоры x86-64 могут работать в 3 основных режимах. Это реальный (real mode), защищённый (protected mode) и 64-разрядный режим (long mode). Обратите внимание, что начиная с защищённого режима, прекращается поддержка прерываний BIOS. Для работы с отдельными комплектующими компьютера, необходимо писать драйвера.

Реальный режим (real mode) - Это режим в который входит процессор после включения или перезагрузки. Это стандартный 16-битный режим в котором доступно только 1Мб физической памяти, и возможности процессора используются в малой степени.

Защищённый режим (protected mode или legacy mode) - Это 32-разрядный режим, считается главным. В защищённом режиме, операционная система может получить максимум от процессора. Этот режим даёт доступ к 4Гб физической памяти. А при включении специального механизма трансляции адресов можно получить доступ к 64Гб физической памяти. В защищённый режим можно перейти только из реального режима. Защищённым этот режим называется потому, что позволяет защитить данные операционной системы от приложений. "Родной" размер данных для этого режима DWORD.

Long mode - 64-разрядный режим. В этом режиме можно получить доступ к 2⁵² байтам физической памяти и 2⁴⁸ байтам виртуальной памяти. В 64-разрядный режим можно перейти только лишь из защищённого режима. В этом режиме "Родной" для процессора размер данных это DWORD, но можно оперировать и QWORD.

Помимо 3 вышеперечисленных режимов, поддерживаются 2 подрежима

Режим виртуального процессора 8086 - Это подрежим защищённого режима для поддержки 16-разрядных приложений. Его можно включить для отдельной задачи в многозадачной 32-битной операционной системе.

Режим совместимости для long mode - В режиме совместимости, приложениям доступны 4Гб памяти и полная поддержка 32 и 16-разрядного кода. Режим совместимости можно включить для отдельной задачи в многозадачной 64-битной операционной системе. В режиме совместимости, размер адреса 32-разрядный, а размер операнда не может быть QWORD.

Помимо выше приведённых режимов, существует Режим Системного Управления (System Management Mode), в который процессор входит при получении специального прерывания SMI. Режим системного управления предназначен для выполнения некоторых действий с возможностью их полной изоляции от прикладного программного обеспечения и даже операционной системы. Режим системного управления может использоваться для реализации системы управления энергосбережением компьютера или функций безопасности и контроля доступа.

Переходим к практике

Загрузчик - это исполняемы код, который загружает операционную систему. Размер загрузчика не должен превышать 512 байт. Этого размера недостаточно чтобы уместить всю операционную систему, для этого мы разместим код загрузчика в загрузочном секторе, а операционную систему за её пределами. Для этого нам понадобится прерывание 0x13.

org 0x7c00

jmp pre_boot

pre_boot:
    cli         ; Запрещаем прерывания
    xor ax, ax  ; Зануляем регистры
    mov ds, ax  ; Зануляем регистры
    mov es, ax  ; Зануляем регистры
    mov ss, ax  ; Зануляем регистры
    mov sp, 0x7c00
    ; Чтение и размещение операционной системы в ОЗУ
    mov ah, 0x02; Функция 0x02 - Работа с жёстким диском
    mov al, 7   ; Количество секторов на чтение. В нашем случае 7 = 7*512 = 3584 байт
    mov ch, 0x00   ; Номер цилиндра
    mov cl, 0x02   ; Номер начального сектора 2. 1 сектор - загрузчик, 2 сектор - ОС.
    mov dh, 0x00   ; Сторона диска
    mov dl, 0x80   ; Номер устройства. Начинается с 0x80 - 0, 0x81 - 1, ...
    mov bx, 0x7e00 ; Адрес загрузки данных
    int 0x13       ; Прерывание чтения сектора
    jc read_error  ; Если возникает ошибка, переходим к выполнению куска кода read_error.

    jmp 0x7e00    ; Если ошибок не возникло, то переходим к загруженному коду. 0x7c00 + 512 = 0x7e00

read_error:
    mov ah, 0x0e ; Номер функции в прерывании 0x10, вывод символа на экран.
    mov al, 'R'  ; Загружаем символ
    int 0x10     ; Выводим символ
    mov al, 'E'
    int 0x10
    mov al, 'A'
    int 0x10
    mov al, 'D'
    int 0x10
    mov al, ' '
    int 0x10
    mov al, 'E'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, 'O'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, '!'
    int 0x10


    jmp $        ; Бесконечный переход к этой метке. Зависаем на месте с выводом ошибки.

times 510 - ($- $$) db 0 ; Заполняем оставшуюся часть кода нулями.
dw 0xaa55 ; Магическое число в конце сектора.

Важное замечание!

Операционная система будет размещена в одном файле и её реальный размер на данный момент составляет 7 секторов. В случае, если реальный размер файла будет меньше чем указано секторов на чтение, будет выводиться ошибка "READ ERROR!". При написании ОС, имейте это ввиду!

Ошибка чтения диска. Указанное количество секторов на чтение превышает реальный размер исполняемого файла.
Ошибка чтения диска. Указанное количество секторов на чтение превышает реальный размер исполняемого файла.

Теперь, когда загрузчик готов, приступаем к написанию библиотеки ввода-вывода. Эти функции я расположу в другом файле, в директории "drivers".

Для удобства добавил комментарии в стиле языка C, что бы быстро определить в какие регистры, какие значения необходимо поместить для вызова определённой функции. А теперь пройдёмся по функционалу:

  • cls - Очистка экрана.

  • out_char - Вывод символа на экран.

  • out_string - Вывод строки на экран.

  • in_char - Пользовательский ввод символа.

  • in_string - Пользовательский ввод строки.

  • compare_str - Сравнение двух строк.

  • clear_buffer - Очистить буфер (Занулить).

  • new_line - Перевод каретки ввода на новую строку.

; ============================================================================
; Библиотека для ввода/вывода текстовой информации, с помощью прерываний BIOS
; ============================================================================
global cls          ; void cls();
global out_char     ; void out_char(char bl);
global out_string   ; void out_string(char* si);

global in_char      ; char in_char() return char al;
global in_string    ; void in_string(char[]* si);

global comapre_strs ; int (const char* first_word[] si, const char* last_word[] bx) return cx (1 - равны, 0 - не равны);

global clear_buffer ; void (const char* buf_address[] si, int buf_size bx);

global new_line ; void new_line();

section .text

new_line:        ; Перевод каретки на новую строку
    push ax      ; Сохраняем значение регистра ax в стеке
    mov ah, 0x0e ; Номер функции прерывания 0x10. Вывод
    mov al, 0x0a ; Символ перевода каретки в начало
    int 0x10     ; Вызов прерывания для работы с видеосервисом
    mov al, 0x0d ; Символ перевода строки
    int 0x10
    pop ax       ; Восстанавливаем значение в регистре ax
    ret          ; Выходим из функции

cls:
    push ax      ; Сохраняем значение ax
    mov ah, 0x00 ; Номер функции прерывания 0x10. Изменение видеорежима
    mov al, 0x03 ; Номер видеорежима. 0x03 - текстовый видеорежим.
    int 0x10     
    pop ax       ; Восстанавливаем значение регистра ax
    ret          ; Выходим из функции

out_char:        ; Вывод символа на экран
    push ax
    mov ah, 0x0e
    mov al, bl   ; В регистр bl мы заранее положили символ на вывод
    int 0x10
    pop ax
    ret

out_string:      ; Вывод строки на экран
    push ax
    mov ah, 0x0e
    call __out_string_next_char
    pop ax
    ret
__out_string_next_char:
    mov al, [si]        ; В регистре si храниться адрес начала строки. Помещаем значение из адреса si в al
    cmp al, 0           ; Затем сравниваем al с 0
    jz __out_string_if_zero  ; если al = 0 значит строка закончилась
    int 0x10                 ; если al != 0 значит, по этому адресу что-то есть, выводим символ на экран
    inc si                   ; Увеличиваем si на 1
    jmp __out_string_next_char ; Выполняем функцию снова
__out_string_if_zero:
    ret                    ; Покидаем функцию

in_char:        ; Пользовательский ввод символа
    push bx
    mov ah, 0
    int 0x16    ; Символ сохранён в регистр al
    mov ah, 0x0e
    mov bh, 0
    mov bl, 0x07
    int 0x10    ; Вывод введённого символа на экран
    pop bx
    ret


comapre_strs:       ; Сравнивание строк
    push si
    push bx
    push ax
__comapre_strs_comp:
    mov ah, [bx]
    cmp [si], ah
    jne __comapre_strs_first_zero
    inc si
    inc bx
    jmp __comapre_strs_comp
__comapre_strs_first_zero:
    cmp byte [bx], 0
    jne __comapre_strs_not_equal
    mov cx, 1
    pop si
    pop bx
    pop ax
    ret
__comapre_strs_not_equal:
    mov cx, 0
    pop si
    pop bx
    pop ax
    ret


clear_buffer:
    ; si - Адрес буфера
    ; bx - Количество байт на очистку
    push cx
    mov cx, 0
__clear_buffer_loop:
    cmp cx, bx
    je __clear_buffer_end_loop
    mov byte [si], 0
    inc si
    inc cx
    jmp __clear_buffer_loop
__clear_buffer_end_loop:
    pop cx
    ret



in_string:             ; Пользовательский ввод строки. Адрес буфера хранится в si
    push ax
    push cx
    xor cx, cx
__input_string_loop:
    mov ah, 0
    int 0x16
    cmp al, 0x0d            ; Если пользователь нажал Enter, то обрабатываем это событие
    je __input_string_enter
    cmp al, 0x08            ; Если пользователь нажал Backspace, то обрабатываем это событие
    je __input_string_backspace

    mov [si], al
    inc si
    inc cx

    mov ah, 0x0e
    mov bh, 0
    mov bl, 0x07
    int 0x10
    cmp cx, 255               ; Если Пользователь ввёл 255 символов
    je __input_string_enter   ; То прыгаем в событие нажатия на Enter
    jmp __input_string_loop
__input_string_enter:
    mov ah, 0x0e ; Номер функции int 0x10 - вывод символа 
    mov al, 0x0d ; Перевод каретки на новую строку
    mov bh, 0
    mov bl, 0x07 ; Цвет выводимого символа 0 - чёрный фон 7 - белый символ
    int 0x10
    mov al, 0xa  ; Перевод каретки в начало строки
    int 0x10

    mov byte [si], 0 ; Помещаем в конец строки 0
    pop cx
    pop ax
    ret
__input_string_backspace:
    cmp cx, 0         ; Проверка номера символа по счёту. Если это 0 символ - значит нужно запретить стирание символа, потому что, пользователь может случайно стереть выводимую ОС информацию.
    je __input_string_loop ; Если это 0 символ, то возвращаемся в цикл ввода
    mov ah, 0x0e            ; Иначе, эмулируем нажатия на Backspace, Пробел, Backspace
    mov al, 0x08            ; Backspace
    int 0x10
    mov al, 0x20            ; Пробел
    int 0x10
    mov al, 0x08            ; Backspace
    int 0x10

    mov byte [si], 0
    dec si                 ; Уменьшаем si на 1. si - адрес cx - номер введённого символа. Уменьшаем два этих регистра на один
    dec cx
    jmp __input_string_loop ; Возвращаемся в цикл ввода

Обратите внимание на строки 157-163. В строке 157 в комментарии я указал, "эмулируем нажатия на Backspace, Пробел, Backspace". Когда пользователь нажимает на Backspace во время ввода, каретка смещается влево на 1 символ и остаётся под предыдущим символом. Пользователь не может её стереть, может только лишь заменить на другой символ (Напоминает включённый режим Insert на клавиатуре). Это можно обойти через набор действий, Backspace, Пробел, Backspace. Что бы изменить такое поведение, в коде заранее прописаны эти действия.

Операционная система

Операционная система должна поддерживать как минимум командную строку и набор команд. Работу с командами реализуем по подобию Callback функций.

Пользователь вводит команду -> Обработчик команд сравнивает пользовательский ввод с существующими командами. Если название команды и пользовательский ввод равны, то начинается исполнение команды, иначе проверяем следующую команду. Если команда не найдена, выводим ошибку "Comand not found!".

Графическое представление принципа обработки команд
Графическое представление принципа обработки команд
jmp boot

boot:
    call cls                 ; Очищаем экран
    call IBM_WELCOME_WINDOW  ; Вызываем функцию вывода логотипа IBM
    call cls                 ; Очищаем экран
    mov si, welcome
    call out_string          ; Выводим приветственное сообщение
    jmp input_loop           ; Переходим к выполнению цикла пользовательского ввода

IBM_WELCOME_WINDOW:
    mov si, IBM_WELCOME
    call out_string

    mov ax, 0x8600  ; Время ожидания, в мс
    mov cx, 30      ; Номер функции 30 - ожидание
    int 0x15        ; Вызываем 0x15 прерывание для ожидания
    ret

input_loop:

    mov si, buffer
    mov bx, 255
    call clear_buffer ; Очищаем буфер от пользовательского ввода

    mov si, prompt  
    call out_string   ; Выводим live@cd>

    mov si, buffer    
    call in_string    ; Даём пользователю возможность ввода команды

    jmp OS_callback   ; Проверяем что ввёл пользователь


    jmp input_loop    ; Повторяем цикл



OS_callback:
    mov si, help_in
    mov bx, buffer
    call comapre_strs   ; Проверяем пользовательский ввод с help
    cmp cx, 1
    je Callback_HELP    ; Если пользователь ввёл help, то прыгаем в Callback_HELP

    mov si, cls_in      ; Проверяем пользовательский ввод с cls
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_CLS     ; Если пользователь ввёл cls, то прыгаем в Callback_CLS

    mov si, info_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_INFO

    mov si, reboot_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_REBOOT

    mov si, echo_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_ECHO


    jne Callback_WRONG  ; Если ни одна команда не подошла, то сообщаем, что команда введена неправильно
    jmp input_loop

Callback_HELP:
    mov si, help_out
    call out_string     ; Выводим справку по командам
    jmp input_loop
Callback_CLS:
    call cls            ; Вызываем функцию очистки экрана
    jmp input_loop

Callback_WRONG:         ; Неверная команда
    mov si, wrong_command_1
    call out_string           ; Выводим первую часть сообщения: Command '
    mov si, buffer            ; 
    call out_string           ; Выводим то, что ввёл пользователь
    mov si, wrong_command_2   ; 
    call out_string           ; Выводим выводим вторую часть сообщения: ' not found. Type 'help' to get all commands
    jmp input_loop            ; Переходим в цикл пользовательского ввода

Callback_INFO:
    mov si, info_out
    call out_string           ; Тот же алгоритм, что и help
    jmp input_loop

Callback_REBOOT:
    mov ah, 0                 ; Номер функции 0 - "тёплая" перезагрузка
    int 0x19                  ; Выполняем 0x19 прерывание
    jmp $                     ; Зависаем. Можно не добавлять, на всякий случай добавил

Callback_ECHO:
    mov si, echo_out          
    call out_string           ; Выводим просьбу ввести слово, которое затем выведем 
    mov si, buffer            
    call in_string            ; Ожидаем пользовательский ввод

    mov si, buffer
    call out_string           ; Выводим введённое слово

    call new_line             ; Переходим на новую строку
    
    jmp input_loop            ; Возвращаемся в цикл ввода


%include "drivers/IO.asm"     ; Подключаем библиотеку IO.asm
; Далее секция данных. тут думаю проблем с пониманием не возникнет.
welcome db "Welcome to TermOS!", 0x0a, 0x0d, "Type 'help' to get command list!", 0x0a, 0x0d, 0
prompt db "live@cd:>", 0

wrong_command_1 db "Command: '", 0
wrong_command_2 db "' not found. Type 'help' to get all commands", 0x0a, 0x0d, 0
echo_out db "Echo: ", 0



help_in db "help", 0
cls_in db "cls", 0
info_in db "info", 0
reboot_in db "reboot", 0
echo_in db "echo", 0



info_out db "TermOS x16 (Terminal Operation System 16-bit) v.0.0:", 0x0a, 0x0d, "        This is operation system in development.", 0x0a, 0x0d, "         Author: Daniil Kulikovskiy.", 0x0a, 0x0d, "          Made in Russia!", 0x0a, 0x0d, 0
help_out db "          cls - Clear screen", 0x0a, 0x0d, "         info - Get system info", 0x0a, 0x0d, "        reboot - Reboot computer", 0x0a, 0x0d, "       echo - Write text in screen", 0x0a, 0x0d, 0




IBM_WELCOME db "                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"              ======== ========    ======          =======                     ", 0x0a, 0x0d,"              ======== =========   ========       ========                     ", 0x0a, 0x0d,"                ===       ==  ===    =======     =======                       ", 0x0a, 0x0d,"                ===       ======     ========   ========                       ", 0x0a, 0x0d,"                ===       ======     ==  ===== =====  ==                       ", 0x0a, 0x0d,"                ===       ==  ===    ==   =========   ==                       ", 0x0a, 0x0d,"              ======== =========  =====    =======    =====                    ", 0x0a, 0x0d,"              ======== ========   =====       =       =====                    ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d," (C) COPYRIGHT 1981, 1996 IBM CORPARATION - ALL RIGHTS RESERVED                ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d, 0


buffer times 255 db 0
Забавы ради, добавил логотип "IBM" на стартовый экран.
Забавы ради, добавил логотип "IBM" на стартовый экран.

Попрошу заметить!

jmp - безусловный переход к метке или адресу.

call - безусловный переход к метке или адресу, с сохранением адреса возврата в стек. После выполнения работы функции, которая вызывается через call, всегда нужно прописывать ret.

ret - перейти к адресу указанному в стеке.

Почему в конце метки с данными указывается 0x0a, 0x0d, 0?

0x0a - Перевод каретки в начало строки.

0x0d - Перевод строки.

0 - К строке добавляется ноль, для того, что бы определить конец строки.

На примере команды "help", рассмотрим принцип работы всех команд. В коде есть 2 метки help_in и help_out. help_in - название команды, с ним будет сравниваться пользовательский ввод. help_out - Выводимая информация на экран.

Первое что происходит в коде, это переход к метке input_loop, где очищается буфер и от пользователя ожидается ввод команды. Введённая пользователем команда сохраняется в буфере, затем происходит переход к метке OS_callback - В этой функции, происходит проверка введённой команды, и если команда сходиться в одной из проверок, начинается исполнение этой команды, иначе вывод ошибки и возвращение в цикл ввода.

Для добавления собственной команды, необходимо прописать её логику в отдельной callback функции, затем прописать название команды и добавить код обработки вашей команды в OS_callback. Важно, после выполнения вашей команды, всегда возвращаться в метку input_loop для корректной работы ОС.

OS_callback:
    mov si, you_command_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_YOU_COMMAND

...

Callback_YOU_COMMAND:
  ...
   jmp input_loop
you_command_in db "you command",0

Напоминаю! При добавлении функционала, не забывайте проверять размер скомпилированного файла. Может оказаться так, что вы допишите функционал, а при запуске он не будет реализован или реализован не полностью. Проверяйте количество секторов на чтение и размер файла!

Сборка и первый запуск

Операционную систему необходимо собирать с помощью NASM в "BIN" формате. У вас заранее должны быть установлены NASM и qemu.

nasm -f bin TermOS.asm -o TermOS.bin
qemu-system-x86_64 TermOS.bin
Получившаяся Операционная Система TermOS
Получившаяся Операционная Система TermOS

Исходный код

drivers/IO.asm

; ============================================================================
; Библиотека для ввода/вывода текстовой информации, с помощью прерываний BIOS
; ============================================================================
global cls          ; void cls();
global out_char     ; void out_char(char bl);
global out_string   ; void out_string(char* si);

global in_char      ; char in_char() return char ax (al);
global in_string    ; void in_string(char[]* si);

global comapre_strs ; int (const char* first_word[] si, const char* last_word[] bx) return cx (1 - равны, 0 - не равны);

global clear_buffer ; void (const char* buf_address[] si, int buf_size bx);

global new_line ; void new_line();

section .text

new_line:
    push ax
    mov ah, 0x0e
    mov al, 0x0a
    int 0x10
    mov al, 0x0d
    int 0x10
    pop ax
    ret

cls:
    push ax
    mov ah, 0x00
    mov al, 0x03
    int 0x10
    pop ax
    ret

out_char:
    push ax
    mov ah, 0x0e
    mov al, bl
    int 0x10
    pop ax
    ret

out_string:
    push ax
    mov ah, 0x0e
    call __out_string_next_char
    pop ax
    ret
__out_string_next_char:
    mov al, [si]
    cmp al, 0
    jz __out_string_if_zero
    int 0x10
    inc si
    jmp __out_string_next_char
__out_string_if_zero:
    ret

in_char:
    push bx
    mov ah, 0
    int 0x16    ; Сохранение символа в регистр al
    mov ah, 0x0e
    mov bh, 0
    mov bl, 0x07
    int 0x10    ; Вывод введённого символа на экран
    pop bx
    ret


comapre_strs:
    push si
    push bx
    push ax
__comapre_strs_comp:
    mov ah, [bx]
    cmp [si], ah
    jne __comapre_strs_first_zero
    inc si
    inc bx
    jmp __comapre_strs_comp
__comapre_strs_first_zero:
    cmp byte [bx], 0
    jne __comapre_strs_not_equal
    mov cx, 1
    pop si
    pop bx
    pop ax
    ret
__comapre_strs_not_equal:
    mov cx, 0
    pop si
    pop bx
    pop ax
    ret


clear_buffer:
    ; si - Адрес буфера
    ; bx - Колчисевто байт на очистку
    push cx
    mov cx, 0
__clear_buffer_loop:
    cmp cx, bx
    je __clear_buffer_end_loop
    mov byte [si], 0
    inc si
    inc cx
    jmp __clear_buffer_loop
__clear_buffer_end_loop:
    pop cx
    ret



in_string:
    push ax
    push cx
    xor cx, cx
__input_string_loop:
    mov ah, 0
    int 0x16
    cmp al, 0x0d            ; Enter
    je __input_string_enter
    cmp al, 0x08            ; Backspace
    je __input_string_backspace

    mov [si], al
    inc si
    inc cx

    mov ah, 0x0e
    mov bh, 0
    mov bl, 0x07
    int 0x10
    cmp cx, 255
    je __input_string_enter
    jmp __input_string_loop
__input_string_enter:
    mov ah, 0x0e
    mov al, 0x0d
    mov bh, 0
    mov bl, 0x07
    int 0x10
    mov al, 0xa
    int 0x10

    mov byte [si], 0
    pop cx
    pop ax
    ret
__input_string_backspace:
    cmp cx, 0
    je __input_string_loop
    ; 0x20                  ; Пробел
    mov ah, 0x0e
    mov al, 0x08            ; Backspace
    int 0x10
    mov al, 0x20            ; Пробел
    int 0x10
    mov al, 0x08
    int 0x10

    mov byte [si], 0
    dec si
    dec cx
    jmp __input_string_loop

TermOS.asm

org 0x7c00

jmp pre_boot

pre_boot:
    cli
    xor ax, ax
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0x7c00
    
    mov ah, 0x02
    mov al, 7   ; Количество секторов на чтение
    mov ch, 0x00
    mov cl, 0x02
    mov dh, 0x00
    mov dl, 0x80
    mov bx, 0x7e00
    int 0x13       ; Прерывание чтения сектора
    jc read_error


    jmp 0x7e00    ; Переход к загруженному коду

read_error:
    mov ah, 0x0e
    mov al, 'R'
    int 0x10
    mov al, 'E'
    int 0x10
    mov al, 'A'
    int 0x10
    mov al, 'D'
    int 0x10
    mov al, ' '
    int 0x10
    mov al, 'E'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, 'O'
    int 0x10
    mov al, 'R'
    int 0x10
    mov al, '!'
    int 0x10


    jmp $

times 510 - ($- $$) db 0
dw 0xaa55

jmp boot

boot:
    call cls
    call IBM_WELCOME_WINDOW
    call cls
    mov si, welcome
    call out_string
    jmp input_loop

IBM_WELCOME_WINDOW:
    mov si, IBM_WELCOME
    call out_string

    mov ax, 0x8600
    mov cx, 30
    int 0x15
    ret

input_loop:

    mov si, buffer
    mov bx, 255
    call clear_buffer

    mov si, prompt
    call out_string

    mov si, buffer
    call in_string

    jmp OS_callback


    jmp input_loop



OS_callback:
    mov si, help_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_HELP

    mov si, cls_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_CLS

    mov si, info_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_INFO

    mov si, reboot_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_REBOOT

    mov si, echo_in
    mov bx, buffer
    call comapre_strs
    cmp cx, 1
    je Callback_ECHO


    jne Callback_WRONG
    jmp input_loop

Callback_HELP:
    mov si, help_out
    call out_string
    jmp input_loop
Callback_CLS:
    call cls
    jmp input_loop

Callback_WRONG:
    mov si, wrong_command_1
    call out_string
    mov si, buffer
    call out_string
    mov si, wrong_command_2
    call out_string
    jmp input_loop

Callback_INFO:
    mov si, info_out
    call out_string
    jmp input_loop

Callback_REBOOT:
    mov ah, 0
    int 0x19
    jmp $

Callback_ECHO:
    mov si, echo_out
    call out_string
    mov si, buffer
    call in_string

    mov si, buffer
    call out_string

    call new_line
    
    jmp input_loop


%include "drivers/IO.asm"
welcome db "Welcome to TermOS!", 0x0a, 0x0d, "Type 'help' to get command list!", 0x0a, 0x0d, 0
prompt db "live@cd:>", 0

wrong_command_1 db "Command: '", 0
wrong_command_2 db "' not found. Type 'help' to get all commands", 0x0a, 0x0d, 0
echo_out db "Echo: ", 0



help_in db "help", 0
cls_in db "cls", 0
info_in db "info", 0
reboot_in db "reboot", 0
echo_in db "echo", 0



info_out db "TermOS x16 (Terminal Operation System 16-bit) v.0.0:", 0x0a, 0x0d, "        This is operation system in development.", 0x0a, 0x0d, "         Author: Daniil Kulikovskiy.", 0x0a, 0x0d, "          Made in Russia!", 0x0a, 0x0d, 0
help_out db "          cls - Clear screen", 0x0a, 0x0d, "         info - Get system info", 0x0a, 0x0d, "        reboot - Reboot computer", 0x0a, 0x0d, "       echo - Write text in screen", 0x0a, 0x0d, 0




IBM_WELCOME db "                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"              ======== ========    ======          =======                     ", 0x0a, 0x0d,"              ======== =========   ========       ========                     ", 0x0a, 0x0d,"                ===       ==  ===    =======     =======                       ", 0x0a, 0x0d,"                ===       ======     ========   ========                       ", 0x0a, 0x0d,"                ===       ======     ==  ===== =====  ==                       ", 0x0a, 0x0d,"                ===       ==  ===    ==   =========   ==                       ", 0x0a, 0x0d,"              ======== =========  =====    =======    =====                    ", 0x0a, 0x0d,"              ======== ========   =====       =       =====                    ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d," (C) COPYRIGHT 1981, 1996 IBM CORPARATION - ALL RIGHTS RESERVED                ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d,"                                                                               ", 0x0a, 0x0d, 0


buffer times 255 db 0

Список литературы

Как запустить программу без операционной системы

О работе ПК ч.3: От включения до полной загрузки Windows 10

Комментарии (32)


  1. dlinyj
    13.04.2024 08:33
    +6

    Классно, что кто-то ещё пишет ОС. Но такого полным полно, море примеров. Круто делать это сразу на си и в защищённом режиме, используя 32 бита, не используя вызовы BIOS и написать такую статью.


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33
      +2

      Спасибо большое за комментарий. Учту ваше предложение


    1. bodyawm
      13.04.2024 08:33
      +3

      Да, UEFI интереснее. Но и у BIOS есть приколюхи - есть множество плохо-документированных прерываний для возможности реализации всяких прикольных штук)


      1. dlinyj
        13.04.2024 08:33
        +2

        Если уж лезть в BIOS (о каком BIOS, кстати мы говорим?), то в ISA видеокартах есть свои приколюхи. Но там даже интереснее читать документацию на камень. Я с одной видяхой долгой проковырялся, пытаясь понять как заставить её работать в режиме VGA с внешним LCD-экраном. Да, были видяхи сразу для LCD.


    1. dlinyj
      13.04.2024 08:33
      +1

      Я, чтобы не быть голословным, автору посоветую глянуть мои публикации по теме:

      Пишем свой ROM BIOS

      Разработка BIOS на языках высокого уровня

      Разница между своим BIOS и операцинке с дискетки не сильно большая (только в стартовом адресе). Ну и особенностях инициализации: ROM может быть исполнено непосредственно в области памяти, либо скопирована и исполнена (много крови попила эта особенность).


      1. Daniil_Kulikovskiy Автор
        13.04.2024 08:33

        Спасибо вам большое за материал. С радостью прочту ваши статьи)


  1. JackKatch
    13.04.2024 08:33
    +3

    Хорошо написана статья, качественно.


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33

      Спасибо большое


  1. VicIamQuick
    13.04.2024 08:33
    +4

    Статья хорошая. Только странно видеть 16-битная операционная система для архитектуры Intel x86_64. Ведь этот режим остался в наследство от куда более ранних архитектур. Интересно почему автор решил писать полностью на ассемблере не переходя на С/C++/Rust или это только для первой статьи и переход в планах? Сам в свободное время изучаю осестроение, благо щас много открытых источников правдо на английском и конечно open source. Даже пытаюсь начать записывать свои эксперименты на бусти. Очень желаю автору удачи в этом деле!


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33
      +1

      Спасибо большое за комментарий. На раннем этапе разработки, писать на ассемблере не трудно, прерывания BIOS делают большую часть работы за программиста.
      Операционная система содержит свой загрузчик, в случае если бы ОС писалась сразу на C, то пришлось бы использовать GRUB. Я не хотел использовать какой либо другой загрузчик, из-за того, что упускается возможность на практике понять как происходит загрузка ОС. А так же, GRUB переводит процессор в защищённый режим, что не даст практического понимания структуры таблицы дескрипторов, необходимой для перехода в защищённый режим и выше.
      В будущем очень хочу всё таки перейти в защищённый режим, поиграться с long mode и подрежимами для 2 режимов и если получится, пощупать SMM. После перехода в защищённый режим, уже можно будет вызывать C из ASM кода.
      Так же ОС не хватает файловой системы, очень хотелось бы разобраться в этом вопросе.


      1. VicIamQuick
        13.04.2024 08:33
        +5

        Для файловой системы нужно сначало написать драйвер диска. Самым простым будет ATA-PIO driver(https://wiki.osdev.org/ATA_PIO_Mode). А файловая система самая простая будет FAT12 или FAT16. У меня в репозитории можно посмотреть реализацию файловой системы и драйвера диска: https://github.com/JustVic/melisa_kernel или https://app.radicle.xyz/nodes/seed.radicle.garden/rad:z41oQJssJbL5T2So5n78ggcqX68Xt. Я не очень умный программист, потому у меня всё как можно проще, из грязи палок :). Не сочтите за рекламу, первоночально я начинал по курсу : https://www.udemy.com/course/writing-your-own-operating-system-from-scratch/, (я никак не связан с автором курса) если вдруг повезёт где-то бесплатно или дёшего получить к нему доступ, то неплохой старт, там загружаются в 64-bit(Long mode) до файловой системы доходят, но там нету драйвера диска, там как-то схитрили. Но там все видео с защитой DRM, скачать неполучается. В курсе используется NASM и пишут свой загрузчик.


        1. VicIamQuick
          13.04.2024 08:33
          +3

          Но перед драйвером диска и файловой системы конечно разумно дойти до Long Mode.

          Чтобы загрузится в Long mode из Protected примерно нужно выполнить следующее( по крайней мере я делал примерно так, после загрузки Grub2 правда):

          Чтобы проверить присутствие 64-бит(long mode) нужно проверить присутствие иструкции cpuid. Потом использовать её для проверкии Long mode.
          Потом включить Paging. Сначало разобравшись как он работает.
          Потом загрузить GDT. Тоже разобравшись как она работает.
          Тут неплохо написано: https://os.phil-opp.com/entering-longmode/
          Ну и конечно на osdev.org смотреть там тоже много всего.


          1. Daniil_Kulikovskiy Автор
            13.04.2024 08:33

            Спасибо вам большое. Ваша помощь не оценима. Обязательно прочту


            1. perfect_genius
              13.04.2024 08:33

              А всё, что есть на Хабре по этой теме, вы уже прочитали?


        1. Daniil_Kulikovskiy Автор
          13.04.2024 08:33

          Спасибо вам огромное за предоставленную информацию и исходники. С радостью прочту всё и разберу код. Если у вас будет желание скооперироваться, напишите в личное сообщение, обменяемся контактами


      1. strvv
        13.04.2024 08:33
        +1

        В принципе, аналогичная программа, это не ОСь, может быть и на uefi. И также запускается без grub. Здесь, на хабре, есть примеры. Там можно писать на С, с привязкой функций на uefi вместо прерываний биос.

        На большинстве новых материнских плат уже зачастую нет legacy режима, там твои программы просто не запустятся.

        Зы.

        1. Немного "стариковского брюзжания". ОСь это система предоставляющая некий набор сервисов для программ, в исходном понимании. Просто сейчас ОСПО и куча прикладного именуется ОС.

        1. Не останавливайся, развивайся, хорошо пишешь.


        1. VicIamQuick
          13.04.2024 08:33
          +1

          Жаль, что переходят полностью на UEFI, мне кажется он слишком перегружен ненужными функциями, из-за чего могут возникать вопросы например с безопастностью. Под UEFI даже IRC-клиенты пишут https://github.com/codyd51/uefirc. Интересно как будет работать GRUB2 и загрузка с ним на таких платах без legacy режима.


  1. NutsUnderline
    13.04.2024 08:33
    +4

    возможно в начале стоит сделать оговорку что в случае uefi нас может ждать совсем другая история. Случайный человек конечно вряд ли будет все это читать и пробовать, но тем не менее, зачем неоднозначность ..


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33

      Спасибо за комментарий. В случае, если ОС будет грузиться на железе с UEFI, то большинство версий UEFI поддерживают эмуляцию BIOS. Источник.
      А вообще, ОС не предназначена для установки на железо, только если позапускать на виртуальной машине, поиграться с кодом, так как для реальных задач, операционная система без файловой системы, работы с сетью, графикой, ограниченным числом команд вряд ли кому-то понадобится.


      1. ciuafm
        13.04.2024 08:33
        +5

        Как раз хотел прокомментировать на эту тему. Выпускайте поскорее сетевой драйвер с tcp/ip библиотекой, делайте управление памятью и хватит. А то все на фс и графике заморачиваться, а до главного руки не доходят. Кстати загрузку с уефи тоже стоит добавить, хотя бы как опцию.

        Графика и фс вряд ли будет работать быстрее в эмуляции, а сетевые изолированные сервисы с супер тонким ядром могут пригодиться и для реальных задач. Сделайте что-то типа мемори рут фс с возможностью моунтить нфс/самбу.


        1. Daniil_Kulikovskiy Автор
          13.04.2024 08:33
          +1

          Спасибо вам большое за оставленный комментарий. На момент написания этой ОС, я понимал что у меня не получится в одиночку довести её до "Повседневной ОС". А как среда исполнения кода вполне себе. Наплодить Docker нодов и разворачивать микросервисы на них. Спасибо вам большое, как будет получаться, обязательно напишу об этом статью.


          1. ciuafm
            13.04.2024 08:33
            +1

            Моя идея была запускать вашу ОС вместо докер контейнера. Но если хотите посерьёзнее, то да, добавьте возможность запустить несколько контейнеров в общем / изолированном окружении. Да, когда закончите POC, сравните с TinyCore и Openwrt по функционалу и размеру. Чтобы понять, нужна ли она...


  1. SmirnoffA
    13.04.2024 08:33

    Вот бы Вы "что бы" слитно писали...


  1. titbit
    13.04.2024 08:33
    +7

    Сколько уже таких систем (по факту загрузчиков) было сделано? Понятно, что написать загрузчик прикольно, но до ОС еще очень далеко. Почему кстати выбрана именно 16-битная система и именно для x86 (а не 64-битная для ARM, например - всяко ж современнее и интереснее, разве нет)? И зачем всю систему писать на ассемблере?

    Ждем следующих статей про основы собственно ОС, например про распределение ресурсов (процессора, памяти, времени и т.д.): как сделать скоростной планировщик, аллокатор памяти, как работать со временем или просто сделать банальное DPC/APC. Или вам больше интересна работа с железом на низком уровне?


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33

      Спасибо вам большое за комментарий. 16-битная операционная система из-за того, это мой первый опыт низкоуровневой разработки. Переход в защищённый или x64 режим, убьёт возможность вызывать прерывания BIOS. Я пока что понимаю, как работать с периферией или ATA интерфейсом в теории, набираюсь практического опыта. Помимо этого, у меня ещё не получалось выйти в защищённый режим, пока что читаю документацию.


      1. titbit
        13.04.2024 08:33
        +1

        Переход в защищённый или x64 режим, убьёт возможность вызывать прерывания BIOS

        Не убьет, просто это будет немного сложнее. Посмотрите исходники почти любого dos-extender'а, или dpmi-сервера, там именно это и реализовано: вызов прерываний из защищенного режима.

        работать с периферией или ATA интерфейсом в теории

        Работать с диском на низком уровне не так и сложно, но по сути это ведь не задача самой ОС - иметь драйвера для всего оборудования в мире. Задача ОС - иметь грамотные интерфейсы для поддержки разных классов оборудования и грамотно распределять ресурсы между пользователями (процессами). Тем более что процедуры работы с дисками условно говоря зависят от протокола диска и там мало где можно разгуляться, а вот в аллокаторе или в планировщике все еще можно придумать что-то оригинальное.


  1. CrashLogger
    13.04.2024 08:33
    +3

    real mode и прерывания BIOS - это скучно и банально. Такое студенты на первом курсе пишут. Сейчас было бы более актуально x86_64 и UEFI, а также работа с современным железом - PCI-express, USB, загрузка с флэшек и по сети. Такую статью я бы почитал с удовольствием.


    1. VicIamQuick
      13.04.2024 08:33
      +1

      Мне тоже интересно было бы почитать про USB, PCI-express или загрузку с флешек по сети, с этим полностью согласен.

      Ну, а так, чтобы до этого дойти человеку нужно достаточно много всего изучить и возможно реализовать в своей ОС, если с нуля начинать. Сразу до USB тяжело допрыгнуть, возможно просто разобрать как это работает в Линуксе. Надежда на то, что после таких начальных статей люди как раз будут больше мотивированы продолжать и доберутся до всего перечисленного вами интересного. :-)


  1. boojum
    13.04.2024 08:33
    +3

    А почему вы решили что это есть операционная система?

    На основании того, что оно запускается биосом и выводит приглашение CLI?


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33

      Спасибо за комментарий. Понимаю, что мой код не является законченной операционной системой. Это больше попытка реализации простого и понятного кода


  1. Tuvok
    13.04.2024 08:33
    +1

    Спасибо автору! Такие статьи нужны, чтобы понимать основы, а возможно и будущим осестроителям пригодятся. Но да, следующий этап это защищенный режим, надеюсь будет серия в продолжение?


    1. Daniil_Kulikovskiy Автор
      13.04.2024 08:33

      Спасибо вам большое за комментарий. Да, продолжение будет. Сейчас как раз разбираю таблицу GDT