Воробьёвы

(-:

Сам себе компилятор (или руководство по мазохизму для дзенствующих)

Предисловие

FatMoon, ты извращенец! Знаешь об этом?
Serrgio

 

Да, об этом я знаю. Подозревать начал еще в раннем детстве, когда

  • пробовал на вкус акварельные "медовые" краски;
  • пытался залить чернила в стержень авторучки;
  • приносил домой гусениц, кормил их листьями и вылуплял бабочек.

    Когда я сел за компьютер, подозрения перешли в твердую уверенность. Да, похоже, что я извращенец. Хотя все относительно - видал я таких извращенцев, по сравнению с которыми я просто … пуританин! А писать на ассемблере под WINDOWS не извращение? Молчите? И в этом молчании я слышу глас рассудка. ї А. Сапковский.
    О чем эта статья? О программировании на ассемблере без компилятора, об отладчике DEBUG из стандартного пакета DOS/WINDOWS, о машинном коде и прочим смежным темам.
    Alex FatMoon

Как обычно пишутся программы на ассемблере? В редакторе - не суть важно, каком - набирается исходный текст, содержащий директивы ассемблера, мнемокоды инструкций, определения данных и метки. Затем вызывается компилятор - не суть важно, какой - который транслирует исходный файл в объектный. Затем линкер собирает исполняемый файл из одного или нескольких объектных модулей. Знакомая картина, не правда ли? Однако необходимо ли все это для создания работоспособной программы? Нет, конечно же. Для создания .com - файлов вполне достаточно отладчика DEBUG. Вот об этом и будет речь.

Почти любой отладчик имеет функцию транслирования мнемокодов в машинный код с возможностью записи на диск набранного блока. Поэтому в принципе эти же приемы могут быть использованы и с другими утилитами. Почему все-таки я описываю DEBUG? Он есть практически на любой машине, где установлена любая из версий MS-DOS или WINDOWS. Он прост в использовании, не требователен к ресурсам, знает почти все машинные коды и даже поддерживает инструкции сопроцессора. Он - первый друг взломщика сейвов к играм и просто хакера. И вообще - одно из лучших творений МикроСофт. Лежит он, родимый, в директории C:DOS или C:WINDOWSCOMMAND - даже если ваши папки называются по-другому и расположены на других дисках, найти его нетрудно. Переходим к непосредственному использованию.

Программирование без компилятора

Русские сначала придумывают себе препятствия, а потом их преодолевают.
(известный факт)
Входите узкими вратами, ибо широки врата и просторен путь, ведущий в геену огненную.
(Евангелие от Матфея)

Да, если у вас есть компилятор, использовать что-то еще нет нужды. Но представим себе, что компилятора нет. Или мы только начали изучать ассемблер и в принципе знаем мнемокоды по богатому опыту программирования на БК, Спектруме или Микроше, но о директивах имеем только общее представление. И напряженно думаем, с какими ключами запускать этот tasm? И почему tasm выдает ошибки при компиляции, хотя все написано правильно (кто бы мог подумать, что ss: mov ax, [si] на самом деле должно быть записано как mov ax, ss:[si]?). Тогда это для вас. А также если есть желание понять, как же работает компилятор и зачем он все-таки нужен. Подумав (от "DOOM") и перекрестясь (наведя перекрестье на последнего из монстров, нажав "fire primary" и выйдя из игры), приступим!

Пример программы.
Главная команда отладчика, которую будем использовать, "А"ssembly. Итак, входим в отладчик, набираем

a 100

и видим адрес ххх:0100. От нас ждут ввода инструкций. Собственно, можно набирать программу. Пример:

a 100
   mov ah,09
   mov dx,0100
   int 21
   ret
   db "Hello, world!$"

и готово. Однако это еще не конец. Надо скорректировать dx - 0100 туда засылается просто для того, чтобы обозначить команду. Ведь когда мы пишем:

mov dx,xxx

мы еще не знаем адреса, с которого начнется строка "Hello, world!$", не так ли? Этот адрес мы узнаем только после того, как введем "ret". Итак, запоминаем в cx длину нашей программы - она равна адресу инструкции после строки, где мы нажали "Enter", чтобы выйти из режима компиляции, за вычетом 0100 - адрес, с которого начинается программа типа "COM". У меня для вышеприведенной программы получилось 016h. Значит, набираем:

rcx
   16
 

А теперь корректируем адрес - строка начинается с 0108. Набираем:

a 102
   mov dx,108

Осталось сохранить программу на диске:

n hello.com
   w

Вуаля! Теперь в директории, где мы находились перед вызовом отладчика, есть файл hello.com длиной в 22 байта, исправно печатающий при запуске строку "Hello, world!" и возвращающий управление ОС. Просто, не так ли? И никаких компиляторов. На самом деле в роли такового выступили мы сами - назначили строке фиктивный адрес (0100), затем заменили его на точный (0108). Вся помощь отладчика заключалась в переводе мнемокодов в машинные. Не можем же мы помнить все машинные коды!

Это кажется элементарным, не спорю. Попробуем сделать что-нибудь более сложное, и затем попытаемся оптимизировать это. То есть, обычная процедура для любой программы - сначала "рыхлый" код, от которого добиваются работоспособности, затем оптимизация. Лично я придерживаюсь именно такой тактики, и нахожу ее достаточно удобной. Для примера выведем бегущую строку на обычный текстовый экран 80*25 - разрешение, к которому все привыкли (надеюсь) в Norton Commander. Не претендуя на оригинальность, скажу, что нам потребуется две строки в качестве данных/переменных. Одна из строк будет оригиналом, а вторая - служить для хранения подстроки. Алгоритм весьма прост:

  • Цикл: вырезать подстроку выбранной длины;
  • переместить курсор на выбранное место на экране;
  • вывести подстроку;
  • сделать задержку;
  • сдвинуться в строке на символ;
  • если не конец строки, то продолжать цикл.

Но перед тем, как набивать в отладчике код, я несколько облегчу вам работу.

Перенаправление ввода-вывода в ДОС.

Каждый раз набивать программу в отладчике было бы очень долго и может навсегда отвратить от программирования. К счастью, в ДОС (и, в частности,("в частности" убрать) в коммандной строке Windows (согласен, переставить слово надо)) можно использовать перенаправление стандартного ввода-вывода. Как это делается? Пишем текстовый файл, например, содержащий предыдущую программу "Hello, world!", в виде:

----8<---
   a 100
   mov ah,09
   mov dx,108
   int 21
   ret
   db "Hello, world!$"
rcx
   16
   n hello.com
   w
   q
--->8---

Не забудьте пустую строку после DB (она выводит отладчик из режима ассемблирования), и не забываем поставить в конце команду выхода! Сохраняем его под именем, например, hello.dbg и пишем в командной строке:

debug < hello.dbg

В результате отладчик берет команды не с клавиатуры, а из файла. Вводит программу на языке ассемблера начиная с адреса 0100, и сохраняет на диск под именем hello.com. И выходит в ДОС. А что, почти обычная программа - только вместо

ORG 0100h

имеем

a 100

и еще команды в конце.

Точно также, чтобы получить красивый листинг при дизассемблировании, не обязательно делать PrintScreen после команды "u". Поскольку это наша программа и мы знаем, где находится код, а где данные, создаем файл d_hello.dbg, содержащий следующие строки:

u 100,107
   d 108,115
   q

И набираем в командной строке

debug hello.com < d_hello.dbg > hello.diz

Здесь мы перенаправляем как ввод (из файла d_hello.dbg), так и вывод (в файл hello.diz).

В результате получаем еще и файл hello.diz, содержащий дизассемблированную программу.

Таким образом, имеем debug в роли компилятора, и некий файл, содержащий команды отладчика и мнемокоды, в качестве программы. Для "компиляции" вызываем отладчик, используя ввод из файла, а для дизассемблирования - используем ввод из файла и вывод в файл вместо ввода с клавиатуры и вывода на экран. Да, не зацикливайтесь на расширениях - они в данном случае произвольны. Все эти файлы в принципе могут быть с расширением ".txt" или вообще без расширений - кому как удобнее.

[прим.: Но это ещё не всё! В отличие от "простого" компилятора DEBUG может также сразу и выполнять (почти "интерпретировать") нашу "программу" (по аналогии с бейсиком), поэтому сохранять результат трансляции в отдельный файл нет необходимости - достаточно вместо команд RCX и N поставить G=100, не забыв завершить нашу программу инструкцией INT 3, если мы желаем увидеть содержимое регистров, с которым наша программа завершается. Итак, делаем следующий файл:

a
     xor ax,ax
     mov es,ax ; сегмент 0
     es:
     mov ax,[46c] ; адрес таймера в области данных BIOS
     es:
     mov dx,[46e]
     int 3 ; DX:AX содержат значение системного таймера
g=100
     q

сохраняем это под именем ticks, даём команду DEBUG<TICKS>RESULT, и наблюдаем в файле RESULT в регистрах DX и AX значение системного таймера. Впрочем, конкретно для данного случая наша программа может быть "короче":

d 0:46c l 4
     q

Использование внешнего файла и комментариев в нём также облегчает "настройку" адресов. Например, в исходном файле на месте меток можно вставить соответствующий комментарий, а в инструкциях перехода вместо имён меток будет фиктивное значение. Разумеется, в исходном файле не должно быть команды G, ведь программа ещё не завершена. Например:

a
     mov ax,100
     mov cx,100
     ;top:
     dec ax
     loop 100 ; top
     int 3
;g=100
     q

После первого "прохода" (выполнения DEBUG с перенаправлением ввода-вывода) следует проанализировать вывод на наличие сообщений об ошибках. Потом следует проверить значения закомментированных меток и скопировать эти значения в соответствующие места (в данном случае значение метки top нужно вставить после инструкции loop). Теперь можно расскоментарить команду G и программа готова. :)]

Вывод "бегущей строки" - создание программы с помощью отладчика.

Итак, набираем сразу в виде файла, чтобы потом "скомпилировать" его отладчиком.

---------Start of file------------
   a 100
   mov cx,555
   push cx
   mov si,200
   sub cx,555
   sub si,cx
   mov di,400
   mov cx,14
   rep movsb
   mov byte ptr [di],24
   mov bh,0
   mov dx,0c1f
   mov ah,02
   int 10
   mov dx,400
   mov ah,09
   int 21
   mov ah, 86
   inc cx
   inc cx
   xor dx,dx
   int 15
   mov ah,01
   int 16
   pop cx
   jnz 0140
   loop 0103
   jmp 0100
   ret
   db " ******** This is a long long string."
   db " It can be up to 64K long! You never see such long string"
   db " before! Enjoy it. Here may be some of your text and "
   db "advertizing. Just type what you want together"
   db " with WISPA chocolate! This program is debug handwork."
   db " All rights reserved. Decompilation and dizassembly "
   db " prohibited! The length of this string now 477 bytes"
   db " but length of code that print them is only 59 bytes."
   db " Sorry for my english, I write as I can. Alex FatMoon. $"
rcx
   400
   n r_string.com
   w
   q
   ------------End of file----------

Не забудьте поставить пробел перед долларом - он нужен, что бы конец строки затирал символы, остающиеся от предыдущего вывода. Сделаю некоторые пояснения:

  • 555 - обозначил длину строки. На самом деле тут должно быть число, равное "адрес конца строки" - "адрес начала строки".
  • 0200 - обозначим адрес начала строки. Мы его пока не знаем, но потом в листинге легко найдем.
  • 0400 - адрес начала подстроки. Подстрока начнется сразу после конца строки. Этот адрес мы тоже пока не знаем.
  • 0140 - адрес инструкции "ret". Он идет непосредственно перед строкой. И его тоже пока не знаем.

Сохраняем на диске 1К, поскольку точной длины программы опять же не знаем. Для тех, кто ввел очень длинную строку - можете сохранять не 0400h, а ff00h. Наверняка где-то в начале этого блока все-таки уместилась наша "гигантская" программа.

Теперь загружаем в отладчик почти готовый r_string.com, и дизассемблируем, отмечая исправления, которые надо внести: строка начинается с 013ch, длина всей программы 0219h, длина строки = 0319h-013ch=01ddh, адрес "ret" - 013bh. Вносим изменения - можно прямо в отладчике, можно в текстовом файле. Все должно работать. Оптимизация, очистка экрана в начале и прочее - на ваше усмотрение.

[прим.: как упоминалось выше, для этого ничего дизассемблировать не нужно, достаточно выполнить один проход "компиляции" с перенаправлением вывода, после чего значения всех меток станут известны]

Послесловие

Блаженны больные, ибо исцелятся.
(почти из Библии)
И тебя вылечат… И меня вылечат!
("Иван Васильевич меняет профессию")

Вот собственно для этого и нужен компилятор - назначить адреса меткам и переменным и правильно вставить их в машинный код. Чем больше переменных и переходов, тем сложнее написать программу в отладчике. И слава богу, что это делать не обязательно, поскольку есть компиляторы. Если же читатель достаточно безумен, чтобы попробовать самостоятельно написать довольно объемистый проект, флаг в руки. Моего терпения хватило однажды на двух-килобайтную видеодемку, и я думаю, что писать программы под ДОС, используя только debug, вполне реально. Хотя и неудобно. А вот для написания небольших скриптов и тестовых фрагментов использование DEBUG в качестве компилятора - самое оно. И, кстати, при этом автоматом решается проблема лицензионности компилятора. J
При этом надо помнить несколько вещей:

  • смена сегментного регистра делается в две команды, поскольку это префикс, как и REP, LOCK и другие. То есть
    mov si, es:[di]
    записывается в виде
    es:
    mov si, [di]
  • переходы типа short возможны лишь в пределах 128 байт выше или ниже. Из-за этого могут быть проблемы при вводе программ вышеописанным способом.
  • отладчик не выдаст ошибки, если какая-то инструкция неправильна. То есть, выдаст, но мы ее можем не увидеть, если используем перенаправленный ввод.
  • некоторые машинные коды 186-го, 286-го, 386-го и выше процов к сожалению остаются для debug'а неизвестными, а именно:
    pusha
    popa
    push <immediate>
    bound
    arpl
    • все команды, появившиеся в 486 и позднее
    • все команды, использующие 32-битную адресацию и операнды
    • insb / insw
    • и некоторые другие.

Проблему можно решить, занося непосредственно код через

db <opcode>

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

ї Alex Antipoff aka FatMoon, 2002.

  [C] FatMoon / HI-TECH