Введение
Пару месяцев назад я решил начать серию статей про написание своей ОС с нуля. Описал написание Legacy MBR загрузчика и переход в защищенный режим (без прерываний) и ещё пару мелочей. Сегодня я решил, что попытаюсь "перезапустить" эту серию (сохранив нумерацию частей). Суть в том, что теперь будут использоваться актуальные на август 2022 года материалы, и разработанное ПО можно будет легко протестировать на своей (U)EFI-машине.
Ассемблер - всему голова!
Не знаю как с этим у других людей, но лично я влюблен в ассемблер. При этом, переходил я на него, предварительно изучив Python, а не С или Фортран. В общем, программировать я буду всё ещё на ассемблере, конкретно - fasm, так как у него очень мощный препроцессор и компоновщик встроен в компилятор.
Загрузчики бывают разные...
Я назову 2 основных: это Legacy, или же MBR, и Secure, или же EFI-загрузчики (надеюсь, с терминами не напутал). В первой части я писал двух-ступенчатый загрузчик: MBR и фиксированно-расположенный SSB (Second-Stage Bootloader, загрузчик 2 стадии). В этот раз будем писать EFI-загрузчик.
Как загружаются UEFI-загрузчики?
Ну, примерно так: запускается биос (вроде с F000:FFFF или где-то там), производит POST (Power On Self Test), если все ОК, то запускает (U)EFI. UEFI в свою очередь ищет диски с известными файловыми системами (ntfs, brtfs, extFAT, FAT32, isofs, cdfs, ext2/3/4, есть ещё, но это уже опционально), ищет на них по адрессу /EFI/BOOT/
файл с расширениет *.efi
, который называется bootX.efi
где Х это платформа, для которой написан загрузчик. В целом, это всё. (ах, да, чуть не забыл: UEFI32 запускают bootia32.efi
в защищенном режиме, а UEFI64 запускают bootx64.efi
в долгом режиме).
Что вообще такое efi-приложение?
Технически, это тот же самый PE (Portable Excutable, формат исполняемых файлов для Microsoft Windows, *.exe, *.msi, *.dll, *.sys, *.mui), он даже разбирается с помощью IDA!
Приступим!
Для начала, просто exe-шник не подойдет. в строку с format нужно писать нечто подобное:
format PE64 DLL EFI
Потом, будет ну прям ОЧЕНЬ (на самом деле не ПРЯМ очень, но будет) сложно писать efi-app без библиотек. поэтому я набросал свою (листинг 1, 2, 3).
Листинг 1 (sysuefi.inc) - некоторые структуры, макросы и функции, без которых жизнь медом не покажется.
Листинг 2 (libuefi.inc) - некоторые полезные функции, начинаются с __, потому, что будут обернуты в Листинге 3
Листинг 3 (macroefi.inc) - обертка над Листингами 1 и 2, полностью из макросов и структур.
Также рекомендую использование такой вещи как struct.inc, гуляющей по всем интернетам. В библиотечках я её не использовал, но в будущем будет удобно свои структурки определять.
В итоге, получается вот-такая файловая система у нашего "проекта" (рис. 1)
листинг 1
; sysuefi.inc for fasm assembly ;
struc int8 {
align 1
. db ?
}
struc int16 {
align 2
. dw ?
align 1
}
struc int32 {
align 4
. dd ?
align 1
}
struc int64 {
align 8
. dq ?
align 1
}
struc intn {
align 8
. dq ?
align 1
}
struc dptr {
align 8
. dq ?
align 1
}
;symbols
EFIERR = 0x8000000000000000
EFI_SUCCESS = 0
EFI_LOAD_ERROR = EFIERR or 1
EFI_INVALID_PARAMETER = EFIERR or 2
EFI_UNSUPPORTED = EFIERR or 3
EFI_BAD_BUFFER_SIZE = EFIERR or 4
EFI_BUFFER_TOO_SMALL = EFIERR or 5
EFI_NOT_READY = EFIERR or 6
EFI_DEVICE_ERROR = EFIERR or 7
EFI_WRITE_PROTECTED = EFIERR or 8
EFI_OUT_OF_RESOURCES = EFIERR or 9
EFI_VOLUME_CORRUPTED = EFIERR or 10
EFI_VOLUME_FULL = EFIERR or 11
EFI_NO_MEDIA = EFIERR or 12
EFI_MEDIA_CHANGED = EFIERR or 13
EFI_NOT_FOUND = EFIERR or 14
EFI_ACCESS_DENIED = EFIERR or 15
EFI_NO_RESPONSE = EFIERR or 16
EFI_NO_MAPPING = EFIERR or 17
EFI_TIMEOUT = EFIERR or 18
EFI_NOT_STARTED = EFIERR or 19
EFI_ALREADY_STARTED = EFIERR or 20
EFI_ABORTED = EFIERR or 21
EFI_ICMP_ERROR = EFIERR or 22
EFI_TFTP_ERROR = EFIERR or 23
EFI_PROTOCOL_ERROR = EFIERR or 24
macro structure name
{
virtual at 0
name name
end virtual
}
;structureures
EFI_SYSTEM_TABLE_SIGNATURE equ 49h,42h,49h,20h,53h,59h,53h,54h
struc EFI_TABLE_HEADER {
.Signature int64
.Revision int32
.HeaderSize int32
.CRC32 int32
.Reserved int32
}
structure EFI_TABLE_HEADER
struc EFI_SYSTEM_TABLE {
.Hdr EFI_TABLE_HEADER
.FirmwareVendor dptr
.FirmwareRevision int32
.ConsoleInHandle dptr
.ConIn dptr
.ConsoleOutHandle dptr
.ConOut dptr
.StandardErrorHandle dptr
.StdErr dptr
.RuntimeServices dptr
.BootServices dptr
.NumberOfTableEntries intn
.ConfigurationTable dptr
}
structure EFI_SYSTEM_TABLE
struc SIMPLE_TEXT_OUTPUT_INTERFACE {
.Reset dptr
.OutputString dptr
.TestString dptr
.QueryMode dptr
.SetMode dptr
.SetAttribute dptr
.ClearScreen dptr
.SetCursorPosition dptr
.EnableCursor dptr
.Mode dptr
}
structure SIMPLE_TEXT_OUTPUT_INTERFACE
;---include ends
struc SIMPLE_INPUT_INTERFACE {
.Reset dptr
.ReadKeyStroke dptr
.WaitForKey dptr
}
structure SIMPLE_INPUT_INTERFACE
struc EFI_BOOT_SERVICES_TABLE {
.Hdr EFI_TABLE_HEADER
.RaisePriority dptr
.RestorePriority dptr
.AllocatePages dptr
.FreePages dptr
.GetMemoryMap dptr
.AllocatePool dptr
.FreePool dptr
.CreateEvent dptr
.SetTimer dptr
.WaitForEvent dptr
.SignalEvent dptr
.CloseEvent dptr
.CheckEvent dptr
.InstallProtocolInterface dptr
.ReInstallProtocolInterface dptr
.UnInstallProtocolInterface dptr
.HandleProtocol dptr
.Void dptr
.RegisterProtocolNotify dptr
.LocateHandle dptr
.LocateDevicePath dptr
.InstallConfigurationTable dptr
.ImageLoad dptr
.ImageStart dptr
.Exit dptr
.ImageUnLoad dptr
.ExitBootServices dptr
.GetNextMonotonicCount dptr
.Stall dptr
.SetWatchdogTimer dptr
.ConnectController dptr
.DisConnectController dptr
.OpenProtocol dptr
.CloseProtocol dptr
.OpenProtocolInformation dptr
.ProtocolsPerHandle dptr
.LocateHandleBuffer dptr
.LocateProtocol dptr
.InstallMultipleProtocolInterfaces dptr
.UnInstallMultipleProtocolInterfaces dptr
.CalculateCrc32 dptr
.CopyMem dptr
.SetMem dptr
}
structure EFI_BOOT_SERVICES_TABLE
struc EFI_RUNTIME_SERVICES_TABLE {
.Hdr EFI_TABLE_HEADER
.GetTime dptr
.SetTime dptr
.GetWakeUpTime dptr
.SetWakeUpTime dptr
.SetVirtualAddressMap dptr
.ConvertPointer dptr
.GetVariable dptr
.GetNextVariableName dptr
.SetVariable dptr
.GetNextHighMonoCount dptr
.ResetSystem dptr
}
structure EFI_RUNTIME_SERVICES_TABLE
struc EFI_TIME {
.Year int16
.Month int8
.Day int8
.Hour int8
.Minute int8
.Second int8
.Pad1 int8
.Nanosecond int32
.TimeZone int16
.Daylight int8
.Pad2 int8
.sizeof rb 1
}
structure EFI_TIME
EFI_LOADED_IMAGE_PROTOCOL_UUID equ 0A1h,31h,1bh,5bh,62h,95h,0d2h,11h,8Eh,3Fh,0h,0A0h,0C9h,69h,72h,3Bh
struc EFI_LOADED_IMAGE_PROTOCOL {
.Revision int32
.ParentHandle int64
.SystemTable dptr
.DeviceHandle int64
.FilePath dptr
.Reserved int64
.LoadOptionsSize int32
.ImageBase dptr
.ImageSize int64
.ImageCodeType int32
.ImageDataType int32
.UnLoad dptr
}
structure EFI_LOADED_IMAGE_PROTOCOL
EFI_BLOCK_IO_PROTOCOL_UUID equ 21h,5bh,4eh,96h,59h,64h,0d2h,11h,8eh,39h,00h,0a0h,0c9h,69h,72h,3bh
struc EFI_BLOCK_IO_PROTOCOL {
.Revision int64
.Media dptr
.Reset dptr
.ReadBlocks dptr
.WriteBlocks dptr
.FlushBlocks dptr
}
structure EFI_BLOCK_IO_PROTOCOL
struc EFI_BLOCK_IO_MEDIA {
.MediaId int32
.RemovableMedia int8
.MediaPresent int8
.LogicalPartition int8
.ReadOnly int8
.WriteCaching int8
.BlockSize int32
.IoAlign int32
.LastBlock int64
}
structure EFI_BLOCK_IO_MEDIA
EFI_GRAPHICS_OUTPUT_PROTOCOL_UUID equ 0deh, 0a9h, 42h,90h,0dch,023h,38h,04ah,96h,0fbh,7ah,0deh,0d0h,80h,51h,6ah
struc EFI_GRAPHICS_OUTPUT_PROTOCOL {
.QueryMode dptr
.SetMode dptr
.Blt dptr
.Mode dptr
}
structure EFI_GRAPHICS_OUTPUT_PROTOCOL
struc EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE {
.MaxMode int32
.CurrentMode int32
.ModeInfo dptr
.SizeOfModeInfo intn
.FrameBufferBase dptr
.FrameBufferSize intn
}
structure EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE
struc EFI_GRAPHICS_OUTPUT_MODE_INFORMATION {
.Version int32
.HorizontalResolution int32
.VerticalResolution int32
.PixelFormat int32
.RedMask int32
.GreenMask int32
.BlueMask int32
.Reserved int32
.PixelsPerScanline int32
}
structure EFI_GRAPHICS_OUTPUT_MODE_INFORMATION
macro InitializeLib
{
clc
or rdx, rdx
jz .badout
cmp dword [rdx], 20494249h
je @f
.badout: xor rcx, rcx
xor rdx, rdx
stc
@@: mov [efi_handler], rcx
mov [efi_ptr], rdx
}
macro uefi_call_wrapper interface,function,arg1,arg2,arg3,arg4,arg5,arg6,arg7,arg8,arg9,arg10,arg11
{
numarg = 0
if ~ arg11 eq
numarg = numarg + 1
if ~ arg11 eq rdi
mov rdi, arg11
end if
end if
if ~ arg10 eq
numarg = numarg + 1
if ~ arg10 eq rsi
mov rsi, arg10
end if
end if
if ~ arg9 eq
numarg = numarg + 1
if ~ arg9 eq r14
mov r14, arg9
end if
end if
if ~ arg8 eq
numarg = numarg + 1
if ~ arg8 eq r13
mov r13, arg8
end if
end if
if ~ arg7 eq
numarg = numarg + 1
if ~ arg7 eq r12
mov r12, arg7
end if
end if
if ~ arg6 eq
numarg = numarg + 1
if ~ arg6 eq r11
mov r11, arg6
end if
end if
if ~ arg5 eq
numarg = numarg + 1
if ~ arg5 eq r10
mov r10, arg5
end if
end if
if ~ arg4 eq
numarg = numarg + 1
if ~ arg4 eq r9
mov r9, arg4
end if
end if
if ~ arg3 eq
numarg = numarg + 1
if ~ arg3 eq r8
mov r8, arg3
end if
end if
if ~ arg2 eq
numarg = numarg + 1
if ~ arg2 eq rdx
mov rdx, arg2
end if
end if
if ~ arg1 eq
numarg = numarg + 1
if ~ arg1 eq rcx
if ~ arg1 in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
mov rcx, arg1
end if
end if
end if
xor rax, rax
mov al, numarg
if interface in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
mov rbx, [efi_ptr]
mov rbx, [rbx + EFI_SYSTEM_TABLE.#interface]
else
if ~ interface eq rbx
mov rbx, interface
end if
end if
if arg1 in <ConsoleInHandle,ConIn,ConsoleOutHandle,ConOut,StandardErrorHandle,StdErr,RuntimeServices,BootServices>
mov rcx, rbx
end if
if defined SIMPLE_INPUT_INTERFACE.#function
mov rbx, [rbx + SIMPLE_INPUT_INTERFACE.#function]
else
if defined SIMPLE_TEXT_OUTPUT_INTERFACE.#function
mov rbx, [rbx + SIMPLE_TEXT_OUTPUT_INTERFACE.#function]
else
if defined EFI_BOOT_SERVICES_TABLE.#function
mov rbx, [rbx + EFI_BOOT_SERVICES_TABLE.#function]
else
if defined EFI_RUNTIME_SERVICES_TABLE.#function
mov rbx, [rbx + EFI_RUNTIME_SERVICES_TABLE.#function]
else
if defined EFI_GRAPHICS_OUTPUT_PROTOCOL.#function
mov rbx, [rbx + EFI_GRAPHICS_OUTPUT_PROTOCOL.#function]
else
if defined EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE.#function
mov rbx, [rbx + EFI_GRAPHICS_OUTPUT_PROTOCOL_MODE.#function]
else
mov rbx, [rbx + function]
end if
end if
end if
end if
end if
end if
call uefifunc
}
section '.text' code executable readable
uefifunc:
mov qword [uefi_rsptmp], rsp
and esp, 0FFFFFFF0h
bt eax, 0
jnc @f
push rax
@@: cmp al, 11
jb @f
push rdi
@@: cmp al, 10
jb @f
push rsi
@@: cmp al, 9
jb @f
push r14
@@: cmp al, 8
jb @f
push r13
@@: cmp al, 7
jb @f
push r12
@@: cmp al, 6
jb @f
push r11
@@: cmp al, 5
jb @f
push r10
@@:
sub rsp, 4*8
call rbx
mov rsp, qword [uefi_rsptmp]
ret
section '.data' data readable writeable
efi_handler: dq 0
efi_ptr: dq 0
uefi_rsptmp: dq 0
листинг 2
пока что пуст, но по мере надобности начнет расти!
if ~ defined __EFICODE__
__EFICODE__ EQU 0
end if
листинг 3
if ~ defined __MACROEFI__
__MACROEFI__ EQU 0
;; --- constants --- ;;
true EQU 1
false EQU 0
null EQU 0
nl EQU 13,10
via EQU ,
;; --- structures --- ;;
struc sString [value] {
common
if ~ value eq
. du value, null
else
. du null
end if
}
struc sStrbuf _len {
if ~ len eq
.len dq _len*2
.val rw _len
else
.len dq 1024*2
.val rw 1024
end if
}
struc sKey scan, utf {
if ~ scan eq
.scancode dw scan
else
.scancode dw null
end if
if ~ utf eq
.unicode du utf
else
.unicode:
end if
du null
}
;; --- macros --- ;;
macro mEntry _ofs {
format pe64 dll efi
entry _ofs
}
macro mInit {
InitializeLib
jnc @f
mExit EFI_SUCCESS
@@:
}
macro mPrint _str {
if ~_str eq
uefi_call_wrapper ConOut, OutputString, ConOut, _str
end if
}
macro mPrintln _str {
if ~ _str eq
mPrint _str
end if
mPrint _crlf
}
macro mReturn [data] {
if ~ data eq
forward
push data
end if
common
ret
}
macro mInvoke func, [arg] {
if ~ arg eq
reverse
push arg
end if
call func
}; vacuum example: mInvoke fSend via message
macro mEfidata {
common
mSect data
__crlf sString nl
__key_buf sKey null, null
}
macro mScankey _key {
if ~ _key eq
mov [_key], dword 0
@@:
uefi_call_wrapper ConIn, ReadKeyStroke, ConIn, _key
cmp dword [_key], 0
jz @b
end if
}
macro mExit status {
if status eq
mov eax, EFI_SUCCESS
else
mov eax, status
end if
retn
}
macro mSect name, type {
if type eq data
section '.#name' data readable writable
else if type eq code
section '.#name' code executable readable
else if type eq text
section '.#name' code executable readable
else if type eq fixup
section '.#name' fixups data discardable
end if
}
end if
Традиции... Привет, мир!
Напишем helloworld для uefi используя мои листинги. Код получился до смешного коротким и интуитивно понятным. Имхо, под Windows консольную писалку создать сложнее!
; импортируем макросы
include "include/macroefi.inc"
; назначаем точку входа
mEntry main
; импортируем вторую часть библиотеки (обязательно после mEntry!)
include "include/sysuefi.inc"
; секция '.text' code executable readable
mSect text, code
; главная функция
main:
; инициализируем библеотеку
mInit
; печатаем хеловорлд
mPrint hello
; аналог _getch из msvcrt.dll,
; ждем клавиши и сохраняем в key
mScankey key
; возвращаемся в UEFI shell
; со статусом ОК
mExit EFI_SUCCESS
; импортируем третью часть библиотеки (желательно посте основного кода)
include "include/libuefi.inc"
; секция '.bsdata' data readable writable
mSect bsdata, data
; utf-8 строка для хеловорлда
hello sString 'Hello UEFI World!'
; ячейка для клавиши
key sKey
; финальный макрос библиотеки. ВСЕГДА в конце всего кода
mEfidata
Как это собрать?
Я создал для этого простенький Makefile. напишите make build и найдете в рабочей папке файл bootx64.efi. Makefile (подсветка от перл потому, что от нее все подсветилось):
build:
fasm BOOTX64.asm
image: build
mkdir tmp
mkdir tmp/efi
mkdir tmp/efi/boot
cp BOOTX64.efi tmp/efi/boot/BOOTX64.efi
genisoimage -o ./image.iso -V BACKUP -R -J ./tmp
rm tmp/efi/boot/*
rmdir tmp/efi/boot tmp/efi tmp
cls
dump: build
hd BOOTX64.efi
Как это запустить?
Создаете FAT32 флешку или GPT раздел, кидаете в /efi/boot файл под названием bootx64.efi и перезагружаете ПК. При запуске откройте меню выбора загрузочного носителя и там найдите свой файл/флешку/раздел. Грузитесь с него и видите сообщение. Чтобы вернуться, нажмите любую клавишу.
Итоги
В течении статьи я изложил вам все преимущества Secure boot над Legacy, рассказал как все это работает и написал helloworld-загрузчик.
Поддержка
Всегда непротив конструктивной критики. Принимаю идеи по улучшению статьи и проекта в целом. Всегда буду рад полезным ссылкам. Спасибо за прочтение!
Комментарии (23)
Samogonshik
01.08.2022 17:12+4Интересно как это будет выглядит для одноплатников типа raspberry pi
svosin
01.08.2022 18:34+5Там ARM, вроде всё проще. При запуске процессор переходит по адресу, лежащему в 0x0 - и поехали, дальше фантазия ограничена только упоротостью SoC и периферии.
TheNavi
01.08.2022 21:49C большой степенью вероятности там будет uboot - https://github.com/u-boot/u-boot
runapa
01.08.2022 22:06+1Можно посмотреть на примере kolibri os. Тоже написано на ассемблере.
TalismanChet Автор
01.08.2022 22:07Ей вдохновляюсь и некоторые файлы (например struct.inc) из её ядра беру :)
DmitryGordeyev
01.08.2022 22:07С выходом первого Raspberry Pi появился обучающий туториал про разработку ОС от Кембриджского университета. Если коротко, то чтобы выдать даже банальное "Hello world!", потребуется сначала нарисовать шрифты.
TalismanChet Автор
01.08.2022 22:09Это по-моему было лишним. Можно было бы хотя бы vga/cga capability прослойку оставить чтобы проще было.
beeruser
01.08.2022 19:06+6в долгом режиме
В «длинном» тогда уж.
Насколько я понимаю, long mode так назван по аналогии с типом данных long в Cи.
CodeRush
01.08.2022 23:17+2Только не Secure Boot, а просто EFI boot, потому что первое - это про то, что собранный загрузчик подписан, и его подпись EFI-совместимая прошивка проверяет до того, как передаст управление на его точку входа.
Не совсем понятно, зачем тут ассемблер, кроме как "захотелось вот на ассемблере", потому что все, что тут на ассемблере написано - это переопределения типов данных из С, части структур из Extended Firmware Interface тоже из С, и вызов функций с интерфейсом MS x64 ABI.
Выучите лучше С, намного легче станет писать что-то сложнее хелловорлдов, а ассемблер оставьте для задач, которые на С либо решаются плохо (мужики из coreboot'а в свое время ROMCC не от хорошей жизни написали), либо не решаются совсем. Ссылку на EDK2 и TianoCore уже сверху давали.
TalismanChet Автор
02.08.2022 11:12Я умею писать на С, просто мне нравится ассемблер. В статье про это написано. И да, не отрицаю что в листингах происходят переопределения сишных типов `\_(;-;)_/`
maximnik0q
02.08.2022 01:18+2ищет диски с известными файловыми системами (ntfs, brtfs, extFAT, FAT32, isofs, cdfs, ext2/3/4, есть ещё, но это уже опционально)
ЭТО не точно, по спецификации ищется специальный загрузочный, имеющий специальный идентификатор UEFI раздел FAT32. И уже на этом разделе в каталоге файл. Правда спецификация не запрещает дополнительно исползовать другие фс, что многие производители и делают-включают драйвера ntfs и exfat. (и исключительно редко ext4).
radiolok
А почему не взяли tianocore edk2? С его помощью приложения и загрузчики, в том числе многопоточные - пишутся на раз два.
TalismanChet Автор
Написание ОС с нуля: ...
Ну а за ссылку спасибо, может из исходников тиана че-нидь таки возьму
radiolok
Написание ОС с нуля уже не получается - так как UEFI уже сделал первоначальную настройку железа, запустил АР-ядра (все кроме первого) и перевел процессор в защищенный режим, сильно упростив задачу будущему ядру.
как минимум из tiano можно взять API доступа к возможностям самой uefi.
TalismanChet Автор
тогда в таком случае написание ОС с нуля не получилось бы и при legacy boot - ведь BIOS уже сделал первоначальную настройку и тестирование железа, настроил прерывания и сегментные регистры. Не спорю, Legacy boot требует больше кода, но по вашей логике это тогда тоже не с нуля.
radiolok
По моему личному мнению - ОС это в первую очередь скедулер, а не работа с железом. Но да, Uefi сделал сильно больше legacy boot и отнял часть работы программиста, что даже хорошо :)
Впрочем статья не столько про ОС с нуля, а про ОС на ассемблер с нуля, так что снимаю шляпу в любом случае - Мне пришлось однажды написать многопоточное приложение, которое должно захватывать процессор в realmode, подменить таблицы прерываний и нагрузить все ядра синтетикой. Вроде и код получился простым, но проштудировать SDM пришлось знатно
TalismanChet Автор
Надо же :) Впрочем, я сейчас работаю над следующей частью - планирую сделать некое подобие DOS Shell, только на свой лад, потом сделать автозагрузку сценария из boot.sh, выйдет, по-моему, неплохо. Однако проблема в том, что UEFI, TianoCore и EDKII рассчитаны на язык С, и мне сложно "добывать" информацию под ассемблер. Что ж, сейчас пытаюсь написать макро, который в цикле сканирует клавишу пока не встретит CRLF. Звучит просто, а с проблемой этой уже весь день сижу, аж стыдно :_)
saboteur_kiev
Сделайте лучше подобие posix shell, не надо dos
TalismanChet Автор
окей, все равно ещё не до конца определился