понедельник, 28 января 2008 г.

Разворот стека методом прямой трассировки кода.

Во, как загнул... и это не шутка, метод уже реально работает.

RISC - это наверное рай для программистов на ассемблере. И для программистов компиляторов заодно. Это же счастье, когда любая инструкция занимает ровно 32 бита, и общее количество инструкций порядка 3 дестков...

Но нет, мы на земле. Количество инструкций IA32 не поддается трезвому подсчету. Но мы всетаки пишем и не компилятор, нам достаточно лишь достоверно выяснить, какая функция какую вызывает, на основании имеющегося под рукой стека.

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

Для разворота стека методом прямой трассировки кода - необходимо для начала определить какую либо точку кода. Чтобы проделать это - мы берем первое слово из стека, причем, это именно голова стека, то есть то место откуда стек начался. И проверяем, не может ли это значение быть адресом возврата? Для этого необходимо убедиться, что перед ним стоит инструкция вызова.

Можно сказать что это режим разворота без точки выполнения. Мы еще будем к нему возвращаться в случае потери адреса.

    инструкция call (в любой форме)
value <-- адрес возврата


Стоит отметить, что в новой версии я везде проверяю доступность памяти. Первая версия разворачивателя вполне могла бы вызвать PageFault.

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

Кого-то, может быть, пугает слово трассировка, но не стоит пугаться. Нам вовсе не обязательно эмулировать все инструкции. Мы анализируем следующие группы инструкций:
Инструкции, модифицирующие содержимое регистра esp, двигают указатель стека; Инструкции безусловных переходов передвигают указатель трассировки, за исключением случая, когда переход осуществляется на саму инструкцию; Инструкции условных переходов проверяются по обоим ветвям. Что касается различных форм инструкции call - то адрес в стеке обязательно должен соответствовать адресу следующей инструкции, иначе этот call протрассировывается мимо. По разумным соображениям я не включаю в список команд вызова различные прерывания. Да и не все call'ы одинаково полезны, как я уже писал раньше. Я могу точно определять явно заданные адреса, но косвенные формы лишь показывают что вызов есть, но куда было передано управление - остается неизвестным, происходит потеря адреса.

Анализируется еще одна ситуация, когда содержимое стека соответствует срабатыванию исключения (адрес возврата, селектор сегмента), но в этом случае происходит потеря адреса, ибо доподлинно не известно, какое исключение сработало.

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

В этом методе осталось еще не реализованной возможность определения количества аргументов, но теоретически это возможно.

Пока что, без переходов через шлюзы - результат вполне обнадеживающий:

CallStack: Stopped in StubKernelUsePage+122 (0x00102a00)
CallStack: Called by StubPageInitMode+31 (0x00100df9)
CallStack: Called by StubEntry+587 (0x00101428)
CallStack: Called by unknown+0 (0x001000d4)

Последняя точка осталась неизвестной, потому, что для определения символа необходимо отмотать по коду несколько команд назад StubEntryLo=0x001000cc, но это мелочи по большому счету.