передача и использование аргументов при раздельных стеках


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

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

Основную трудность, конечно, представляет засылка аргументов в рукотворный стек. Это под MS-DOS мы могли выделить отдельный сегмент и использовать PUSH с префиксом "GS:", а под Windows приходится использовать MOV [EBP+XXh], YYYY и это при том, что адресации типа "память - память" в x86 процессорах не было и нет. В практическом плане это означает, что нам придется использовать промежуточные регистры: MOV EAX, [YYYY]/MOV [EBP+XXh], EAX. Впрочем, это можно оптимизировать, если использовать команду STOSD, занимающую в машинном представлении всего один байт и копирующую содержимое EAX в ячейку на которую указывает EDI одновременно с увеличением последнего на размер двойного слова. Стаскивать аргументы с рукотворного стека можно командой LODSD.

Окончательно расхулиганившись, можно создать целых три стека — один, "стандартный" для хранения адресов возврата, другой — для аргументов и третий для локальных переменных. Чтобы не расходовать регистры понапрасну, можно хранить указатели на вершины двух "рукотворных" стеков в оперативной памяти, загружая их то в регистр EBP, то в ESI/EDI в зависимости от того, какой из них окажется удобнее в данный конкретный момент. Падения производительности можно не опасаться. Большую часть своего времени указатели будут проводить в кэш-памяти, извлекаясь всего за один-два такта.


Естественно, все, сказанное выше, относится _только_ к нашим собственным функциям, а API- функции операционной системы таких извращений не понимают и ожидают аргументов в "стандартном" стеке. Ну… что тут можно сказать… "Персонально" для API-функций аргументы можно передать и в стандартном стеке, предварительно убедившись, что при данных аргументах функция гарантированно не вызовет переполнения (что вовсе не факт, особенно при работе с функциями из библиотеки mshtml.dll). К тому же, в 64-битной редакции Windows аргументы API-функциями в большинстве случаев передаются не через стек, а через регистры, поэтому описанная методика к ним вполне применима.

А вот как защитить от переполнения функции обычных библиотек? Самое простое решение — вызвать функции не по CALL, а по JMP, разместив адрес возврата на вершине страницы памяти, доступной только на чтение. Ниже ее будут только аргументы (доступные так же только на чтение), а вот локальные переменные, создаваемые функцией будут доступны и на чтение и на запись. Естественно, этот трюк будет работать только с теми функциями, которые не изменяют своих аргументов (а многие из них изменяют их только так), но по другому просто не получается!


Содержание раздела