; Copyright (c) 2008-2009, diamond
; All rights reserved.
;
; Redistribution and use in source and binary forms, with or without
; modification, are permitted provided that the following conditions are met:
;       * Redistributions of source code must retain the above copyright
;       notice, this list of conditions and the following disclaimer.
;       * Redistributions in binary form must reproduce the above copyright
;       notice, this list of conditions and the following disclaimer in the
;       documentation and/or other materials provided with the distribution.
;       * Neither the name of the <organization> nor the
;       names of its contributors may be used to endorse or promote products
;       derived from this software without specific prior written permission.
;
; THIS SOFTWARE IS PROVIDED BY Alexey Teplov aka <Lrz> ''AS IS'' AND ANY
; EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
; WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
; DISCLAIMED. IN NO EVENT SHALL <copyright holder> BE LIABLE FOR ANY
; DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
; (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
; ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
; (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
; SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
;*****************************************************************************

						Нет повести печальнее на свете,
						Чем повесть о заклинившем Reset'е...

Загрузчик для FAT- и NTFS-томов для случаев, когда основной бутсектор загружает
Windows, для носителей с размером сектора 512 байт.

=====================================================================

Требования для работы:
1) Все используемые файлы должны быть читабельны.
2) Минимальный процессор - 80386.
3) В системе должно быть как минимум 592K свободной базовой памяти.
4) Пути к используемым файлам не должны содержать символических ссылок NTFS
	(жёсткие ссылки допускаются).
5) Используемые файлы не должны быть сжатыми или разреженными файлами
	(актуально для NTFS, для FAT выполнено автоматически).

=====================================================================

Документация в тему (ссылки проверялись на валидность 08.08.2008):
	официальная спецификация FAT: http://www.microsoft.com/whdc/system/platform/firmware/fatgen.mspx
		в формате PDF: http://staff.washington.edu/dittrich/misc/fatgen103.pdf
		русский перевод: http://wasm.ru/docs/11/fatgen103-rus.zip
	спецификация NTFS: file://C:/windows/system32/drivers/ntfs.sys
		и file://C:/ntldr либо file://C:/bootmgr
	неофициальное описание NTFS: http://sourceforge.net/project/showfiles.php?group_id=13956&package_id=16543
	официальная спецификация расширения EDD BIOS 3.0: http://www.phoenix.com/NR/rdonlyres/19FEBD17-DB40-413C-A0B1-1F3F560E222F/0/specsedd30.pdf
		то же, версия 1.1: http://www.phoenix.com/NR/rdonlyres/9BEDED98-6B3F-4DAC-BBB7-FA89FA5C30F0/0/specsedd11.pdf
	описание функций BIOS: Interrupt List by Ralf Brown: http://www.cs.cmu.edu/~ralf/files.html
	официальная спецификация Boot BIOS: http://www.phoenix.com/NR/rdonlyres/56E38DE2-3E6F-4743-835F-B4A53726ABED/0/specsbbs101.pdf
	официальное описание bcdedit для Vista: http://www.microsoft.com/whdc/system/platform/firmware/bcdedit_reff.mspx
	официальное описание работы с базой данных загрузчика Vista: http://www.microsoft.com/whdc/system/platform/firmware/bcd.mspx
	формат таблицы разделов жёсткого диска: http://www.microsoft.com/technet/prodtechnol/windows2000serv/reskit/prork/prcb_dis_qxql.mspx

=====================================================================

Схема используемой памяти:
	600-2000	код загрузчика (и данные)
	2000-3000	стек
	3000-3200	сектор MBR
	3200-3400	бутсектор логического диска
	3400-3C00	информация о кэше для таблиц FAT16/FAT32:
			для FAT16 - массив на 0x100 байт, каждый байт равен
			0 или 1 в зависимости от того, загружен ли
			соответствующий	сектор таблицы FAT16;
			для FAT32 - 100h входов по 8 байт: 4 байта
			(две ссылки - вперёд и назад) для организации L2-списка
			всех прочитанных секторов в порядке возрастания
			последнего времени использования + 4 байта для номера
			сектора; при переполнении кэша выкидывается элемент из
			головы списка, то есть тот, к которому дольше всех
			не было обращений
	3400-3440	информация о кэше для файловых записей NTFS в
			таком же формате, как и кэш для FAT32, но на 8 входов
	3480-34C0	заголовки для кэшей записей индекса NTFS
	3500-3D00	информация о кэшах записей индекса NTFS: с каждой
			файловой записью связан свой кэш для
			соответствующего индекса
	4000-8000	место для информации об атрибутах для NTFS
	60000-80000	таблица FAT12 / место под таблицу FAT16 /
				кэш для таблицы FAT32 / кэш для структур NTFS
	80000-90000	текущий рассматриваемый кластер
	90000-92000	FAT: кэш для корневой папки
	92000-...	FAT: кэш для некорневых папок (каждой папке отводится
			2000h байт = 100h входов, одновременно в кэше
			может находиться не более 7 папок;
			точный размер определяется размером доступной
			физической памяти - как правило, непосредственно
			перед A0000 размещается EBDA, Extended BIOS Data Area)

=====================================================================

Основной процесс загрузки.
0a. Загрузка из-под DOS и Win9x: установка kordldr.win осуществляется
	размещением команды install=c:\kordldr.win в первой строке config.sys;
	при этом основной загрузчик системы загружает kordldr.win как обычный
	com-файл, в какой-то сегмент по смещению 100h и передаёт управление
	в начало кода (xxxx:0100).
0б. Загрузка из-под WinNT/2000/XP: установка kordldr.win осуществляется
	добавлением строки наподобие c:\kordldr.win="KordOS" в секцию
	[operating systems] файла boot.ini; если загружаемый файл имеет размер
	не менее 8 Кб (0x2000 байт) и по смещению 3 содержит сигнатуру 'NTFS'
	(в случае kordldr.win так и есть), то основной загрузчик каждой из
	этих систем загружает kordldr.win по адресу 0D00:0000 и передаёт
	управление на адрес 0D00:0256.
0в. Загрузка из-под Vista: установка kordldr.win осуществляется манипуляциями
	с базой данных основного загрузчика через bcdedit и подробно описана в
	инструкции к kordldr.win; основной загрузчик загружает целиком
	kordldr.win по адресу 0000:7C00 и передаёт управление в начало кода.
1. При загрузке из-под DOS/9x основной загрузчик не ожидает, что загруженная
	им программа окажется в свою очередь загрузчиком, и в этом случае
	kordldr.win оказывается в условиях, когда основной загрузчик уже
	установил какое-то окружение, в частности, перехватил некоторые
	прерывания. Поэтому перед остальными действиями загрузчик должен
	восстановить систему в начальное состояние. (При загрузке под
	NT-линейкой такой проблемы не возникает, поскольку там основной
	загрузчик ничего в системе не трогает.) Поэтому перед собственно
	инициализацией KordOS при работе из-под DOS/9x производятся
	дополнительные действия. Первым делом kordldr проверяет, какой из
	случаев 0а и 0в имеет место (случай 0б отличается тем, что передаёт
	управление не на начало кода): определяет значение ip (команда call
	помещает в стек адрес следующей после call инструкции, команда pop si
	выталкивает его в регистр si), и если оно равно 100h, то kordldr
	загружен как com-файл из-под DOS/9x. Тогда он спрашивает подтверждения
	у пользователя (поскольку в этой схеме kordldr загружается всегда,
	он должен оставить возможность продолжить загрузку DOS/9x). Если
	пользователь хочет продолжить обычную загрузку, kordldr завершается.
	Иначе используется тот факт, что при выдаче прерывания перезагрузки
	int 19h система предварительно снимает все свои перехваты BIOSовских
	прерываний, а потом в свою очередь выдаёт int 19h уже BIOSу. Так что
	kordldr устанавливает свой обработчик трассировочного прерывания,
	устанавливает флаг трассировки и передаёт управление DOSовскому
	обработчику. Обработчик трассировочного прерывания ничего не делает
	до тех пор, пока следующей инструкцией не оказывается int 19h, а
	в этот момент отбирает управление и продолжает загрузку KordOS.
	При этом BIOSовские обработчики восстановлены за исключением,
	быть может, прерывания таймера int 8, которое, возможно, восстановлено
	до команды jmp far на оригинальный обработчик. В последнем случае его
	нужно восстановить явно.
2. Загрузчик перемещает свой код на адрес 0000:0600.
3. (метка real_entry) Загрузчик устанавливает сегментные регистры ds = es = 0,
	настраивает стек ss:sp = 0000:3000 и устанавливает bp так, чтобы
	все данные можно было адресовать через [bp+N] с однобайтовым N
	(в дальнейшем они так и будут адресоваться для освобождения ds и
	экономии на размере кода). Разрешает прерывания на случай, если
	они были запрещены. Выдаёт сообщение о начале загрузки, начинающееся
	с весёлой рожицы (символ с ASCII-кодом 2).
4. Определяет характеристики жёсткого диска, указанного в качестве
	загрузочного: проверяет поддержку LBA (функция 41h прерывания 13h),
	если LBA не поддерживается, то определяет геометрию - число дорожек
	и число секторов на дорожке (функция 8 прерывания 13h), эти параметры
	нужны функции чтения с диска.
5. (метка new_partition_ex) Устраивает цикл по разделам жёсткого диска.
	Цель цикла - для каждого логического диска попытаться загрузиться с
	него (действия по загрузке с конкретного логического диска начинаются
	с метки not_extended), при ошибке загрузки управление передаётся
	назад этому циклу (метка next_partition), и поиск подходящего раздела
	продолжается. На выходе заполняется одна переменная partition_start,
	имеющая смысл начала текущего рассматриваемого логического диска,
	но по ходу дела из-за приколов таблиц разделов используются ещё четыре
	переменных. cur_partition_ofs - фактически счётчик цикла, формально
	указатель на текущий вход в текущей загрузочной записи. Сама
	загрузочная запись считывается в память начиная с адреса 3000h.
	Три оставшихся нужны для правильной работы с расширенными разделами.
	В каждой загрузочной записи помещается не более 4 записей о разделах.
	Поэтому главной загрузочной записи, размещающейся в первом физическом
	секторе диска, может не хватить, и обычно создаётся так называемый
	расширенный раздел с расширенными загрузочными записями, формат
	которых почти идентичен главной. Расширенный раздел может быть только
	один, но в нём может быть много логических дисков и расширенных
	загрузочных записей. Расширенные загрузочные записи организованы
	в односвязный список, в каждой такой записи первый вход указывает
	на соответствующий логический диск, а второй - на следующую расширенную
	загрузочную запись.
	При этом в главной загрузочной записи все адреса разделов являются
	абсолютными номерами секторов. В расширенных же записях адреса разделов
	относительны, причём с разными базами: адрес логического диска
	указывается относительно расширенной записи, а адрес следующей
	расширенной записи указывается относительно начала расширенного
	раздела. Такой разнобой выглядит несколько странно, но имеет место
	быть. Три оставшихся переменных содержат: extended_part_start -
	начало расширенного раздела; extended_parent - текущая рассматриваемая
	расширенная загрузочная запись; extended_part_cur - следующая
	загрузочная запись для рассмотрения.
	Цикл выглядит так: просматриваются все разделы, указанные в текущей
	(главной или расширенной) загрузочной записи; для нормальных разделов
	(они же логические диски) происходит переход на not_extended, где
	устанавливается partition_start и начинается собственно загрузка
	(последующие шаги); при встрече с разделом, тип которого указывает
	на расширенность (5 или 0xF), код запоминает начало этого раздела
	(в главной загрузочной записи такой тип означает расширенный раздел,
	в расширенной - только указатель на следующую расширенную запись,
	в обоих случаях он может встретиться только один раз в данной записи);
	когда код доходит до конца списка, все нормальные разделы, описываемые
	в этой записи, уже просмотрены, так что код с чистой совестью переходит
	к следующей расширенной записи. Если он её не встретил, значит, уже
	все логические разделы были подвергнуты попыткам загрузиться, и все
	безрезультатно, так что выводится ругательство и работа останавливается
	(jmp $).
	Может возникнуть вопрос, зачем нужна такая сложная схема и почему
	нельзя узнать нужный логический диск заранее или хотя бы ограничиться
	первым попавшимся логическим диском, не крутя цикл. Так вот, вариант
	с предварительным определением нужного раздела в данном случае не
	используется, поскольку повлёк бы за собой нетривиальные лишние
	действия по установке (в текущем виде установку можно провести вручную,
	и она сводится к указанию системному загрузчику на существование
	kordldr); кстати, в альтернативной версии загрузки после
	Windows-загрузчика, когда установка осуществляется не вручную, а
	специальной программой под Windows, используется модифицированная
	версия, в которой как раз начальный физический сектор нужного раздела
	прописывается установщиком. Сам kordldr не может установить, с какого
	раздела его загрузил Windows-загрузчик (и вообще под NT/2000/XP обязан
	быть файлом на диске C:\). Вариант с первым попавшимся логическим
	диском был реализован в первой версии загрузчика, но по ходу дела
	обнаружилось, что таки нужно крутить цикл: во-вторых, может быть
	приятным, что сама система может стоять вовсе не на системном C:\, а и
	на других дисках; во-первых, диск C: может и не быть первым логическим
	разделом - Vista любит создавать скрытый первичный раздел перед
	системным, и тогда диск C: становится вторым логическим.
6. Извещает пользователя о том, что происходит попытка загрузки с очередного
	логического диска.
7. Читает первый сектор логического диска и определяет файловую систему.
	И в FAT, и в NTFS поле со смещением +11 содержит число байт в секторе
	и должно совпадать с характеристикой физического носителя, то есть
	200h байт. И в FAT, и в NTFS поле со смещением +13 содержит число
	секторов в кластере и должно быть степенью двойки.
	Критерий NTFS: поле со смещением +3 содержит строку NTFS и поле со
	смещением +16 нулевое (в FAT оно содержит число таблиц FAT и обязано
	быть ненулевым).
	Критерий FAT: загрузчик вычисляет число кластеров, определяет
	предположительный тип (FAT12/FAT16/FAT32) и проверяет байт по смещению
	+38 для FAT12/16, +66 для FAT32 (он должен быть равен 0x29).
	После определения типа файловой системы извещает пользователя об
	определённом типе. Если файловая система не распознана, выдаёт
	соответствующее сообщение и переходит к следующему логическому диску.
8a. Для FAT12-томов: засовывает в стек идентификатор файловой системы -
	константу '12'; устанавливает указатель на функцию получения следующего
	в цепочке FAT кластера на FAT12-обработчик; считывает в память всю
	таблицу FAT12 (она не превосходит 0x1800 байт = 6 Кб), при ошибке
	чтения пытается использовать другие копии FAT.
8б. Для FAT16-томов: засовывает в стек идентификатор файловой системы -
	константу '16'; устанавливает указатель на функцию получения следующего
	в цепочке FAT кластера на FAT16-обработчик; инициализирует информацию
	о кэше секторов FAT (массив байт с возможными значениями 0 и 1,
	означающими, был ли уже загружен соответствующий сектор - всего в
	таблице FAT16 не более 0x100 секторов) - ни один сектор ещё не
	загружен, все байты нулевые.
8в. Для FAT32-томов: засовывает в стек идентификатор файловой системы -
	константу '32'; устанавливает указатель на функцию получения следующего
	в цепочке FAT кластера на FAT16-обработчик; инициализирует информацию
	о кэше секторов FAT (формат информации описан выше, в распределении
	используемой загрузчиком памяти) - ни один сектор ещё не загружен.
8г. Общее для FAT-томов: определяет значения служебных переменных
	root_start (первый сектор корневого каталога в FAT12/16, игнорируется
	при обработке FAT32-томов), data_start (начало данных с поправкой,
	вводимой для того, чтобы кластер N начинался с сектора
	N*sectors_per_cluster+data_start), root_clus (первый кластер корневого
	каталога в FAT32, 0 в FAT12/16); устанавливает указатель на функцию
	загрузки файла на FAT-обработчик.
8д. Для NTFS-томов: засовывает в стек идентификатор файловой системы -
	константу 'nt'; определяет значение служебной переменной frs_size
	(размер в байтах файловой записи, File Record Segment), для полной
	корректности проверяет, что это значение (равное 0x400 байт на всех
	реальных NTFS-томах - единственный способ изменить его заключается
	в пересоздании всех системных структур вручную) не превосходит 0x1000
	и кратно размеру сектора 0x200 байт; инициализирует кэш файловых
	записей - ничего ещё не загружено; считывает первый кластер $MFT
	и загружает информацию о расположении на диске всей таблицы $MFT
	(атрибут 0x80, $Data); устанавливает указатель на функцию загрузки
	файла на NTFS-обработчик.
9. (метка load_secondary) Вызывает функцию загрузки файла для файла вторичного
	загрузчика. При обнаружении ошибки переходит на обработчик ошибок с
	соответствующим сообщением.
10. Устанавливает регистры для вторичного загрузчика: al='h' (жёсткий диск),
	ah=номер диска (для готового бинарника - 0 (BIOS-идентификатор 80h),
	может быть изменён путём модификации константы в исходнике или
	специальным установщиком), bx=идентификатор файловой системы (берётся
	из стека, куда ранее был засунут на шаге 8), ds:si=указатель на
	callback-функцию.
11. Передаёт управление вторичному загрузчику дальним переходом на 1000:0000.

Функция обратного вызова для вторичного загрузчика:
	предоставляет возможность чтения файла.
Вход и выход описаны в спецификации на загрузчик.
Чтение файла:
1. Сохраняет стек вызывающего кода и устанавливает свой стек:
	ss:sp = 0:3000, bp=dat: пара ss:bp при работе с остальным
	кодом должна указывать на 0:dat.
2. Разбирает переданные параметры и вызывает процедуру загрузки файла.
3. Восстанавливает стек вызывающего кода и возвращает управление.

Вспомогательные процедуры.
Процедура чтения секторов (read):
на входе должно быть установлено:
	ss:bp = 0:dat
	es:bx = указатель на начало буфера, куда будут прочитаны данные
	eax = стартовый сектор (относительно начала логического диска)
	cx = число секторов (должно быть больше нуля)
на выходе: es:bx указывает на конец буфера, в который были прочитаны данные,
	флаг CF установлен, если возникла ошибка чтения
1. Переводит стартовый сектор (отсчитываемый от начала тома) в сектор на
	устройстве, прибавляя номер первого сектора логического диска,
	найденный при переборе дисков.
2. В цикле (шаги 3-6) читает секторы, следит за тем, чтобы на каждой итерации
	CHS-версия: все читаемые секторы были на одной дорожке.
	LBA-версия: число читаемых секторов не превосходило 7Fh (требование
	спецификации EDD BIOS).
CHS-версия:
3. Переводит абсолютный номер сектора в CHS-систему: сектор рассчитывается как
	единица плюс остаток от деления абсолютного номера на число секторов
	на дорожке; дорожка рассчитывается как остаток от деления частного,
	полученного на предыдущем шаге, на число дорожек, а цилиндр - как
	частное от этого же деления. Если число секторов для чтения больше,
	чем число секторов до конца дорожки, уменьшает число секторов для
	чтения.
4. Формирует данные для вызова int 13h (ah=2 - чтение, al=число секторов,
	dh=головка, (младшие 6 бит cl)=сектор,
	(старшие 2 бита cl и весь ch)=дорожка, dl=диск, es:bx->буфер).
5. Вызывает BIOS. Если BIOS рапортует об ошибке, выполняет сброс диска
	и повторяет попытку чтения, всего делается не более трёх попыток
	(несколько попыток нужно в случае дискеты для гарантии того, что
	мотор раскрутился). Если все три раза происходит ошибка чтения,
	переходит на код обработки ошибок с сообщением "Read error".
6. В соответствии с числом прочитанных на текущей итерации секторов
	корректирует текущий сектор, число оставшихся секторов и указатель на
	буфер (в паре es:bx корректируется es). Если всё прочитано, заканчивает
	работу, иначе возвращается на шаг 3.
LBA-версия:
3. Если число секторов для чтения больше 7Fh, уменьшает его (для текущей
	итерации) до 7Fh.
4. Формирует в стеке пакет для int 13h (кладёт все нужные данные командами
	push, причём в обратном порядке: стек - структура LIFO, и данные в
	стеке хранятся в обратном порядке по отношению к тому, как их туда
	клали).
5. Вызывает BIOS. Если BIOS рапортует об ошибке, переходит на код обработки
	ошибок с сообщением "Read error". Очищает стек от пакета,
	сформированного на предыдущем шаге.
6. В соответствии с числом прочитанных на текущей итерации секторов
	корректирует текущий сектор, число оставшихся секторов и указатель на
	буфер (в паре es:bx корректируется es). Если всё прочитано, заканчивает
	работу, иначе возвращается на шаг 3.

Процедура обработки ошибок (find_error_si и find_error_sp):
на входе: указатель на сообщение об ошибке в si либо на верхушке стека
0. Если вызывается find_error_si, она помещает переданный указатель в стек.
1. Если ошибка произошла в процессе работы callback-функции, то
	(метка error_in_callback) обработчик просто возвращает управление
	вызвавшему коду, рапортуя о ненайденном файле.
2. Если же ошибка произошла до передачи управления вторичному загрузчику,
	обработчик выводит сообщение типа "Error: <текущий объект>: <ошибка>"
	и (восстановив стек) переходит к следующему логическому диску.

Процедура чтения файла/атрибута по известному размещению на диске
	(read_file_chunk):
на входе должно быть установлено:
	ds:si = указатель на информацию о размещении
	es:bx = указатель на начало буфера, куда будут прочитаны данные
	ecx = лимит числа секторов для чтения, старшее слово должно быть 0
на выходе: es:bx указывает на конец буфера, в который были прочитаны данные,
	флаг CF установлен, если возникла ошибка чтения
1. Определяет, является ли атрибут резидентным (возможно только в NTFS
	и означает, что данные файла/атрибута уже были целиком прочитаны при
	обработке информации о файле) или нерезидентным (означает, что данные
	хранятся где-то на диске, и имеется информация о том, где именно).
2. Для резидентных атрибутов (метка read_file_chunk.resident) просто копирует
	данные по месту назначения (с учётом указанного лимита).
3. Для нерезидентных атрибутов информация состоит из пар <размер очередного
	фрагмента файла в кластерах, стартовый кластер фрагмента>; процедура
	читает фрагменты, пока файл не закончится или пока не будет достигнут
	указанный лимит.

Процедура просмотра кэша (cache_lookup):
на входе должно быть установлено:
	eax = искомое значение
	ss:si = указатель на структуру-заголовок кэша
на выходе: ss:di = указатель на вход в кэше; флаг CF установлен, если значение
	было только что добавлено, и сброшен, если оно уже было в кэше.
1. Просматривает кэш в поисках указанного значения. Если значение найдено
	(при этом флаг CF оказывается сброшенным), переходит к шагу 4.
2. Если кэш уже заполнен, удаляет из кэша самый старый вход (он находится в
	голове двусвязного списка), иначе добавляет к кэшу ещё один вход.
3. Устанавливает в полученном входе указанное значение. Устанавливает флаг
	CF, последующие шаги не меняют состояния флагов. Переходит к шагу 5.
4. Удаляет вход из списка.
5. Добавляет сектор в конец списка (самый новый вход).
