Написание ОС с нуля: Часть 3 — Терминал
Ссылки:
Вступление
В предыдущей статье мы написали простейший загрузчик, печатающий на экран «Hello, World!» и завершающийся, но нажанию клавиши. Сегодня напишем терминал, у которого будет несколлько команд, обновим библиотеку и сделаем ещё пару вещей.
Библиотека
По сравнению с библиотекой из предыдущей статьи, до сегодня она стала массивнее, и поддерживать её в таком виде, как на рис. 1 становится почти невозможно. Поэтому было принято решение поменять структуру библиотеки. Итоговая структура изображена на рис. 2:
рис. 1: старая структура проекта и библиотекирис. 2: новая структура проекта и библиотеки. Немного страшно, но когда я посмотрел на структуру проекта EDK II, я почувствовал куда более сильные эмоции :)
Что там нужно уметь. Ага, нашел…
Для самого базового терминала нам нужно уметь следующее:
Печатать в консоль
Принимать ввод
определять комманды
выполнять комманды
Печатать мы научились ещё в предыдущей статье. Дальше по списку: «принимать ввод». В предыдущей статье был макрос для ожидания нажатия любой клавиши, но этого точно мало для того, чтобы получать строки с коммандами. Дак сделаем новый макрос! Макросы для взаимодействия с вводом / выводом я определяю в файле ioefi.inc
. Для приема строки нужно:
принять клавишу.
если нажат
Backspace
, то убрать последний символ.если нажат
Enter
, то поставитьnull
в буффере и закончить работу макроса.если это обычная клавиша, то напечатать её на экран, сохранить в буффер, сместить курсор буффера и перейти к
п.1
.
В коде примерно так:
; libuefi.inc
__scanln:
pushaq
mScankey __key_buf
popaq
mov bp, word [__key_buf+2]
cmp bp, word kEnter
je .exit
cmp bp, word kBackspace
je .bs
mPrint __key_buf+2
mov [rbx], word bp
add rbx, 2
jmp __scanln
.exit:
;mPrintln
mov [rbx], word cNull
ret
.bs:
cmp rbx, rcx
je __scanln
mPrint __bs_string
sub rbx, 2
mov [rbx], word cNull
jmp __scanln
; ioefi.inc
macro mScanln _buffer {
if ~ buffer eq
mov rbx, _buffer
mov rcx, rbx
call __scanln
end if
}
Ура! Но это не всё. если последний пункт выполняется условным прыжком, то сравнение строк ещё не реализовано! Что же, не проблема — я набросал 2 макроса: для сравнения строк целиком и для сравнения, начинается одна строка с другой или нет. Выжло как-то так:
; libuefi.inc
__compare_unicode_strings:
rep cmpsw
mov rbx, rsi
mCall __length_unicode_string
mov rcx, rdx
mov rbx, rdi
mCall __length_unicode_string
cmp rcx, rdx
ret
__compare_unicode_strings_prefixes:
repnz cmpsw
ret
__length_unicode_string:
mov ax, word [rbx]
test ax, ax
jz .exit
inc rcx
add rbx, 2
jmp __length_unicode_string
.exit:
ret
; macroefi.inc
macro mCompareStrings str1, str2 {
lea rsi, [str1]
lea rdi, [str2]
mCall __compare_unicode_strings
}
macro mCompareStringsPrefixes str1, str2 {
lea rsi, [str1]
lea rdi, [str2]
mCall __compare_unicode_strings_prefixes
}
macro mLenString _str, _dest {
mov rbx, _str
xor rcx, rcx
mCall __length_unicode_string
mov _dest, rcx
}
; так же набросал эти два макроса, но они не так важны и используется
; только в одной функции из файла libuefi.inc
macro pushaq {
push rax rbx rcx rdx
push rbp rsi rdi;rsp
}
macro popaq {
pop rdi rsi rbp;rsp
pop rdx rcx rbx rax
}
Соединяем всё вместе
Итак, напишем программу, которая будет имитировать терминал:
; bootx64.asm
; импортируем библиотеку
include "include/uefi+.inc"
; определяем макросы
mDefMacros
; простите, не могу без этого :_)
nl EQU cNl
; определяем точку входа
mEntry main
; определяем системные функции
mDefSystem
; секция кода
section ".text" code executable readable
; главная функция
main:
; готовим библиотеку
mInit fatal_error
; пишем, что все ОК
mPrintln str_start_OK
; внимание: мы - это админ
mPrintln str_admin_WARN
; переход на новую строку
mNewline 1
.command_loop:
mNewline 1
; читаем строку
mScanln command
; сравниваем и исполняем
mCompareStrings command, str_cmd_help
je execute.help
mCompareStrings command, str_cmd_exit
je execute.exit
mCompareStrings command, str_cmd_boot
je execute.boot
mCompareStringsPrefixes command, str_cmd_echo
je execute.echo
jmp .command_loop
; выход со статусом УСПЕШНО
.exit:
;
mExit EFI_SUCCESS
; фатальная ошибка
fatal_error:
mPrint str_fatal_FAIL
mWaitkey kEnter
mExit EFI_ERROR
jmp fatal_error
; функции - комманды
execute:
.help:
mPrintln str_help_NORM
jmp main.command_loop
.echo:
mPrintln command+10
jmp main.command_loop
.exit:
mPrint str_exit_NORM
@@: ; в цикле ждем [Enter] или [Escape]
mScankey key
cmp word [key.unicode], kEnter
je main.exit
cmp word [key.unicode], kEscape
je main.command_loop
jmp @b
.boot:
mPrintln str_boot_FAIL
jmp main.command_loop
; определяем системные данные
mDefLibrary
section ".data" data readable writeable
; тут всякие строки
str_start_OK sString '[ OK ] Started: /efi/boot/boox64.efi'
str_admin_WARN sString "[ WARN ] You are logged in as admin"
str_fatal_FAIL sString '[ FAIL ] Fatal error occurred. Press [Enter] for exit...'
str_help_NORM sString 'VUS - Very Useless Shell', nl, nl, "help - shows this message", nl, 'echo [message] - echoes typed message', nl, "exit - exit VUS", nl, "boot - not implemented"
str_boot_FAIL sString "[ FAIL ] Not implemented"
str_exit_NORM sString "Press [Enter] for exit or [Esc] for cancel"
str_cmd_help sString "help"
str_cmd_echo sString "echo "
str_cmd_exit sString "exit"
str_cmd_boot sString "boot"
; буффер для комманды
command sStrbuf 512
; буффер клавиши
key sKey
; объявляем системные поля
mEfidata
; зачем-то нужна `\_(._.)_/`
section ".reloc" fixups data discardable
Итоги
В течении статьи мы обновили библиотеку и создали простой терминал. В целом, из этого уже можно что-нибудь да сделать (там, псевдогравические игры или программы без сохрания изменений и пр.). Надеюсь, эта статья будет кому-нибудь полезна. Не забудьте посетить мой гитхаб! Там есть всякие интересности :).
Поддержка
Я буду рад полезным ссылкам на документацию, рекоммендациям как улучшить статью и всяким советам. Так же приминаются полезные пулл реквесты :). Спасибо за внимание!