Реферат Курсовая Конспект
Add b, [00100],002 - раздел Информатика, NAG SCREEN 00000105: 00B406B2 Add [Si][Ob206],dh | ----------Ip ...
|
00000105: 00B406B2 add [si][OB206],dh
|
----------ip
Т.е. текущая команда станет на байт короче! И "отрезанный" ноль теперь — часть другой команды! Но при выполнении на "живом" процессоре этого не произойдет, так как следующее значение ip вычисляется еще до выполнения команды на стадии ее декодирования.
Совсем другое дело — отладчики, и особенно отладчики-эмуляторы, которые часто вычисляют значение ipпосле выполнения команды (это легче запрограммировать). В результате наступает крах. Несущественное, казалось бы обстоятельство — до или после выполнения команды вычисляется ip — оказалось роковым. Приведу в подтверждение дамп экрана:
Заметим, что этот прием может быть бессилен против трассирующих отладчиков (Debug.com, DeGlucker, Cup386), поскольку значение ip за них вычисляет процессор и вычисляет его правильно.
Однако на "обычные" отладчики управа всегда найдется (см. гл. "Приемы против отладчиков реального режима"), а с эмуляционными справиться гораздо труднее и приведенный пример один из немногих, где обеспечивается воздействие на виртуальный процессор.
Перейдем теперь к рассмотрению префиксов. Они делятся на четыре группы:
1, Префиксы блокировки и повторения:
• OxFO LOCK-префикс;
« OxF2 REPNZ (только для строковых инструкций);
• OxF3 REP (только для строковых инструкций).
2. Префиксы переопределения сегмента:
« Ох2Е CS:
« 0х36 SS:
' ОхЗЕ DS:
« 0х26 ES:
« 0х64 FS:
. 0х65 GS:
3. Префикс переопределения размеров операндов:
«0х66
4. Префикс переопределения размеров адреса:
«0х67
Если используется более одного префикса из той же самой группы, то действие команды не определено и по-своему реализовано на разных типах процессоров.
Префикс переопределения размера операндов используется в 16-разрядном режиме для манипуляции с 32-битными операндами и наоборот. При этом он может стоять перед любой командой, например 0х66 : CLI будет работать! А почему бы и нет? Интересно, но отладчики этого не учитывают и отказываются работать. То же относится к к дизассемблерам, к примеру IDA:
seg000:0100 start proc near
seg000:0100 db 66h
seg000;0100 cli
seg000:0102 db 67h
seg000:0102 sti
seg000:0104 retn
На этом же основан один очень любопытный прием противостояния отладчикам, в том числе и знаменитому отладчику-эмулятору cup386. Рассмотрим, как работает конструкция 0х66 : RETN. Казалось бы, коль скоро функция retn не имеет операндов, то префикс 0х66 можно просто игнорировать. На самом деле все не так просто. Retn работает с неявным операндом — регистром ip/eip. Именно это и изменяет префикс. Разумеется, в реальном и 16-разрядном режиме указатель команд всегда обрезается до 16 бит, и поэтому на первый взгляд возврат сработает корректно. Но стек-то окажется несбалансированным! Из него вместо одного слова взяли целых два! Так нетрудно получить и исключение ОхС — исчерпание стека. Попробуйте отладить чем-нибудь пример cracklE.com — даже сир386 во всех режимах откажется это сделать, a Tui'bo-De-buger вообще зависнет! IDA не сможет отследить стек и вместе с ним все локальные переменные.
Как видим, прием крайне прост, но и крайне надежен. Впрочем, следует признать, что перехват int ОхС под операционной системой Windows бесполезен и, несмотря на все ухищрения, приложение, породившее такое исключение, будет безжалостно закрыто. Хотя в реальном режиме это работает превосходно. Попробуйте убедиться в этом па примере cracklE.com. Забавно наблюдать реакцию на него эмулирующих отладчиков. Все они либо неправильно работают (снимают одно слово из стека, а нс два), либо совершают очень далекий переход по 32-битному eip (в результате чего виснут), либо — чаще всего — просто аварийно прекращают работу по исключению ОхС (так ведет себя сир386).
Еще интереснее получится, если попытаться исполнить в 16-разрядном сегменте команду CALL. Если адрес перехода лежит в пределах сегмента, то ничего необычного ожидать не приходится. Инструкция работает нормально. Чудеса начинаются, когда адрес выходит за эти границы. В защищенном 16-разрядном режиме при уровне привилегий CLO с большой вероятностью регистр EIP "обрежется" до шестнадцати бит и инструкция сработает (но похоже, что не на всех процессорах). Если уровень не CLO, то генерируется исключение защиты O.xD. В реальном же режиме эта инструкция может вести себя непредсказуемо. Хотя в общем случае должно генерироваться прерывание int OxD. В реальном режиме его нетрудно перехватить и совершить далекий 'Гаг* переход в требуемый сегмент. Так поступает, например, моя собственная операционная система OS7R, дающая в реальном режиме плоскую память. Разумеется, такой поворот событий не может пережить ни один отладчик. Ни трассировщики реального режима, ни v86, ни protect-mode debuger, ни даже эмуляторы (во всяком случае те, что мне известны), с этим справиться не в состоянии.
Одно плохо — все эти приемы не работают под Windows и другими операционными системами. Это вызвано тем, что обработка прерываний типа исключения общей защиты всецело лежит на ядре операционной системы и оно не позволяет приложениям распоряжаться им по своему усмотрению. Забавно, что в режиме эмуляции MS-DOS некоторые EMS-драйверы ведут себя в этом случае совершенно непредсказуемо. Часто при этом они не генерируют ни исключения ОхС, ни OxD. Это следует учитывать при разработке защит, основанных на приведенных выше приемах.
Обратим внимание также на последовательности типа 0х66 0х66 [ххх]. Хотя фирма Intel не гарантирует поведение своих процессоров в такой ситуации, но фактически все они правильно ее интерпретируют. Иное дело некоторые отладчики и дизассемблеры, которые спотыкаются и начинают вести себя некорректно.
Есть еще один интересный момент, связанный с работой декодера микропроцессора.
F E D C B A 9 8 7 6 5 4 3 2 1
Декодер
Декодер за один раз считывает только 16 байт, и если команда "не уместится", то он попросту не сможет считать "продолжение" и сгенерирует общее исключение защиты. Однако иначе ведут себя эмуляторы, которые корректно обрабатывают "длинные" инструкции.
Впрочем, все это очень процессорно-зависимо. Никак не гарантируется сохранность и преемственность работы в будущих моделях. Поэтому и злоупотреблять этим не стоит. Иначе ваша защита откажет в работе.
Префиксы перекрытия сегмента могут встречаться перед любой командой, в том числе и не обращающейся к памяти, — например, CS:NOP вполне успешно выполнится. А вот некоторые дизассемблеры сбиться могут. К счастью, IDA к ним не относится. Самое интересное, что комбинация из DS:FS:FG:CS:MOV дх, (100]
работает вполне нормально (хотя не гарантируется фирмой Intel). При этом последний префикс в цепочке перекрывает все остальные. Некоторые отладчики, наоборот, ориентируются на первый префикс в цепочке, что дает неверный результат. Этот пример хорош тем, что великолепно выполняется под Windows и другими операционными системами. К сожалению, на декодирование каждого префикса тратится один такт и все это может медленно работать.
Вернемся к формату опкода. Выше была описана структура первого байта. Отметим, что это фактически недокументировано и Intel уделяет ему всего два слова. Действительно, формат команд разнится от одной команды к другой. Однако можно выделить и некоторые общие правила.
Практически для каждой команды, если регистром-приемником фигурирует AX (AL) существует специальный однобайтовый опкод, который в трех младших битах содержит регистр-источник. Этот факт следует учитывать при оптимизации. Так, среди двух инструкций XCHG АХ,ВХ и XCHG BX.DX следует всегда автоматически выбирать первую, так как она на байт короче. (Кстати, инструкций XCHG АХ,АХ более известна нам как NOP. О достоверности этого факта часто спорят в конференциях, но на странице 340 руководства 24319101 фирмы Intel об этом сказано недвусмысленно. Выходит, никто из многочисленных спорщиков не знаком даже с оригинальным руководством производителя.)
Для многих команд (Jx) четыре младшие бита обозначают условие операции. Точнее говоря, условие задается в битах 1-2-3, а младший бит приводит к его инверсии.
Код | Мнемоника | Условие |
Переполнение | ||
B, NAE | Меньше | |
Z | Равно | |
оно | BE, NA | Меньше или равно |
S | Знак | |
P. РЕ | Четно | |
L, NGE | Меньше (знаковое) | |
LE, NO | Меньше или равно (знаковое) |
Как видим, условий совсем немного, — чтобы никаких проблем их запоминания не возникало. Теперь уже не нужно мучительно вспоминать: 'JZ' — это 0х74 или 0х75. Так как младший бит первого равен нулю, то jz это 0х74, a jnz соответственно 0х75.
Далеко не все опкоды смогли поместиться в первый байт. Инженеры Intel задумались о поиске дополнительного места для размещения еще нескольких бит н при этом обратили внимание на байт modR/M. Подробнее он описан ниже, а пока рассмотрим приведенный выше рисунок.
Трехбитовое поле reg, содержащее регистр-источник, очевидно, не используется, когда вслед за ним идет непосредственный операнд. Так почему бы его не использовать для задания опкода? Однако требуется указать процессору на такую ситуацию. Это делает префикс OxF, размещенный в первом байте опкода. Да, именно префикс, хотя документация Intel этого прямо и не подтверждает. При этом на не ММХ-процессорах для его декодирования требуется дополнительный такт. Intel же предпочитает называть первый байт основным, а второй уточняющим опкодом. Заметим, что то же поле используют многие инструкции, оперирующие с одним операндом (jmp, call). Это все очень сильно затрудняет написание собственного ассемблера/дизассемблера, но зато дает простор для самомодифицирующегося кода и, кроме того, вызывает уважение к инженерам Intel, до минимума сократившим размеры команд. Конечно, это далось весьма не просто. И далеко не все дизассемблеры работают правильно. С другой стороны, именно благодаря этому и существуют успешно противостоящие им защиты.
Чтобы избежать этого, нужно четко представлять себе сам принцип кодировки команд, а не просто "мертвую" таблицу опкодов, которую многие вводят в дизассемблер и на этом успокаиваются. Ведь внешне все работает правильно.
К тонкостям кодирования команд мы вернемся ниже, а пока приготовимся к разбору поля modR/M. Два трехбитовых поля могут задавать код регистра общего назначения по следующей таблице:
8 бит операнд | 16 бит операнд | 32 бит операнд | ||
Код | AL | AX | EAX | |
CL | CX | ECX | ||
DL | DX | EDX | ||
BL | BX | EBX | ||
AH | SP | ESP | ||
CH | BP | EBP | ||
DH | SI | ESI | ||
BH | Dl | EDI |
Опять можно восхититься лаконичностью решения инженеров Intel, которые ухитрились закодировать столько регистров всего в трех битах. Отсюда, кстати, становится 'ясно, почему нельзя выборочно обращаться к старшим и младшим байтам регистров SP, BP, SI, DI и, аналогично, старшему слову всех 32-битных регистров. Во всем "виновата" оптимизация и архитектура команд. Просто нет свободных полей, в которые можно было бы "вместить" дополнительные регистры. Сегодня мы вынуждены расхлебывать результаты архитектурных решений, выглядевших такими удачными всего лишь десятилетие назад.
Обратите внимание на порядок регистров AX-CX-DX-BX-SP-BP-SI-DI. Алфавитный порядок немного нарушен, верно? Особенно странно в этом отношении выглядит BX. Но если понять причины, то не будет никакой нужды запоминать это исключение, все станет на свои места. BX — это индексный регистр. И стоит первым среди индексных.
Таким образом, мы уже можем "вручную", без дизассемблера, распознавать в шестнадцатиричном дампе регистры-операнды. Очень неплохо для начала! Или писать самомодифицирующийся код. Например:
00000000; 800Е070024 or b,[00007],024
00000005; FA cli
00000006: ЗЗСО xor ax,ax
00000008; FB sti
Он изменит строку 0х6 на xor sp,sp. Это "завесит" многие отладчики и, кроме того, не позволит дизассемблерам отслеживать локальные переменные адресуемые через SP. Хотя IDA позволяет скорректировать стек вручную, но для этого сперва нужно понять, что sp обнулился. В приведенном примере это очевидно (хотя, кстати, и не бросается в глаза), а если это произойдет в многопоточной системе? Тогда изменение кода очень трудно будет отследить, особенно в листинге дизассемблера. Однако нужно помнить, что самомодифицирующийся код все же уходит в историю. Сегодня он встречается все реже и реже.
2-битная кодировка | 3-битная кодировка |
(X) ES | 000 ES |
01 CS | 001 CS |
10 SS | 010 SS |
11 DS | 010 DS |
100 FS | |
101 GS | |
110 Reserved | |
111 Reserved |
Первоначально сегментные регистры кодировались всего двумя битами, и этого хватало, так как их было всего четыре. Позже, когда их стало больше, перешли на трехбитную кодировку. При этом два регистра llOb и Illb пока отсутствуют и вряд ли будут добавлены в ближайшем будущем. Но что же будет, если попытаться их использовать? Генерация int 0х6. А вот отладчики — эмуляторы могут вести себя странно. Иные при этом не генерируют прерывания, чем себя и выдают, а другие часто ведут себя непредсказуемо, так как требуемый регистр может находиться в области памяти, занятой другой переменной (это происходит, когда ячейка памяти определяется по индексу регистра, при этом считываются три бита и суммируются с базой, но никак не проверяются пределы).
Поведение дизассемблеров так же разнообразно. Вот, например:
HIEW:
00000000: 8Е ???
00000001: F8 с1с
00000002: СЗ retn
QVIEM:
00000000: 8EF8 mov !s,ax
00000002: СЗ ret
IDA:
seg000:0100 start db 8Eh
seg000:0101 db 0F8h
seg000:0102 db 0C3h
Кстати, IDA вообще отказывается анализировать весь последующий код. Как это можно использовать? Да очень просто — если эмулировать еще два сегментных регистра в обработчике int 0х6, то очень трудна будет как отладка, так и дизассемблирование. Однако это опять-таки не работает под Win32!
Управляющие/отладочные регистры кодируются нижеследующим образом:
Control Register | Debug Register | |
CRO | DRO | |
Reserved | DRI | |
CR2 | DR2 |
Control Register | Debug Register | |
CR3 | DR3 | |
CR4 | Reserved | |
Reserved | Reserved | |
Reserved | DR6 | |
Reserved | DR7 |
Заметим, что опкоды операций mov, манипулирующих с ними, различны, поэтому-то и получается кажущееся совпадение имен.
С управляющими регистрами связана одна любопытная мелочь. Регистр CRI, как известно, в настоящее время зарезервирован и не используется. Так во всяком случае написано в русскоязычной документации. На самом же деле регистр CRI просто не существует! И любая попытка обращения к нему вызывает генерацию исключения int 0х6. Например, cup386 в режиме эмуляции процессора этого не учитывает и неверно исполняет программу. А все дизассемблеры, за исключением IDA, неправильно дизассемблируют этот несуществующий регистр:
IDA
seg000:0100 start db 0Fh
seg000:0101 db 20h
seg000:0102 db 0C8h
seg000;0103 db 0C3h
SOURCER:
43C5:0100 start
4305:0100 OF 20 С8 mov eax,Cr1
43C5;0103 C3 retn
Или:
43C5:0100 start
4305:0100 OF 20 F8 mov eax,Cr7
43C5;0103 C3 retn
Всех этих команд на самом деле не существует, и они приводят к вызову прерывания int 0х6. Не так очевидно, правда? И еще менее очевидно, что при обращении к регистрам DR4-DR5 исключения не генерируется. Между прочим, IDA 3.84 не дизассемблирует ни один регистр. Зато великолепно ассемблирует все (кстати, ассемблер был добавлен другим разработчиком).
Пользуясь случаем, акцентируем внимание на сложностях, которые подстерегают при написании собственного ассемблера (дизассемблера). Документация Intel местами все же недостаточно ясна (как в приведенном примере), и неаккуратность в обращении с ней приводит к ошибкам, которыми может воспользоваться разработчик защиты против хакеров.
Теперь перейдем к описанию режимов адресации микропроцессоров Intel. Тема очень интересная и познавательная — не только для оптимизации кода, но и для борьбы с отладчиками.
Первым ключевым элементом является байт modR/M.
| | | | |
mod reg r/m
Если mod == I lb, то два следующие поля будут представлять собой регистры. (Это так называемая регистровая адресация.
Например: оооооооо: ззсз -----------àxor ax bx
00000000:32СЗ-àxor аl bl
Как отмечалось выше, по байту modeR/M нельзя точно установить регистры. В зависимости от кода операции и префиксов размера операндов, результат может варьироваться в ту или иную сторону.
Биты 3-5 могут вместо регистра представлять уточняющий опкод (в случае если один из операндов представлен непосредственным значением). Младшие три бита всегда либо регистр, либо способ адресации. Последнее зависит от значения 'mod*. Отметим, что биты 3-5 никак не зависят от выбранного режима адресации и всегда задают либо регистр, либо непосредственный операнд.
Формат поля R/M, строго говоря, не документирован, однако достаточно очевиден. Во всяком случае понимание этого позволяет избежать утомительного запоминания совершенно нелогичной на первый взгляд таблицы адресаций (см. ниже).
R/M
– Конец работы –
Эта тема принадлежит разделу:
Каждому предоставлена полная свобода выбора — или терпи Nag Screen...
Если Вам нужно дополнительный материал на эту тему, или Вы не нашли то, что искали, рекомендуем воспользоваться поиском по нашей базе работ: Add b, [00100],002
Если этот материал оказался полезным ля Вас, Вы можете сохранить его на свою страничку в социальных сетях:
Твитнуть |
Новости и инфо для студентов