Нестандартный загрузчик

Дата публикации 21 июл 2003

Нестандартный загрузчик — Архив WASM.RU

Данная статья предназначена для тех, кто хочет подробнее узнать о процессе начальной загрузки компьютера и поэкспериментировать с ним. Никакого специального программного обеспечения не потребуется: все описываемые действия проделываются с использованием отладчика debug, входящего в состав Windows 95/98. Проделав описанные в статье действия на своем компьютере, вы сможете создать вспомогательную программу wb.com и установить в реестре ее ассоциацию с расширением файлов .bot, что позволит одним щелчком мыши переносить созданные с помощью debug файлы на дискету, сделав ее загружаемой.

Начальная загрузка

После проверки программой BIOS подключенного оборудования управление передается процедуре начальной загрузки с дисков, доступ к которой можно получить через прерывание INT 19h. Данная процедура загружает всего один сектор (первый) с нулевой дорожки нулевой головки, размещая его в памяти по адресу 7С00h, и передавая управление после загрузки сектора по этому же адресу. Ответственность за дальнейшую загрузку компьютера ложится на код, содержащийся в этом секторе.

Совершенно очевидно, что из-за ограниченного объема сектора (512 байт) загрузочный код разнообразием не блещет и в основном сводится к операции загрузки с диска в оперативную память дополнительных секторов, содержащих ядро операционной системы, и передаче управления по соответствующему адресу памяти. Кроме кода загрузки, в стандартном загрузочном секторе содержится также приведенный ниже в таблице блок параметров BIOS с данными о форматировании диска.

СмещениеРазмер, байтСодержание
3h8Аббревиатура и номер версии ОС
Bh2Число байтов в секторе (512)
Dh1Число секторов в кластере
Eh2Число резервных секторов в резервной области раздела
10h1Число копий FAT в разделе (2)
11h2Количество 32-байтных дескрипторов файлов в корневом каталоге
13h2Общее число секторов в разделе
15h1Тип носителя информации (для современных дискет F0h)
16h2Количество секторов, занимаемых одной копией FAT
18h2Число секторов на дорожке
1Ah2Число головок
1Ch4Число скрытых секторов перед началом раздела (для дискет 0)
20h40 (используется FAT32)
24h1Номер дисковода (для дискет - от 0 до 3)
25h10 (для Windows NT)
26h1Признак расширенной загрузочной записи (29h)
27h4Номер логического диска (создается при форматировании)
2Bh11Метка диска (текстовая строка)
36h8Аббревиатура типа файловой системы
3EhНачало загрузочного кода

Первые три байта загрузочного сектора содержат инструкцию безусловного перехода (JMP) на код, начинающийся после блока параметров BIOS. Кроме того, последними двумя байтами являются 55h и AAh (сигнатура - признак загрузочного сектора логического диска).

Загрузочный код

Приступим к созданию нашего загрузчика. Первым делом надо загрузить весь код с дискеты в оперативную память, затем передать ему управление. При этом необходимо учесть, что мы не можем воспользоваться услугами операционной системы - можно использовать лишь низкоуровневые функции BIOS. Для работы с дисками существуют функции прерывания INT 13h, при вызове которых используются следующие регистры:

Код (Text):
  1.  
  2. AH - функция (0 - сброс, 2 - чтение сектора, 3 - запись сектора);
  3. AL - число секторов (для чтения или записи);
  4. BX - буфер памяти для ввода-вывода данных (в паре ES:BX);
  5. CH - номер дорожки;
  6. CL - номер начального сектора;
  7. DH - номер головки (для дискет - 0 или 1);
  8. DL - номер дисковода (0=А, 1=B).

В случае успешного завершения функции флаг переноса (CF) сбрасывается; при ошибке он установлен.

Работа с дисководом для гибких дисков имеет свои особенности; это довольно медленное устройство, дискеты можно вынимать, поэтому ошибки случаются сравнительно часто (например, при обращении к дисководу мотор не успевает "разогнаться"), поэтому операции чтения или записи при ошибках обычно повторяют по 3 раза, и лишь после трех последовательных неудач выдают сообщение об ошибке конечному пользователю.

Для наших целей мы обойдемся без использования файловой системы. Используем простую линейную модель: весь код у нас будет записан последовательно за загрузочным сектором - сектора 2-18 нулевой дорожки со стороны нулевой головки (в предположении, что используется стандартная дискета на 1,44 Мб), затем сектора 1-18 нулевой дорожки со стороны первой головки; далее переходим на первую дорожку со стороны нулевой головки и т.д. Считывание, естественно, должно производиться в том же порядке. При этом необходимо где-то записать количество секторов, которые необходимо таким образом считать; сохраним это значение в двух байтах после блока параметров BIOS (со смещением 3Eh). Не забудьте, что загрузочный сектор размещается в памяти, начиная с адреса 7C00; это значит, что в памяти число секторов, которые необходимо прочитать с дискеты, будет находиться по адресу 7C3E, а с адреса 7C40 будет код. По адресу же 7C00 должна находится команда безусловного перехода

Код (Text):
  1.  
  2. JMP 7C40

Для последовательного чтения секторов организуем цикл, после каждой итерации значение по адресу 7C3E будем уменьшать на 1, пока оно не достигнет 0 (это и будет сигналом выхода из цикла). Необходимо также предусмотреть сообщение об ошибке на случай трех последовательных безуспешных попыток чтения сектора; для этой цели используем незамысловатый текст "Read error" (необходимо помнить, что при начальной загрузке кодовая таблица с кириллицей в видеопамять не загружена, поэтому для сообщений нельзя использовать русский шрифт).

Итак, по адресу 7C40 размещается следующий код (все числа в коде - шестнадцатеричные):

Код (Text):
  1.  
  2. MOV AL,1
  3. MOV BX,7E00 ; разместить в памяти, начиная с адреса 7E00, т.е.    
  4.             ; следующие 512 байт после 7C00
  5. XOR CH,CH
  6. MOV CL,2 ; сектор 2 (1-й сектор уже считан)
  7. XOR DX,DX ; DH=0 (головка 0), DL=0 (дисковод 0,  
  8.             ; т.е. А:

Начальная инициализация произведена. Далее начинается цикл, в котором читается сектор (до трех попыток при ошибке). После успешного считывания данных номер сектора (AL) увеличивается на 1, и если он превысил значение 18, устанавливается равным 1, а номер головки (DH) увеличивается на 1. Аналогично номеру сектора, если номер головки превысит 1, устанавливается 0, и увеличивается номер дорожки (CH). Номер дорожки должен находиться в пределах 0-79, т.е. не должен достигать 80 (50h); если это произошло, выдается сообщение об ошибке. Значение счетчика считываемых секторов (по адресу 7C3E) уменьшается на 1, и если оно еще не достигло 0, цикл повторяется:

Код (Text):
  1.  
  2. LOOP: MOV SI,3
  3. REPEAT: MOV AH,2
  4.  INT 13
  5.  JNC OK
  6.  
  7.  ; Попадание сюда означает, что была ошибка чтения
  8.  DEC SI
  9.  JZ ERROR ; 3 безуспешные попытки
  10.  XOR AH,AH ;
  11.  INT 13 ; сброс дисковода для повторной попытки
  12.  JMP REPEAT
  13.  
  14. OK: ; Сюда попадаем, если не было ошибки чтения
  15.  INC CL
  16.  CMP CL,12
  17.  JNG NEXT
  18.  ; (Число секторов превысило 18):
  19.  MOV CL,1
  20.  INC DH
  21.  CMP DH,1
  22.  JNG NEXT
  23.  ; (Номер головки превысил 1):
  24.  XOR DH,DH
  25.  INC CH
  26.  CMP CH,50
  27.  JGE ERROR
  28.  
  29. NEXT: DEC WORD PTR [7C3E] ; уменьшить счетчик читаемых    
  30.                             ; секторов (по адресу 7C3E) на 1
  31.  JZ END ; если достигнут 0 - загрузка окончена    
  32.  
  33.  ; Сюда попадаем, если прочитаны не все сектора
  34.  ADD BX,200 ; следующий сектор разместить в памяти непосредственно после настоящего
  35.  JMP LOOP
  36.  
  37. END: JMP 7E00 ; все сектора прочитаны: управление      
  38.                     ; передается по адресу загрузки второго    
  39.      ; сектора

Осталось только реализовать вывод сообщения об ошибке. Для этого воспользуемся функцией 13h прерывания BIOS 10h. Данная функция выводит символьную строку. В регистрах должны быть следующие значения:

Код (Text):
  1.  
  2. AH - функция (13h);
  3. AL - один из 4 возможных сервисов (мы используем 3 - вывод символа с атрибутом и перевод курсора);
  4. BH - страница видеопамяти (обычно 0);
  5. BP - адрес выводимой строки (в паре ES:BP);
  6. CX - длина строки (в символах);
  7. DX - координаты на экране (DH - строка, DL - столбец).

Итак, ошибку обрабатывает следующий код:

Код (Text):
  1.  
  2. ERROR:MOV AH,13
  3.  MOV AL,3
  4.  MOV BH,0
  5.  MOV BP,(TEXT) ; адрес выводимой строки
  6.  MOV CX,0C
  7.  XOR DX,DX
  8.  INT 10

После вывода текста необходимо приостановить выполнение программы до того момента, пока пользователь не прочтет сообщение и явным образом не даст сигнал, что можно продолжать дальше. Обычно для этого используют запрос на ввод данных с клавиатуры через прерывание BIOS 16h. Выполнение программы при этом приостанавливается до тех пор, пока не будет нажата какая-нибудь клавиша на клавиатуре. Итак:

Код (Text):
  1.  
  2.  XOR AX,AX
  3.  INT 16

Программа завершилась, что дальше? Обычно в этом месте ставят команду возврата в операционную систему. Однако, в нашем случае это не имеет смысла - операционная система не загружена. Поэтому просто поставим команду перезагрузки системы:

Код (Text):
  1.  
  2.  INT 19

Если дискета оставлена в дисководе, наша программа загрузится, и будет выполнена снова; если ее вытащить, загрузится обычная ОС (DOS или Windows).

Пора приступить к вводу и ассемблированию программы; для этого, как я уже говорил, используем отладчик debug. Однако сначала нужно вместо текстовых меток расставить действительные значения адресов памяти, а сделать это можно только с помощью "двух проходов". При первом проходе вместо реальных значений адресов подставляются произвольные числа; необходимо только следить, чтобы они отличались от настоящих адресов не слишком радикально (особенно в случаях команд ближних условных переходов: их можно использовать лишь при переходах в пределах 128 байт). Составляем таблицу, в которую вписываем все метки, встречающиеся в нашей программе; по мере набора программы и "прохождения" соответствующих меток проставляем в таблице рядом с каждой меткой ее действительный адрес. Затем повторно вводим все те команды, которые содержали метки, уже с реальными значениями адресов.

В случае с "реальным" ассемблером всей этой канителью занимается компилятор. В нашем случае, поскольку вся эта работа была уже проделана автором, вы можете просто воспользоваться уже готовыми адресами, поверив мне на слово, что это действительно те самые адреса, которые нужны. Итак, начинаем работать. Щелкните на кнопке "Start" ("Старт") и выберите пункт "Run" ("Выполнить"). Наберите в командной строке debug. Откроется окно сеанса DOS с черточкой - приглашением отладчика debug. Программа должна начинаться у нас с адреса 7C00, поэтому набираем:

Код (Text):
  1.  
  2. a 7C00

В ответ появится что-то вроде

Код (Text):
  1.  
  2. 200E:7C00

Эти числа означают адрес вводимой команды в виде сегмент:смещение. Адрес сегмента в вашем случае будет другим, но это не играет роли. Внимание обращать следует лишь на смещение. Вводим нашу первую команду:

Код (Text):
  1.  
  2. JMP 7C40

После ввода каждой команды нажимаем <Enter>; в ответ debug выдаст адрес следующей вводимой команды. В нашем случае, после первой команды следует область блока параметров BIOS, которую следует пропустить. Для этого еще раз нажимаем <Enter>, затем, чтобы перейти к вводу команд с адреса 7C40, набираем:

Код (Text):
  1.  
  2. а 7C40

Вот листинг оставшейся части программы (учтите, что некоторые одинаковые команды могут обозначаться по-разному, например, JNC и JNB, JNG и JLE и др., так что здесь нет ошибки):

Код (Text):
  1.  
  2. 200E:7C40 MOV     AL,01
  3. 200E:7C42 MOV     BX,7E00
  4. 200E:7C45 XOR     CH,CH
  5. 200E:7C47 MOV     CL,02
  6. 200E:7C49 XOR     DX,DX
  7. 200E:7C4B MOV     SI,0003
  8. 200E:7C4E MOV     AH,02
  9. 200E:7C50 INT     13
  10. 200E:7C52 JNB     7C5D
  11. 200E:7C54 DEC     SI
  12. 200E:7C55 JZ      7C85
  13. 200E:7C57 XOR     AH,AH
  14. 200E:7C59 INT     13
  15. 200E:7C5B JMP     7C4E
  16. 200E:7C5D INC     CL
  17. 200E:7C5F CMP     CL,12
  18. 200E:7C62 JLE     7C76
  19. 200E:7C64 MOV     CL,01
  20. 200E:7C66 INC     DH
  21. 200E:7C68 CMP     DH,01
  22. 200E:7C6B JLE     7C76
  23. 200E:7C6D XOR     DH,DH
  24. 200E:7C6F INC     CH
  25. 200E:7C71 CMP     CH,50
  26. 200E:7C74 JGE     7C85
  27. 200E:7C76 DEC     WORD PTR [7C3E]
  28. 200E:7C7A JZ      7C82
  29. 200E:7C7C ADD     BX,0200
  30. 200E:7C80 JMP     7C4B
  31. 200E:7C82 JMP     7E00
  32. 200E:7C85 MOV     AH,13
  33. 200E:7C87 MOV     AL,03
  34. 200E:7C89 MOV     BH,00
  35. 200E:7C8B MOV     BP,7C9B
  36. 200E:7C8E MOV     CX,000D
  37. 200E:7C91 XOR     DX,DX
  38. 200E:7C93 INT     10
  39. 200E:7C95 XOR     AX,AX
  40. 200E:7C97 INT     16
  41. 200E:7C99 INT     19
  42. 200E:7C9B

По адресу 7C9B у нас должен находиться текст "Read error", который (в коде ASCII с добавленными байтами-атрибутами) вводится следующим образом:

Код (Text):
  1.  
  2. 200E:7C9B DW 752,765,761,764,720,765,772,772,76F,772
  3. 200E:7CAF

Наконец, в последних двух байтах первого сектора должна быть сигнатура. Нажимаем еще раз <Enter>, затем вводим:

Код (Text):
  1.  
  2. a 7CFE
  3. 200E:7CFE DB 55,AA
  4. 200E:7E00

Строго говоря, это еще не вся программа. Если вы вспомните, после загрузки всех секторов управление передается по адресу 7E00, а у нас по этому адресу ничего нет, да и никаких других секторов тоже нет. Тем не менее, мы можем сохранить этот кусок и использовать его в виде шаблона при создании других программ. В этом случае все, что нам потребуется, - это добавить код, начиная с адреса 7E00 - и независимо от того, сколько места он будет занимать, при записи на дискету наша программа сможет без всякой посторонней помощи сама себя загрузить и выполнить.

Сначала надо дать нашему шаблону имя, скажем, "template.bot". О расширении .bot немного позже, сейчас же присвоим это имя:

Код (Text):
  1.  
  2. n template.bot

И сохраним его; для этого сначала надо в регистр CX внести размер сохраняемой программы. Размер определяется вычитанием из последнего смещения, полученного нами при вводе программы, числа 100 (не забудьте, что все числа - шестнадцатеричные). В нашем случае это будет 7D00:

Код (Text):
  1.  
  2. r cx <Enter>

Выводится текущее значение регистра CX и двоеточие для ввода нового значения. Вводим:

Код (Text):
  1.  
  2. 7D00 <Enter>

Теперь записываем:

Код (Text):
  1.  
  2. w <Enter>
После успешной записи на экран будет выведено:
Код (Text):
  1.  
  2. Writing 7D00 bytes

Шаблон готов. Попробуем создать с его помощью простейшую тестовую программу. Скопируем файл шаблона под другим именем, например, "test.bot". Теперь откроем этот файл в отладчике debug. Это удобно сделать следующим образом. В файловом менеджере DOS (например, FAR или Norton Commander) перейдите в каталог, в котором вы сохранили файл "test.bot". В командной строке наберите:

Код (Text):
  1.  
  2. debug test.bot

Теперь по адресу 7E00 (набрав 'a 7E00') к этой программе можно добавить дополнительный код, например, такой:

Код (Text):
  1.  
  2. 200D:7E00 MOV AH,13
  3. 200D:7E02 MOV AL,3
  4. 200D:7E04 XOR BH,BH
  5. 200D:7E06 MOV BP,7E17
  6. 200D:7E09 MOV CX,9
  7. 200D:7E0C XOR DX,DX
  8. 200D:7E0E INT 10
  9. 200D:7E10 XOR AX,AX
  10. 200D:7E12 INT 16
  11. 200D:7E14 JMP 8000
  12. 200D:7E17 DW  753,765,763,774,76F,772,720,732,720
  13. 200D:7E29

Этот код аналогичен коду, выводящему сообщение об ошибке на экран. В данном случае выводится сообщение "Sector 2"; управление передается по адресу 8000h. Ценность этого небольшого фрагмента кода в том, что его можно с небольшой модификацией разместить в различных секторах и сделать так, чтобы он выводил на экран номер сектора, в котором данный код находится. Это позволяет проконтролировать, что сектора загружаются, и передают друг другу управление нормально. Так, в третьем секторе (по адресу 8000) можно набрать этот же код, только по адресу 8006 на этот раз должна быть команда 'MOV BP, 8017' (т.е. необходимо соответственно увеличивать этот адрес на 200h). По адресу 8014 можно вставить команду 'INT 19', чтобы завершить программу; если же нужно задействовать еще один сектор, надо просто передать ему управление, например, 'JMP 8200', и т.д. В области данных необходимо также произвести изменения в соответствии с номером сектора: в третьем секторе предпоследнее число (732) должно быть 733, в четвертом - 734 и т.д. Для 10 сектора меняются уже два последних числа - '731,730' (что соответствует символам '1' и '0'). Не забудьте после добавления кода сохранить программу, предварительно записав в регистр CX ее новый размер (по последнему смещению минус 100).

Программа готова, но запустить ее в таком виде нам не удастся. Начало нашей программы должно быть записано в загрузочном секторе, причем так, чтобы сохранить блок параметров BIOS. Дело не только в том, что штатными средствами операционной системы этого сделать нельзя; дело еще и в том, что с помощью отладчика debug можно ассемблировать и сохранять небольшие исполняемые файлы, но они будут в формате COM. Формат COM предполагает, что начало файла соответствует смещению 100h, и debug записывает программу на диск соответствующим образом. Поскольку наш загрузчик (как и вообще любой загрузчик) размещается в памяти, начиная с адреса 7С00, debug сохранит в нашем bot-файле перед началом собственно программы 7B00h байт "мусора". Поэтому придется создать вспомогательную программу wb.com (от "Write Boot"), чтобы сохраненный нами файл в "формате" .bot адекватным образом переписать на дискету.

Вспомогательная программа

Алгоритм действий следующий. При запуске программы wb.com ей в командной строке в качестве параметра передается название bot-файла, который необходимо скопировать на дискету. Далее открываем этот файл с использованием функции 3Dh прерывания DOS 21h, и сохраняем в памяти дескриптор открытого файла. Файловый указатель перемещаем в позицию 7B00h (пропуская "мусор" в начале файла). Считываем первые 512 байт (будущий загрузочный сектор), и сохраняем его в отдельном буфере, поскольку, во-первых, нам надо будет добавить туда реальный блок параметров BIOS, считанный из загрузочного сектора дискеты (иначе для повторного использования в DOS или Windows дискету придется форматировать заново), а во-вторых, по смещению 3E (непосредственно после блока параметров) нам необходимо поместить число записанных на дискету секторов (после их успешной записи). Для этой цели загрузочный сектор дискеты также считываем и сохраняем в отдельном буфере; данные блока параметров BIOS (смещения с 3h по 3Eh) копируем из второго буфера в первый. Далее организуем цикл:

  • считываем 512 байт из открытого нами bot-файла в третий буфер;
  • записываем данные из этого буфера в соответствующий сектор дискеты;
  • увеличиваем значение счетчика секторов по смещению 3Eh в первом буфере;
  • когда при очередной итерации будет прочитано 0 байт (конец файла), запишем данные из первого буфера (с окончательным значением счетчика записанных секторов) в загрузочный (первый) сектор дискеты.

Обработка ошибок в программе сведена к минимуму, но при использовании файловых операций совсем обойтись без вывода сообщений невозможно. Первым делом в начале работы программы выводится сообщение: "Вставьте в дисковод А: чистую дискету и нажмите любую клавишу". Поскольку вывод осуществляется посредством функции 40h прерывания DOS 21h, можно использовать русские буквы, а также символы перевода строки, возврата каретки, табуляции и т.д. Второе сообщение выводится при ошибке открытия файла (например, если неправильно указано имя файла): "Ошибка открытия файла". Сообщение "Ошибка чтения дискеты" используется для экономии сразу в двух случаях: при ошибке чтения bot-файла выводится лишь часть сообщения ("Ошибка чтения"), при ошибке чтения с гибкого диска - все сообщение. Последнее сообщение - "Ошибка записи" - для случая безуспешной попытки записи сектора на дискету. В области данных отводятся также три буфера по 512 байт для операций чтения-записи, о которых говорилось выше. И, наконец, два байта отводятся для сохранения дескриптора открытого файла.

Приступим к написанию кода. Область данных расположим в начале файла, поэтому первой командой будет безусловный переход для обхода этих данных:

Код (Text):
  1.  
  2. JMP INVITE

Условно обозначим наши данные следующими метками (впоследствии при вводе с помощью debug подставим вместо них реальные адреса; пока же вместо адреса будем ставить соответствующую метку в скобках): выводимые сообщения - TXT1, TXT2, TXT3 и TXT4 соответственно; FIRST - первый буфер (для считывания первых (после "мусора") 512 байт bot-файла, FDD - второй буфер (для считывания загрузочного сектора дискеты), BUF - третий буфер (для последовательного копирования секторов из bot-файла на дискету); HANDLE - 2 байта (слово) для хранения дескриптора открытого файла.

Непосредственно после данных расположим универсальную процедуру вывода сообщений на экран. Для этой цели используем функцию 40h прерывания DOS 21h. При вызове этой функции в регистрах должны находится следующие значения:

Код (Text):
  1.  
  2. AH - функция (40h);
  3. BX - устройство вывода (1 - экран, 3 - внешнее устройство, 4 - печать);
  4. CX - максимальное число байтов;
  5. DX - адрес области данных.

Для каждого сообщения устанавливаются свои значения числа выводимых символов и адрес начала текста, общая же часть кода выглядит следующим образом:

Код (Text):
  1.  
  2. TXT_OUT: MOV AH,40
  3.   MOV BX,1
  4.   INT 21
  5.   XOR AX,AX
  6.   INT 16
  7.   CMP DX,(TXT1) ; проверка, не выводится ли 1-е      
  8.                   ; сообщение
  9.   JE OPEN_FILE ; если да - открыть файл
  10. ; Сюда попадаем, если выводится одно из сообщений об ошибке
  11.   JMP OUT ; выход из программы

Далее следуют специфические для каждого выводимого сообщения данные:

Код (Text):
  1.  
  2. INVITE: MOV DX,(TXT1) ; адрес первого сообщения
  3.   MOV CX,3F  
  4.   JMP TXT_OUT  ; вывести строку
  5. FILE_ERR: MOV DX,(TXT2) ; при ошибке открытия файла
  6.   MOV CX,17
  7.   JMP TXT_OUT
  8. READ_ERR: MOV DX,(TXT3) ; при ошибке чтения bot-файла
  9.   MOV CX,0E  ; вывод лишь части TXT3
  10.   JMP TXT_OUT
  11. READ_FDD: MOV DX,(TXT3) ; при ошибке чтения дискеты
  12.   MOV CX,17  ; вывод всего TXT3
  13.   JMP TXT_OUT
  14. WRITE_ERR: MOV DX,(TXT4) ; при ошибке записи на дискету
  15.   MOV CX,0F
  16.   JMP TXT_OUT

Для открытия файла используется функция 3Dh прерывания DOS 21h. В регистрах должны находиться следующие значения:

Код (Text):
  1.  
  2. AH - функция (3Dh);
  3. AL - код доступа (0 - для чтения, 1 - для записи, 2 - для чтения и записи);
  4. DX - адрес строки с именем файла в ASCIIZ-формате.

В случае ошибки устанавливается флаг переноса (CF); при успешном открытии файла он сброшен, а в регистре AX находится дескриптор открытого файла - по нему впоследствии можно обращаться к этому файлу при операциях чтения или записи.

ASCIIZ-формат представляет собой строку в кодировке ASCII, завершающуюся двоичным нулем. Имя bot-файла будет передаваться в командной строке; как получить к нему доступ? Здесь нам придется использовать так называемый префикс программного сегмента, который операционная система размещает в памяти перед каждой COM- или EXE- программой при ее запуске. Префикс программного сегмента имеет начальное смещение 0 и размер 256 (100h) байт (именно поэтому COM-файлы начинаются со смещения 100h). Начиная со смещения 80h в префиксе программного сегмента располагается область, называемая буфером передачи данных (DTA). В первом байте этого буфера размещается длина строки параметров программы. Начиная со второго байта размещаются введенные символы (если таковые имеются), а затем следует всевозможный "мусор".

Таким образом, в нашем случае после имени программы (wb.com) будет следовать пробел, затем имя bot-файла - по смещению 80h будет число, на 1 превышающее число букв в имени файла. Само имя начинается со смещения 82h. Этот адрес можно записать в регистр DX для функции открытия файла; однако сначала надо в конце имени файла (по смещению 81h + число символов в имени, т.е. число, хранящееся по адресу [80h]) поместить 0. Такую несколько громоздкую конструкцию закодируем следующим образом:

Код (Text):
  1.  
  2. OPEN_FILE: XOR BX,BX
  3.   MOV BL,[80] ; в BX - число по адресу 80h, т.е.      
  4.               ; число введенных символов      
  5.      ; (вместе с пробелом)
  6.   XOR AX,AX
  7.   MOV [BX+81],AX ; поместить 0 по адресу, равному
  8.                   ; сумме 81 и числа, хранящегося в BX - т.е. в конец имени файла
  9.   MOV AH,3D
  10. ; в AL уже находится 0 - открываем файл для чтения
  11.   MOV DX,82 ; адрес начала имени файла (без      
  12.               ; пробела) в DTA
  13.   INT 21
  14.   JC FILE_ERR
  15.   MOV [HANDLE],AX ; сохранить дескриптор файла

Следующим действием необходимо установить файловый указатель на значение 7B00h байт от начала файла. Для управления файловым указателем предназначена функция 42h прерывания DOS 21h. В регистрах должны быть следующие значения:

Код (Text):
  1.  
  2. AH - функция (42h);
  3. AL - точка отсчета смещения (0 - от начала файла, 1 - от текущего значения файлового указателя, 2 - от конца файла);
  4. BX - дескриптор файла;
  5. CX:DX - смещение в байтах (DX - младшее слово, CX - старшее).

При ошибке, как обычно, устанавливается флаг переноса (CF).

Итак, продолжаем:

Код (Text):
  1.  
  2. MOV AH,42
  3. XOR AL,AL
  4. MOV BX,[HANDLE]
  5. XOR CX,CX
  6. MOV DX,7B00  
  7. INT 21
  8. JC READ_ERR

Дальше нам нужно прочитать первые 512 байт bot-файла. Для чтения файла используется функция 3Fh прерывания DOS 21h. В регистрах должны содержаться:

Код (Text):
  1.  
  2. AH - функция (3Fh);
  3. BX - дескриптор файла;
  4. CX - число байтов для чтения;
  5. DX - адрес области ввода.

При успешном выполнении функции флаг переноса CF сбрасывается, а в регистре AX содержится число действительно прочитанных байтов. Если это число равно 0, достигнут конец файла. Итак:

Код (Text):
  1.  
  2. MOV AH,3F
  3. ; дескриптор файла сохранился в регистре BX после операции  
  4. ; установки файлового указателя
  5. MOV CX,200
  6. MOV DX,(FIRST) ; адрес первого буфера
  7. INT 21
  8. JC READ_ERR

Следующим действием считаем первый (загрузочный) сектор дискеты. Для чтения физических секторов необходимо использовать низкоуровневые функции прерывания BIOS 13h. Мы уже проделывали это при создании файла "template.bot", поэтому это не должно вызвать проблем:

Код (Text):
  1.  
  2.  MOV SI,3 ; 3 попытки при ошибках
  3. RETRY:MOV AH,2
  4.  MOV AL,1 ; один сектор
  5.  MOV BX,(FDD); адрес второго буфера
  6.  XOR CH,CH ; дорожка 0
  7.  MOV CL,1 ; сектор 1
  8.  XOR DX,DX ; диск А: (0), головка 0
  9.  INT 13
  10.  JNC OK
  11.  ; Сюда попадаем при ошибке чтения
  12.  XOR AH,AH ; функция 0 - сброс дисковода
  13.  DEC SI
  14.  CMP SI,0
  15.  JE READ_FDD
  16. ; Сюда попадаем, если нужно еще раз попытаться прочесть сектор
  17.  INT 13
  18.  JMP RETRY

Если чтение загрузочного сектора с дискеты было успешным, необходимо скопировать блок параметров BIOS из второго сектора (FDD) в первый (FIRST). Для побайтного копирования данных из одной области памяти в другую используется инструкция MOVSB в сочетании с префиксом REP. Команда MOVSB копирует байт по адресу DS:SI в новое место по адресу ES:DI, при этом значения SI и DI после пересылки байта изменяются на 1: при сброшенном (0) флаге направления DF - увеличиваются на 1, при установленном (1) - уменьшаются на 1. Префикс REP заставляет команду MOVSB повторяться столько раз, сколько записано в регистре CX (при каждом повторе значение CX уменьшается на 1). Таким образом, в CX должно быть записано число байтов, которые необходимо скопировать из одного места в другое.

Код (Text):
  1.  
  2. MOV SI,(FDD)+3 ; исходный адрес - смещение 3 от начала    
  3.                 ; второго буфера (FDD)
  4. MOV DI,(FIRST)+3 ; конечный адрес - смещение 3 от начала    
  5.                     ; первого буфера (FIRST)
  6. CLD   ; сбросить флаг направления (SI и DI будут    
  7.             ; возрастать
  8. MOV CX,3B  
  9. REP MOVSB  
  10. MOV WORD PTR [(FIRST)+3E],1 ; установить в счетчике секторов
  11.                             ; (по смещению 3Eh в первом буфере) значение 1

Можно считать, что первый сектор скопирован (хотя пока еще не записан физически на дискету). Для копирования оставшихся секторов организуется цикл, в котором в один и тот же буфер BUF производится сначала чтение очередных 512 байт из bot-файла с использованием уже знакомой нам функции 3Fh прерывания DOS 21h, а затем запись этих данных в соответствующий сектор на дискете уже с использованием низкоуровневой функции 3 прерывания BIOS 13h. Операция записи физического сектора аналогична операции чтения сектора; как и в случае чтения секторов в программе "template.bot", необходимо организовать смену головки и увеличение номера дорожки по мере заполнения секторов. Попытки записи также повторяются по 3 раза. Небольшая сложность лишь в том, что чередуются процесс чтения с использованием функции DOS и процесс записи сектора с использованием функции BIOS; необходимо сохранять в стеке текущие значения регистров для сектора, дорожки и головки (регистры CX и DX), а затем восстанавливать их оттуда. Код выглядит следующим образом:

Код (Text):
  1.  
  2.  MOV CL,2 ; сектор 2,
  3.  XOR CH,CH ; дорожка 0
  4.  XOR DH,DH ; головка 0
  5. LOOP: PUSH CX ; сохранить в стеке текущие значения дорожки    
  6.                 ; (CH), сектора (CL)
  7.  PUSH DX ; и головки (DH)
  8.  MOV AH,3F
  9.  MOV BX,[HANDLE] ; загрузить дескриптор файла
  10.  MOV CX,200
  11.  MOV DX,(BUF); адрес буфера ввода-вывода
  12.  INT 21
  13.  JNC M1
  14.  JMP READ_ERR
  15. M1:

Последняя конструкция - как раз тот случай, когда переход по адресу READ_ERR превысил 128 байт, и пришлось использовать сочетание условного и безусловного переходов. При ближних переходах можно использовать просто 'JC READ_ERR'.

Код (Text):
  1.  
  2. M1: CMP AX,00 ; проверка на конец файла
  3.  JE WRITE_FIRST ; если конец - перейти на запись      
  4.                  ; первого сектора (выход из цикла)
  5.  
  6. ; Очередные данные считаны, конец файла не достигнут,  
  7. ; продолжаем
  8.  POP DX ; восстановить сохраненные ранее значения
  9.  POP CX ; дорожки, сектора и головки.    
  10.  MOV SI,3 ; 3 раза для повторов
  11. REWRITE: MOV AH,3
  12.  MOV AL,1 ; один сектор
  13.  MOV BX,(BUF); адрес буфера
  14.  XOR DL,DL ; диск А: (0)
  15.  INT 13
  16.  JNC OK2
  17.  ; Сюда попадаем, если была ошибка записи
  18.  XOR AH,AH
  19.  DEC SI
  20.  CMP SI,0
  21.  JE WRITE_ERR; если да - выдать сообщение об ошибке
  22.  ; Сюда попадаем, если необходимо повторить попытку записи
  23.  INT 13
  24.  JMP REWRITE ; повтор попытки записи
  25.  
  26. OK2: ; Очередной сектор был успешно записан
  27.  INC CL
  28.  CMP CL,12
  29.  JNG NEXT
  30. ; номер сектора превысил 18:
  31.  MOV CL,1 ; установить сектор 1
  32.  INC DH ; увеличить номер головки
  33.  CMP DH,1
  34.  JNG NEXT
  35. ; номер головки превысил 1:
  36.  XOR DH,DH ; установить головку 0
  37.  INC CH ; увеличить номер дорожки
  38.  CMP CH,50
  39.  JG WRITE_ERR
  40.  
  41. NEXT: INC WORD PTR [(FIRST)+3E] ; увеличить счетчик  
  42.                                     ; записанных секторов (по смещению 3Eh от начала первого буфера
  43.  JMP LOOP
  44.  
  45. WRITE_FIRST: ; все данные из bot-файла переписаны в  
  46.                 ; соответствующие сектора на дискете
  47.     ; число записанных секторов
  48.     ; сохранено в первом буфере. Необходимо записать лишь сам  
  49.     ; первый сектор. Процедура записи аналогична рассмотренной.
  50.  MOV SI,3
  51. N3: MOV AH,3
  52.  MOV AL,1
  53.  MOV BX,(FIRST)
  54.  XOR DX,DX ; диск А: (0), головка 0
  55.  XOR CH,CH ; дорожка 0
  56.  MOV CL,1 ; сектор 1
  57.  INT 13
  58.  JNC OUT ; если запись успешна - выход
  59.  ; Ошибка записи:
  60.  XOR AH,AH
  61.  DEC SI
  62.  CMP SI,0
  63.  JNE N2
  64.  JMP WRITE_ERR ; число повторов = 0 - ошибка
  65. N2: INT 13 ; сброс дисковода
  66.  JMP N3 ; повтор попытки записи
  67.  
  68. OUT: ; Выход из программы. Необходимо закрыть файл, если он  
  69.         ; был открыт
  70.  CMP WORD PTR [HANDLE],0 ; если файл был открыт,        
  71.                          ; дескриптор файла не равен 0
  72.  JE END
  73. ; Файл был открыт - необходимо его закрыть
  74.  MOV AH,3E  
  75.  MOV BX,[HANDLE]
  76.  INT 21  
  77. END: MOV AH,4C
  78.  INT 21

Чтобы приступить к вводу программы с использованием debug, необходимо, как и в случае с программой "template.bot", вычислить все адреса и подставить их вместо меток. Разница же между двумя программами в том, что на этот раз мы создаем com-программу, поэтому вводить надо начинать со смещения 100h (команда отладчика 'a 100'):

Код (Text):
  1.  
  2. 2039:0100 JMP     0795
  3. 2039:0103

Вводим текст первого сообщения (оно начинается со смещения 103h):

Код (Text):
  1.  
  2. 2039:0103 DB " Вставьте в дисковод А: чистую дискету и нажмите любую клавишу",0D,0A
  3. 2039:0142

Второе сообщение начинается со смещения 142h:

Код (Text):
  1.  
  2. 2039:0142 DB "Ошибка открытия файла",0D,0A
  3. 2039:0159

Третье сообщение со смещения 159h:

Код (Text):
  1.  
  2. 2039:0159 DB "Ошибка чтения дискеты",0D,0A
  3. 2039:0170

Четвертое сообщение со смещения 170h:

Код (Text):
  1.  
  2. 2039:0170 DB "Ошибка записи",0D,0A
  3. 2039:017F

Далее должен следовать наш первый буфер (FIRST), его начальным смещением будет 17Fh, а его размер равен 512 (200h) байт, поэтому адресом второго буфера (FDD) будет смещение 37Fh, а третьего (BUF) - 57Fh. После него по смещению 77Fh будут два байта для дескриптора файла (HANDLE); здесь вначале должно быть число 0. Набираем 'a 77E <Enter>':

Код (Text):
  1.  
  2. 2039:077E DW 0
  3. 2039:0781

Код начинается со смещения 781h:

Код (Text):
  1.  
  2. 2039:0781 MOV     AH,40
  3. 2039:0783 MOV     BX,0001
  4. 2039:0786 INT     21
  5. 2039:0788 XOR     AX,AX
  6. 2039:078A INT     16
  7. 2039:078C CMP     DX,0103
  8. 2039:0790 JZ      07BD
  9. 2039:0792 JMP     08AA
  10. 2039:0795 MOV     DX,0103
  11. 2039:0798 MOV     CX,003F
  12. 2039:079B JMP     0781
  13. 2039:079D MOV     DX,0142
  14. 2039:07A0 MOV     CX,0017
  15. 2039:07A3 JMP     0781
  16. 2039:07A5 MOV     DX,0159
  17. 2039:07A8 MOV     CX,000E
  18. 2039:07AB JMP     0781
  19. 2039:07AD MOV     DX,0159
  20. 2039:07B0 MOV     CX,0017
  21. 2039:07B3 JMP     0781
  22. 2039:07B5 MOV     DX,0170
  23. 2039:07B8 MOV     CX,000F
  24. 2039:07BB JMP     0781
  25. 2039:07BD XOR     BX,BX
  26. 2039:07BF MOV     BL,[0080]
  27. 2039:07C3 XOR     AX,AX
  28. 2039:07C5 MOV     [BX+0081],AX
  29. 2039:07C9 MOV     AH,3D
  30. 2039:07CB MOV     DX,0082
  31. 2039:07CE INT     21
  32. 2039:07D0 JB      079D
  33. 2039:07D2 MOV     [077F],AX
  34. 2039:07D5 MOV     AH,42
  35. 2039:07D7 XOR     AL,AL
  36. 2039:07D9 MOV     BX,[077F]
  37. 2039:07DD XOR     CX,CX
  38. 2039:07DF MOV     DX,7B00
  39. 2039:07E2 INT     21
  40. 2039:07E4 JB      07A5
  41. 2039:07E6 MOV     AH,3F
  42. 2039:07E8 MOV     CX,0200
  43. 2039:07EB MOV     DX,017F
  44. 2039:07EE INT     21
  45. 2039:07F0 JB      07A5
  46. 2039:07F2 MOV     SI,0003
  47. 2039:07F5 MOV     AH,02
  48. 2039:07F7 MOV     AL,01
  49. 2039:07F9 MOV     BX,037F
  50. 2039:07FC XOR     CH,CH
  51. 2039:07FE MOV     CL,01
  52. 2039:0800 XOR     DX,DX
  53. 2039:0802 INT     13
  54. 2039:0804 JNB     0812
  55. 2039:0806 XOR     AH,AH
  56. 2039:0808 DEC     SI
  57. 2039:0809 CMP     SI,+00
  58. 2039:080C JZ      07AD
  59. 2039:080E INT     13
  60. 2039:0810 JMP     07F5
  61. 2039:0812 MOV     SI,0382
  62. 2039:0815 MOV     DI,0182
  63. 2039:0818 CLD
  64. 2039:0819 MOV     CX,003B
  65. 2039:081C REPZ
  66. 2039:081D MOVSB
  67. 2039:081E MOV     WORD PTR [01BD],0001
  68. 2039:0824 MOV     CL,02
  69. 2039:0826 XOR     CH,CH
  70. 2039:0828 XOR     DH,DH
  71. 2039:082A PUSH    CX
  72. 2039:082B PUSH    DX
  73. 2039:082C MOV     AH,3F
  74. 2039:082E MOV     BX,[077F]
  75. 2039:0832 MOV     CX,0200
  76. 2039:0835 MOV     DX,057F
  77. 2039:0838 INT     21
  78. 2039:083A JNB     083F
  79. 2039:083C JMP     07A5
  80. 2039:083F CMP     AX,0000
  81. 2039:0842 JZ      0887
  82. 2039:0844 POP     DX
  83. 2039:0845 POP     CX
  84. 2039:0846 MOV     SI,0003
  85. 2039:0849 MOV     AH,03
  86. 2039:084B MOV     AL,01
  87. 2039:084D MOV     BX,057F
  88. 2039:0850 XOR     DL,DL
  89. 2039:0852 INT     13
  90. 2039:0854 JNB     0865
  91. 2039:0856 XOR     AH,AH
  92. 2039:0858 DEC     SI
  93. 2039:0859 CMP     SI,+00
  94. 2039:085C JNZ     0861
  95. 2039:085E JMP     07B5
  96. 2039:0861 INT     13
  97. 2039:0863 JMP     0849
  98. 2039:0865 INC     CL
  99. 2039:0867 CMP     CL,12
  100. 2039:086A JLE     0881
  101. 2039:086C MOV     CL,01
  102. 2039:086E INC     DH
  103. 2039:0870 CMP     DH,01
  104. 2039:0873 JLE     0881
  105. 2039:0875 XOR     DH,DH
  106. 2039:0877 INC     CH
  107. 2039:0879 CMP     CH,50
  108. 2039:087C JLE     0881
  109. 2039:087E JMP     07B5
  110. 2039:0881 INC     WORD PTR [01BD]
  111. 2039:0885 JMP     082A
  112. 2039:0887 MOV     SI,0003
  113. 2039:088A MOV     AH,03
  114. 2039:088C MOV     AL,01
  115. 2039:088E MOV     BX,017F
  116. 2039:0891 XOR     DX,DX
  117. 2039:0893 XOR     CH,CH
  118. 2039:0895 MOV     CL,01
  119. 2039:0897 INT     13
  120. 2039:0899 JNB     08AA
  121. 2039:089B XOR     AH,AH
  122. 2039:089D DEC     SI
  123. 2039:089E CMP     SI,+00
  124. 2039:08A1 JNZ     08A6
  125. 2039:08A3 JMP     07B5
  126. 2039:08A6 INT     13
  127. 2039:08A8 JMP     088A
  128. 2039:08AA CMP     WORD PTR [077F],+00
  129. 2039:08AF JZ      08B9
  130. 2039:08B1 MOV     AH,3E
  131. 2039:08B3 MOV     BX,[077F]
  132. 2039:08B7 INT     21
  133. 2039:08B9 MOV     AH,4C
  134. 2039:08BB INT     21
  135. 2039:08BD <Enter>

Дадим программе имя: 'n wb.com <Enter>', укажем ее размер - 'r cx <Enter>' и '7BD <Enter>', затем сохраним: 'w <Enter>'. Если при вводе не было сделано ошибок, программа должна сразу заработать в вывести на экран первое сообщение. Первое условие, однако, маловероятно, поэтому, скорее всего, программу придется отлаживать. Отладка больших (сравнительно) программ на ассемблере - занятие неблагодарное, особенно без специальных инструментальных средств; поэтому имеет смысл наращивать нашу программу "кусками", на каждом этапе проверяя набранное и добиваясь работоспособности программы. Для этого можно завершающий фрагмент кода ('MOV AH,4C' и 'INT 21') ставить после набора очередного смыслового блока, сохранить в таком виде и попробовать запустить (при этом не надо забывать подставлять в соответствующих местах вместо ссылок вперед на несуществующий еще код адрес этого завершающего фрагмента).

В частности, подобную процедуру можно проделать, набрав данные и процедуру вывода сообщений (до адреса 7BDh), затем набрав процедуру открытия файла (до адреса 7D5h), затем после перемещения файлового указателя (до адреса 7E6h), после чтения загрузочного сектора (до адреса 812h), после цикла копирования секторов (до адреса 887h). На каждом этапе "своя" часть функциональности должна быть обеспечена. В любом случае должно выводиться начальное сообщение. Затем, если в командной строке не было указано имя существующего файла (при необходимости с полным путем к нему), должно выводиться сообщение "Ошибка открытия файла". После реализации чтения загрузочного сектора должно появиться обращение к дисководу и т.д.

Для отладки используем тот же debug, для этого, собственно, он и предназначен. Чтобы вывести ассемблированный код, необходимо набрать 'u [адрес] <Enter>' и сравнить введенный код с тем, который должен быть. Особенно тщательно следует следить за соответствием адресов ссылок и данных. При попытке дизассемблировать область данных мы получим бессмыслицу - для отображения данных служит команда 'd [адрес] <Enter>', данные отображаются в виде шестнадцатиричных чисел.

Обработка ошибок в нашей программе сведена к минимуму, поэтому она требует корректного к себе обращения. При запуске вместе с названием файла wb (расширение указывать не обязательно) через один пробел должно следовать название bot-файла. Необходмо помнить, что формат загружаемого файла нашей программой не проверяется; она с одинаковым успехом сможет открыть файл любого формата - и txt, и bmp, и jpg, и exe и т.д. - и скопирует его на дискету, убрав начальные 7B00h байтов.

Отладка программы wb.com - это только полдела; она может успешно записать загрузочный сектор дискеты, а вот то, что мы туда записываем, само может потребовать отладки. Для этой цели и предназначается относительно простая программа "test.bot". Сначала эту программу можно составить так, чтобы в ней был всего один дополнительный сектор. После записи "test.bot" на дискету с помощью wb.com следует перезагрузить компьютер, оставив дискету в дисководе. При этом последовательность загрузки с помощью BIOS Setup должна быть установлена в порядке "A, C". Если надпись "Sector 2" отображается, можно добавить еще несколько секторов и проверить их работу. Данный шаг позволит убедиться, что программа wb.com правильно записывает последовательность секторов на дискету, а bot-программа - правильно их считывает.

Изменения в реестре

Если все работает нормально, можно несколько повысить для себя уровень сервиса при работе с этими программами. Для начала разместим wb.com в каталоге Windows (например, C:\WINDOWS). Теперь wb можно набирать, как обычную команду операционной системы, и она будет работать независимо от каталога, в котором мы находимся. Чтобы еще больше облегчить себе работу, создадим ассоциацию bot-файлов с программой wb.com. Желающие могут воспользоваться для этой цели проводником Windows, а для любителей программирования ;) я привожу здесь соответствующий reg-файл.

Откройте "Блокнот" Windows и наберите в нем следующий текст:

Код (Text):
  1.  
  2. REGEDIT4
  3.  
  4. [HKEY_CLASSES_ROOT\.bot]
  5. @="botfile"
  6.  
  7. [HKEY_CLASSES_ROOT\botfile\Shell]
  8. @=""
  9.  
  10. [HKEY_CLASSES_ROOT\botfile\Shell\open]
  11. @=""
  12.  
  13. [HKEY_CLASSES_ROOT\botfile\Shell\open\command]
  14. @="C:\\WINDOWS\\wb.com %1"

В последней строке вы должны указать каталог Windows на своей машине, например, C:\\Win98\\wb.com, если у вас Windows размещена в каталоге Win98. Обратите внимание, что обратная косая черта должна быть продублирована. Еще раз тщательно проверьте правильность всех данных и сохраните файл с расширением .reg, например, "bot.reg". Из проводника Windows дважды щелкните на названии этого файла; появится запрос на подтверждение изменений в реестре. Щелкните "OK".

Теперь, если не было сделано ошибок, при двойном щелчке на имени файла с расширением .bot откроется окно DOS и появится знакомое нам приглашение вставить чистую дискету в дисковод А:. Вставив дискету и нажав любую клавишу, мы получим загрузочную дискету. Остается только пожелать успеха в ваших экспериментах с загрузочным сектором. © Roustem


0 1.540
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532