Инструменты
Чтобы вводить двоичные значения в компьютер, необходим шестнадцатеричный редактор. Поскольку мы решили обходиться стандартными средствами, имеющимися в любой типичной поставке Windows, используем в качестве шестнадцатеричного редактора старый досовский отладчик debug. Рассмотрим лишь те возможности этого отладчика, которые нам понадобятся в работе.
Сначала имеет смысл создать отдельную папку для проводимых экспериментов, например, \exp. Теперь запустим командную строку DOS, перейдем в созданный каталог (cd \exp) и наберем: debug. Появляется черточка - приглашение отладчка; можно набирать команды. Сразу о том, как завершить работу debug: для этого служит команда q (quit).
Debug позволяет создавать и записывать на диск файлы, но у этого процесса есть некоторые особенности. Дело в том, что создаваемые файлы будут в старом досовском формате com. Для нас это означает, что при записи на диск отладчик использует данные, начиная со смещения 100h кодового сегмента (адрес которого содержится в регистре CS), это надо учитывать. Если наши данные будут начинаться со смещения 0, первые 256 (100h) байтов окажутся утерянными (для содержимого регистров CS и DS по умолчанию). Либо надо вручную изменить (увеличить на 10h) значение регистра DS.
Попробуем создать простейший файл. Запускаем debug. Для записи служит команда w (write); однако вначале должно быть определено имя файла с помощью команды n (name). В принципе, имя может быть любым досовским именем (в коротком формате 8.3), но расширение не может быть exe или hex. Лучше использовать расширение bin, а потом переименовать файл. Набираем:
n first.bin
Теперь необходимо указать размер создаваемого файла. Это значение должно быть в регистрах BX:CX, причем младшее слово содержится в CX, старшее слово - в BX (отладчик debug 16-разрядный, поэтому он не работает с 32-разрядными регистрами и смещениями). Для начала запишем лишь 1 байт; введем с помощью команды r (register) 1 в регистр CX (в BX по умолчанию содержится 0):
r cx 1
Таким способом можно изменять значения любых регистров. Собственно запись осуществляется командой w. Смотрим - в нашем каталоге должен появиться файл 'first.bin' размером в 1 байт.
Перейдем к формированию наших данных. Одна из полезных команд - f (fill), она позволяет заполнить участок памяти указанными данными. После f первым параметром идет смещение (начальный адрес) заполняемого блока, затем либо параметр l (length) и число, указывающее на длину заполняемого участка в байтах, либо смещение его конца. После этого - собственно данные, которыми будет заполняться данный участок. Причем данные могут быть как в виде 16-ричных чисел, так и в виде заключенных в апострофы или кавычки строк, причем их можно чередовать. Например, заполним первые 256 (100h) байт строкой "This is the filling string":
f 0 l 100 'This is the filling string'
Чтобы просмотреть содержимое памяти, служит команда d (dump). Как и в случае с командой f, первый параметр указывает смещение начала отображения данных (дампа), а за ним - либо l с указанием размера дампа, либо конечное смещение. Используем для разнообразия второй вариант (учтите, все используемые числа в debug - 16-ричные):
d 0 ff
Как видим, указанный участок заполнен повторяющейся строкой, которую мы указали в качестве параметра команды f. Разумеется, это лишь пример, а в реальности мы будем эту команду использовать для очистки (заполнения нулями) блоков памяти. Например, очистим первый килобайт (400h байт):
f 0 400 0
Если в команде d указать лишь один параметр, она по умолчанию отображает 80h байт, начиная с данного смещения. А если не указать и его, то отображаются очередные 80h байт с того места, на котором остановились в прошлый раз. Поэтому мы можем набрать:
d 0
Посмотрев первые 80h байт дампа, набираем d и смотрим следующую порцию и т.д.
Теперь проделаем эксперимент, демонстрирующий особенность сохранения файлов в debug. Создадим файл, первые 100h байт которого заполнены символами '0', вторые 100h байт - символами '1' и т.д. до, скажем, '9'. Дадим файлу имя 'first.txt' (или любое другое с расширением .txt), а размер его будет a00h (2,5 Кб).
n first.txt r cx a00 f 0 l 100 30 f 100 l 100 31
и т.д. до f 900 l 100 39. 16-ричные числа 30, 31, ... , 39 являются ASCII-кодами цифр 0-9. После этого набираем w и смотрим, что получилось.
Открываем 'first.txt' в Блокноте. Но что это? Файл начинается с единиц, а в конце какой-то мусор? Смотрим в debug'е: d 0 ff - все нормально, заполнено цифрами 0 (30h). Вот это и есть та особенность отладчика, о которой мы говорили в начале. В файл записываются данные начиная со смещения 100h относительно кодового сегмента.
Исправить эту ситуацию можно попытаться двумя способами. Рассмотрим еще одну команду отладчика: m (move). Она позволяет копировать данные из одной области памяти в другую. Первый параметр, как и ранее, является смещением начала участка памяти, который необходимо скопировать, второй - либо смещением конца копируемого участка, либо (при наличии буквы l) его длиной, третий параметр - смещение места назначения, куда надо скопировать данные. С помощью этой команды мы можем "передвинуть" весь наш блок данных так, чтобы он начинался со смещения 100h:
m 0 l a00 100
Теперь снова попробуем записать эти данные в тот же файл. Открываем в Блокноте - то что надо! Начинается нулями, заканчивается девятками, ничего лишнего, только то, что мы сами вводили.
Второй способ - изменить значение регистра сегмента данных DS таким образом, чтобы он указывал на область со смещением 100h относительно начала кодового сегмента. Т.е. надо просто добавить к старому значению DS 10h. Допустим, в DS было значение 2020. Изменим его на 2030:
r ds 2030
Теперь затрем старые данные, скажем, числом ff:
f 0 l a00 ff
Запишем это в старый файл командой w и убедимся, что файл изменился. И повторим старую операцию:
f 0 l 100 30 f 100 l 100 31 ... f 900 l 100 39 w
Результат аналогичный ранее сделанному.
Команда e (enter) позволяет вводить данные по конкретным адресам. Первый параметр указывает начальный адрес, остальные рассматриваются как данные для ввода. Причем здесь тоже можно использовать как 16-ричные числа, так и символьные строки, чередуя их между собой произвольным образом.
В связи с данной командой рассмотрим особенность процессоров IA-32, о которой говорилось в прошлой статье. Речь идет об "обратном" представлении чисел в памяти; хотя по внимательном рассмотрении этого вопроса представление чисел в процессарах IA-32 как раз является естественным ("нормальным"), а "обратным" оказывается наша традиционная запись. Попробуем разобраться.
Мы читаем и записываем слева направо. Если записать порядковые номера, они будут увеличиваться тоже слева направо. Естественно таким же образом нумеровать объекты, скажем, байты памяти: 1, 2, 3, 4 и т.д. Значения возрастают слева направо. Теперь посмотрите на числа, у которых увеличиваются разряды: 1, 10, 100, 1000. Каждый новый разряд мы добавляем слева, т.е. возрастание числа получается справа налево - порядок, противоположный традиционному письму. Если сохранять в памяти текст, т.е. строку символов, при добавлении новых символов они будут помещаться "правее", т.е. по возрастающим адресам памяти (поскольку мы нумеруем их слева направо). А как быть, если увеличивается значение числа и оно перестает помещаться на старом месте? Скажем, вместо байта требуется уже слово (два байта)? Новый байт можно добавить "слева" (с меньшим адресом) или "справа" (с большим адресом). Поскольку адресом многобайтной конструкции по соглашению считают самый младший адрес, он может указывать либо на байт, в котором хранятся старшие разряды числа, либо на байт, в котором хранятся младшие разряды. Первый способ называется "big-endian", второй - "little-endian". Так вот, в процессорах IA-32 используется "little-endian", т.е. старшие разряды добавляются "справа" (по старшим адресам памяти) - порядок, обратный нашей записи чисел. Говорят, в свое время Фибоначчи, заимствуя цифры у арабов, не учел особенностей их письма: арабы пишут справа налево, в отличие от нас. И так же располагались разряды их цифр. Фибоначчи использовал тот же порядок, хотя европейцы писали в обратном направлении - вот где корень всех наших бед :).
Таким образом, если мы хотим разместить по адресу 10h число 12h, мы набираем:
e 10 12
Если же мы хотим разместить по этому же адресу число 1234h, два байта, его составляющих, нам придется вводить следующим образом:
e 10 34 12
А если по тому же адресу нужно записать число 12345678h, ввод будет таким:
e 10 78 56 34 12
Только в этом случае в результате исполнения инструкции копирования данных из памяти (по адресу 10h) в регистр EAX, которую мы рассматривали в прошлой статье, в регистре EAX окажется нужное нам значение 12345678h.
Как вы уже, очевидно, заметили, в 16-разрядной системе используется сегментная модель памяти. Это создает дополнительные проблемы; в частности, команды заполнения (f) и перемещения (m) не работают через границы сегментов. Поэтому, хотя debug в принципе позволяет сохранять файлы размером более одного 16-разрядного сегмента (64 Кб), при составлении таких файлов у нас могут возникнуть проблемы. Их можно решить другим путем - собирая в debug отдельные "модули", не превышающие 64 Кб, и соединяя их с помощью команды DOS copy.
Для доказательства такой возможности соберем простой текстовый файл размером в 1 Мб. Собирать будем из 16 модулей в 64 Кб, сохраненных средствами debug; каждый модуль будет заполнен единственным символом - 16-ричной цифрой, значение которой равно номеру модуля (для контроля).
Сначала настроим регистр DS (если он не был настроен ранее), увеличив его значение на 10h. В регистр CX должно быть значение 0, в BX - 1 (это соответствует размеру файла 10000h байт, или ровно 64 Кб):
r ds <ввести значение на 10h большее прежнего> r cx 0 r bx 1
Если параметр с буквой l в командах равен 0, длина участка памяти считается равной размеру полного сегмента, т.е. 64 Кб. Будем последовательно заполнять весь сегмент символом очередной 16-ричной цифры (от 0 (30h) до 9 (39h) и далее от A (41h) до F (46h)) и сохранять его под новым именем:
n 0.bin f 0 l 0 30 w n 1.bin f 0 l 0 31 w . . . n 15.bin f 0 l 0 46 w
В нашем каталоге должны появиться 16 файлов с расширением bin и размером 64 Кб каждый. Теперь выходим из debug (q) и набираем в командной строке:
copy /b 0.bin+1.bin+2.bin+3.bin+...+15.bin 16.txt
Естественно, вместо "..." здесь должны быть имена остальных файлов, соединенных знаком "+". Откроем итоговый файл 16.txt в WordPad (Блокнот для этой цели не годится - слишком большой файл) и убедимся, что он заполнен введенными нами символами и что в нем нет ничего лишнего.
Осталось рассмотреть лишь некоторые методы автоматизации нашей работы. Работать с debug, все время вводя данные в интерактивном режиме, может оказаться утомительным - удобнее использовать заранее подготовленные шаблоны, внося в них каждый раз небольшие изменения. Для этого воспользуемся еще одной возможностью ОС - перенаправлением ввода-вывода.
Все необходимые команды для debug записываются в текстовый файл, который затем подается на вход отладчика при его запуске следующим образом:
debug < batch.txt
Для испытания этого способа повторим тот же алгоритм, который мы использовали при создании файла "first.txt". В Блокноте создаем файл "batch.txt" со следующим содержимым:
n first.txt r cx a00 f 0 l 100 30 f 100 l 100 31 . . . f 900 l 100 39 m 0 l a00 100 w q
В конце файла надо не забыть поставить q - иначе мы останемся в отладчике. Результаты работы все еще выводятся на экран. Их можно записать в файл (иногда это бывает полезно), использовав второе перенаправление:
debug < batch.txt > batch.lst
Теперь в консольном окне сообщения не выводятся, зато в файле batch.lst оказались записанными введенные нами команды и ответы на них отладчика. Заметим, что таким способом мы не можем использовать команды, требующие анализа ответов отладчика. Например, мы не сможем воспользоваться изменением значения регистра DS, поскольку заранее (в общем случае) неизвестно его значение.
Наконец, рассмотрим еще одну команду - a (assemble). Эта команда позволяет войти в режим ассемблирования, т.е. ввода инструкций на ассемблере, которые debug автоматически преобразует в машинные коды. Однако делает это он в 16-разрядном режиме, что нам совершенно не подходит. Но мы можем воспользоваться в этом режиме директивой db, позволяющей вводить отдельные байты, как в команде e. Это может напомнить путешествие из Петербурга в Москву через Владивосток; однако, удобство этого метода в том, что отладчик будет автоматически подсчитывать смещение следующей вводимой инструкции (в нашем случае - байта), и можно не считать все самим.
Параметром команды a является адрес (смещение), с которого мы начинаем вводить инструкции. Чтобы выйти из режима ассемблирования, необходимо просто нажать 'Enter' еще раз (в тексте пакетного файла в этом месте должна быть пустая строка). Потренируемся в использовании этой команды с использованием инструкций в машинных кодах, которые мы составляли в прошлый раз (впрочем, ничто не мешает составить и новые). Сначала поработаем в интерактивном режиме:
a 100
В ответ слева появится что-то типа 2020:0100. Старшее слово (сегмент) нам неинтересно, а младшее (справа) как раз и является текущим смещением от начала сегмента. Набираем:
db b8 01 00 00 00
После нажатия 'Enter' появляется новое смещение - 105. К старому смещению автоматически прибавилась длина введенных нами данных. Вводим:
; конец инструкции
Смещение осталось тем же. Все содержимое строки после точки с запятой игнорируется. Очень удобно - можно использовать, как метки для соответствующих смещений. Продолжаем:
db b4 01 ; конец второй инструкции db "Some text"
Как видим, длина текстовых строк тоже подсчитывается автоматически и добавляется к смещению. Запишем все в файл, выйдя из режима ассемблирования (для этого просто нажимаем на 'Enter' еще раз):
<Enter> n second.bin r cx 10 w
Заметим, что длину введенных данных подсчитать теперь очень просто: отнимаем от конечного смещения (в данном случае 110h) начальное: 110-100=10h.
Но преимущества режима ассемблирования станут очевидными при работе с перенаправлениями. Создадим файл "second.txt" и наберем в нем те же данные (не забыв про пустую строку в соответствующем месте и команду q в конце). В командной строке DOS запишем:
debug < second.txt > second.lst
В данном случае нас особо интересует именно выходной файл - second.lst. Теперь все смещения записаны в файле. Это дает возможность при "первом проходе" (черновом) вводить приблизительные значения (здесь это могло бы быть, например, значение регистра CX). Выходной файл используется затем для получения точных значений смещений и подстановки их в исходный файл с командами для "второго прохода" (чистового).
Завершим знакомство с отладчиком способами загрузки созданных заранее шаблонов. Для загрузки файлов служит команда l (load). При этом имя файла должно быть уже указано командой n. Файл загружается по смещению 100h. Либо имя файла можно указать в качестве параметра при вызове отладчика:
debug first.txt
Произведя нужные изменения, файл можно сохранить под другим именем. При этом, если работа с debug ведется со смещениями, меньшими 100h, новое имя файла нужно вводить заранее, т.к. debug записывает имя в эту область, и данные могут оказаться испорчеными.
Для примера рассмотрим, как можно загрузить в качестве шаблона созданный ранее файл "first.txt" и сохранить его после сделанных изменений. Сначала создаем "автоматизирующий" файл с командами ("third.txt"):
n third.bin m 100 l a00 0 f 100 l 100 ff m 0 l a00 100 w q
Теперь в командной строке набираем:
debug first.txt < third.txt > third.lst
В данном случае отладчик загружается вместе с файлом "first.txt", затем он исполняет команды, содержащиеся в "third.txt" (создавая в процессе работы файл "third.bin"), а отчет записывает в файл "third.lst".
Финальный штрих - полная автоматизация создания исполняемого файла. Для этого создается bat-файл, в котором записываются вызов самого debug, а также другие необходимые действия, например, составление одного большого файла из отдельных модулей с помощью команды copy или переименование файла с расширением bin в файл с расширением exe. Создадим в Блокноте файл "make.bat":
@echo off debug < second.txt
ren second.bin second.exe
Теперь можно запустить этот файл на исполение (двойным щелчком по его имени в Проводнике или набрав имя в командной строке). Строка "@echo off" нужна для того, чтобы команды в bat-файле не выводились на экран. Однако, результат работы debug все равно будет отображаться на экране; чтобы его не было, можно использовать второе перенаправление - либо в файл, либо, если файл с результатами работы не нужен, сюда можно записать nul:
debug < second.txt > nul
Результатом работы будет файл second.exe. Кстати, можете попробовать запустить его - ничего страшного не произойдет, система просто сообщит, что это не настоящий исполняемый файл Windows. Отметим, что это простейший случай; на самом деле в bat-файле может быть записано множество вызовов debug с различными заранее подготовленными файлами для создания сразу нескольких модулей и последующего их объединения в один результирующий.
Каков же итог? Любой файл - будь то картинка, векторная или трехмерная графика, музыка, видео или исполняемый - это всего лишь сохраненный набор двоичных чисел. А итог таков, что мы умеем теперь создавать файлы практически любого размера и с любым содержанием. Единственное, что при этом надо - это изучить формат соответствующего типа файла. Этим мы и займемся в следующей статье применительно к исполняемым файлам Windows.