Top-office11.ru

IT и мир ПК
14 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Push offset ассемблер

Основы Ассемблера

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

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

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.

Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV. Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

Вы можете копировать значение из одного регистра в другой.

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми,

например: эта команда — НЕ допустима:

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:

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

смещение3435363738393A3B3C3D3E3F404142
данные0D0A50324457257A5E72EF7DFFADC7

(Каждый блок представляет байт)

Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:

Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:

  • dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10
  • word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

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

Иногда размер можно не указывать:

Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.

Можно также непосредственные значения:

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции — PUSH, POP. Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:

  • 1: поместить 100 в ecx
  • 2: поместить 200 в eax
  • 3: разместить значение из ecx (=100) в стеке (размещается первым)
  • 4: разместить значение из eax (=200) в стеке (размещается последним)
  • 5/6/7: выполнение операций над ecx, значение в ecx изменяется
  • 8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
  • 9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата. Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:

Когда выполняется команда call, процессор передает управление на код с адреса 455659, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой. Вы можете поместить код, который вы часто используете в процедуру и каждый раз когда он вам нужен вызывать его командой call.

Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

Читать еще:  Php if echo

Одно важное замечание: регистр eax почти всегда используется для хранения результата процедуры.

Это также применимо к функциям windows. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.

Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.

Дневники чайника. Чтива 0, виток0

Стек.
Восьмой день

Стек — специально выделенная область памяти для передачи или сохранения данных.

Мы всё время наблюдаем такую форму записи (prax03.com):

Я имею в виду, что адреса растут вниз. 0 — выше всех (как первая строка в книге), 28 — нижняя строчка.

Так вот в этой системе отображения стек растёт вверх.

Дно стека находится по самому старшему адресу, а вершина — по самому младшему адресу.

На вершину стека указывает регистр-указатель ESP — это его назначение (Stack Pointer — указатель стека).

Видите, prax03 в строке 12h сохраняет значение регистра DX, а в строке 07 при следующем проходе цикла обратно восстанавливает это значение. В данном примере стек использовать неудобно, но если в программе нужно много раз сохранять разные регистры, то лучше делать это через стек. Для записи в стек есть команда PUSH.

Причём значений в стек можно укладывать очень много.

Обратите внимание, что значения извлекаются в обратном порядке. Вершина выходит первой, а дно последним. На практике, слава Богу, мудрить со стеком придётся мало, но если вы не уловите принцип, то писать программы будет трудновато.

Матрос! Ну-ка иди сюда. Что тут у тебя в корзинке? О, завтрак! Так, сверху яйца, подержи, бутерброд, бутылка. подушка? А зачем тебе подушка? Ты что, на вахте спать собрался! А ну, отдай сюда корзину. Яйца-то сырые, чёрт тебя дери! И бутерброды измазал, и подушку испортил. Какого шлейфа ты их не сварил? Иди отсюда, сам знаю, что яйца сверху кладут.

Память под стек выделяет Windows.

Теперь осмысленно вернёмся к нашей первой программе под форточки:

Команда Push кладёт в стек указанное в ней значение. Оно может быть два байта или четыре. В 32-битных программах это будет 32-битное значение, то есть dword (4 байта). Так что для нас теперь одно значение в стеке 4 байта.

Это, естественно, целое и для «удобства» чтения, младший байт уже справа, старший слева.

Матрос, ты запомнил, что на наших чертежах в целой боевой единице Бинарников байты строятся по старшинству слева направо? Каждый младший — самый левый, каждый старший — самый правый (на фиг такие чертёжи :).
У них там в главном штабе есть ещё запутка — стек. Он в чертежах идёт от дна наверх. Целые (word или dword) в нём укладываются именно так: от дна наверх.

Действительно, стек проще представлять в высоту, чем в одну строку. Вот что будет содержаться в стеке перед выполнением строки 40100Eh в нашей программе.

Каждая строка здесь — одно значение в стеке. Понятие дно очень наглядно объясняет, как устроен стек.

Давайте удалим из исходника эту строку:

И впишем вместо неё вот такие команды:

Соберите программу ещё раз. Откройте в Olly.

То, что вы делали в отладчике CodeView клавишей F10, называется по-умному:
пошаговая трассировка исполняемого кода без захода в процедуры. В Olly такой «шаг» выполняет клавиша F8.

Когда вы дойдёте до строки 401014h (F8 четыре раза), стек будет выглядеть вот так:

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

Вы должны были понять, что каждое новое значение укладывается сверху. Получается, стек растёт вверх. Получается, регистр ESP с каждым новым значением стека уменьшается. И каждый раз, когда из стека извлекают значение, ESP увеличивается на 4 (или на 2 в 16-битных программах).

А ещё стек нужно выравнивать, иначе программа будет работать неправильно. После того, как ваши данные в стеке отработали или просто больше не нужны, возвращайте вершину в положение, которое она занимала раньше — это и есть выравнивание. Если вы так не сделаете, программа вызовет ошибку.

Стек, пожалуй, самое сложное понятие, которое нужно усвоить для написания программ на Ассемблере. Честно-честно, если вы поймёте это, значит, вы точно сможете освоить всё остальное. Конечно, нужна практика, помню, я три дня потратил на понимание стека. Калашников обманул, сказал, что это просто. Так и было, когда я его примеры использовал, а вот когда свои программки стал писать, стек вызвал наибольшие затруднения.

Три дня — думаю, это немного для самого сложного фокуса.

Чтоб положить что-то в стек, пишите:

Команда PUSH

Происхождение

От англ. слова push — толкать

Формат

push операнд

Действие

Толкает значение в стек

Примечание

Если нужно толкнуть в стек все 8 регистров общего назначения (EAX, EBX, ECX, EDX, EBP, ESP, ESI, EDI), то лучше использовать одну команду PUSHA/PUSHAD

Чтоб достать число с «верхушки стека» куда-то, пишите:

Команда POP

Происхождение

От англ. слова pop — извлекать, раскошеливаться

Формат

POP операнд

Действие

Извлекает значение из стека

Примечание

Если нужно достать из стека все РОН, уложенные командой PUSHA, используйте POPA/POPAD

Следующий пример (prax06.com).

Наберите в Hiew’e эти байты:

Теперь посмотрите на асмовый вид:

Давайте проанализируем этот пример.

Эта строка помещает в регистр AX значение 20CD.

Эта строка кладёт в стек word (слово) 20CDh из регистра EAX. Причём в памяти слово вы увидите как положено — CD,20h.

А вот эта строка зацикливает выполнение строки с адресом 03. То есть после выполнения строки 03 в строке 04 будет совершаться безусловный переход к строке 03.

Так почему же этот пример не вешает комп в ДОС и не виснет сам в WinXP? Запустите и вы увидите, что прога выполнится за доли секунды.

Можете попробовать разобрать хитрость в CV сами, но это займёт некоторое время :).

Честно скажу, что отладчик типа CodeView не очень подходит для разбора этого пустячка.

Дело в том, что простые DOS-отладчики используют стек программы в своих целях, что сбивает значение регистра ESP между шагами. Здесь бы лучше всего запустить пример под SoftIce’ом. И тогда вы могли бы «ровно» увидеть, как программа 32637 раз запишет в стек два байта CD20h. Последний раз байты будут вписаны вместо команды «jmp 03».

CD20h — это машинный код команды int 20h (вызов прерывания завершения программ, как вы уже знаете).

Стек в com-программе начинается на дне её сегмента (FFFE — последний адрес, кратный двум).

Получается, что стек всё растёт и растёт вверх и ничего его не остановит, кроме завершающего или ошибочного кода.

И вот он заполняет всё свободное место в сегменте, а затем начинает затирать данные (в этом примере их нет). Далее затирается код программы.

Такая ситуация называется ошибкой переполнения стека. Для DOS-программ данная ошибка — одна из самых распространённых.

В Win32-программах механизм выделения стека более развит (ОСь сама занимается этим вопросом, используя средства защищённого режима). Однако ошибки переполнения стеков — всё ещё настоящий вызов для программистов, особенно в сфере безопасности. Вы только что могли наблюдать, как из-за такой ошибки стек начинает выполняться.

Ведь изначально программа просто отправляла данные CD20h в стек. Но затем стек вырос (адрес в ESP уменьшился!) до значения, равного адресу команды JMP 0103 (в CodeView этого не видно!). И вот, после следующего выполнения команды push AX, происходит чудесное превращение данных в исполняемый код.

В CodeView тоже можно проследить самый интересный момент этого примера. Измените регистр указателя стека (ESP). Например, установите в нём значение 200h (чтоб долго не трейсить). После этого — F10 много раз, и прога завершится. Посмотрите ещё и ещё раз. Может быть, вы придумаете, как такой фокус можно использовать в своих целях ;).

Предлагаю вам ещё один пример Win32.

Учтите, что русские буквы должны быть в кодировке ANSI (стандарт для форточек).

Наберите, пожалуйста, сами секцию кода. Я уверен, что у вас будут опечатки, и это очень ускорит процесс обучения.

Для того, чтоб пример сделал то, что ему положено, запустите его с каким-нибудь ключом типа:
prax07.exe qwerty

Ну что ж, отладчик Olly — ваш лучший друг. При открытии впишите ключ в поле «Arguments». Не торопитесь, подумайте что к чему.

Здесь используются 3 API-функции:

  • GetCommandLine — возвращает в регистр EAX адрес командной строки программы вместе с путём в кавычках. Например:
    «D:tutprax07.exe » qwerty
  • MessageBox — вызывает функцию вывода сообщений на экран.
  • ExitProcess — функция завершения.

Осталась только одна команда, которую вы пока не знаете в этой программе. Команда CALL, но это целая тема. И, между прочим, последняя теоретическая тема в наших уроках.

Основы Ассемблера

Когда вы пишете программу на ассемблере, вы просто пишете команды процессору. Команды процессору — это просто коды или коды операций или опкоды. Опкоды — фактически «читаемый текст»- версии шестнадцатеричных кодов. Из-за этого, ассемблер считается самым низкоуровневым языком программирования, все в ассемблере непосредственно преобразовывается в шестнадцатеричные коды. Другими словами, у вас нет компилятора, который преобразовывает язык высокого уровня в язык низкого уровня, ассемблер только преобразовывает коды ассемблера в данные.

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

Комментарии в ваших программах оставляются после точки с запятой. Точно также как в дельфи или си через //.

Числа в ассемблере могут представляться в двоичной, десятеричной или шестнадцатеричной системе. Для того, чтобы показать в какой системе использовано число надо поставить после числа букву. Для бинарной системы пишется буква b (пример: 0000010b, 001011010b), для десятеричной системы можно ничего не указывать после числа или указать букву d (примеры: 4589, 2356d), для шестнадцатеричной системы надо указывать букву h, шестнадцатеричное число надо обязательно писать с нулём в начале (примеры: 00889h, 0AC45h, 056Fh, неправильно F145Ch, С123h).

Самая первая команда будет хорошо всем известная MOV.

Эта команда используется для копирования (не обращайте внимания на имя команды) значения из одного места в другое. Это ‘место’ может быть регистр, ячейка памяти или непосредственное значение (только как исходное значение). Синтаксис команды:

mov приемник, источник

Вы можете копировать значение из одного регистра в другой.

Вышеприведенная команда копирует содержание ecx в edx. Размер источника и приемника должны быть одинаковыми, например: эта команда — НЕ допустима:

mov al, ecx ; не правильно

Этот опкод пытается поместить DWORD (32-битное) значение в байт (8 битов). Это не может быть сделано mov командой (для этого есть другие команды).

А эти команды правильные, потому что у них источник и приемник не отличаются по размеру:

mov al, bl
mov cl, dl
mov cx, dx
mov ecx, ebx

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

смещение3435363738393A3B3C3D3E3F404142
данные0D0A50324457257A5E72EF7DFFADC7

(Каждый блок представляет байт)

Значение смещения обозначено здесь как байт, но на самом деле это это — 32-разрядное значение. Возьмем для примера 3A, это также — 32-разрядное значение: 0000003Ah. Только, чтобы с экономить пространство, некоторые используют маленькие смещения.

Посмотрите на смещение 3A в таблице выше. Данные на этом смещении — 25, 7A, 5E, 72, EF, и т.д. Чтобы поместить значение со смещения 3A, например, в регистр, вы также используете команду mov:

mov eax, dword ptr [0000003Ah]

Означает: поместить значение с размером DWORD (32-бита) из памяти со смещением 3Ah в регистр eax. После выполнения этой команды, eax будет содержать значение 725E7A25h. Возможно вы заметили, что это — инверсия того что находится в памяти: 25 7A 5E 72. Это потому, что значения сохраняются в памяти, используя формат little endian . Это означает, что самый младший байт сохраняется в наиболее значимом байте: порядок байтов задом на перед. Я думаю, что эти примеры покажут это:

dword (32-бит) значение 10203040 шестнадцатиричное сохраняется в памяти как: 40, 30, 20, 10

word (16-бит) значение 4050 шестнадцатиричное сохраняется в памяти как: 50, 40

Вернемся к примеру выше. Вы также можете это делать и с другими размерами:

mov cl, byte ptr [34h] ; cl получит значение 0Dh
mov dx, word ptr [3Eh] ; dx получит значение 7DEFh

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

Byte — 1 байт
Word — 2 байта
Dword — 4 байта

Иногда размер можно не указывать:

mov eax, [00403045h]

Так как eax — 32-разрядный регистр, ассемблер понимает, что ему также требуется 32-разрядное значение, в данном случае из памяти со смещением 403045h.

Можно также непосредственные значения:

Эта команда просто запишет в регистр edx, значение 5006. Скобки, [ и ], используются, для получения значения из памяти (в скобках находится смещение), без скобок, это просто непосредственное значение.

Можно также использовать регистр как ячейку памяти (он должен быть 32-разрядным в 32-разрядных программах):

mov eax, 403045h ; пишет в eax значение 403045
mov cx, [eax] ; помещает в регистр CX значение (размера word) из памяти
указанной в EAX (403045)

В mov cx, [eax], процессор сначала смотрит, какое значение (= ячейке памяти) содержит eax, затем какое значение находится в той ячейке памяти, и помещает это значение (word, 16 бит, потому что приемник, cx, является 16-разрядным регистром) в CX.

Стековые операции — PUSH, POP.

Перед тем, как рассказать вам о стековых операциях, я уже объяснял вам, что такое стек. Стек это область в памяти, на которую указывает регистр стека ESP. Стек это место для хранения адресов возврата и временных значений. Есть две команды, для размещения значения в стеке и извлечения его из стека: PUSH и POP. Команда PUSH размещает значение в стеке, т.е. помещает значение в ячейку памяти, на которую указывает регистр ESP, после этого значение регистра ESP увеличивается на 4. Команда Pop извлекает значение из стека, т.е. извлекает значение из ячейки памяти, на которую указывает регистр ESP, после этого уменьшает значение регистра ESP на 4. Значение, помещенное в стек последним, извлекается первым. При помещении значения в стек, указатель стека уменьшается, а при извлечении — увеличивается. Рассмотрим пример:

(1) mov ecx, 100
(2) mov eax, 200
(3) push ecx ; сохранение ecx
(4) push eax
(5) xor ecx, eax
(6) add ecx, 400
(7) mov edx, ecx
(8) pop ebx
(9) pop ecx

Анализ:
1: поместить 100 в ecx
2: поместить 200 в eax
3: разместить значение из ecx (=100) в стеке (размещается первым)
4: разместить значение из eax (=200) в стеке (размещается последним)
5/6/7: выполнение операций над ecx, значение в ecx изменяется
8: извлечение значения из стека в ebx: ebx станет 200 (последнее размещение, первое извлечение)
9: извлечение значения из стека в ecx: ecx снова станет 100 (первое размещение, последнее извлечение)

Чтобы узнать, что происходит в памяти, при размещении и извлечении значений в стеке, см. на рисунок ниже:

Смещение1203120412051206120712081209120A120B
Значение000000000000000000
ESP

(стек здесь заполнен нулями, но в действительности это не так, как здесь). ESP стоит в том месте, на которое он указывает)

mov ax, 4560h
push ax

Смещение1203120412051206120712081209120A120B
Значение000060450000000000
ESP

mov cx, FFFFh
push cx

Смещение1203120412051206120712081209120A120B
ЗначениеFFFF60450000000000
ESP

pop edx

Смещение1203120412051206120712081209120A120B
ЗначениеFFFF60450000000000
ESP

edx теперь 4560FFFFh.

Вызов подпрограмм возврат из них — CALL, RET. Команда call передает управление ближней или дальней процедуре с запоминанием в стеке адреса точки возврата. Команда ret возвращает управление из процедуры вызывающей программе, адрес возврата получает из стека. Пример:

..code..
call 0455659
..more code..
Код с адреса 455659:
add eax, 500
mul eax, edx
ret

Когда выполняется команда call, процессор передает управление на код с адреса 455659, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой. Вы можете поместить код, который вы часто используете в процедуру и каждый раз когда он вам нужен вызывать его командой call.

Подробнее: команда call помещает регистр EIP (указатель на следующюю команду, которая должна быть выполнена) в стек, а команда ret извлекает его и передаёт управление этому адресу. Вы также можете определить аргументы, для вызываемой программы (процедуры). Это можно сделать через стек:

push значение_1
push значение_2
call procedure

Внутри процедуры, аргументы могут быть прочитаны из стека и использованы. Локальные переменные, т.е. данные, которые необходимы только внутри процедуры, также могут быть сохранены в стеке. Я не буду подробно рассказывать об этом, потому, что это может быть легко сделано в ассемблерах MASM и TASM. Просто запомните, что вы можете делать процедуры и что они могут использовать параметры.

Одно важное замечание:
регистр eax почти всегда используется для хранения результата процедуры.

Это также применимо к функциям windows. Конечно, вы можете использовать любой другой регистр в ваших собственных процедурах, но это стандарт.

Вот и кончился очередной урок. На следующем уроке мы напишем первую программу на ассемблере.

Квайн на Ассемблере? Это просто

Известно, что квайн (куайн) — программа, результатом работы которой является вывод текста самой программы. При этом предполагается, что обращения к файлу, содержащему программу, не происходит.

Квайны могут быть составлены практически на всех языках программирования за исключением, быть может, некоторых экзотических. Рассмотрим задачу составления квайна на ассемблере, предполагая, что программа должна быть Win32 приложением и должна выводить свой текст в окно.

Один и тот же машинный код может быть представлен на ассемблере по-разному. Допустим, что требуется затолкнуть в стек число 48. Соответствующая двухбайтовая инструкция может быть записана как мнемонически

push 48 (или же push 30h),

так и с использованием директивы определения данных db

db 06Ah,030h (или же db 106,48).

Возможен также вариант db ‘j0’. Во всех перечисленных случаях при компиляции получается один и тот же машинный код. Написать ассемблерный квайн, содержащий мнемоническое представление инструкций, затруднительно — по-видимому, потребуются тысячи строк программы. Поэтому основная идея, которой будем руководствоваться — получение квайна с директивами db в виде

(используется ассемблер MASM). Трудность в том, что напрямую написать такой квайн почти невозможно — это фактически программирование в машинных кодах. Однако начать можно с записи программы с помощью обычной ассемблерной мнемоники — см. приводимый ниже исходник 2. Важно при этом, что при компиляции исходников 1 и 2 получается один и тот же машинный код.

Принцип работы ясен из приведенных в тексте комментариев. Отметим только, что программа состоит из трех частей. В первой части в стеке формируется текст квайна (т.е. исходник 1). Количество байтов, определяемых одной директивой db (следовательно, и ширина строк квайна), задается константой bStr — она установлена равной 25-и. Поскольку во время компиляции не импортируются какие-либо API функции, то во второй части динамически определяются адреса ряда функций. Вначале это адрес функции GetProcAddress, получаемый трассировкой таблицы экспорта модуля kernel32.dll. Затем — адреса остальных функций, получаемых с помощью GetProcAddress. Наконец, в третьей части осуществляется вывод квайна в окно MessageBox. Поскольку по умолчанию выводимый текст статический и его нельзя куда-либо скопировать, дополнительно устанавливается хук, позволяющий копировать текст из окна. Возможны другие варианты третьей части квайна — например, с помощью создания очереди сообщений и написания оконной функции.

Итак, исходник 2 (не являющийся квайном) необходимо сохранить в пакетном файле quine.cmd и запустить его — получается исполняемый файл quine.exe. Далее запускается quine.exe, и из появившегося окна следует, выделив мышью, скопировать текст в файл quine.asm. Исполняемый файл можно удалить. Таким образом, в quine.asm — полноценный квайн (см. исходник 1). Транслируется он с помощью

ml /c /coff quine.asm

а линкуется с помощью

link /subsystem:windows /section:.text,RW quine.obj

(в секцию кода должна быть разрешена запись). Размер len секции кода составил 820 байт.

Отладку программы я производил турбо дебаггером, но при этом дополнительно включал строку

перед точкой входа start (можно взять любую другую функцию), и строки

include masm32includekernel32.inc
includelib masm32libkernel32.lib

Иначе дебаггер зависает — по-видимому, из-за отсутствия таблицы импорта в исполняемом файле.

Ссылка на основную публикацию
Adblock
detector