Как было упомянуто ранее, стек позволяет решить многие задачи. Во-первых, обеспечить локальное хранение переменных и данных функции. Во-вторых, передавать параметры в вызываемую функцию. В этой части главы будет рассказано, как компиляторы передают параметры вызываемым функциям и как это влияет на стек. Кроме того, будет уделено внимание разъяснению вопросов использования стека в командах процессора вызов функции call и возврата из нее ret.
Основные сведения
Стековый фрейм функции (stack frame) – область памяти, выделяемая всякий раз, когда вызывается функция. Она предназначается для временного хранения параметров, содержимого регистра EIP и, возможно, любых других регистров, а также локальных переменных функции. Ранее внимание читателя было заострено на использовании стека при хранении локальных переменных, а теперь будет рассказано о других возможностях его использования.
Для того чтобы понять, как работает стек, следует немного знать о командах процессора Intel call и ret. Команда call – основная команда для существования функции. Команда позволяет выполнить другую часть кода, запомнив при этом адрес точки возврата в стеке. Для этого команда call работает следующим образом:
1) проталкивает в стек адрес следующей команды, который является адресом точки возврата – точки, куда процессор передаст управление (возвратится) после выполнения функции;
2) передает управление по указанному в команде call адресу для выполнения команд функции.
А команда ret делает противоположное. Ее задача состоит в том, чтобы возвратиться из вызываемой функции к команде, следующей за командой call. Для этого команда ret выполняет следующие действия:
1) извлекает из стека сохраненный адрес точки возврата;
2) передает управление по только что извлеченному из стека адресу точки возврата.
Комбинация этих двух команд позволяет легко организовать передачу управления командам функции и вернуться обратно по ее завершении. Кроме того, благодаря сохраненному в стеке содержимому регистра EIP всегда можно прочитать из стека адрес точки перехода. После изучения принципов работы фреймового стека функции об этом будет сказано подробнее.
Передача параметров в функцию. Простой пример
В разделе приведен пример простой программы, иллюстрирующий использование фреймового стека функции для передачи параметров функции. В программе создаются несколько локальных переменных, инициализируется и вызывается функция callex, входными параметрами которой являются только что проинициализированные переменные. Функция callex отображает свои параметры на экране монитора.
На рисунке 8.4 приведена программа, которая поясняет структуру фреймового стека функции и его использование в командах call и ret.
Рис. 8.4. Пример программы, демонстрирующей использование стека в командах вызова и возврата
Дизассемблирование
Приведенная на рис. 8.4 программа была скомпилирована как консольное приложение Windows в режиме построения окончательной версии Release. Результаты дизассемблирования функций callex() и main() приведены на рис. 8.5 и демонстрируют машинный код функций callex() и main() после компиляции. Обратите внимание на передачу по ссылке буфера памяти buffer из функции main() функции callex(). Другими словами, функция callex() получает указатель на буфер buffer, а не копию содержащихся в нем данных. Это означает, что все изменения в буфере buffer, выполненные в функции callex(), тут же отражаются на содержимом буфера buffer в main(), поскольку на самом деле это одна и та же переменная.
Рис. 8.5. Дизассемблированный вид функции callex()
Дампы стека
На рисунках 8.6–8.9 представлен стек в различные моменты выполнения программы. Воспользуемся приведенными на рисунках 8.6–8.9 дампами, исходным текстом программы на языке C и ее дизассемблерным видом, для того чтобы лучше понять происходящие в стеке изменения и их причины. Это поможет понять принципы работы фреймового стека функции и его роль и место в программе.
На рисунке 8.6 показан дамп стека сразу после инициализации переменных, но до операций вызова функции и записи в стек ее входных параметров. Это пример «чистого» стека функции.
Рис. 8.6. Дамп стека после инициализации переменных в функции main()Далее, перед вызовом функции callex() в стек были помещены ее три параметра (см. рис. 8.7).
Рис. 8.7. Дамп стека до вызова функции callex() из функции main()Обратите внимание на произошедшие изменения в дампе стека по сравнению с рис. 8.6. После размещения переменных в области стека функции main() в стек были записаны параметры вызываемой функции callex(), но сама функция пока еще не была вызвана. На рисунке 8.8 приведен дамп стека функции callex() после ее вызова.
Рис. 8.8. Дамп стека после вызова функции callex() и выполнения команд пролога, но перед выполнением оператора printf в функции callex()Показанный на рис. 8.8 стек проинициализирован функцией callex(). Единственное, что осталось выяснить, – это вид стека перед обращением к функции printf(), список параметров которой состоит из четырех элементов.
Наконец, перед обращением в функции callex() к функции вывода значений переменных printf() в стек помещаются четыре параметра. Это видно из дампа стека, представленного на рис. 8.9.
Рис. 8.9. Дамп стека перед обращением к функции printf() в функции callex()Приведенные дампы стека позволят читателю хорошо понять принципы заполнения стека. Приобретенные знания пригодятся при обсуждении способов переполнения буфера.