пятница, 21 сентября 2007 г.

Разворот стека

Я в репозитории уже писал 101 способ разворота стека (шучу конечно). Но на практике все оказалось, конечно, сложнее.

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

IA32 насчитывает порядка 33 варианта внутрисегментных вызовов функций. 32 из которых хранят адреса в регистрах или в памяти, докопаться до этих адресов при развороте стека нереально (закон жизни?). Но есть одна форма, использующаяся в большинстве случаев, при которой не просто реально, а тривиально (закон Фостерс!).

Эта форма выглядит так: call rel32
rel32 задает смещение относительно адреса следующей команды, который лежит у нас в стеке как адрес возврата. То есть эта форма позволяет одним легким движением найти адрес функции которая была вызвана.

foo(); bar();

Стек: ...
... call foo ; e8 (foo - <адрес возврата>)
адрес возврата ---> ...
...

При обнаружении остальных форм команды call мы не можем точно определить адрес начала функции, но, мы знаем хотя бы, что этот адрес в стеке на самом деле является адресом возврата а не мусором, а это уже что-то.

Но не все гладко. В зависимости от режима оптимизации да и сам по себе стек может содержать адреса, которые на самом деле не относятся к текущей иерархии вызовов а остались в стеке с былых времен.

foo(); tar();

Стек: ...
... call moo ; e8 (moo - <адрес возврата>)
адрес возврата ---> ...
... ret

bar();
...
... call foo ; e8 (foo - <адрес возврата>)
адрес возврата ---> ...
...

При сканировании стека мы сперва обнаруживаем ссылку на tar, который на самом деле не вызывает foo! Такая ссылка может возникнуть при резервировании пространства для локальных переменных foo.

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

Но об этом пожалуй в другой раз.

PS: Сбивчивая получилась статья, особенно со слайдами. :(