И года не прошло с момента публикации мной последней статьи*, как я... Я нет, кажется год как-раз прошел... Ну и ладно! Хабр, привет!
* тут такое дело - прошло :)
сейчас 17 мая, и я только заканчиваю работу над статьей.
SectorOS (SOS) – это небольшая операционная система (далее для удобства я буду использовать сокращения "ось" или "ОС"), написанная на ассемблере x8086, умеющая запускать пользовательские программы и предоставляющая для этих программ минимальный интерфейс взаимодействия со своей собственной файловой системой – SFS, но обо всем по порядку.
Важно
эта статья подразумевает что вы знаете достаточно об ассемблере и у вас не возникает вопросов "а что такое mov ax, [es:si]". Если же вы все равно не понимаете код я советую кинуть его в нейронку и попросить объяснить, я думаю она разжует его лучше, чем я.
Объяснять что такое прерывания и их функции я не буду.
Вдохновитель и идея
Если название этой статьи вам что-то напоминает, то вы, наверное читали статью SectorC: компилятор Си в пределах 512 байт. И я её обожаю: перечитывал раза 3, и каждый раз интересно.
Но, наверное, пол года назад я нашел прекрасный проект: x16-PRos, я влился в его комьюнити, и, пообщавшись с обитателями, внеся свой вклад в проект – написав ed (текстовый редастор) и чуть улучшив тамошний shell, – где-то на подкорке сознания ко мне закралась мысль: "надо написать свою ОС".
Я думаю, всем очевидно, что человек, который писал язык ради экономии 15 байт, просто так писать ось не будет... Но идей по "приколофикации" этой идеи у меня долго не было, пока, наконец, я не вспомнил о, уже вышеупомянутой, статье про SectorC – было решено: "Буду писать ось размером в сектор - SectorOS"
Небольшая table of contents
-
Детали
А что вообще можно назвать "операционной системой"?
Ну для начало нигде не написано "ОС - это то, что...", эти границы придется определять самим.
Я всегда считал, что "ОС - это то, что в том или ином виде может передать управление данными пользователю"... замудрено да?
Ну, например, ваши данные - это научная работа о корреляции числа файлов, имеющих расширение .rs в проекте, и длинной чулков его создателя. Скорее всего ваша работа будет проектом MS Word-а в формате .docx.
В таком случае инструментом для управления этими данными будет MS Word (ну или LibreOffice если длинна ваших чулок имеет коэффициент, равный двум)
Это конечно все классно, но давайте ближе к конкретному списку того, что я собираюсь реализовать (ну или хотя бы попытаться)
запуск программ с диска с возможностью передавать программам аргументы
создание, удаление, чтение и запись в файлы
управление питанием (выключение и перезагрузка)
Важно сказать, что под "реализацией ОС" я имею ввиду не только ядро, но и стандартный пак программ. На самом ядре будет лежать ответственность за запуск программ (то бишь shell), минимальную работу с файловой системой и... и все, больше в 512 байт впихнуть у меня не получилось
Не густо... Но у меня есть оправдание: по большому счету, сама 16-ти битная ОС - это лишь терминал, для запуска программ (ну и осуществляет работы с файловой системой конечно, хотя это и может лежать на программах), а все остальное - сами программы.
Если бы я писал программы для работы с графикой, датой/временем, сторонними устройствами эта статья бы вышла в... не вышла бы...
Цикл ОС
Для пользователя, когда он запускает комп, происходит следующее: экран очищаеться и... и все - он в терминале. Пользователь набирает условную строку
touch hello.txt
ОС вычленяет слово touch, ищет программу с таким же именем, загружает в память и передает на неё управление. После окончания программы ОС снова ждет ввод от пользователя - все как и в других ОС. Только вот "под капотом" все устроено гораздо интересней.
Пристегивайте ремни - я собираюсь разобрать все 300 строк кода SectorOS
Первые шаги SectorOS
Как и у большинства загрузчиков первым кодом, что выполняется в SectorOS является настройка сегментов, стека и загрузка прерываний (так же тут есть очистка экрана, чтобы пользователь не читал тирады о том как BIOS искал наш загрузчик):
cli ; настройка сегментов и стэка xor ax, ax mov ds, ax mov es, ax mov ax, 0x1000 mov ss, ax mov sp, 0xFFFE ; загрузка прерываний mov di, 0x21 * 4 mov si, int_table mov cx, 3 .set_ivt: lodsw stosw mov ax, cs stosw loop .set_ivt sti ; очистка экрана mov ax, 0x0600 mov bh, 0x0F xor cx, cx mov dx, 0x184F int 0x10 mov ah, 0x02 xor bh, bh xor dx, dx int 0x10 ; код ос, бла, бла, бла ; список прерываний в конце файла int_table dw int_get_file_text ; int 0x21 dw int_set_file_text ; int 0x22 dw int_get_file_header ; int 0x23
Ничего необычного, кроме, кода загрузки прерываний, ведь он немного минимизирован - загрузка производиться из подготовленного списка адресов (простые мувы занимали слишком много места в итоговом бинарнике), и того, что прерывания на всю ос 3 штуки, но про это мы ещё поговорим.
SectorOS File System (SFS)
Перед следующими главами важно поговорить о том, как SectorOS видит файлы - про файловую систему
Скорее всего вы знаете, что диск оперирует не байтами, а секторами - блоками по 512 байт. В SFS за несколькими первыми секторами закреплены четкие роли:
1-й сектор - сама SectorOS
2-й сектор - SFSDD (SectorOS File System Disk Data)
здесь пока что занято только одно слово (2 байта) - последний свободный сегмент, но не удивляйтесь такой растрате места - это ещё цветочки. Взамен размеру SectorOS пришлось сделать очень прожорливую файловую систему :)
3-й сектор - FHT (File Headers Table) - самое интересное
FHT - это массив заголовков файлов каждый заголовок представляется такой структурой
struct FileHeader { char file_name[11]; uint8_t file_type; uint16_t data_start_sec; uint16_t data_size; };
file_name - это null-padded название файла, то-есть оно должно не просто заканчиваться нулем, а все неиспользуемые символы должны быть нулями
file_type - уникальное свойство SFS - это буква, являющаяся маркером для системы и программ, например, если в этом поле 'E' - то этот файл может быть исполнен как программа
data_start_sec - это номер сектора (счет начинается с нуля), с которого начинаются данные. То-есть два файла не могут делить сектор, файл всегда начнется с нового сектора.
data_size - размер данных файла в байтах, ничего необычного.
Даже если размер файла - 12 символов, занимать он будет весь сектор
Давайте посмотрим на дамп диска:
00000400: 7368 7574 646f 776e 0000 0045 0300 2a00 shutdown...E..*. 00000410: 7265 626f 6f74 0000 0000 0045 0400 1000 reboot.....E.... 00000420: 6865 6c70 0000 0000 0000 0045 0500 7401 help.......E..t. 00000430: 6469 7200 0000 0000 0000 0045 0600 dc00 dir........E....
Здесь видно что по смещению 0x400 (третий сектор) находиться эта самая таблица.
мы видим название файла.
После которого нулями дополнены 11 байт, после 45 - это код символа 'E' в кодировки шрифта биоса, она означает что файл - это программа.
Далее идет два двух байтовых поля в lettle-endiane - номер сегмента данных и размер соответственно.
4-й сектор и выше - данные файлов
Давайте разберем примерчик и закончим с файловой системой: файл test.txt с текстом
Hello, its test text file for SectorOS Writen by Desvor. YOOOO!!!
Теперь сгенерируем диск .img с помощью python скрипта mksf.py (вынужден признать что его написала нейронка, тк мне было жутко лень), написав такой конфиг:
{ "size": "4K", // размер итогового диска "boot": "sos.bin", // файл бут-лоадер (в нашем случае самой SectorOS) "data": [ // данные диска { "type": "F", // тип файла "name": "test.txt", // имя файла на диске "data": "test.txt" // имя файла, откуда скрпит возмет данные для записи } ] }
Наконец откроем дамп сгенерированного диска с SFS и увидим следующее (с моими комментариями):
00000000: fa31 c08e d88e c0b8 0010 8ed0 bcfe ffbf .1.............. 1 сектор (номер 0) ... код SectorOS 000001f0: 0000 0000 0000 0000 0000 0000 0000 55aa ..............U. 00000200: 0400 0000 0000 0000 0000 0000 0000 0000 ................ 2 сектор (номер 1) ^--- это номер следующего свободного сектора в lettle endiane 00000400: 7465 7374 2e74 7874 0000 00 46 0300 4700 test.txt...F..G. 3 сектор (номер 2) |-------------------------| ^- ^--- ^--- имя заполнено 0 до 11 байт тип номер сегмента с данными размер данных в байтах .. куча нулей 00000600: 4865 6c6c 6f2c 2069 7473 2074 6573 7420 Hello, its test 00000610: 7465 7874 2066 696c 6520 666f 7220 5365 text file for Se 00000620: 6374 6f72 4f53 0d0a 0d0a 5772 6974 656e ctorOS....Writen 00000630: 2062 7920 4465 7376 6f72 2e0d 0a59 4f4f by Desvor...YOO 00000640: 4f4f 2121 210d 0a00 0000 0000 0000 0000 OO!!!........... собственно вот и текст файла ... куча нулей
Инициализация работы с SFS
После того, как мы разобрали устройство SFS, давайте поговорим о подготовке системы к работе с SFS, но для начало я расскажу как нам, со стороны ассемблирятора получать данные с диска и писать их обратно.
За работу с диском отвечает прерывание 0x13, нам понадобятся всего две его функции - 0x42 (чтений) и 0x43 (запись). Обе эти функции работают со специальной DAP структурой, ей формат вот такой:
align 4 DAP_struct: .DAP_size db 0x10 .res db 0x00 .sec_num dw 1 .buf_ptr dw 0x0800 dw 0x0000 .sec_ptr dq 2
aling 4 - это выравнивание по 4 байтовой границе, тк это требуют некоторые BIOS.
DAP_size - это байт, хранящий размер DAP структуры, и это всегда 0x10 (это поле создавалось "на вырост", на случай добавления в структуру чего-то ещё, но этого так и не случилось)
res - зарезервированный байт для нужд BIOS (всегда 0)
sec_num - количество секторов для чтения
buf_ptr - обычный far указатель (первые два байта - смещение, вторые два - сегмент) на буфер "куда записывать" для 0x42, или "откуда записывать" для 0x43
sec_ptr - аж 8 байт, хранящих номер сектора с которого начнется работа (чтение или запись)
В таком виде DAP структура храниться изначально
Теперь можно и на код посмотреть
pop dx ; load FHT mov ah, 0x42 mov si, DAP_struct int 0x13 jc err_de mov word [DAP_struct.sec_ptr], 1 mov word [DAP_struct.buf_ptr], 0x0600 ; load SFSDD mov ah, 0x42 mov si, DAP_struct int 0x13 jc err_de mov [0x602], dl
Тут достаем dl (номер диска) и после комментария "load FHT" идет код загрузки 3-го (в DAP структуре номер сегмента указывается с нуля) сектора (наша таблица файлов).
ah - номер функции (в нашем случае чтение - 0x42)
dl - номер диска (его мы pop-нули ранее)
ds:si - DAP структура
Если при работе произошла ошибка связанная с диском, то прерывание 0x13 установить флаг CF и мы сможем это отловить переходом jc. (код err_de мы разберем чуть позже)
Далее мы меняем номер сектора на 1 (сектор информации о диске SFSDD) и буфер на 0x600
Из этого кода можно понять, можно понять, что FHT лежит по адресу 0x800, а до неё целые 512 байт места, ради 2 байт данных, занимает SFSDD сектор
Основной цикл терминала
Ввод от пользователя
После всех немногочисленных настроек SectorOS переходит в терминал:
для начала читаем строку от пользователя:
shell_loop: mov di, 0x500 xor cx, cx .read_char: xor ah, ah int 0x16 cmp al, 0x0D jz .done cmp al, 0x08 jz .backspace stosb mov ah, 0x0E int 0x10 inc cx jmp .read_char .backspace: test cx, cx jz .read_char mov ah, 0x0E int 0x10 mov al, ' ' int 0x10 mov al, 0x08 int 0x10 dec di dec cx jmp .read_char .done: mov byte [di], 0
К сожалению здесь нету ничего необычного, и разбирать здесь просто нечего (об этой проблеме этого проекта я поговорю в итогах).
Splitting строки
После прочтения команды в буфере по адресу 0x500 находиться, например, такая строка
touch hello.ext
Что бы запустить программу мы должны получить её имя из всей команды.
За это отвечает следующий код:
mov ax, 0x0E0A int 0x10 mov al, 0x0D int 0x10 mov si, 0x500 mov di, 0x580 .slice_loop: lodsb cmp al, ' ' jbe .slice_done stosb jmp .slice_loop .slice_done: xor al, al mov cx, 11 rep stosb
сначала мы переносим строку для красоты.
тут можно увидеть один прием, встречающийся по всему коду
mov ax, 0x0E0A
вместо
mov ah, 0x0E mov al, 0x0A
Очевидно, что второй вариант читаемей, но первая запись экономит нам целый байт!
Как бы это не звучало, но в моменты написания кода, не задумываясь о размере, это очень экономит время и силы
Идя далее по коду можно понять что копируем мы из 0x500 в 0x580, а следовательно команда может быть всего 64 байта длинной, тк все, что выше, будет затерто при копировании имени программы.
В конце мы видим один ход:
xor al, al mov cx, 11 rep stosb
Мы добавляем 11 байт нулей после копирования, тк, как мы помним, имя файла должно быть null-padded (почему это так мы поговорим, когда будет рассматривать прерывания), и делаем это минимальным способом - через префикс rep.
Запуск
После того, как мы узнали имя программы, которую нужно запустить, мы загружем её в память, передаем управление, и по окончанию, прыгаем в начало shell loop-а:
mov si, 0x580 mov dx, 0x2000 int 0x21 cmp ah, 1 jz err_fnf ja err_de cmp al, 'E' jnz err_wft mov ax, 0x2000 mov ds, ax mov es, ax call 0x2000:0x0000 xor ax, ax mov ds, ax mov es, ax jmp shell_loop
Для загрузки данных из файла в память используется собственное прерывание SectorOS 0x21. В ds:si оно принимает строку с именем файла для загрузки (в нашем случае это имя программы), а в dx - номер сегмента, в который нужно будет выгрузить текст программы.
После вызова прерывания в ah оно кладет код ошибки (0 - успешно чтение, 1 - File not found, 2 - Disk error), а в al - тип прочтенного файла.
В нашем случаает если тип не E - это не исполняемый файл и мы выводим ошибку Wrong file type.
Если же все в порядке, то мы настраиваем сегменты ds и es и вызываем код программы, после отработки которого, мы снова затираем es и ds.
Ошибки ОС
Что ж, во и время для разбора тех самых меток err_fnf, err_de, err_wft:
err_fnf: mov ax, 0x0946 ; 0x46 - F jmp general_err err_de: mov ax, 0x0944 ; 0x44 - D jmp general_err err_wft: mov ax, 0x0957 ; 0x57 - W general_err: mov cx, 1 mov bx, 0x04 int 0x10 mov ah, 0x0E int 0x10 mov al, 0x0A int 0x10 mov al, 0x0D int 0x10 jmp shell_loop
На самом деле мы видим тут достаточно популярный "патерн" для ассемблера:
Ошибки отличаются только сообщением (в нашем случает сообщение - это одна буква - F - "File not found", W - "Wrong file type", D - "Disk error"), а код вывода этого символа одинаков, поэтому мы можем менять только сам символ и прыгать на метку, где происходит вывод символа и возврат в shell_loop.
Тут, в силу компактности, в err_fnf, err_de и err_wft заполняеться весь ax, так как это компактней, чем если бы мы заполняли ah в general_error.
Собственно поговорим ещё о выводе символа: находимся мы в режиме VGA 80x25, а следовательно, функция 0x0E прерывания 0x10 не умеет выводить символы, разными цветами. Для того чтобы окрасить букву ошибки в красный мы будем использовать функцию 0x09 прерывания 0x10 - печать символа с атрибутами, но дело в том, что это прерывание не двигает каретку, следовательно нам нужно будет вызвать 0x0E для "подтверждения" символа и сдвига каретки.
вот код с моими обьяснялкиными:
general_err: ; в функции 0x09 в cx помещаеться количестао символов, которе нужно напечатать mov cx, 1 mov bx, 0x04 ; печатаем символ (ah (номер функции) и al (символ) ; были заполнены выше в одной из меток ошибок) int 0x10 ; теперь "подтверждаем" символ функцией 0x0E mov ah, 0x0E int 0x10 ; переносим строку mov al, 0x0A int 0x10 mov al, 0x0D int 0x10 ; домой, уолтер jmp shell_loop
Прерывания
По всем законам логики, начнем мы с последнего по номеру прерывания - 0x23 GetFileHeader
int 0x23 GetFileHeader
Это прерывание позволяет получить FileHeader.
В ds:si оно принимает null-padded имя файла.
Возвращает в ah код ошибки и в es:ds - FileHeader
Его величество код:
; int 0x23 ; in: ds:si - name of file ; out: ah - err code ; 0 - ok ; 1 - fnf ; es:di - file header ; destruct: es, ax, bx, cx, si, di int_get_file_header: xor ax, ax mov es, ax mov di, 0x0800 .search_loop: cmp byte [es:di], 0 jz .err_fnf push si push di mov cx, 11 repe cmpsb pop di pop si jz .search_done add di, 0x10 jmp .search_loop .search_done: xor ah, ah iret .err_fnf: mov ah, 1 iret
Ну первое же, что бросается в глаза в "оглавлении" это destruct. Прерывание? разрушает регистры?
Да! И тут вполне понятная причина: ОС написана для процессора 8086, а команд pusha и popa для сохранения всех регистров в пару байт в нем нет, они появились только в 80186, а если по отдельности пушить каждый регистр отдельно, то исходный бинарник раздуется до размеров, что llvm сможет позавидовать.
В коде все ещё ничего необычного нету, кроме, разве что, кода сравнения имен:
push si push di mov cx, 11 repe cmpsb pop di pop si jz .search_done
Использую я тут связку repe cmpsb, которая позволяет сравнивать байты из ds:si и es:si 11 раз (cx = 11) и пока флаг ZF равен нулю (то-есть два байта равны). Именно поэтому все имена должны быть дополнены нулями до 11 байт, чтобы мусор, лежащий после null-terminator-а не помешал сравнению. Такой код сэкономил мне около 12 байт, а это достаточно много для такого проекта.
int 0x21 GetFileText
Это прерывание уже нужно для чтения данных из файла в сегмент памяти.
Для начала давайте взглянем на заголовок:
; int 0x21 ; in: ds:si - name of file ; dx - segment to writing ; out: ah - err code ; 0 - ok ; 1 - fnf ; 2 - de ; al - file type ; bx - file size in bytes ; destruct: es=0, ds=0, si, di
В ds:si помещается null-padded имя файла,
В dx - номер сегмента, в который будет прочтен текст файла (да, прочесть его в произвольный буфер внутри сегмента нельзя, тк текст может занять весь сегмент, а проверку пересечения границ и тд делать было накладно по размерам бинарника).
Вернет прерывание нам код ошибки в ah, тип файла в al и в bx размер файла в байтах
И снова код:
int_get_file_text: int 0x23 test ah, ah jnz .err xor ax, ax mov ds, ax mov ax, [di + 12] mov [DAP_struct.sec_ptr], ax mov ax, [di + 14] add ax, 0x1FF mov cl, 0x09 shr ax, cl mov [DAP_struct.sec_num], ax mov word [DAP_struct.buf_ptr], 0 mov word [DAP_struct.buf_ptr + 2], dx mov ah, 0x42 mov dl, [0x602] mov si, DAP_struct int 0x13 jc .err_de mov al, [di + 11] mov bx, [di + 14] xor ah, ah iret .err_de: mov ah, 2 .err: iret
Здесь мы видим что прерывание из под себя вызывает другое прерывание - 0x23 для получения заголовка нужного файла.
Далее, занулив ds для более легкой адресации без постоянного es:di (тк это добавляет байт префикса к каждой команде), мы настраиваем DAP структуру. Единственное что тут стоит внимания это строки
mov ax, [di + 14] add ax, 0x1FF mov cl, 0x09 shr ax, cl mov [DAP_struct.sec_num], ax
Тут происходит заполнения поля sec_num - количество секторов для прочтения - как мы помним размер в заголовке файла указывается в байтах, и чтобы получить его в секторах по 512 байт мы реализуем следующую формулу:
511 надо добавить, так как, если просто делить на 512, при, например размере в 513 байт, деление даст нам 1 сектор, что неверно.
Так же в программировании разумней использовать сдвиг вправо на 9 (равносильно делению на 512). Тут же мы натыкаемся на ограничение процессора 8086 - возможность выполнять сдвиг только на значение из регистра cl, поэтому мы сначала пишем в cl 0x09, а потом выполняем сдвиг.
int 0x22 SetFileText
а вот и самое большое прерывание - 0x22. Для начала заголовочек:
; int 0x22 ; in: ds:si - name of file ; dx - segment for writing ; cx - size of text in bytes ; out: ah - err code ; 0 - ok ; 1 - fnf ; 2 - de ; destruct: es=0, ds=0, ax, cx, bx, si, di
Собственно на вход мы даем прерыванию:
ds:si - уже стандартно, имя файла
dx - сегмент для записи данных из него в файл
cx - размер данных для записи
А на выход получаем только ah - уже привычный нам код ошибки
А теперь к коду:
int_set_file_text: push cx int 0x23 pop cx test ah, ah jnz .err xor ax, ax mov ds, ax mov bx, [di + 14] add bx, 0x1FF push cx mov cl, 9 shr bx, cl pop cx test bx, bx jnz .bx_not_null inc bx .bx_not_null: mov [di + 14], cx add cx, 0x1FF mov cl, 9 shr cx, cl cmp cx, bx ja .need_alloc mov bx, cx jmp .write .need_alloc: push dx push bx mov [di + 14], bx mov cx, [0x600] mov [di + 12], cx add [0x600], bx mov ax, 0x0301 mov cx, 0x0002 mov dx, 0x0080 mov bx, 0x0600 int 0x13 mov ax, 0x0301 mov cx, 0x0003 mov dx, 0x0080 mov bx, 0x0800 int 0x13 pop bx pop dx jc .err_de .write: mov ax, [di + 12] mov [DAP_struct.sec_ptr], ax mov [DAP_struct.sec_num], bx mov word [DAP_struct.buf_ptr], 0 mov word [DAP_struct.buf_ptr + 2], dx mov ax, 0x4300 mov dl, [0x602] mov si, DAP_struct int 0x13 jc .err_de xor ah, ah iret .err_de: mov ah, 2 .err: iret
Много да? Ща разберемся.
Итак, сначала мы вызываем int 0x23, сохранив регистр cx (так как int 0x23 разрушает cx), и при ошибки просто возвращаем её:
push cx int 0x23 pop cx test ah, ah jnz .err
Далее идет блок проверок размера, ведь если размер текста, который мы хотим записать в файл, превышает размер этого файла в секторах на диске, нам нужно выделить новый блок секторов под новый текст.
mov bx, [di + 14] add bx, 0x1FF push cx mov cl, 9 shr bx, cl pop cx test bx, bx jnz .bx_not_null inc bx .bx_not_null: mov [di + 14], cx add cx, 0x1FF mov cl, 9 shr cx, cl cmp cx, bx jbe .need_alloc mov bx, cx jmp .write
Собственно, после зануления ds выше, мы копируем в bx размер файла из его FileHeader (его мы получили из прерывания 0x23) и переводим его в размер в секторах по знакомой нам формуле, сохраняя перед делением cx - ничего необычного.
на этом этапе важно, чтобы в bx не оказался 0 (что бывает, если мы создали новый файл), поэтому со строк 8 по строку 11 идет проверка.
Далее, независимо от результата bx мы сохраняем в поле размера файла размер из cx.
После, мы проводим преобразование размера уже для cx (размера текста для записи)
Теперь мы сравниваем два размера - bx и cx, если cx > bx, то мы должны перевыделить сектора для записи текста, в ином случае просто копируем cx в bx, для дольнейшего использования только регистра bx.
Собственно, если размер нового текста оказался больше нужного код прыгает на need_alloc:
.need_alloc: push dx push bx push cx mov cx, [0x600] mov [di + 12], cx pop cx add [0x600], cx ; запись SFSDD mov ax, 0x0301 mov cx, 0x0002 mov dl, [0x602] mov bx, 0x0600 int 0x13 ; запись FHT mov cx, 0x0003 mov bx, 0x0800 int 0x13 pop bx pop dx jc .err_de
Здесь мы сохраняем dx и bx, так как затронем их в коде, после чего на время сохраняем и cx, копируем в него значение первого же свободного сектора (храниться оно по аддресу 0x600) и записываем новое значение в FileHeader.
После достаем cx и прибавляем размер файла в секторах, чем теперь и является cx к значению первого свободного сектора.
После записи, мы должны записать на диск SFSDD и FHT. Для этого я использую более легкую функцию прерывания 0x10 0x03 - более простую версию 0x43 с DAP структурой. А поскольку, ax и dl не изменился, мы их не перезаписываем при записи FHT - простейшая экономия места.
Из этого кода следует, что сектора со старым текстом будут навсегда утеряны, пока вы ручками их не подвигаете :)
Ну и место, где происходит непосредственная запись в файл:
.write: mov ax, [di + 12] mov [DAP_struct.sec_ptr], ax mov [DAP_struct.sec_num], bx mov word [DAP_struct.buf_ptr], 0 mov word [DAP_struct.buf_ptr + 2], dx mov ax, 0x4300 mov dl, [0x602] mov si, DAP_struct int 0x13 jc .err_de xor ah, ah iret .err_de: mov ah, 2 .err: iret
Ээээм... ну тут действительно нечего говорить... Самый обычный код...
Для желающих запустить ОС
Немного информации о том, как её запустить и попытать на своем компе:
Запуск "с места"
Для запуска вам понадобиться: nasm, make, qemu, python - стандартный набор утилит, для OSdev-а.
Для быстрого запуска его отдельная make-задача:
make run
она вам соберет стандартный диск, саму ОС, все программы и запустит все через qemu.
Получения img диска
Если же вам нужен только диск в формате .img вы можете использовать задачу all:
make
после этого в рабочей директории появиться disk.img - как раз то, что вам нужно.
Настройка диска, добовления своей программы, или текстового файла
Если вы хотите добавить свой текстовый файл в систему, то откройте файл disk_example.sfs. Там вы увидите следующее:
{ "size": "10K", "boot": "sos.bin", "data": [ { "type": "E", "name": "shutdown", "data": "shutdown.bin" }, { "type": "E", "name": "reboot", "data": "reboot.bin" }, // куча программ { "type": "F", "name": "test.txt", "data": "test.txt" } ] }
тут вы должны добавить запись о своем файле:
{ "size": "10K", "boot": "sos.bin", "data": [ { "type": "E", "name": "shutdown", "data": "shutdown.bin" }, { "type": "E", "name": "reboot", "data": "reboot.bin" }, // куча программ { "type": "F", "name": "test.txt", "data": "test.txt" }, { "type": "F", "name": "my_SOS_file", // имя файла, которое будет видеть SectorOS "data": "my_file" // имя файла, текст из которого будет скопирован в файл в SectorOS } ] }
В начале файла вы можете видеть поля size и boot - это размер диска и файл, для размещения в первом секторе соответственно.
Если же вы написали программу, то вы также должны указать запись в .sfsd файле, но тип обязательно дожен быть 'E'. Также вы должны указать её в Makefile, для того, чтобы она автоматически компилировалась:
# это самое начало файла PROGRAMS := hello \ help \ dir \ memd \ rdsd \ cat \ shutdown \ reboot \ touch \ wrt \ del
Тут вы добовляете свою программу, которая должны находиться в папке programs/ и иметь расширение .asm, которое вы не указываете. на выходе получиться файл .bin.
Итог
Собственно из последней фразы в прерываниях можно выстроить весь итог - этот проект, в отличие от того же SectorC - это не набор интересных решений по оптимизации места. Это очень кастрированная ОС.
Я почти что вымучивал эту статью, так как, по факту в ОС нечего сокращать и упрощать, кроме файловой системы. И это грустна. У меня даже появилась идея, как можно реализовать директории в SFS, но я, к моменту этой идеи уже начал уставать от проекта, я банально на половине статьи уже не сильно был заинтересован этим проектом, да и писать особо не о чем было, а просто сделать статью, мол, "Глядите! я сделал ОС в секторе и... И все!" я не хотел. Плюс, обьяснять код, написанный на ассемблере, я просто не мог, тогда бы статья была бы на час прочтения, а тем, кто ассемблер уже знает, не составит труда прочитать код без моей болтовни - сам по себе он не очень сложный.
И в итоге эта статья мне не очень нравиться, я начал писать её, будучи уверенным в её интересности, а закончил с ощущением "Да наконец-то!". Поэтому я реально прошу прощения за отнятое у вас время на прочтение этой статьи.
Чуть позже, когда у меня снова появиться желание позаниматься этим проектом я сделаю директории, расширю стандартный пак программ, но писать ещё одну статью - излишне, дополнять эту - никто не прочитает.
Статью я писал ещё и с головными болями, слабостью, так что жду нового карантина :)
А ещё и ОГЭ скоро...
Чтож, а на этом я, пожалуй, закончу:
Большое спасибо создателю и перевотчику статьи об SectorC,
А сурсы SectorOS вы сможете потрогать туть,
Спасибо большое PRox-у за прекрасное комьюнити, которое натолкнуло меня на мысль об этом проекте.
Всем. Большое. Спасибо. За прочтение.
Внезапный мем в конце статьи

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

KarmaCraft
17.05.2026 13:33Отличная статья и проект, вписать ось в пол Кб пусть даже урезанную, это вам не blink от ардуинщиков на 1Кб. Автору респект!

Desvor Автор
17.05.2026 13:33спасибо, я старался (убрать все что можно убрать)

vladk67
17.05.2026 13:33"происходит следующее: экран очищаеться"
что экрану нужно сделатЬ? очиститЬся
что экран делаетЬ? очищаетЬся
что экран делает? очищается

Moog_Prodigy
17.05.2026 13:33Нужны! И теперь есть такие системы, типа 286 компов, которые заменить - это сотни млн рублей (которых разумеется нету у и так бедной нашей промки).
А также энтузиастам, которые эти системы эмулируют на чипах esp, чтобы копаться в железяках или играть в игры.
И Nc нужен. А Far даже на современных компах используется.
Вообще vc чем хорош, что это com файл, он не может быть больше 64 килобайта. Однако демосценеры демонстрируют такие вещи, что стоит им захотеть - они и интерфейс современной винды туда уместят и еще места под программы останется. Может быть, ИИ сподобится, когда-нибудь (сейчас он вообще плохо умеет в оптимизацию).

Radisto
17.05.2026 13:33Достойная цель.
Теперь оболочка нужна. СекторКоммандер. Тоже 512 размером))))

forthuse
17.05.2026 13:33С ассемблером по компактности сопоставим Форт (Forth) не путать с Фортраном и в 512 байт его умещали и не в одномм исполнении, хотя исторически у него блоки были в 1024 байт.
P.S. А, минимальным для ДОС был VC (Волков командер) исходники которого опубликовали для Истории

anonymous
17.05.2026 13:33
Desvor Автор
17.05.2026 13:33да, только смысл писать на forth?
асм, по проще, и ты уверен в отсутствии накладных расходов
forthuse
17.05.2026 13:33А, Вы пробовали? Форт как раз своими возможностями хорошо показывает себя во встраиваемых системах, хотя и высокоуровневое программирование ему не чуждо.
P.S. Смысл есть, если Форт систему на всевозможное железо реализуют начиная с ассемблера и “кончая” Питоном и Rust, помимо других популярных языков.

Desvor Автор
17.05.2026 13:33forth - интерпритируемый, а если и есть компиляторы, то помни, что он хранит данные на стеке, это накладно, если нужно доставать далекие переменные, да и не нравиться мне синтаксис, он банально мение читаемый

forthuse
17.05.2026 13:33Форт может и в компилирование как разные его современные реализации. “Синтаксис” Форта построен на последовательном прочтении слов (т.к. он относится к конкатенативным языкам близких к естественным), а стек(и) это элемент сцепления слов по данным эффективным способом. И у стека есть “свойство” - он может рассматриваться как “кэш” временем жизни данных на которои мы управляем.
P.S. Вероятно Вы правы т.к. Форт не многие могут оценить и принять как рабочий инструмент. (Форт это система в его понимании). Сам начинал с ассемблера … На Форт ассемблер пишется легко, а на ассемблере Форт посложнее. Открытая реализация OpenBios - это 99,9% - Форт (фактически реализация “ОС”), а на Форт есть ещё разной успешности и известности реализации ОС

Desvor Автор
17.05.2026 13:33Я про то, что скомпилированный код на форте, скорее всего, был бы объемней, чем на чистом асме

forthuse
17.05.2026 13:33Отнюдь,зависит от исполнения целевого результата.Для AVR, к примеру ассемблерный “Форт” один японский разработчик уместил в 512 флеша (или 256 ассемблерных команд). SPF4 для КолибриОС ужатый исполняемый Файл около 24Кб от начального где то 136Кб (пол размера при этом встроенныый макрооптимизатор дающий нативный 32-ух битный x86 код)
P.S. А, по классике - шитый код в МикроКонтроллере даёт реализовать типично. Форт систему в 8-16Кб

Desvor Автор
17.05.2026 13:33Так а если интерпретатор в 512 байт, то код мне куда пихать?

forthuse
17.05.2026 13:33В следующие сектора, Разместить FORTH в 512 байтах https://habr.com/ru/articles/563250/ Он прочитает листинг и добавит к своему телу расширив Форт. И эта самораскрутка может быть разной сложности.
P.S. Но, чтобы Ось назвать Осью она должна предоставить определённый сервис программам, а не базовые встроенные в него возможности.

Desvor Автор
17.05.2026 13:33только вот то, что описал ты нельзя назвать осью в секторе, тут уже 2 сектора получится

forthuse
17.05.2026 13:33Тогда имеет смысл определится в минимальных терминах, что считать Осью. Это Ось? https://github.com/nanochess/bootOS
P.S. @"Занятия в грузинской школе. Учитель:
Гиви, скажи нам, что такое “ос”?
Это большой полосатый мух, учитель!
Нэт. Гиви. Большой полосатый мух - это шмел, а ос - это то, вокруг чего вертится Земля! "

Desvor Автор
17.05.2026 13:33Не, прикол то в том, что тут не важна болтология, ведь априори один сектор будет занимать интерпретатор форта. А к компиляторам меньше доверия по оверхеду

TIEugene
17.05.2026 13:33Добавить планировщик задач и будет полноценная многозадачная ОС.
(всё остальное - в модули/драйверы/задачи).
Desvor Автор
17.05.2026 13:33в real-mode возможна только кооперативная многозадачность, а это не очень приколько, плюс планировщик в, оставшиеся 13, что ли, байт не поместиться при всем желании мне кажется, только если отдать ответственность за сохранения контекста на программы, а это уже фигня какая-то на не многозадачность

TIEugene
17.05.2026 13:33В real-mode это достаточно прикольно. Вытесняющая многозадачность на i8080 - это очень прикольно. По NMI.
Планировщик у меня уместился в ~200 байт (8080). x86 будет больше. Наверное.

SIISII
17.05.2026 13:33в real-mode возможна только кооперативная многозадачность
Неверно. В реальном режиме невозможна виртуальная память и невозможна защита задач друг от друга и системы от задач, потому что нет соответствующей аппаратной базы, -- но к типу многозадачности это вообще никакого отношения не имеет. Другое дело, что в 512 байт "настоящую" ОС не всунуть :)

BiTL
17.05.2026 13:33А вот, почему бы защищенный режим не заюзать? :)
Нет, конечно не для этой версии. А сделать под 386-й (тогда и текущий код можно уменьшить).
Пример (256 кб интро, Protected mode, Multi-threading): https://www.pouet.net/prod.php?which=96105
Desvor Автор
17.05.2026 13:33Потому что от 32х битной оси будет больше требований для называния моей поделки "ОС"

BiTL
17.05.2026 13:33Не уверен, что битность является определяющим фактором в классфикации ) Но ведь в текущем виде SecctorOS уже некуда особо развиваться? Наверное, можно еще пару десятков байт выкроить, может больше. Но что дальше?

Desvor Автор
17.05.2026 13:33Я кстати загуглил: аппаратный таймер всё-таки был, и поэтому, наверное, возможна нормальная многозадачность.
А по поводу "настоящей ОС": что ты этим называешь? Можно буквально отдельной программой подменить или добавить прерывания, или добавить свои, которые смогут использовать другие программы. Я писал в статье, что нет четкого определения "ОС - это"

SIISII
17.05.2026 13:33Наличие/отсутствие таймера к процессору отношения не имеет. Конкретно в IBM PC и всех последующих более-менее совместимых машинах он был. Изначально это была микросхема 8253 или 8254, один из каналов которого в качестве таймера, кидающегося прерываниями, и использовался; позднее таймер стал частью более сложных микросхем, но для программиста это роли не играет.
Возможно, мы используем термины не совсем идентичным образом, что может создавать проблемы с пониманием. Вообще, вытесняющая многозадачность -- это когда ОС может по своему усмотрению снять с процессора один поток и поставить другой. Таймер для этого не обязателен; скажем, ОС может передать управление вышедшему из ожидания завершения операции ввода-вывода высокоприоритетному потоку, сняв с процессора низкоприоритетный поток, который работал, пока высокоприоритетный поток вынужденно простаивал, ожидая завершение ввода-вывода. В кооперативной многозадачности такое невозможно: там поток должен сам тем или иным способом отдать управление. Таймер же нужен для реализации разделения времени, т.е. когда потокам выделяется определённый отрезок (квант) времени, в течение которого они могут непрерывно занимать процессор, и если поток весь свой квант выжрал, ОС принудительно снимает его с процессора и отдаёт процессор другому потоку -- и так до тех пор, пока все готовые к выполнению потоки не отработают свои кванты, и лишь после этого происходит возврат к первому из них. Таким образом, любая система с разделением времени будет системной с вытесняющей многозадачностью, но не каждая система с вытесняющей многозадачностью обязана быть системой с разделением времени (исторический пример -- OS/360, которая, возможно, вообще первой "большой и совсем-совсем настоящей" ОС в истории была; разделение времени там было необязательной опцией, но вытесняющая многозадачность была (почти) всегда, причём и в случае отсутствия таймера).
Чёткого определения ОС действительно нет, т.е. опять возвращаемся к терминологии. Для меня более-менее полноценная ОС -- это (а) средство управления ресурсами вычислительной системы (памятью, процессорным временем, периферией в широком смысле слова), распределения их между приложениями и самой ОС, и (б) набор сервисов, предоставляемых прикладным программам и абстрагирующих их от особенностей железа, насколько это возможно. MS DOS и CP/M, например, совсем полноценными ОС для меня не являются -- они однозадачные, а соответственно, в принципе не предусматривают разделения ресурсов между приложениями, а вот Винда с Линухом или упомянутая мной выше ОС/360 -- являются.
tr0llcr4ck
хорошая статья. заморочался. молодец