Kris Kaspersky - Тонкости дизассемблирования
Заметим, что коды операций mov, манипулирующих этими регистрами, различны, поэтому-то и возникает кажущееся совпадение имен. С управляющими регистрами связана одна любопытная мелочь. Регистр CR1, как известно большинству, в настоящее время зарезервирован и не используется. Во всяком случае, так написано в русскоязычной документации. На самом деле регистр CR1 просто не существует! И любая попытка обращения к нему вызывает генерацию исключение INT 06h. Например, cuр386 в режиме эмуляции процессора этого не учитывает и неверно исполняет программу. А все дизассемблеры, за исключением IDA Pro, неправильно дизассемблируют этот несуществующий регистр:
Все эти команды на самом деле не существуют и приводят к вызову прерывания INT06h. Не так очевидно, правда? И еще менее очевидно обращение к регистрам DR4-DR5. При обращении к ним исключения не генерируется. Между прочим, IDA Pro 3.84 дизассемблирует не все регистры. Зато великолепно их ассемблирует (кстати, ассемблер этот был добавлен другим разработчиком).
Пользуясь случаем, акцентируем внимание на сложностях, которые подстерегают при написании собственного ассемблера (дизассемблера). Документация Intel местами все же недостаточно ясна (как в приведенном примере), и неаккуратность в обращении с ней приводит к ошибкам, которыми может воспользоваться разработчик защиты против хакеров.
Теперь перейдем к описанию режимов адресации микропроцессоров Intel. Тема очень интересная и познавательная не только для оптимизации кода, но и для борьбы с отладчиками.
Первым ключевым элементом является байт modR/M.
Как отмечалось выше, по байту modR/M нельзя точно установить регистры. В зависимости от кода операции и префиксов размера операндов, результат может коренным образом меняться.
Биты 3-5 могут вместо определения регистра уточнять код операции (в случаи, если один из операндов представлен непосредственным значением). Младшие три бита всегда либо регистр, либо способ адресации, что зависит от значения 'mod'. А вот биты 3-5 никак не зависят от выбранного режима адресации и задают всегда либо регистр, либо непосредственный операнд.
Формат поля R/M, строго говоря, не документирован, однако достаточно очевиден, что позволяет избежать утомительного запоминания совершенно нелогичной на первый взгляд таблицы адресаций (таблица 3).
Возможно, кому-то эта схема покажется витиеватой и трудной для запоминания, но зубрить все режимы без малейшего понятия механизма их взаимодействия еще труднее, кроме того, нет никакого способа себя проверить и проконтролировать ошибки.
Действительно, в поле R/M все три бита тесно взаимосвязаны, в отличии от поля mod, которое задает длину следующего элемента в байтах.
Разумеется, не может быть смещения 'offset 12', (т.к. процессор не оперирует с полуторными словами), а комбинация '11' указывает на регистровую адресацию.
Может возникнуть вопрос, как складывать с 16-битным регистром 8 битное смещение? Конечно, непосредственному сложению мешает несовместимость типов, поэтому процессор сначала расширяет 8 бит до слова с учетом знака. Поэтому, диапазон возможных значений составляет от —127 до 127. (или от —0x7F до 0x7FF).
Все вышесказанное проиллюстрировано в приведенной ниже таблице 3. Обратим внимание на любопытный момент — адресация типа [BР] отсутствует. Ее ближайшим эквивалентом является [BР + 0]. Отсюда следует, что для экономии следует избегать непосредственного использования BР в качестве индексного регистра. BР может быть только базой. И mov ax,[bp] хотя и воспринимается любым ассемблером, но ассемблируется в mov ax,[bр+0], что на байт длиннее.
Исследовав приведенную ниже таблицу 1, можно прийти к выводу, что адресация в процессоре 8086 была достаточно неудобной. Сильнее всего сказывалось то ограничение, что в качестве индекса могли выступать только три регистра (BX, SI, DI), когда гораздо чаще требовалось использовать для этого CX (например, в цикле) или AX (как возвращаемое функцией значение).
Поэтому, начиная с процессора 80386 (для 32-разрядного режима), концепция адресаций была пересмотрена. Поле R/M стало всегда выражать регистр независимо от способа его использования, чем стало управлять поле 'mod', задающие, кроме регистровой, три вида адресации:
Видно, что поле 'mod' по-прежнему выражает длину следующего поля — смещения, разве что с учетом 32-битного режима, где все слова расширяются до 32 бит.
Напомним, что с помощью префикса 0x67 можно и в 16-битном режиме использовать 32-битный режимы адресации, и наоборот. Однако, при этом мы сталкиваемся с интересным моментом — разрядность индексных регистров остается 32-битной и в 16-битном режиме!
В реальном режиме, где нет понятия границ сегментов, это действительно будет работать так, как выглядит, и мы сможем адресовать первые 4 мегабайта памяти (32 бита), что позволит преодолеть печально известное ограничение размера сегмента 8086 процессоров в 64К. Но такие приложения окажутся нежизнеспособными в защищенном или V86 режиме. Попытка вылезти за границу 64К сегмента вызовет исключение 0Dh, что приведет к автоматическому закрытию приложения, скажем, под управлением Windows. Аналогично поступают и отладчики (в том числе и многие эмуляторы, включая cuр386).
Сегодня актуальность этого приема, конечно, значительно снизилась, поскольку «голый DOS» практически уже не встречается, а режим его эмуляции Windows крайне неудобен для пользователей.
Изучив эту таблицу, можно прийти к заключению, что система адресации 32-битного режима крайне скудная, и ни на что серьезное ее не хватит. Однако, это не так. В 386+ появился новый байт SIB, который расшифровывается 'Scale-Index Base'.
Процессор будет ждать его вслед за R/M всякий раз, когда последний равен 100b. Эти поля отмечены в таблице как '[-]'. SIB хорошо документирован, и назначения его полей показаны на рисунке 6. Нет совершенно никакой необходимости зазубривать таблицу адресаций.
'Base' это базовый регистр, 'Index' — индексный, а два байта 'Scale' — это степень двойки для масштабирования. Поясним введенные термины. Ну, что такое индексный регистр, понятно всем. Например SI. Теперь же в качестве индексного можно использовать любой регистр. За исключением, правда, SР; впрочем, можно выбирать и его, но об этом позже.
Базовый регистр, это тот, который суммируется с индексным, например, [BР+SI]. Аналогично, базовым теперь может быть любой регистр. При этом есть возможность в качестве базового выбрать SР. Заметим, что если мы выберем этот регистр в качестве индексного, то вместо 'SР' получим — «никакой»: в этом случае адресацией будет управлять только базовый регистр.
Ну и, наконец, масштабирование — это уникальная возможность умножать индексный регистр на 1,2,4,8 (т.е. степень двойки, которая задается в поле Scale). Это очень удобно для доступа к различным структурам данных. При этом индексный регистр, являющийся одновременно и счетчиком цикла, будет указывать на следующий элемент структуры даже при единичном шаге цикла (что чаще всего и встречается). В таблице 4 показаны все возможные варианты значений байта 'SIB'.
Если при этом в качестве базового регистра будет выбран EBР, то полученный режим адресации будет зависеть от поля MOD предыдущего байта. Возможны следующие варианты:
Итак, мы полностью разобрались с кодировкой команд. Осталось лишь выучить непосредственно саму таблицу кодов, и можно отправляться в длинный и тернистый путь написания собственного дизассемблера.
За это время, надеюсь, у вас разовьются достаточные навыки для ассемблиро-вания/дизассемблирования в уме. Впрочем, есть множество эффективных приемов, позволяющих облегчить сей труд. Ниже я покажу некоторые из них. Попробуем без дизассемблера взломать crackme01.com. Для этого даже не обязательно помнить коды всех команд!
Итак, для начала поищем, кто выводит текст 'Crack me… Туре password:'. В самом файле начало текста расположено со смещением 77h. Следовательно, учитывая, что com файлы загружаются, начиная со смещения 100h, эффективное смещение, равняется 100h+77h=177h. Учитывая обратное расположение старших и младших байт, ищем в файле последовательность 77h 01h.
Вот она! Но что представляет собой код 0BAh? Попробуем определить это по трем младшим битам. Они принадлежат регистру DL(DX). А 0B4h 09h — это *AH,9. Теперь нетрудно догадаться, что оригинальный код выглядел как:
И это при том, что не требуется помнить код команды MOV! (Хотя это очень распространенная команда и запомнить ее код все же не помешает).