Когда-то давно я постоянно занимался ремонтом компьютеров, и под рукой часто не было хорошего, качественного загрузочного CD. То одного нет, то
другого, а уж говорить про то, что когда его вынешь ничего не работает, и не приходится. Была ещё одна проблема - часто лезть в bios и переставлять откуда грузится. А если забудешь вынуть диск, так с руганью жмёшь Ctrl-Alt-Del и лезешь в BIOS... Вот и решил я
создать нормальный во всех отношениях загрузочный CD. И решил начать делать его... с программы, которая позволяла бы не залезая в BIOS выбирать, откуда грузиться. Вот с этого мы и начнём создание крутого загрузочного диска!
В этой статье я постарался подробно описать, как создать такую программу с неплохим интерфейсом. Она может оказаться полезной как для новичков, так и для продвинутых людей. В любом случае понадобится хотя
бы небольшое знание ассемблера и желание 😉
Программа работает в реальном режиме процессора, ни о каких функциях DOS, Windows и других ОС не
может быть и речи, ведь никакая ОС ещё не загружена! В нашем распоряжении есть только 1
Мегабайт оперативной памяти (для нашей цели это больше, чем предостаточно) и прерывания BIOS.
Сначала рассмотрим процесс загрузки. После включения компьютера BIOS, в соответствии со своими
настройками, находит устройство, с которого можно загрузиться. Происходит это следующим образом: программа BIOS читает первый сектор диска размером 512 байт и проверяет, является ли он загрузочным. В основном BIOS проверяет это по наличию в конце сектора (511, 512 байты) двухбайтовой сигнатуры 55AAh, но,
судя по моему опыту, на некоторых компьютерах загрузится возможно и при отсутствии этой сигнатуры, но это, скорее, исключения. В случае успешной проверки на пригодность к загрузке BIOS читает этот сектор по адресу 7C00h и передаёт управление ему. В случае загрузки с CD-ROM'а всё происходит несколько по другому. Здесь возможны три варианта: эмуляция дискеты, эмуляция жёсткого диска и без эмуляции. При эмуляции дискеты 1.44 Мб представляются как дискета, а доступ к остальному месту осуществляется уже после загрузки ОС, например MS-DOS. Эмуляция жёсткого диска аналогична эмуляции дискеты, только с той разницей, что в принципе возможно организовать доступ сразу ко всему пространству. При загрузке без эмуляции просто считывается программа указанного размера по указанному адресу, и управление передаётся ей.
Естественно, самый простой и распространённый способ - первый. Чтобы где-то применялась эмуляция жёсткого диска я не встречал, а вот последний способ загрузки использует установочный диск Windows
NT.
Нашей программе будет все равно, каким конкретно образом происходит загрузка (разве что в последнем случае всё может быть гораздо сложнее), для простоты и из-за наибольшего
распространения я выбрал всё-таки дискету (с жёстким диском всё будет практически то
же).
В зависимости от типа операционной системы загрузочный сектор (bootsector) имеет различную структуру. Мы подробнее рассмотрим структуру загрузочного сектора DOS при файловой системе FATxx, а точнее FAT12 - файловой системы, используемой на дискетах, так как при загрузке с CD-ROM'а практически происходит та же загрузка с дискеты, образ которой записан на компакт-диске.
Структура загрузочного сектора DOS:
Смещение |
Длина |
Содержимое |
+0 |
3 |
JMP xx xx переход на код загрузки |
+3 |
8 |
OEM-имя компании и версия системы |
+0Bh |
2 |
SectSiz байт на сектор ?? начало BPB |
+0Dh |
1 |
ClustSiz секторов на единицу |
+0Eh |
2 |
ResSecs резервных секторов (секторов |
+10h |
1 |
FatCnt число таблиц FAT |
+11h |
2 |
RootSiz макс.число 32-байтовых элементов |
+13h |
2 |
TotSecs общее число секторов на носителе |
+15h |
1 |
Media дескриптор носителя (то же, что |
+16h |
2 |
FatSize число секторов в одной FAT ?? |
+18h |
2 |
TrkSecs секторов на дорожку (цилиндр) |
+1ah |
2 |
HeadCnt число головок чтения/записи (поверхностей) |
+1bh |
2 |
HidnSec спрятанных секторов (исп. в |
1Eh |
размер форматированной порции |
|
1FEh 2 |
55AAh |
сигнатура загрузочного сектора |
Итак, первые три байта являются командой JMP xxxx. Они нас особо не интересуют. Вместо OEM-имени компании и
версии системы можно писать всё, что угодно. BPB - подмножество данных, содержащих сведения, необходимые для правильной инициализации файловой системы. Здесь есть одна интересная вещь - ResSecs - число резервных секторов (секторов перед первой FAT). Понятно, что наша будущая программа никак не уместится в 512 байт при всём нашем желании, в будущем мы воспользуемся этой возможностью для беспрепятственного размещения нашей программы. Всё остальное является бесполезным для нашей задачи.
Ну что ж, приступаем к самой интересной и продолжительной части. Для написания я использовал ассемблер Fasm 1.40 и блокнот. Этот ассемблер безо всяких проблем даёт чистый бинарный код наименьшего размера. Также нам понадобится эмулятор Bochs (http://bochs.sourceforge.net). Я использовал версию 2.1. Он без особого труда
находится в инете. Можно использовать и другой, например WmWare Workstation (www.wmware.com), но, во первых он платный, а во вторых весит около 12
Мегов. В принципе можно обойтись без эмулятора, если вам не надоест перезагружать компьютер каждый раз, когда вы захотите посмотреть результат. В этом случае,
естественно, надо поставить в BIOS'е загрузку с дискеты.
bootsect.asm
Создадим файл bootsect.asm. В нём у нас будет часть программы, располагающаяся в загрузочном секторе. Будем писать под смещение 7С00h, так как именно туда грузится бутсектор.
org 7C00h
use16 ; естественно код у нас 16 битный 🙂
jmp Beginning ; прыжок на начало кода, так как сейчас пойдут данные
nop ; так как предыдущая команда занимает 2 байт, команда nop даст третий
db 'bootsect' ; вот они - 8 байт
SectSize dw 00200h
ClustSize db 001h
ResSecs dw 00001h ; это то самое число резервных секторов (секторов перед первой
FAT)
FatCnt db 002h
RootSiz dw 000E0h
TotSecs dw 00B40h
Media db 0F0h
FatSize dw 00009h
TrkSecs dw 00012h
HeadCnt dw 00002h
HidnSec dw 00000h
Beginning: ; начало программы
cli ; подготовим регистры
mov ax, cs
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 7c00h
sti
; Команды cli и sti (запретить и разрешить прерывания) необязательны, но в их отсутствии если произойдёт прерывание таймера, то так как не заполнена таблица прерываний (или заполнена не полностью), прыжок, скорее всего, осуществится туда, где находятся данные или вообще ничего нет (какие-нибудь нули), и всё повиснет.
А ведь BIOS прочитал в память только первые 512 байт, поэтому прочитаем остальное "вручную" по адресу 7E00h (7C00h + 512 уже прочитанных байт). Для этого служит функция ah=02h прерывания int 13h: dl - номер диска, dh - номер головки, ch - номер цилиндра (дорожки), cl - номер сектора, al - число секторов для чтения (не больше цилиндра), es:bx - адрес, куда будут считаны данные.
Это то, что касается дискет. При чтении с жесткого диска ch и два старших бита cl (используются как старшие биты) отводятся для номера цилиндра (дорожки), а младшие шесть бит cl - номер сектора. В случае ошибки устанавливается флаг CF (проверяется командой jc xxxx). dl = 00h или 01h - дисководы A или B, пока нас будет интересовать A.
xor ax,ax
mov es,ax
mov bx,7e00h
mov ah,02h
mov al,8
xor dx,dx
mov ch,00000000b
mov cl,2
int 13h
; Теперь можно перейти к основной части программы, лежащей после 7E00h. Так сделано, поскольку впоследствии придётся подгружать бутсектора на 7E00h и они затрут нашу программу.
jmp Main_Program
; Не забудем про сигнатуру бутсектора 55AAh, поэтому
times (512-2-($-7C00h)) db 0
db 055H,0AAH
; И конечно подключим следующий файл с основной программой
include 'main.asm'
Конечно жалко потраченные зря драгоценные байты, но зачем, с другой стороны, из-за них усложнять программу. Ничего, там можно размещать данные, которые предполагается использовать до подгрузки других бутсекторов, например какой-либо текст.
Не пропадут.
main.asm
Итак, в нашем распоряжении есть экран 80x25 и 256 символов. Возможны два варианта:
- Нарисовать интерфейс программно.
- Только вывести на экран уже готовый интерфейс.
Первое, естественно, займёт меньше места в программе, но более трудоёмко и сложнее поддаётся модификации. Второе займёт лишние 80*25=2000 символов, но гораздо проще в исполнении и легче поддаётся модификации. Лично я остановился на втором способе.
Понятно, ни о каких функциях DOS или Windows не может быть и речи, поскольку ещё ни одна ОС не загружена. Так как нам надо заполнить весь экран, самый простой способ сделать это - записать образ экрана в видеопамять. Она начинается по адресу 0B800:0000 и имеет следующую структуру: сначала идёт символ, потом его атрибут (цвет), поэтому поместим в ah цвет, а в al будем загружать
символы и записывать ax в видеопамять.
Создадим файл graphic.inc.
DrawInterface: ; эта процедура выведет на экран наш интерфейс
pusha
mov ax,0B800h ; сегмент видеопамяти
mov es,ax
mov si,InterfaceData
xor di,di
mov ah,2 ; в ah - цвет (зелёный)
@print_char:
lodsb ; загрузим в al байт из InterfaceData
mov [es:di],ax
cmp di,4000 ; 4000 так как сначала идёт символ, потом его атрибут
jz @end_print_char
add di,2
jmp @print_char
@end_print_char:
popa
ret
InterfaceData file 'IntData.bin'
Для рисования интерфейса я применил программу ASCII Character Studio (www.torchsoft.com), но можно использовать любую другую аналогичную программу. Полученный текстовый файл необходимо преобразовать, ведь разделители строчек
- символы 10,13 - будут выводится на экран вместе с остальными, а это недопустимо, за перевод на другую строку они в нашей программе не отвечают. Если вы знаете какой-то другой язык программирования, кроме ассемблера, (впрочем, можно и на ассемблере) вам не составит труда написать программу для удаления этих символов из файла. Вот пример такой программы, реализованной на Delphi
- delnls.dpr и готовый файл delnls.exe. Эта программа работает через командную строку: необходимо указать имя файла-источника и имя нового файла:
delnls IntData.txt IntData.bin
Чтобы избежать лишнего кода, мы вместе с интерфейсом выведем на экран и все надписи, в том числе и пункты меню. То есть теперь нашей задачей будет
являться создание курсора, которым мы будем выбирать тот или иной пункт меню. Естественно, нам понадобится ввод данных с клавиатуры. Для этого в BIOS есть функции прерывания int 16h.Нас будет интересовать только одна из них ah=00h - чтение с клавиатуры одного символа. После выполнения прерывания она возвращает в ax сканкод (в ah) и ASCII код (в al) нажатой клавиши или комбинации клавиш. Сканкод фактически является тем, что передала клавиатура после нажатия клавиши. Сканкод преобразуется при выполнении прерывания в ASCII код, т.е. тот, к которому все привыкли. Если в al будет 0, то в ah содержится расширенный код ASCII (в основном это комбинации клавиш). Какой код соответствует какому символу всегда можно посмотреть в таблицах ASCII.Скорее всего, предпочтительнее использовать ASCII, так как сканкоды на разных компьютерах могут различаться, да и неизвестно, как там обстоит дело с USB клавиатурами, а ASCII код считается универсальным (по крайней мере первые 127 символов). Несмотря на это раньше я использовал сканкоды, и никаких проблем не возникало.
(Продолжение следует)