Урок ОСдева №7: первичный загрузчик, финал. / программирование :: длиннопост :: ассемблер :: разработка :: Операционная система :: OSDev :: geek (Прикольные гаджеты. Научный, инженерный и айтишный юмор)

программирование geek OSDev Операционная система разработка ассемблер длиннопост песочница 

Урок ОСдева №7: первичный загрузчик, финал.

В прошлый раз мы написали процедуру загрузки данных и использовали ее для того, чтобы 

поместить корневую директорию нашей дискеты в оперативную память сразу после собственно

программы-загрузчика по адресу 07C0h:0200h. План действий на сегодня:


-Найти в КД номер первого кластера файла.

-Загрузить первый кластер.

-Следуя по цепочке записей в FAT, загрузить остальные кластеры.


Перед тем, как кодить дальше, давайте  разберёмся, что такое КД и как её использовать для

поиска файлов*.


По сути корневая директория в FAT12 - это таблица, в которой каждому файлу или

поддиректории соответствует одна 32-байтная запись. Давайте посмотрим, что в ней есть.


Байты 0-10: имя файла в формате 8:3. Этот формат подразумевает, что имя файла занимает

ровно 8 байтов, а расширение - 3. Если имя файла меньше 8 символов, оно дополняется

пробелами: так, файл 'loader.bin' в КД будет проходить под именем 'LOADER  BIN'.


Байт 11: атрибуты записи. Набор флагов, позволяющий придать записи особые свойства.

          00000001b = только для чтения

          00000010b = скрытый

          00000100b = системный

          00001000b = метка раздела

          00010000b = директория

          00100000b = архив

          00001111b = LFN (long file name), запись имеет особый формат, поддерживающий длинные

                              имена файлов.


Байт 12: зарезервирован для Windows NT.


Байт 13: время создания в десятых секунды (почему-то 0-199 согласно OSDev Wiki).


Байты 14-15: время, когда был создан файл. Младшие 5 бит - секунды/2 (то есть при интерпретации

значения, например, для вывода на экран, эту часть надо умножать на 2). Следующие 6 - минуты.

Последние 5 бит - часы.


Байты 16-17: дата создания файла. Примерно та же история. День(0-4), месяц(5-8), год(9-15).


Байты 18-19: дата последнего доступа в том же формате, что и дата создания.


Байты 20-21: старшие 16 бит номера первого кластера файла. В FAT12 и FAT16 не используется.


Байты 22-23: время последнего изменения в том же формате, что и время, когда был создан файл.


Байты 24-25: дата последнего изменения в том же формате, что и дата создания.


Байты 26-27: младшие 16 бит номера первого кластера файла.


Байты 28-31: размер файла в байтах.

Довольно много информации, но нас интересуют только два поля: имя записи и младшая часть номера

стартового кластера (старшая половина в FAT12 не используется). Вырисовывается в общих чертах

алгоритм поиска файла? Если нет, я помогу:


1. Переходим к началу КД

2. Считываем имя записи

3. Сравниваем имя записи с именем искомого файла

4. Если имена совпали, файл найден, SUCCESS!

5. Записи кончились?

6. Если кончились - файла нет, аварийно завершаемся

7. Переходим к следующей записи

8. goto 2

Вот таким нехитрым способом мы сможем найти на диске начало файла или, если его нет, уведомить

об этом пользователя и свернуть выполнение загрузчика. Я решил, начиная с этого поста, не

перепечатывать весь листинг из предыдущих уроков. Вместо этого я приложу ссылку на файл в

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

нечитабельных размеров. А теперь давайте выполним наш поисковый алгоритм в коде. После

call read_sectors пишите:


                   mov cx,BPB_RDentries

                   mov di,0200h

                   mov si,offset fname

                   mov bp,si


next_entry:   mov ax,cx

                   mov bx,di

                   mov cx,11

                   rep cmpsb

                   mov si,bp

                   mov di,bx

                   mov cx,ax

                   je load_FAT

                   add di,32

                   loop next_entry


                   mov ah,3

                   xor bh,bh

                   int 10h


                   mov ax,1300h

                   mov bx,0007h

                   mov cx,22

                   mov bp,offset fname

                   int 10h


                   cli

                   hlt

Что всё это значит? В строчке mov cx,BPB_RDentries мы устанавливаем счётчик основного

цикла. Напоминаю, что в переменной BPB_RDentries у нас хранится число записей корневой

директории. 0200h - смещение загруженной в RAM КД. В SI мы помещаем смещение строки с

именем искомого файла. Кстати, впишите в переменные fname db 'LOADER  BIN'. После этого

мы сохраняем это же смещение в регистре BP. Это может быть пока неочевидно, но позже вы

поймёте, зачем.


Следующий блок кода, начинающийся с метки next_entry, - это собственно цикл просмотра

записей КД и сравнения имён. Первым делом мы сохраняем счётчик цикла и смещение текущей

записи. Счётчик сохраняем потому, что будет вложенный цикл, а смещение - потому, что

строковые инструкции вроде cmpsb изменяют значения регистров SI и DI. Кстати, теперь вам

должно быть понятно, зачем мы сохраняли указатель на строку с именем в BP.


mov cx,11 - установка счётчика вложенного цикла. Имена в FAT12 хранятся в формате 8:3,

значит, нам нужно сравнить две строки по 11 символов. Надеюсь, тут вопросов нет?

Инструкция cmpsb сравнивает значения двух байтов (в нашем случае символов), находящихся

в DS:SI и ES:DI. Префикс rep повторяет инструкцию, пока не обнулится счётчик в CX.

Далее мы восстанавливаем счётчик основного цикла в CX, смещение текущей записи в DI и

смещение строки с именем файла в SI. В старых версиях здесь у меня были пары инструкций

push/pop, но потом я подумал, что трансфер из регистра в регистр быстрее, чем обращение

к стеку, и поменял. Никогда не вредно сэкономить пару циклов.


Если в результате rep cmpsb все символы совпали, в регистре флагов будет установлен бит

ZF. Команда je load_FAT выполняет переход к метке load_FAT если флаг ZF установлен.

В случае если строки не совпали, мы переводим DI к следующей записи в КД и продолжаем

цикл командой loop next_entry. Тут бы можно было и закончить, но нужно обработать

отсутствие файла. С этим набором инструкций мы уже знакомы по предыдущему посту.

Первый блок возвращает положение курсора в DH,DL, а второй выводит от этой позиции

сообщение. Отличается только само сообщение. Вместо 'Disk read error.' мы выводим строку

с именем файла. Внимание, тут небольшой хак. Идея в том, чтобы вывести следующий текст:

'{filename} not found!'. Вызвать вывод строки два раза, но зачем? Если поместить в

разделе переменных текст ' not found!' сразу после переменной fname, а при вызове int 10h

указать в CX не 11 символов, а 22, то выведется сначала имя файла, а потом ' not found!'

Конечно же, этот текст обязательно должен быть сразу после fname. Добавьте строчкой ниже

db ' not found!' После этого останавливаем процессор парой команд cli и hlt. Не так-то

сложно, да? Впрочем, файл ещё нужно загрузить.


Для этого нам нужно будет загрузить в память FAT и разобраться, как ею пользоваться.

Давайте начнём с первой задачи, она чисто техническая и не требует умственного напряжения.

После hlt набирайте:


Load_FAT:          mov ax,[di+26]

                         mov cluster,ax

                         mov ax,BPB_reserved

                         mov cx,total_FATs_size

                         mov bx,BPB_RDentries

                         shl bx,5

                         add bx,0200h

                         mov FAT_offset,bx

                         call read_sectors

В строчке mov ax,[di+26] мы считываем из записи КД номер первого кластера файла, а затем

сохраняем его в переменной cluster. Далее, мы помним, что FAT у нас идут сразу после

зарезервированных секторов, поэтому в AX помещаем BPB_reserved. В CX у нас будет число

секторов, которое надо загрузить, то есть total_FATs_size. Загружать FAT будем сразу после

КД, то есть в 07С0h:0200h+размер КД. Размер КД = число записей КД*размер записи (32 байта).

Помещаем в BX число записей (BPB_RDentries), умножаем на 32 (shl bx,5 эквивалентно умножению

на 32, но выполняется быстрее) и добавляем 0200h. Готово! Сохраняем на будущее в переменной

FAT_offset (кстати, объявите её рядом с прочими) и вызываем read_sectors.


А теперь время вернуться к теории. Что такое FAT? Не поверите, но это тоже таблица, и её

структура ещё проще, чем у КД. Каждая запись в FAT соответствует кластеру на диске. FAT

можно назвать оглавлением диска (украл с OSDev Wiki). Кластер может быть свободен, занят

частью файла, зарезервирован ОС или испорчен. Если кластер хранит часть файла, то его

запись в FAT будет содержать номер следующего кластера файла. Понятно? Зная номер первого

кластера файла, мы можем загрузить его в память, потом заглянуть в FAT, найти нужную запись

и считать номер следующего кластера. Повторять до конца файла. Звучит просто, но, как

всегда, есть большое "НО"! Размер записи в FAT12 - 12 бит. Мы не можем оперировать

12-битными ячейками. Мы можем считать 8 или 16. То есть, если мы загрузим в AX начало FAT,

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

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

наглядности. В верхней строчке будет часть FAT, разделённая на записи, а в нижней она же,

но поделенная на 8-битные куски.


0 0 0 1 0 1 1 1 0 0 1 0|0 1 1 1 0 0 1 0 1 0 0 0|0 0 1 0 0 1 0 0 0 1 1 1          3 Записи.

0 0 0 1 0 1 1 1|0 0 1 0 0 1 1 1|0 0 1 0 1 0 0 0|0 0 1 0 0 1 0 0|0 1 1 1         4,5 байта.


Решение в том, чтобы, считывая каждый нечётный кластер, сдвигать значение на 4 бита вправо, а

у чётного - обнулять 4 старших бита. Зная всё это, давайте писать код:


                             push 0050h

                             pop es

                             xor bx,bx

read_cluster:           mov ax,cluster

                             sub ax,2

                             movzx cx,BPB_secperclust

                             mul cx

                             add ax,datasector

                             call read_sectors

                             mov ax,cluster

                             mov si,ax

                             shr ax,1

                             add si,ax

                             add si,FAT_offset

                             mov dx,[si]

                             mov ax,cluster

                             test ax,1

                             jnz odd_cluster

                             and dx,0000111111111111b

                             jmp short done

odd_cluster:           shr dx,4

done:                     mov cluster,dx

                             cmp dx,0FF7h

                             jb read_cluster

Финальный рывок. Первое, что мы делаем - устанавливаем сегмент для загрузки файла. Так как

BIOS нам больше не указ, выбирать можно самостоятельно. Я бы с удовольствием отправил его

в 0000h:0000h, но первые 1280 байт заняты важными вещами, о которых поговорим позже.

Ближайший свободный участок RAM - 0050h:0000h (или 0000h:0500h, это тот же самый адрес

если вы вдруг забыли правила адресации сегмент:смещение). Обнуляем BX, так чтобы пара

ES:BX указывала на 0050h:0000h. Считываем в AX номер первого кластера файла. Дальше мы

вычитаем 2 из этого номера. Зачем? Затем, что значения 0 и 1 в FAT зарезервированы и не

используются в качестве номеров, а номер, указанный в таблицах, на 2 больше, чем правильное

значение. Да, это идиотизм.


Загружать будем не сектор, а кластер (что в нашем случае одно и то же, но всё-таки),

поэтому в качестве числа секторов помещаем в CX переменную BPB_secperclust и на неё же

умножаем номер кластера. AX*CX в данном случае дадут нам номер первого сектора нужного

кластера. А так как кластеры в FAT начинают считаться от начала области данных,то для

абсолютного значения добавляем к AX datasector. Готово. Вызываем read_sectors и загружаем

первый кластер файла в RAM.


Дальше будет немножко математической магии, объяснять которую я не буду. Если интересно -

разберётесь самостоятельно, там не так сложно. Остальным предлагаю просто поверить, что

смещение записи кластера внутри FAT = 3/2 номера кластера. Значит, берём в AX номер

кластера, его же помещаем в SI, делим AX на 2 и прибавляем к SI. Вуаля, смещение

записи от начала FAT найдено. Добавляем к нему смещение FAT_offset и считываем в DX

значение записи.


Теперь надо проверить, чётная ли запись. Для этого опять берём в AX номер кластера и

делаем сравнение с 1. Если флаг ZF не установлен (то есть 0 бит значения равен 1),

значит, номер записи - нечётный, переходим к odd_cluster и сдвигаем значение вправо на

4 позиции. Если чётный - делаем логическое "И" с маской 0000111111111111b и обнуляем

тем самым 4 старших бита. Теперь у нас есть содержимое нужной записи без всяких

посторонних хвостов, то есть номер следующего кластера. Сохраняем его в переменной

cluster. Дальше у нас идёт сравнение с номера с числом 0FF7h. Дело в том, что,

начиная от этого значения в FAT идут специальные коды, которые могут означать конец

файла, испорченный сектор и т.д. Для нас это значит, что если в качестве номера

кластера мы получили 0FF7h или больше, продолжать загрузку не имеет смысла. Поэтому

продолжаем цикл только если DX меньше 0FF7h. Я умышленно оставляю здесь дыру и

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

связанной с битым кластером (код 0FF7h). Код конца файла, кстати, 0FF8h. Вся необходимая

для этой задачи информация и примеры кода уже есть в этом посте.


А мне остаётся только добавить в конце три строчки:


                    push 0050h

                    push 0000h

                    retf

Этот набор команд мы уже помним из старых постов. Помещаем в стек сегмент, потом

смещение, и передаём управление загруженному файлу командой retf. Поздравим себя!

Первичный загрузчик готов. Да, он умеет немного, но и задача у него всего одна:

найти загрузчик второго уровня, поместить его в RAM и отдать штурвал. Если вы

скомпилируете файл без инструкций org 1FEh и dw 0AA55h, то увидите, что программа

занимает всего 447 байт. Значит, у нас есть в запасе ещё 63. Как раз должно

хватить на проверку успешного считывания кластеров. У меня вместе с ней вышло 497

байт. Можете подсмотреть в приложенном файле, хоть это и неспортивно. Если вы

поместили загрузчик на дискету и получили в bochs (или на реальной машине) вот такой

экран, то всё работает как надо!


Plex86/Bochs UGABios (PCI) current-cvs 08 Jul 2014 This UGA/UBE Bios is released under the GNU LGPL
Please visit :
. http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs BIOS - build: 07/10/14
$Revis ion: 12412 $ $Date: 2014-07-10 09:28:59


Чистая дискета:

https://drive.google.com/file/d/1Bold4ds8oEruHQ7fJZKHglVo7A2Vc5MR/view?usp=sharing


Листинг:

https://drive.google.com/file/d/1Q5EtKX5kyF4MWcBeD8a6Jz5cPtqZja9C/view?usp=sharing


Bochs:

https://drive.google.com/file/d/16k2Gpr7oPSekq4rAhmtBV0IPnIteDLlE/view?usp=sharing


* FAT поддерживает вложенные директории, и они ничем принципиально не отличаются

от корневой, так что всё нижеизложенное касается и их. 



Подробнее
Plex86/Bochs UGABios (PCI) current-cvs 08 Jul 2014 This UGA/UBE Bios is released under the GNU LGPL Please visit : . http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios Bochs UBE Display Adapter enabled Bochs BIOS - build: 07/10/14 $Revis ion: 12412 $ $Date: 2014-07-10 09:28:59 +0200 (Do, 10. Jul 2014) $ Options: apmbios pcibios pnpbios eltorito rombios32 ataO master: Generic 1234 ATA-6 Hard-Disk (1023 MBytes) Press F12 for boot menu. Booting from Floppy... LOADER BIN not found!
программирование,geek,Прикольные гаджеты. Научный, инженерный и айтишный юмор,OSDev,Операционная система,разработка,ассемблер,длиннопост,песочница
Еще на тему
Развернуть
Охуеть понял наверно всего лишь 15-20%
Kzawr Kzawr 17.07.202011:50 ответить ссылка -0.3
ассемблер не знаю, но мне норм
Я под заказ писал лабораторные 6 лет назад
Kzawr Kzawr 17.07.202012:01 ответить ссылка 0.0
Ну так, если в повседневной жизни знания 6 лет не используешь - конечно, они архивируются. Ты скачай листинг по ссылке внизу, сразу понятно станет.
это издевательство
kokainix kokainix 17.07.202012:01 ответить ссылка -0.2
Какая дискета в 2020 году? Уже давно появился UEFI в котором совершенно другой принцип загрузки. Сколько можно переписывать то, что было актуально лет 30 назад? Что ни статья про "свою" ОС так обязательно дискеты, биос и прочее говно мамонта. Одно и то же.
Ну так задай себе вопрос, почему так. Потому что все вокруг тупые деграданты, а ты один д'Артаньян? Или, возможно, ты чего-то не догоняешь? Возможно, ты просто не очень хорошо себе представляешь, что может сподвигнуть человека в 21 веке писать свою ОС. Ты наверное думаешь, что этим начинают заниматься с мыслью подвинуть всех этих Гейтсов, Джобсов и Торвальдсов. Так вот, нет. Обычно толчок - любовь к низкоуровневому системному программированию. Мне нравится разбираться в работе железа и писать драйверы, это доставляет мне удовольствие и это единственная объективная причина, почему я пишу свою ОС. UEFI меня лишает большой части этого удовольствия. Тем не менее, я несколькими постами ранее уже писал, что статьи про загрузку через UEFI позже появятся. Про дискеты даже объяснять не буду, заебали, башкой подумай - поймёшь.
можно под травой попробовать почитать
nefr1t nefr1t 17.07.202012:32 ответить ссылка -0.1
Я сейчас под травой и чтиво занудное настолько, что я чуть не уснул
Зачем читал-то? Мне вот про раскрашивание минек в вахе читать неинтересно, я и не читаю.
Бро, я ни в коем случае тебя не упрекаю.
Просто скучновато было.
Но ты молодец и все такое)
вы перепутали реактор с хабром
А все, кто выкладывает кулинарные статьи - перепутали реактор и книгу о вкусной и здоровой пище? Технические - реактор и кружок "паяльник и я"? Биографии - реактор и википедию? Исторические - реактор и опять википедию?
Да. А ты дружок - петушатню и комментарии
Какой ты дерзкий.
В студенческие годы получил полный зачет и "отлично" за все годы обучения программированию (кто там кого учил непонятно), за программку которая на диске занимала 800 байт, а в ОЗУ - 400.
Программка "заражала" command.com, не изменяя его размера, запускалась вместе с ним, не отображалась отдельно в ОЗУ и контролировала дисковый I\O, не перехватывая векторов прерываний.

Написал прогу после того, как в ВУЗовских аудиториях стали ограничивать дисковый доступ на ПК. Но там это делали через стандартный перехват вектора 13H прерывания. Ну и соответственно - зная стандартное значение вектора, взламывалась такая "защита" минуты за три - записал в таблицу векторов адрес стандартного обработчика, получил доступ к дискам, "убил" загрузку "защиты", перезагрузил.))

Ради смеха я поставил на пару ВУЗовских ПК свою прогу - за пару недель даже местные "лаборанты" не смогли снять.))
После того мне поставили "отлично" и сразу зачли все практики, лабы и др. по программированию, а моей прогой заменили свою "защиту" в аудиториях.

Технология которую я применил:
1 .В файле command.com имеется солидный кусок занятый исключительно нулями. Кусок, достаточный для размещения там 800 байтового кода.
2. Т.к. по сути запускается модифицированный command.com, то отдельной программы в памяти нет. Но конечно "весить" в ОЗУ command.com стал больше.
3. Управление "13м прерыванием" выполнялось так:
На входе в DOS-обработчике прерывания вшивался переход на свой обработчик, а замещенные байты "переносились" в свой код и выполнялись там. После своих действий.
Таким образом, вектор прерывания в OS-таблице векторов просто был неважен - перехват был на уровне базового обработчика к которому в итоге приходили все запросы дискового I\O.
Fosgen Fosgen 17.07.202012:44 ответить ссылка 2.5
Вот ради таких историй я и пилю посты тут. Давайте ещё! Старые добрые 16-битные денёчки.
"Старперская" ностальгия. )))))
Fosgen Fosgen 17.07.202015:00 ответить ссылка 0.1
Еще история из студенческих лет, но уже про "плюсы".
Мои студенческие годы охватывают период 1993-1999 годов.

Был такой предмет "начертательная геометрия". И в силу "новомодных веяний" и профиля специальности (АСОиУ) нам даже рассказали (!) про применение ПК в задачах конструирования и разработки чертежей.

Я довольно качественно филонил со сдачей альбома чертежей по данному предмету. Сдавал, без грубых ошибок, но "под занавес". И когда под конец семестра поднялись вопросы про оценку "автоматом", препод меня обломал, сообщив что "надо его чем-нить удивить". Например, в области построения линии пересечений пары геометрических примитивов.

На тот момент я уже слегонца "заболевал" геймдевом, и начал познавать 3D-моделирование, как на примере 3D Studio for DOS, так и на первоначальном опыте использования OpenGL.
Вообще так сошлись звезды, что изучение кодинга я начинал с Turbo Pascal 7.0, и совершенно логично, что продолжил переходом на Delphi.
Но во времена модемов найти библиотеку OpenGL под Паскаль или Дельфи было слабореально, поэтому пришлось засесть за C++.

В общем, через недельку преподу я предоставил DOS-прогу, которая умела строить линии пересечения для цилиндров \ конусов \ сфер \ кубов (в любой комбинации и задаваемых пользователем размеров) с отрисовкой двух проекций выбранной комбинации и выделением линии пересечения.
Препод видимо был настолько в офигении от моей поделки, что с этой же моей прогой "автомат" получила еще пара студентов (деваха, которой я симпатизировал - с моего согласия, и ее туповатый бойфренд, что для меня - "ботана", было неприятным следствием). По сути они просто меняли комбинацию объектов на другую и выдавали как "другую" программу.

Вот такой опыт кодинга, ну и личный - до кучи.)))
Fosgen Fosgen 17.07.202015:20 ответить ссылка 0.1
Ага, у меня тоже был "паскально-дельфийский" период с уклоном в геймдев, только чуть попозже - уже были переведённые библиотеки OpenGL и DX. Правда, в учёбе эти знания так и не пригодились.
Я в первой половине 2000х пытался что-то "сгеймдизайнить" на GLScene под Дельфи. Дошел до того что начал переделывать сам движок (в рамках доступных классов), потом написал свой 2D-движок на Дельфи+OpenGL, но в итоге уперся в производительность как движка так и кода на Дельфи.
Потом ушел на C# и в итоге на Unity.
Ею и живу последние несколько лет.
Fosgen Fosgen 17.07.202018:33 ответить ссылка 0.0
Да, сам юнити ковыряю потихоньку, когда время есть.
Только зарегистрированные и активированные пользователи могут добавлять комментарии.
Похожие темы

Похожие посты
2015
but I wanna keep Windows 7
2022
please don’t leave me Programmers in Enterprise Company
Programmers in Startup Company
Programmers in Government 		i-^ * TI Plex86/Bochs UGABios (PCI) 0.7b 03 Jan 2020 This UGA/UBE Bios is released under the GNU LGPL
Please visit :
. http://bochs.sourceforge.net . http //www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs 2.6.10.svn BIOS - build: 01/05/20
^Revision: 13752 $ $Date: 2019-12-30 14:16:18 +0
подробнее»

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

Plex86/Bochs UGABios (PCI) 0.7b 03 Jan 2020 This UGA/UBE Bios is released under the GNU LGPL Please visit : . http://bochs.sourceforge.net . http //www.nongnu.org/vgab ios Bochs UBE Display Adapter enabled Bochs 2.6.10.svn BIOS - build: 01/05/20 ^Revision: 13752 $ $Date: 2019-12-30 14:16:18 +0
^ Bochs for Windows - Display
USER ,__£
m2
■+Щ
•te
ТА
Reset susPEno Rower-
Û *
ujf'tnu vvwet
ù ф
Please visit :
. http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios
Bochs UBE Display Adapter enabled
Bochs 2.6.10.sun BIOS - build: 01/05/20
^Revision: 13752 $ $Date: 2019-12-
подробнее»

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

^ Bochs for Windows - Display USER ,__£ m2 ■+Щ •te ТА Reset susPEno Rower- Û * ujf'tnu vvwet ù ф Please visit : . http://bochs.sourceforge.net . http ://www.nongnu.org/vgab ios Bochs UBE Display Adapter enabled Bochs 2.6.10.sun BIOS - build: 01/05/20 ^Revision: 13752 $ $Date: 2019-12-