Uploaded by Иван Иванов

20 уроков Ассемблера (2018)

advertisement
Семён Леонидович Углев
20 уроков Ассемблера
SelfPub; 2018
2
Аннотация
Уроки Ассемблера x86 под DOS от основ до создания антивируса. Основной упор
автор делает на быстрое изучение языка и конкретные примеры. Рекомендуется всем, кто
хочет быстро изучить Ассемблер.
Ассемблер
Урок 1. Вступление
Почему ассемблер? Перечитав энное количество книг, мне стало понятно, что
хорошего самоучителя для изучения данного языка мне не найти. У каждого автора имеются
свои плюсы и минусы в изложении; ряд авторов начинают с классических вещей – вводной
лекции, теории, изучения переменных и операторов. Другие же начинают сразу с заумных
вещей, публикуя "тяжеленные" тексты программ. Часть авторов изучают ассемблер в связке
с языками высокого уровня. И лишь небольшое их количество идёт "заочным" путём –
опуская заметную часть сложных операторов и объясняя простые; впрочем, разъясняя
сложные операторы в дальнейшем. Так поступим и мы. Задача блога – быстро научить
читателя программированию на ассемблере, и дать ему возможность самостоятельно изучать
материал уже имея за плечами накопленные знания.
Что для этого нужно? Для начала – установите соответствующую операционную
систему семейства Windows (мне, например, пришлось ради такого случая установить её в
качестве виртуальной машины) – 98, 2000, XP (подойдёт и Vista). Далее – соответствующий
набор инструментов, я использую Far manager в связке с плагином Colorer. Также
понадобится сам ассемблер masm, отладчик AFDPRO и справочная информация HELP.EXE.
Все исполняемые файлы, за исключением LINK.EXE, я копирую в папку ../Far2 – чтобы
из любого места можно было компилировать созданные текстовые файлы. LINK.EXE лучше
копировать в папку с будущей программой.
Если всё прошло успешно, вы должны получить такого рода окна:
Просмотр исходного текста на языке assembler через Far (F4)
3
Запуск файла помощи HELP.EXE
Урок 2. Первая программа
Итак, вот как будет выглядеть наша первая программа.
Не обращайте внимания на излишние комментарии справа от каждого оператора. Это
справочная подробная информация для желающих детально разобраться, что происходит в
программе. Мы же просто хотим вывести на экран MS-DOS строку «Hello, world!»
с помощью программы типа .com. Для вывода текста мы будем использовать функцию 9
прерывания 21h.
Сама программа будет выглядеть примерно так:
Суть достаточно проста.
а. Командой mov ah,9 мы загружаем в регистра ah число 9. На языке "Бейсик" это
выглядело бы примерно так: LET A=9. Следует отметить в данном случае, что ah
предназначен "для служебного пользования" – в частности, использования тех же функций.
Также стоит отметить, что на самом деле ah – это старший байт регистра ax, состоящего на
самом деле из двух частей – старшего байта (ah) и младшего (al). Аналогично работают и
остальные служебные регистры (bx, cx, dx).
b. Командой mov dx,offset helloworld мы загружаем фразу "Hello, world!". Однако вся
фраза, конечно, не поместится в регистр, поэтому мы используем приставку "offset" –
смещение. Грубо говоря, это адрес, указывающий компилятору, где на самом деле находится
эта фраза. Определим это в конце программы.
c. int 21h – данной командой осуществляем прерывание, то есть собственно вывод
текста на экран.
Узнаем об этом поподробнее.
1. Открываем файл "HELP.EXE".
2. Нажимаем любую клавишу.
3. Заходим в раздел "Указатель функций DOS/BIOS".
4. Заходим в раздел "Прерывания DOS".
5. Заходим в раздел "INT 21H".
Последовательность 1-5 в дальнейшем будет обозначена нами так: HELP.EXE ->
Указатель функций DOS/BIOS -> Прерывания DOS -> INT 21H
Как стало видно из текста, мы действительно должны загрузить в регистр ah номер
желаемой функции (в нашем случае -"9"), и выполнить прерывание (int 21h). Всё просто.
Однако, конечно, вышеприведённый текст – не вся программа. Она будет иметь
определённые признаки оформления, которые мы будем использовать во всех примерах.
Начинаться любая наша программа будет так:
4
А заканчиваться так:
В середине и будет располагаться наш текст, а чуть ниже него ещё два элемента:
– int 20h – завершающее программу прерывание, выход в DOS. Если его не указать,
программа выполнит свою работу и "зависнет".
– helloworld db 'Hello, world!$' – собственно определение переменной helloworld.
Директива db (define byte) определяет область памяти, доступную побайтно. Фраза
"Hello, world!" указана в одинарных кавычках, а за знаком "!" указан знак "$" – конец строки.
Почему так, поясним чуть позже. Что ещё следует знать, что определяем переменные мы в
самом конце, чтобы при ассемблировании их не приняли за команды ассемблера. Итак,
полностью оформленный текст программы будет выглядеть чуть более обширно.
Совет: разберитесь со структурой файла помощи HELP.EXE!
Урок 3. Системы счисления
Итак, продолжим изучение языка. Опубликую текст программы ещё раз:
5
Связка mov ah,9 и int 21h и есть по сути одна команда, если сравнивать с языками
высокого уровня, конечно. Однако на языке ассемблера первая указанная команда
называется функцией, а последняя – прерыванием. Прерывание выполняет команду с
заданной функцией. Есть прерывания и без функций, например, то же int 20h. Возможно,
пока это несколько сложно для восприятия, но такую форму составления программы
необходимо запомнить. В этом же примере мы столкнулись и с другими ключевыми
понятиями ассемблера, такими, как регистры и шестнадцатеричная система счисления.
Начнём по порядку.
Десятичная и шестнадцатеричная системы счисления
Так получилось, что в современных компьютерах минимальной единицей памяти
является 8-битный байт, значения которого удобно записывать двумя шестнадцатеричными
цифрами. Для обозначения шестнадцатеричного числа мы будем использовать букву "h",
которую будем ставить позади такой цифры. Это обозначение общепринятое, хотя
некоторые платформы, например мой любимый ZX Spectrum, в своих ассемблерах
использовали запись вида #05B3. Ноль впереди обозначается "ведущим", так как число
#05B3 = #5B3, и служит для удобочитаемости и называется "выравниванием" (обычно
выравнивают до одного или двух байт: #05B3). Нам необходимо научиться переводить числа
из одной системы счисления в другую. Для этой цели прекрасно служит стандартный
калькулятор "Windows" в инженерном режиме. Однако, если под рукой калькулятора нет,
преобразование чисел, представленных в двоичной и шестнадцатеричной системах
счисления, в десятичную выполнить довольно легко. Для этого необходимо записать число в
развёрнутой форме и вычислить его значение.
Перевод числа из двоичной системы в десятичную. Возьмём любое двоичное число,
например 10,112. Запишем его в развёрнутой форме и произведём вычисления:
10,112 = 1 × 21 + 0 × 20 + 1 × 2-1 + 1 × 2-2 = 1 × 2 + 0 × 1 + 1 × 1/2 + 1 × 1/4 = 2,7510.
Перевод чисел из шестнадцатеричной системы в десятичную. Возьмём любое
шестнадцатеричное число, например 19F 16. Запишем его в развёрнутой форме (при этом
необходимо помнить, что шестнадцатеричная цифра F соответствует десятичному числу 15)
и произведём вычисления:
19F16 = 1 × 162 + 9 × 161 + F × 160 = 1 × 256 + 9 × 16 + 15 × 1 = 41510.
Алгоритм перевода целых десятичных чисел в двоичную систему счисления.
Пусть Ацд – целое десятичное число. Запишем его в виде суммы степеней основания 2 с
двоичными коэффициентами. В его записи в развёрнутой форме будут отсутствовать
отрицательные степени основания (числа 2):
Ацд = аn-1 × 2n-1 + аn-2 × 2n-2 + … + а1 × 21 + а0 × 20.
На первом шаге разделим число Ацд на основание двоичной системы, то есть на 2.
Частное от деления будет равно
аn-1 × 2n-2 + аn-2 × 2n-3 + … + а1 ,
а остаток – равен a0.
6
На втором шаге целое частное опять разделим на 2, остаток от деления будет теперь
равен a1.
Если продолжать этот процесс деления, то после n-го шага получим
последовательность остатков:
а0 , а1 , … , аn-1.
Легко заметить, что их последовательность совпадает с обратной последовательностью
цифр целого двоичного числа, записанного в свёрнутой форме:
A2 = an-1 … a1 a0
Таким образом, достаточно записать остатки в обратной последовательности, чтобы
получить искомое двоичное число.
Алгоритм перевода целого десятичного числа в двоичное будет следующим:
1. Последовательно выполнять деление исходного целого десятичного числа и
получаемых целых частных на основание системы (на 2) до тех пор, пока не получится
частное, меньшее делителя, то есть меньшее 2.
2. Записать полученные остатки в обратной последовательности.
Перевод чисел из десятичной системы в двоичную, восьмеричную и
шестнадцатеричную более сложен и может осуществляться различными способами.
Рассмотрим один из алгоритмов перевода на примере перевода чисел из десятичной системы
в двоичную. При этом необходимо учитывать, что алгоритмы перевода целых чисел и
правильных дробей будут различаться.
Регистры
Существуют регистры общего назначения, сегментные регистры, счётчик команд и
регистры флагов. Здесь мы встречаемся впервые с регистрами общего назначения ax и dx.
Причём каждый из них состоит из двух частей – старшей (ah) и младшей (al) (для ax):
Каждое имя регистра несёт какой-либо смысл.
В нашей программе мы использовали старшую часть регистра ax (аккумулятор) и
регистр dx (разместили данные). Каждый регистр состоит из двух байт – старшего (идёт
первым) и младшего. Например, число 3DEFh можно занести в регистр ax двумя путями.
Первый – прямым:
и отдельно к старшему и младшему байту:
7
Надеюсь, с этим всё ясно. Скомпилируем нашу программу, создав с помощью Far
новый файл test.asm (Shift+F4) и поместив в каталог с ним программы MASM.EXE, ML.EXE,
LINK.EXE (либо прописав соответствующие системные пути для них. Для LINK.EXE у меня
это сделать не получилось, он остаётся в папке с программой). Не забудьте выбрать
кодировку файла 866 (клавишей F8), иначе увидите на экране кракозябры.
Выполняем: ML test.asm /AT
В папке с программой должно появиться ещё два файла – test.obj и test.com. Последний
нам и нужен. Запускаем его и видим на экране фразу "Hello, world!".
Урок 4. Отладчик
Итак, понемногу мы подвигаемся вперёд. Сегодня мы узнаем о такой важной вещи, как
отладчик. В нашем комплекте программ он есть и называется AFD Pro. Для чего нужен
отладчик? Как ясно из его названия, для отладки программы. Например, у вас что-то не
работает, и нужно найти причину. У нас пока всё работает, но мы хотим посмотреть на
работу программы "изнутри". Откроем нашу программу test.com в отладчике:
Файл afdpro.exe должен быть прописан в системных путях или находиться в папке с
программой.
Что же мы видим? В верхней части – значения регистров, о которых мы писали в
предыдущей статье. Правее – состояние Stack – стека, о чём мы будем говорить позже. Ниже –
командная строка, а ещё ниже, как мы, наверное, догадались, наша программа (первые четыре
строчки соответствуют нашему коду). Теперь немного об управлении:
F1 – пошаговая трассировка с заходом в прерывания и процедуры.
F2 – то же самое, но без захода в процедуры и прерывания.
Чтобы перемещаться и изменять, например, регистры, дамп памяти – используйте клавиши
F7 (вверх), F8 (вниз), F9 (влево), F10 (вправо).
Нажмём F2: программа переместится на следующую строчку, при этом регистры будут
отображать нам числовые значения, в них хранящиеся. Первой командой мы занесли в ah число
9. Всё правильно: в левом верхнем углу значение регистра ax показывает 0900 (ah=9, al=0 – он не
менялся). Следующее нажатие F2 – значение регистра dx стало равным 109h. Почему 109?
Сейчас узнаем. Ещё раз нажимаем F2 и ещё раз – и мы видим надпись "Program terminated OK" –
программа успешно завершила свою работу. Если хотите, можете проверить работу программы
8
ещё раз – для этого нужно нажать клавишу F3 и Enter. Но где же хранят нашу фразу "Hello,
world!"?
Рассмотрим подробнее команду mov dx,offset helloworld. В отладчике мы видим иное: MOV
DX,0109. Почему так?
Дело в том, что как мы узнали из предыдущего урока, регистры общего назначения, в том
числе и dx, хранят всего лишь 2 байта. Вся фраза никак не поместится в регистр dx. Поэтому
запись MOV DX,0109 указывает на смещение, где хранится наша фраза. Проверим это.
Нажмём клавишу F8 так, чтобы курсор переместился в окно 2. Заменяем значения DS
0000 на DS 0109. В правой части мы видим фразу "Hello, world!".
Что же такое смещение? Представить это можно в виде линеек координат. За линейку X
можно представить себе сегмент, а за линейку Y – смещение. Чтобы найти адрес чего-либо,
например, символа, нужно знать эти два параметра. Почему же мы здесь указываем только
смещение? Да потому, что наши сегментные регистры сейчас имеют одинаковое значение
(cs=ds=es=ss). Все они имеют значение первого свободного адреса, которое находит процес сор и
куда загружает в память. Чуть позже мы рассмотрим эти понятия на примерах.
Урок 5. Подпрограммы
Давайте подведём итоги, что мы узнали за прошедшие уроки. А мы узнали вот что:
– Как установить прикладное программное обеспечение;
– Как воспользоваться файлом помощи;
– Как пользоваться отладчиком;
– Как составить и скомпилировать программу на языке ассемблера.
Также, мы узнали немного о десятичной и шестнадцатеричной системах счисления и
основных регистрах.
Нормально, если что-то непонятно, да ещё и при таком быстром изучении. В дальнейшем мы
ещё раз будем останавливаться на непонятных вопросах, да и тексты программ будут снабжены
комментариями.
Сегодня мы узнаем о циклах, операторе безусловного перехода и вызове подпрограмм.
Соответственно это операторы loop, jmp и call. Сейчас мы рассмотрим их применение.
loop – оператор, позволяющий организовать цикл. Он использует значение регистра cx, о
котором мы писали в предыдущем уроке, что он является счётчиком. Допустим, нам надо 5 раз
вывести строку "Hello, world". Как это будет выглядеть?
Сохраняем в файле test.asm в кодировке 866, а далее ml test.asm /ATЗапускаем
файл test.com (через тот же Far, естественно). Мы видим фразу "Hello, world", написанную 5 раз.
Если не успеваем её увидеть, нажимаем Ctrl+O (скрыть панели). Запустим отладчик: afdpro
test.com.
Используя клавишу F2, посмотрите, как меняется содержимое регистров в верхнем левом
углу (для перехода к началу нажимайте F3). Для выхода из отладчика введите в командной
строке: quit. Следующий оператор – jmp. Это оператор безусловного перехода, кто изучал
"Бейсик", знает похожий оператор GOTO. Он также использует метки для своего "прыжка".
Рассмотрим следующую программу.
9
Сохраняем в файле test.asm в кодировке 866, а далее ml test.asm /AT
Запускаем файл test.com. Программа успешно завершает свою работу, но ничего не выводит
на экран. Думаю, с этим понятно.call – оператор вызова подпрограммы. Сама подпрограмма
оформляется так:
Начало – NameProg proc, где
NameProg – метка начала подпрограммы,
proc – оператор;
Конец – ret и в следующей строчке NameProg endp
Почему оператора два, рассмотрим чуть позже. Для усвоения примера возьмём прошлую
программу, где ничего не выводится, и несколько её модернизируем.
Что же получилось? Командой jmp exit мы "прыгаем" в конец программы. Сразу после этого
мы вызываем (call) подпрограмму и выходим. Сама подпрограмма у нас находится выше под
командой jmp. Таким образом, теперь мы можем размещать подпрограммы в разных местах. Для
чего это нужно? Для сокращения текста программы, когда приходится много раз применять
одинаковые "куски" программ (например, для вывода различных текстов). В последующих
примерах мы закрепим это. Не забудьте посмотреть нашу программу через отладчик (afdpro
test.com)!
Урок 6. Сложение, вычитание и условный
переход
Сегодня мы узнаем о сложении и вычитании целых чисел, а также о командах условного
перехода. Первая часть достаточно простая. Вот основные операторы данного типа:
add – позволяет складывать целые числа,
10
sub – позволяет вычитать целые числа,
inc – команда инкремента (увеличение на 1),
dec – команда декремента (уменьшение на 1).
Все эти операция производятся не напрямую с целыми числами, а при занесении их в
определённый регистр. Рассмотрим программу.
Сохраняем в файле test.asm в кодировке 866, а далее ml test.asm /AT
Далее открываем в отладчике: afdpro test.com.
Используя клавишу F2, смотрим, как меняется значение регистра ax при каждом шаге. Ещё
раз напомню, что для перехода к началу надо нажать клавишу F3, для выхода – ввести в
командной строке отладчика слово quit. Запускать эту программу мы не будем, поскольку на
экране ничего не увидим. Теперь узнаем о командах условного перехода для беззнаковых
данных.
je (иногда пишут jz) – Переход, если равно/ноль
jne (иногда пишут jnz) – Переход, если не равно/не ноль
Для тех, кто будет просматривать этот урок из последующих, опубликую полный перечень
команд условных переходов:
Вспомним программу из предыдущего выпуска:
11
Как мы помним, оператор loop позволяет нам организовать нужное количество циклов,
загружая необходимое число в регистр cx. Однако, всё то же самое можно сделать и без
применения данного оператора (однако, значение регистра cx продолжаем проверять):
Можно использовать не оператор jne, а je, тогда немного изменится наш алгоритм:
Не забудьте просмотреть представленные примеры в отладчике!
Урок 7. Снова о регистрах
В этом уроке не будет практических занятий, лишь теория. В рамках того, что было изучено
ранее.
12
Процессоры 8086/8088 имеют 14 шестнадцатиразрядных регистров, используемых для
yправления выполняющейся программой, для адресации памяти и для обеспечения
арифметических вычислений. В то время, как в языках высокого уровня можно поместить два
числа в переменные, а затем сложить эти переменные, то в языке ассемблера эти числа
помещаются в регистры микропроцессора, а затем складываются значения, содержащиеся в
регистрах. Каждый регистр имеет длину в одно слово (16 бит) и адресуется по имени. Биты
регистра обычно нумеруют слева направо:
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
Процессоры 80286 и 80386 имеют ряд дополнительных регистров, причём некоторые из них
16-битовые. Эти регистры здесь не рассматриваются.
Регистры общего назначения
Регистры AX, BX, CX и DX являются регистрами общего назначения. Операции могут
производиться не только над содержимым всего регистра, но также и над половиной. Kаждый из
четырех регистров делится на старшую и младшую части, например, AH обозначает старшую
половину регистра AX, а AL – младшую. Точно так же ассемблерная программа может иметь
доступ к BH, BL, CH, CL, DH и DL. Регистры BP, SI и DI тоже достаточно удобны, хотя они
могут принимать только 16-битные значения. Дополнительно, каждый бит регистра флагов
сообщает о соответствующем статусе процессора (например, о том, что при выполнении
арифметической операции был перенос за разрядную сетку).
1. Регистр AX. Регистр AX применяют для всех операций ввода-вывода, некоторых операций
над строками и некоторых арифметических операций. Например, команды умножения, деления и
сдвига предполагают использование регистра AX.
2. Регистр BX. Регистр BX является базовым регистром. Это единственный регистр общего
назначения, который может использоваться в качестве "индекса" для расширенной адресации.
Другое общее применение его – вычисления.
3. Регистр CX. Регистр CX является счётчиком. Он необходим для управления числом
повторений циклов и для операций сдвига влево или вправо.
4. Регистр DX. Регистр DX является регистром данных. Он применяется для некоторых
операций ввода/вывода и тех операций умножения и деления над большими числами, которые
используют регистровую пару DX и AX.
Любые регистры общего назначения можно использовать для cложения и вычитания как
8-ми, так и 16-ти битовых значений.
Указатели, индексные и другие регистры
Регистровые указатели: SP и BP
Регистровые указатели SP и BP обеспечивают системе доступ к данным в сегменте стека.
Реже они используются для операций сложения и вычитания.
1. Регистр SP. Указатель стека обеспечивает использование стека в памяти, позволяет
временно хранить адреса и иногда данные. Этот регистр связан с регистром SS для адресации
стека.
2. Регистр BP. Указатель базы облегчает доступ к параметрам: данным и адресам переданным
через стек.
Индексные регистры: SI и DI
Оба индексных регистра возможны для расширенной адресации и для использования в
операциях сложения и вычитания.
13
1. Регистр SI. Этот регистр является индексом источника и применяется для некоторых
операций над строками. В данном контексте регистр SI связан с регистром DS.
2. Регистр DI. Этот регистр является индексом назначения и применяется также для
строковых операций. В данном контексте регистр DI связан с регистром ES.
Регистр командного указателя: IP
Регистр IP содержит смещение на команду, которая должна быть выполнена. Обычно этот
регистр в программе не используется, но он может изменять свое значение при использовании
отладчика DOS DEBUG для тестирования программы.
Флаговый регистр
Девять из 16 битов флагового регистра являются активными и определяют текущее состояние
машины и результатов выполнения. Многие арифметические команды и команды сравнения
изменяют состояние флагов. Назначение флаговых битов:
Сегментные регистры CS, DS, SS и ES
Каждый сегментный регистр обеспечивает адресацию 64К памяти, которая
называется текущимсегментом. Рассмотрим их подробнее.
1. Регистр CS. Регистр сегмента кода содержит начальный адрес сегмента кода. Этот адрес
плюс величина смещения в командном указателе (IP) определяет адрес команды, которая должна
быть выбрана для выполнения. Для обычных программ нет необходимости делать ссылки на
регистр CS.
2. Регистр DS. Регистр сегмента данных содержит начальный адрес сегмента данных. Этот
адрес плюс величина смещения, определенная в команде, указывают на конкретную ячейку в
сегменте данных.
3. Регистр SS. Регистр сегмента стека содержит начальный адрес в сегменте стека.
4. Регистр ES. Некоторые операции над строками используют дополнительный сегментный
регистр для управления адресацией памяти. В данном контексте регистр ES связан с индексным
14
регистром DI. Если необходимо использовать регистр ES, ассемблерная программа должна его
инициализировать.
Примеры операций
После того как программист на ассемблере установил три сегментных регистра (CS, DS и SS)
и загрузил данные в регистры микропроцессора он имеет широкий набор встроенных средств,
которыми процессор может помочь программисту на ассемблере. Роберт Журден приводит
наиболее распространённые из них:
Синтаксис объявления данных
Объявлять данные очень просто – например, чтобы объявить байт cо значением 5 достаточно
написать:
где x – название нашей переменной или константы, db – директива объявления байта, а 5 –
значение. С помощью названия в программе можно будет обращаться к ячейке памяти,
содержащей наш байт.
Объявление последовательностей (массивов)
Иногда в программе требуется объявить массив, то есть несколько переменных одинакового
размера, расположенных в памяти друг за другом. Например, чтобы объявить массив из 5
двухбайтных чисел, можно написать:
15
где array1 – название массива, 1,2,3,4,5 – значения элементов.
Объявление строк
Строка представляет собой массив байтов-символов и записывается в одинарных кавычках:
Для обозначения конца строки используется специальный символ. Обычно это нулевой байт,
но для функций DOS используется символ ’$’.
Резервирование данных (точнее памяти для них)
Можно объявлять переменные, не имеющие определённого начального значения. Объявлять
такие переменные можно с помощью директив db, dw, dd, … и знака вопроса вместо значения.
Директива file
file – это особая директива объявления данных, которая позволяет добавить в исполняемый
файл последовательность байтов из внешнего файла. Иногда это может быть очень удобно.
Например, если вы хотите добавить изображение в исполняемый файл (в виде данных), или
большой кусок текста, или даже код из другого файла. Директива используется следующим
образом:
И, наконец, ассемблер имеет возможность, которой завидуют (или, по крайней мере, должны
завидовать) все кто программирует только на языках высокого уровня. Имеется ввиду
возможность оптимальным образом использовать прерывания операционной системы. Ведь это
ничто иное, как готовые процедуры. Однако вместо того, чтобы вызывать их по CALL, они
вызываются инструкцией INT.
INT21H вызывает прерывание с шестнадцатеричным номером 21. Имеется ряд таких
прерываний, как в базовой системе ввода/вывода ПЗУ, так и в операционной системе, причем
некоторые из этих процедур необычайно мощны.
Урок 8. Запись в файл
А не пора ли нам, друзья, замахнуться на что-либо более серьёзное? Я имею в виду запись в
файл. Впрочем, это вы сможете сделать уже и без меня. Спрашиваете как? Очень просто.
Откроем наш файл HELP.EXE:
16
Перед началом всех действий нам нужно открыть файл.
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 3Dh Open file
Итак, мы видим входные значения – те данные, которые надо загрузить в соответствующие
регистры:
– 3Dh – в регистр AH, а режим открытия в регистр AL;
– адрес буфера, содержащего записываемые данные – в DS:DX (так как у нас сегмент на
самом деле один, то просто DX).
Затем нам нужно туда что-нибудь записать.
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 40h Write file
Далее, мы видим входные значения:
– 40h – в регистр AX;
– описатель файла – в регистр BX;
– адрес буфера, содержащего записываемые данные – в DS:DX (т.е. DX);
– число записываемых байт – в CX.
В какое место записывать?
Читаем ниже приписку о том, что нужно воспользоваться функцией 42h, чтобы установить
указатель файла (куда, собственно будем писать – в начало, конец и т.д.).
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 42h Lseek file
Необходимо обратить внимание на значения регистров cx и dx, указывающих нам место
записи.
И конечно, нам надо закрыть файл. HELP.EXE -> Указатель функций DOS/BIOS -> Функции
DOS -> 3Eh Close file
Здесь всё просто, однако нужно не забыть записать в bx идентификатор открытого файла,
полученного при открытии.
Создадим пустой файл 1.txt в каталоге с программой (Far это делает посредством Shift-F4
(имя файла) -> F2, выход). Проверим, чтобы он был равен 0 байт. Затем – файл test.asm в
кодировке 866:
17
Делаем так: ml test.asm /AT
Имейте в виду, пока мы не проверяем наличие в каталоге файла 1.txt, поэтому проследите за
этим сами. Текст программы, отслеживающей отсутствие файла:
18
19
Обязательно посмотрите программу через отладчик. Помните, пока для нас связка
"HELP.EXE – текст программы в Far – отладчик" – неотъемлемая часть обучения. Иначе
обучение сильно затянется!
Урок 9. Стек
Сегодня мы поговорим о стеке (stack). Что же это такое? По простому – это выделенная
область памяти для хранения произвольных данных. Мы помещаем туда на какое-то время
значения, содержащиеся в регистрах. Например, если необходимо туда загрузить какое-то другое
значение, а потом можем его вернуть назад, причём не обязательно в этот регистр. Если мы
откроем программу AFDPro, в самом верху (чуть правее середины) мы увидим надпись "Stack", а
под ним – его первые значения (верхушку). По мере того, как мы кладём туда какие-нибудь
данные, стек растёт – последнее записанное значение оказывается сверху, а остальные
смещаются вниз. Это то, что мы видим в отладчике. На самом деле надо запомнить, что стек
растёт снизу вверх, т.е. его начало находится по адресу 0FFFFh (и хранится в регистре SP, а
точнее – SS:SP, когда наша программа будет состоять не из одного сегмента), а самый конец – в
00000h. Помещает данные в стек команда push, а возвращает – pop. Например:
Как видим, оперировать мы можем только последним значением в стеке. Однако, стек
используем не только мы, но и сам ассемблер. Например, при использовании оператора call туда
заносится адрес выхода из подпрограммы. Возьмём программу из предыдущего урока:
20
Запустим отладчик afdpro test.com и посмотрим, как меняется стек. При выполнении
оператора call отладчик "прыгает" на адрес mov ah,9, при этом в стек заносится адрес выхода из
подпрограммы (используется при выполнении ret). Попробуем воспользоваться этим (жирным
шрифтом выделены добавленные строки):
Ассемблируем программу: ml test.asm /AT. Запускаем. Вместо одинарного вывода фразы
"Hello, world!" фраза выводится два раза! Почему?
Понаблюдаем в отладчике: afdpro test.com. Мы видим, как подменивается адрес выхода из
подпрограммы, и вместо перехода с ret на int 20 программа снова "прыгает" на mov ah,9 – фраза
выводится второй раз. Последнее значение стека команда ret обнуляет, поэтому при следующем
заходе в ret программа никуда не "прыгает" и корректно оканчивает свою работу. Данный
пример достаточно простой. Давайте его усложнив, поменяв адрес стека. Возьмём программу из
того же урока (выводящую строчку "Hello, world!" 5 раз):
21
И преобразуем к такому виду:
Вроде ничего не изменилось, весь код, кроме трёх строк (выделенных жирным шрифтом),
сохранён. Но первая программа выведет строчку "Hello, world!" 5 раз, а вторая – 9. Потому что
"на лету" мы подменили в самом коде программы цифру "5" на "9". Такого не могут себе
позволить языки высокого уровня.
Урок 10. Сегменты
Сегодня мы поговорим о сегментах. Возьмём для примера программу вывода строки из урока
2:
22
Обратим внимание, что в тексте программы хранятся не только коды программ, но и
переменные, которые мы умышленно указываем в конце кода. Иначе может получиться так, что
эти данные ассемблер примет за часть кода и… тогда случится непоправимое. Таким образом,
получается, что в тексте одной программы хранить и код, и данные плохо и неудобно. Поэтому
обычно программы, написанные на ассемблере, состоят из сегментов.
Давайте попробуем вывести на экран DOS две фразы вида "Hello, world!" и "Hello, world! (2)".
Как это сделать привычным нам способом вы, думаю, знаете:
23
А теперь поместим данные об этих фразах в два разных сегмента. Внимание! До нынешнего
момента мы создавали программы .com, работающие только с одним сегментом! Пора перейти к
созданию *.exe файлов. Создаём нижеследующий файл test.asm в кодировке 866 и вводим нашу
команду без атрибутов: ml test.asm:
24
У нас должен быть создан файл test.exe, правда, с сообщением об ошибке вроде: "LINK :
warning L4021: no stack segment". Сейчас это информационное сообщение (для небольших
программ можно проигнорировать). Стек имеет смысл задавать, если это действительно надо,
например, для больших и многосегментных программ. Но чтобы убрать сообщение об ошибке,
нам достаточно после директивы .286 дописать следующую часть кода:
Что действительно достойно внимания в этой программе.
MESSAGE1 segment USE16 – Без "USE16" программа-ассемблер будет использовать USE32
(т.е. 32-х разрядные данные) и выдавать ошибку на строках вида mov dx,offset Message;
assume cs:CSEG, ds:MESSAGE1 – Указываем masm, что в значение сегмента CSEG установлен
сегментный регистр cs, а в значение сегмента MESSAGE1 установлен сегментный регистр ds;
В сегменте MESSAGE1 хранится наша первая строка, мы и направляем его в ds;
;org 100h – Все COM программы начинаются с адреса 100h. Директива "org 100h" нужна для
резервирования места для PSP (префикс программного сегмента). Когда COM-программа
начинает работать, все сегментные регистры содержат адрес PSP – 256-байтового (т.е. 100h в
шестнадцатеричном исчислении) блока, который резервируется операционной системой DOS
непосредственно перед COM или EXE программой в памяти. Так как адресация начинается со
шестнадцатеричного смещения 100 от начала PSP, то в программе после оператора SEGMENT
кодируется директива ORG 100h. EXE файлы же используют дополнительно регистры (и стек), и
необходимость непосредственного указания в адресации со шестнадцатеричного смещения 100
отпадает (но PSP остаётся!).
mov ax,MESSAGE1
25
mov ds,ax – Заносим данные о MESSAGE1 в ds, но не напрямую, а через регистр ax
(напрямую занести их нельзя). Направляем регистр ds на наш первый сегмент. Чуть дальше мы
определим в dx смещение для фразы "Hello, world!". Теперь нам должны быть понятен текст
вроде: "Данные о фразе 'Hello, world!' хранятся в DS:DX". Далее мы сменим сегмент и определим
в dx новое смещение;
mov ah,4Ch – Раньше для выхода в DOS мы использовали прерывание 20h. Для
*.exe-программы мы будем использовать функцию 4Ch прерывания 21h.
Теперь давайте усложним предыдущий пример. Все данные у нас будут находиться в одном
сегменте, а исполняемый код – сначала в одном, потом в другом. То есть в определённый момент
мы "прыгнем" из сегмента в сегмент. Посмотрим, как это будет выглядеть:
Здесь мы видим новую инструкцию – JMP FAR PTR newmylabel
Она означает, что мы "прыгаем" (вспоминаем команду jmp) в сегмент, указанный в ds, на
метку newmylabel.
Команда jmp имеет пять разновидностей:
– переход прямой короткий (в пределах -128… + 127 байт);
– переход прямой ближний (в пределах текущего программного сегмента) ;
26
– переход прямой дальний (в другой программный сегмент);
– переход косвенный ближний;
– переход косвенный дальний.
Все разновидности переходов имеют одну и ту же мнемонику jmp, хотя и различающиеся
коды операций. Во многих случаях транслятор может определить вид перехода по контексту, в
тех же случаях, когда это невозможно, следует использовать атрибутные операторы:
short – прямой короткий переход;
near ptr – прямой ближний переход;
far ptr – прямой дальний переход;
word ptr – косвенный ближний переход;
dword ptr – косвенный дальний переход.
На сегодня это, пожалуй, всё.
Урок 11. Патчи
В 8-м уроке мы узнали, как можно записывать информацию в файлы. Давайте попробуем
создать патч для обновления нашей программы. Допустим, у нас есть программа из второго
урока, выводящая на экран строчку «Hello, world!». Нам нужно написать обновление для неё.
Вместо строки «Hello, world!», после выполнения нашего патча, будем выводить строку
«Goodbye, world!».
Из второго урока возьмём исходный код, сохраним в файле hello.asm в кодировке 866 и
скомпилируем его в файл hello.com: ml hello.asm /AT.
Текст программы из урока 2:
Теперь создаём программу test.com, позволяющую исправить наш файл, не считывая его в
память:
27
Комментариев я уже использую чуть меньше, но думаю, если вы разобрались с 8-м уроком,
то здесь всё понятно. Теперь усложним задачу.
Задача будет аналогичная, но файл должен быть загружен в память, и там изменён.
28
29
Новое в программе:
mov ah,3Fh – Изучаем функцию 3fh прерывания 21h: HELP.EXE -> Указатель функций
DOS/BIOS -> Функции DOS -> 3fH Read File.
rep movsb и предыдущие три строки – программка переноса данных из одного места в другое.
Разберёмся с ней чуть позже.
Finish equ $ – Новая для нас инструкция. Является меткой конца файла. При
использовании mov dx,offset Finish в dx записывается адрес смещения данной метки – т.е. конца
программы. Рекомендую проверить это в отладчике AFDPro.
Урок 12. Видеобуфер
А сейчас мы рассмотрим, друзья, команды прямого отображения символов в видеобуфер.
ml test.asm /AT
Внимания заслуживают две строчки:
mov ax,0B800h (и следующая mov es,ax) – установка адреса сегмента видеопамяти, он
начинается с адреса 0B800h.
mov es:[di],ax – запись в видеобуфер значения, находящегося в ax. В этот момент значение ax
равно 1F03h (то, что мы и занесли), es равен 0B800h (начало адреса сегмента видеопамяти), а di
равен 0. Мы можем поместить символ и в следующую позицию, однако di будет равен 2 (потому
что символ занимает два байта – сам символ и его атрибут. Мы его и помещаем в ax).
И вот ещё что – внимание! Квадратные скобки [ ] указывают на то, что надо загрузить число
не в регистр, а по адресу, который содержится в этом регистре (т.е. 0B800h:0000h). Кроме того,
отметьте, что для прямого отображения в видеобуфер рекомендуется использовать сегментный
регистр ES.
Урок 13. Перехват прерываний
Сегодня мы завершаем нашу подготовительную часть самым сложным, на мой взгляд, для
новичка вопросом. Мы научимся оставлять нашу программу в памяти и перехватывать
прерывания из других программ.
30
Допустим, у нас есть программа из второго урока, выводящая на экран строку "Hello, world!".
Нам нужно написать свою программу, которая бы осталась резидентной в памяти, но при этом
перехватывала прерывания 21h таким образом, что при запуске первой программы
(откомпилируем её как hello.com) выводилась бы строка не "Hello, world!", а "Goodbye, world!".
Рассмотрим текст такой программы.
31
Сохраним её как test.com (ml test.asm /AT). Запускаем сначала её, а затем hello.com (при
использовании Far2 поместите hello.com в каталог с нашей программой. Дело в том, что при
32
запуске test.com мы в оболочку не вернёмся и просто будем вводить в командной строке
hello.com).
Мы видим, что на экран выводится фраза "Goodbye, world!", при этом текст программы
hello.com остался прежним. Что же произошло?
Рассмотрим нашу подпрограмму, которая будет резидентной. Она начинается
словами Rezident procи заканчивается Rezident endp. Первоначально мы сохраняем все флаги в
стеке (новая для нас функция pushf), затем проверяем, используется ли какой-либо программой
функция 9h (вывод строки на экран). Если нет, переходим на "настоящий" обработчик функции
9h прерывания 21h (перед этим восстановив флаги, "испорченные" je mylabel. Если же да, то
сохраняем значение в регистров ds и dx, и заносим в ds уже другое число – из cs. Что в нём было?
Посмотрите в отладчике.
Затем добавляем в регистр dx адрес смещения, где располагается наша новая фраза. Значение
ds и dx полностью изменились с прошлого раза. И сохраняем флаги. Строчка call dword ptr
cs:[Variable_21h] служит нам для вывода нашей новой строки.
Далее мы "вынимаем" старые значения ds и dx из стека, а затем восстанавливаем флаги.
Обратите внимание на порядок. Потом идёт команда выхода из прерывания (опять новый
оператор, здесь прослеживание аналогия с командой ret.
Теперь рассмотрим остальную (нерезидентную) часть программы, которую не будет больше
использовать процессор.
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 35H GetVector
Читаем: DOS Fn 35H: Дать вектор прерывания. Вход: AH – 35h. AL – номер прерывания (00H
до 0ffH).
Выход: ES:BX – адрес обработчика прерывания.
Описание: Возвращает значение вектора прерывания для INT (AL); то есть, загружает в BX
0000:[AL*4], а в ES – 0000:[(AL*4)+2].
Предупреждение: Эта функция изменяет сегментный регистр ES.
Благодаря этой функции мы получаем в BX адрес обработчика прерывания, который и
используем в следующих командах. Определяя переменную Variable_21h dd ? и зарезервировав
под неё двойное слово, мы можем занести в неё данные из BX и ES.
Следующие три строчки (начиная с mov ax,2521h):
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 25H Set Vector
Вход: AH – 25H
AL – номер прерывания
DS:DX – вектор прерывания: адрес программы обработки прерывания.
Следующие две строчки (начиная с mov dx,offset Nerezident):
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS по группам-> INT 27H:
Завершиться, но остаться резидентным
Обращаем внимание на запись: "Вход: DX – адрес первого байта за резидентным участком
программы. (DX интерпретируется как смещение от PSP (DS/ES при запуске)". Мы используем
запись mov dx,offset Nerezident.
Немного сложновато. Но я уверен, всё получится! Чаще используйте отладчик: afdpro test.com
Урок 14. Поиск и изменение файлов
А давайте, друзья, попробуем массово поизменять какие-нибудь файлы, лежащие в текущем
каталоге.
Для этого нам понадобятся функции 1Ah (установка PSP), 4Eh (поиск первого файла), 4Fh
(поиск следующего файла) прерывания 33 (т.е. 21h).
Возьмём за основу код из восьмого урока и изменим его:
33
34
Разберём, по традиции, наши функции.
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 1aH Set DTA
Вход: AH – 1aH
DS:DX: адрес для DTA
Описание: Устанавливает адрес DTA.
• Все FCB-ориентированные операции работают с DTA.
• DOS не позволяет операциям в/в пересекать границу сегмента.
• Функции поиска: 11H 12H 4eH и 4fH помещают данные в DTA.
• DTA глобальна, поэтому будьте осторожны, назначая ее в рекурсивной
или реентерабельной процедуре.
• При запуске программы ее DTA устанавливается по смещению 80H
относительно PSP.
Зачем же нам устанавливать PSP (и DTA) в другое место? PSP – Program Segment Prefix –
префикс программного сегмента, а DTA – Data Transfer Area – область переноса данных. DTA
находится по умолчанию по адресу 80h. Так вот, проблема в том, что по этому адресу (80h)
располагается изначально командная строка DOS. Как только мы найдём первый файл, мы
затрём командную строку, а как только будем осуществлять вывод информационных сообщений,
программа зависнет. Мы используем самый простой способ – переустановка адреса PSP (и DTA
вместе с ним). Можно временно перенести PSP, а потом восстановить перед началом
соответствующей команды. Куда переносить – это уже следующий вопрос.
HELP.EXE -> Указатель функций DOS/BIOS -> Функции DOS -> 4eH Fnd1stFile
Вход: AH – 4fH
DS:DX: адрес строки ASCIIZ с именем файла (допускаются ? и *)
CX: атрибут файла для сравнения
Выход: AX– код ошибки если CF установлен
DTA: заполнена данными (если не было ошибки)
Описание: DS:DX указывает на строку ASCIIZ в форме: "d:\путь\имяфайла",0. Если диск
и/или путь опущены, они подразумеваются по умолчанию. Обобщённые символы * и ?
допускаются в имени файла и расширении. HELP.EXE -> Указатель функций DOS/BIOS ->
Функции DOS -> 4fH FndNxtFile
Вход: AH – 4fH
DS:DX: адрес данных, возвращенных предыдущей 4eH Найти 1-й Файл
Выход: AX – код ошибки если CF установлен
DTA: заполнена данными
Описание: DS:DX указывает на 2bH-байтовый буфер с информацией,
возвращённой функцией 4eH Найти 1-й (либо DTA, либо буфер, скопированный из DTA).
Используйте эту функцию после вызова 4eH. Следующее имя файла, совпадающее по
обобщённому имени и атрибуту файла, копируется в буфер по адресу DS:DX вместе с другой
информацией (см. функцию 4eH о структуре файловой информации в буфере, заполняемом
DOS).
Дальше всё просто. ml test.asm /AT. Перед этим, правда, необходимо отключить антивирус,
так как мой ESET NOD32 показал информацию об обнаружении вируса.
Вот что подумал, друзья. А давайте изменим нашу программу из нашего так, чтобы наш файл
был загружен в память и там изменён. Помните, мы это проделывали в 11-м уроке? Вот её текст
для самых любопытных (рекомендую составить её самостоятельно, а затем проверить):
35
36
Пожалуйста, помните, что эта программа будет корректно работать с файлами длиной не
более 1Кб. А вообще, максимальная длина программы типа . СОМ составляет 65536 байт минус
длина префикса (256 байт) и обязательное слово стека (2 байта). Когда управление передается
программе типа . СОМ, все регистры указывают на префикс. В указатель стека SР, если
позволяет память, помещается число 0FFFFН, в противном случае – максимальный адрес памяти
минус 2 байта. (DOS при входе в программу помещает в стек нулевое слово).
Урок 15. Массивы
Друзья, чтобы продолжить изучение нами ассемблера, нам нужно разобраться в форматах
хранения данных и организации массивов. С массивами на самом деле всё проще, чем в языках
высокого уровня.
Массив – структурированный тип данных, состоящий из некоторого числа элементов
одного типа.
Описание и инициализация массива в программе
Специальных средств описания массивов в программах ассемблера, конечно, нет. При
необходимости использовать массив в программе его нужно моделировать одним из следующих
способов:
1) Перечислением элементов массива в поле операндов одной из директив описания данных.
При перечислении элементы разделяют запятыми. К примеру:
2) Используя оператор повторения dup. К примеру:
Такой способ определения используется для резервирования памяти с целью размещения и
инициализации элементов массива.
Также существуют способы с использованием директив label и rept, а также цикла для
инициализации значениями области памяти, которую можно будет впоследствии трактовать как
массив.
Доступ к элементам массива
При работе с массивами необходимо чётко представлять себе, что все элементы массива
располагаются в памяти компьютера последовательно.
Само по себе такое расположение ничего не говорит о назначении и порядке использования
этих элементов. И только лишь программист с помощью составленного им алгоритма обработки
определяет, как нужно трактовать эту последовательность байт, составляющих массив. Так, одну
и ту же область памяти можно трактовать как одномерный массив, и одновременно те же самые
данные можно трактовать как двумерный массив. Всё зависит только от алгоритма обработки
этих данных в конкретной программе. Сами по себе данные не несут никакой информации о
своём “смысловом”, или логическом, типе. Помните об этом принципиальном моменте.
Эти же соображения можно распространить и на индексы элементов массива. Ассемблер не
подозревает об их существовании и ему абсолютно всё равно, каковы их численные смысловые
значения.
Для того чтобы локализовать определённый элемент массива, к его имени нужно добавить
индекс. Так как мы моделируем массив, то должны позаботиться и о моделировании индекса. В
языке ассемблера индексы массивов – это обычные адреса, но с ними работают особым образом.
Другими словами, когда при программировании на ассемблере мы говорим об индексе, то скорее
подразумеваем под этим не номер элемента в массиве, а некоторый адрес.
Давайте ещё раз обратимся к описанию массива. К примеру, в программе статически
определена последовательность данных:
Пусть эта последовательность чисел трактуется как одномерный массив. Размерность
каждого элемента определяется директивой dw, то есть она равна 2 байта. Чтобы получить
доступ к числу 6677h, нужно к адресу массива прибавить 6. Нумерация элементов массива в
ассемблере начинается с нуля.
37
В общем случае для получения адреса элемента в массиве необходимо начальный (базовый)
адрес массива сложить с произведением индекса этого элемента на размер элемента массива:
база + (индекс*размер элемента)
Архитектура микропроцессора предоставляет достаточно удобные программно-аппаратные
средства для работы с массивами. К ним относятся базовые и индексные регистры, позволяющие
реализовать несколько режимов адресации данных. Используя данные режимы адресации,
можно организовать эффективную работу с массивами в памяти.
В прошлом уроке мы рассматривали программу по изменению файла в памяти. А почему бы
для этих целей нам не использовать массив? Вот как будет выглядеть программа после такого
нововведения:
38
39
Урок 16. Модификация com-файлов
До сих пор мы баловались с вами, друзья, изменением текстовых файлов. Давайте побалуемся
модификацией com-файлов. А именно – будем дописывать в начало нашего знаменитого файла
hello.com некоторое количество байт – например, ничего не делающих операторов NOP. Только
чур – изменённая программа hello.com должна работать, как и раньше!
За основу возьмём программу из 11-го урока.
40
41
И конечно же, ml test.asm /AT (не забывая при этом устанавливать в текстовом файле
кодировку 866).
Не забываем помещать файл hello.com в каталог с нашей программой.
На что следует обратить внимание? Нижеследующий текст и есть то, что мы приписываем в
начало нашего файла hello.com:
На NOP'ы мы внимания не обращаем, а вот модификация регистра DS уже в программе
hello.comдолжна нас заинтересовать. Как мы помним, текст выводимой строки "Hello, world"
располагается по адресу DS:DX… Но почему мы увеличиваем DS всего лишь на единицу?
Урок 17. Модификация сложных com-файлов
В прошлый раз мы писали программу по модификации com-файлов.
Немного огорчу – она будет работать не со всеми программами, а простыми
вроде hello.com(можете проверить).
Давайте напишем другую программу, которая будет работать по следующему алгоритму (на
основе всё той же программы из 16-го урока):
1. Считает файл hello.com себе в память позади основного кода.
2. Сохранит первые три байта программы hello.com в нашей памяти.
3. Заменит первые три байта программы hello.com, находящейся в памяти, конструкцией jmp
xx, где xx – вычисленное значение в памяти позади считанной программы и записанных за ней
3-х байт.
4. Запишет далее в хвост произвольный код, содержащий как минимум программу
восстановления файла hello.com в частности и большинства других файлов в целом.
5. Сохранит новый файл hello.com таким образом, чтобы при запуске он переходил по адресу
нашей программы, восстанавливал первые 3 байта и запускал её. Файл hello.com должен
выводить строчку, как и раньше.
Поехали!
42
43
Урок 18. Массовое изменение файлов
После некоторого перерыва продолжим наше изучение ассемблера. Давайте модернизируем
программу из прошлого урока так, чтобы она обновляла все находящиеся в текущем каталоге
файлы типа *.com. А проще говоря, "скрестим" программы из 17-го урока и 14-го.
Маленький совет – не забываем проверить на наличие в каталоге файла TEST.COM, чтобы
его случайно не изменить (он же тоже имеет маску *.com).
44
45
Новая конструкция для нас repe cmpsb Сравнивает побайтно 8 байт, находящихся в регистрах
si и di (число 8 указано в cx).
Урок 19. Подготовка к созданию антивируса
В прошлом уроке, друзья, у нас получилась забавная программа. Между метками Add_ nop и
End_nop получается, мы можем разместить любой код, который запишется в другие com-файлы –
причём так, что первые три байта изменённой программы будут указывать на начало нашего
кода. Такой код можно использовать как программу – носитель для модификации конкретного
файла, так и как исходный код для написания программы, работающей внутри заражённого
программного обеспечения, проще говоря, вируса. Напомним, что создание, распространение
или использование компьютерных программ либо иной компьютерной информации, заведомо
предназначенных для несанкционированного уничтожения, блокирования, модификации,
копирования компьютерной информации или нейтрализации средств защиты компьютерной
информации является наказуемым по закону деянием, поэтому информация ниже представлена
для понимания структуры и алгоритма работы вредоносной программы при создании
антивируса.
Приведём программу из 18-го урока в удобный для нас вид:
46
47
Данный файл записываем под именем test.asm (в кодировке 866, разумеется). И в этом же
каталоге создадим файл virus.asm – который, как мы видим, указан в тексте программы. Всё это
делается исключительно для нашего удобства – на самом деле при выполнении ml test.asm
/AT компилятор обрабатывает их единым файлом (в дальнейшем для функционирования
программы TEST.COM файл virus.asm не нужен).
Структура нашей программы (рассмотрено на конкретном примере с заражённым hello.com):
Теперь кладём в один каталог файлы TEST.COM и hello.com и запускаем TEST.COM один раз
(можно и больше, ничего страшного не произойдёт). Теперь наш hello.com и есть та программа,
которая будет клонировать вирус во все найденные им com-файлы в текущем каталоге.
Позаботьтесь, естественно, о том, чтобы в каталоге находились и другие файлы (например,
hello2.com, hello3.com и т.д. и даже GAME.COM из приложения), чтобы проверить её
работоспособность.
Урок 20. Антивирус
Сегодня мы, друзья, попробуем написать антивирус для собственного вируса.
Писать его, разумеется, гораздо проще, чем вирус. Нужно всего лишь найти требуемый файл
по метке в самом конце, и восстановить три байта, сохранённые вирусом где-то посередине, в
начало. Затем записать файл с новой длиной.
Этим мы сейчас и займёмся, взяв за основу программу из того же 18-го урока.
48
49
Компилируем представленный файл под любым именем и запускаем в каталоге, где у нас
находятся заражённые файлы. О, чудо – они вылечены!
Приложение. Примеры программ на
ассемблере
Геометрические фигуры
Рисуем линии. В качестве примера я выбрал горизонтальную линию в графическом режиме
80х25 (такой работает на моём ноутбуке). Причём мы нарисуем не просто линию, а линию путём
ввода с клавиатуры (клавиша "вправо"). Здесь и далее компиляция: ml test.asm /AT
Вот текст программы:
Рисунок прямоугольника. Текст снабжён комментариями. Кодировка 866.
50
51
Рисунок круга:
52
Музыка (Гимн Советского Союза)
53
Замена данных
Допустим, нам дан файл 111.txt с содержанием: "012345678" (цифры внутри кавычек). Нам
нужно написать программу, переделывающую строку с кодом символа, увеличенным на 1. То
есть – "123456789".
54
55
Игра
Грузовичок едет по двухполосной дороге, объезжая препятствия. Требуется очень быстрая
реакция!
ml test.asm /AT
56
Download