База кадра стека что это
Стековый кадр
Сте́ковый кадр (англ. stack frame ) — механизм передачи аргументов и выделения временной памяти (в процедурах языков программирования высокого уровня) с использованием системного стека.
Содержание
Технология
Обычно системный стек используется для сохранения адресов возврата при вызове подпрограмм, а также сохранения/восстановления значений регистров процессора.
Передача аргументов
При вызове процедуры аргументы отправляются в стек, и только потом производится вызов подпрограммы. Таким образом, процедура получает стек, на вершине которого лежит адрес возврата, а под ним — аргументы, с которыми она была вызвана.
При возвращении из процедуры (или после него, см. ниже) аргументы должны быть сняты со стека.
Выделение временной памяти
Если указатель стека сместить «выше» (в сторону увеличения стека), то часть памяти в стеке окажется незадействованной (в том числе и при вызове третьей процедуры) и может использоваться процедурой по своему усмотрению, вплоть до момента возврата в вызвавшую её процедуру. Таким образом, языки высокого уровня организуют переменные, существующие только внутри процедуры (язык Си называет их «автоматическими»).
Перед возвратом процедура должна вернуть указатель стека в оригинальное положение (то есть на адрес возврата).
Соглашения для разных языков программирования
Различные компиляторы языков высокого уровня по-разному подходят к организации стекового кадра в зависимости от особенностей аппаратной платформы и стандартов конкретного языка. Основные отличия касаются порядка передачи аргументов в стек и их снятии со стека при возврате.
Недостатки стекового кадра
Стековый кадр — удобная технология выделения временной памяти для передачи произвольного числа аргументов или внутреннего использования. Однако она имеет ряд недостатков.
Производительность
Передача данных через память без необходимости замедляет выполнение программы (по сравнению с программами на языке ассемблера, в которых большинство аргументов и временных данных размещают в регистрах процессора).
Для уменьшения обращений к локальным переменным программа оптимизируется при компиляции для использования регистров вместо переменных в памяти или для хранения их промежуточных значений.
Некоторые языки используют соглашения вызова, поддерживающие передачу целочисленных аргументов через регистры.
Безопасность
Стековый кадр перемежает данные приложения с критическими данными — указателями, значениями регистров и адресами возврата. Это, в сочетании с архитектурными особенностями некоторых процессоров (а именно — направлением роста стека), делает злонамеренное перекрытие критических данных в результате переполнения буфера очень легко достижимым (разумеется, прежде всего, программа должна содержать ошибку, которая позволит выполнить переполнение).
Такое «неудачное», с точки зрения переполнения буфера, направление роста машинного стека имеют аппаратные платформы: X86.
Атака по переполнению буфера в стеке обычно реализуется следующим образом:
. when altering one’s mind becomes as easy as programming a computer, what does it mean to be human.
19 февраля 2015 г.
Фреймы на стеке (стековые фреймы)
Что такое стековые фреймы
Семейство процессоров x86 (как 32-битных, так и 64-битных) использует аппаратный стек процессора для хранения последовательности вызова подпрограмм (т.е. процедур, функций и методов) по мере их выполнения. Другие процессоры могут использовать для этого и другие способы. Стековые фреймы (также иногда называемые «фреймами вызовов», «записями активации» или «фреймами активации») представляют собой структуры в стеке процессора, которые содержат информацию о состоянии подпрограммы. Каждый стековый фрейм соответствует вызову какой-то подпрограммы, которая была вызвана, но ещё не завершилась. Например, если подпрограмма DrawLine была вызвана подпрограммой DrawSquare и пока ещё выполняется, то верхняя часть стека может выглядеть примерно так:
Наличие такого поля в заранее известном месте позволяет коду перебрать цепочку фреймов, имея на руках указатель на текущий фрейм. И конечно же, это позволяет текущей подпрограмме легко восстановить состояние до её вызова. Другими словами, стековые фреймы образуют цепочку: каждый фрейм содержит указатель на предыдущий фрейм в цепочке.
Под стековым фреймом можно понимать как структуру целиком (аргументы, адрес возврата, ссылку на предыдущий фрейм, локальные переменные), так и только комбинацию «ссылка на предыдущий фрейм + адрес возврата». С точки зрения инструментов трассировки под фреймом обычно понимают второе.
Трассировка стека
Как уже было сказано выше, фреймы вызовов не всегда создаются для архитектуры x86-32 (в отличие от x86-64). Это означает, что на x86-64 всегда можно построить гарантированно верный стек вызовов (конечно же, при условии, что данные в самом стеке не повреждены). Это невозможно для x86-32. Для x86-32 методы трассировки делятся на два класса: т.н. RAW и т.н. frame-based.
Frame-based методы трассировки
Примечание: можно сказать, что метод трассировки стека для x86-64 относится к классу frame-based методов.
Методы трассировки RAW
Примечание: на платформе x86-64 нет нужды в RAW-методах, поскольку существует способ точного построения стека по фреймам и мета-информации.
Когда создаются стековые фреймы (только для x86-32)
В x86-32 в некоторых случаях стековый фрейм может не создаваться (omitted). Посмотрите на такой код:
Заметьте, что слева от строки » begin » нет синей точки. Что это значит? Это значит, что строка » begin » не генерирует машинного кода. Т.е. в этой подпрограмме отсутствует код по созданию фрейма на стеке. Это произошло потому, что эта подпрограмма очень простая, в нет ней необходимости для создания фрейма.
Примечание: напротив, строка с » end » генерирует код. В данном случае в этой строке будет стоять возврат управления в вызывающую подпрограмму ( TControl.Click ).
Эти факты можно подтвердить просмотром кода Button1Click в машинном отладчике:
Машинный код обработчика Button1Click из примера выше |
Попробуем включить опцию «Stack Frames»:
Примечание: хотя это не указано в документации, но опция «Stack Frames» ничего не делает на платформе x86-64.
Вот как выглядит новая версия кода под машинным отладчиком:
Но посмотрим теперь на такой код:
Если бы мы не использовали строки в подпрограмме (а, скажем, использовали бы только Integer ), то код подпрограммы остался простым, так что компилятор смог бы обойтись без создания стекового фрейма.
Как стековые фреймы влияют на стеки вызовов (только для x86-32)
Я приведу два примера как стековые фреймы влияют на стеки вызовов.
Фреймы и смещения
Трейсеры исключений (и другие отладочные инструменты) позволяют вам просматривать т.н. «строковые смещения». Строковое смещение вычисляется как разница между текущим положением и началом подпрограммы. Например:
Номера строк для подпрограммы со стековым фреймом |
Вызов метода Hide расположен в строке 28[1], что читается как «строка №28, она отстоит от начала подпрограммы на одну строчку».
В этом случае первой строкой подпрограммы стала строка с » Hide «, а строка с » begin » и вовсе не генерирует кода.
В результате, если вы получили отчёт со стеком вызовов от старой версии программы, то вам нужно использовать не точные значения номеров строк, а имена подпрограмм и строковые смещения. Вы должны учитывать опции, с которыми создан ваш код, чтобы правильно отсчитать строки. И если вы меняли опции между сборками, то вам, возможно, потребуется делать правки на +/-1.
Фреймы и методы трассировки
Модули RTL и VCL скомпилированы с выключенной опцией «Stack Frames». Это означает, что любой frame-based метод трассировки стека не сможет находить «простые» подпрограммы в модулях RTL/VCL.
Однако, этот факт также имеет менее очевидное следствие. Посмотрите на такой код:
Примечание: для этого примера вы также можете использовать возбуждение исключения и трейсер исключений, но вам нужно быть уверенным, что вы возбуждаете исключение внутри кода RTL/VCL.
Но этого не произойдёт.
Примечание: при желании вы можете перекомпилировать RTL/VCL с включенной опцией «Stack Frames».
11.8 – Стек и куча
Память, которую использует программа, обычно делится на несколько разных областей, называемых сегментами:
В этом уроке мы сосредоточимся в первую очередь на куче и стеке, так как именно там происходит большинство интересных вещей.
Сегмент кучи
Сегмент кучи (также известный как «free store», «свободное хранилище») отслеживает память, используемую для динамического распределения памяти. Мы уже говорили немного о куче в уроке «10.13 – Динамическое распределение памяти с помощью new и delete », поэтому это будет резюме.
В C++, когда вы используете оператор new для выделения памяти, эта память выделяется в сегменте кучи приложения.
Адрес этой памяти возвращается оператором new и затем может быть сохранен в указателе. Вам не нужно беспокоиться о механизме того, где расположена свободная память, и как она выделяется пользователю. Однако стоит знать, что последовательные запросы памяти могут не привести к выделению последовательных адресов памяти!
Когда динамически размещаемая переменная удаляется, память «возвращается» в кучу и затем может быть переназначена по мере получения будущих запросов на выделение. Помните, что удаление указателя не удаляет переменную, а просто возвращает память по соответствующему адресу обратно операционной системе.
У кучи есть достоинства и недостатки:
Стек вызовов
Стек вызовов (обычно называемый «стеком») играет гораздо более интересную роль. Стек вызовов отслеживает все активные функции (те, которые были вызваны, но еще не завершились) от начала программы до текущей точки выполнения и обрабатывает размещение всех параметров функций и локальных переменных.
Стек вызовов реализован в виде структуры данных стек. Итак, прежде чем мы сможем говорить о том, как работает стек вызовов, нам нужно понять, что такое структура данных стек.
Структура данных стек
Структура данных – это программный механизм для организации данных таким образом, чтобы их можно было эффективно использовать. Вы уже видели несколько типов структур данных, таких как массивы и структуры. Обе эти структуры данных предоставляют механизмы для хранения данных и эффективного доступа к ним. Существует множество дополнительных, обычно используемых в программировании структур данных, многие из которых реализованы в стандартной библиотеке, и стек является одной из них.
Представьте себе стопку тарелок в кафетерии. Поскольку каждая тарелка тяжелая и они сложены друг на друга, вы можете сделать только одно из трех:
В компьютерном программировании стек – это структура контейнера данных, который содержит несколько переменных (как массив). Однако в то время как массив позволяет вам получать доступ к элементам и изменять их в любом порядке (так называемый произвольный доступ), стек более ограничен. Операции, которые могут быть выполнены со стеком, соответствуют трем вещам, упомянутым выше:
Стек – это структура типа «последним пришел – первым ушел» (LIFO, «last-in, first-out»). Последний элемент, помещенный в стек, будет первым извлеченным элементом. Если вы положите новую тарелку поверх стопки, первая тарелка, удаленная из стопки, будет тарелкой, которую вы только что положили последней. Последней положена, первой снята. По мере того, как элементы помещаются в стек, стек становится больше – по мере того, как элементы извлекаются, стек становится меньше.
Например, вот короткая последовательность, показывающая, как работает стек при вставке (push) и извлечении (pop) данных:
Аналогия с тарелками – довольно хорошая аналогия того, как работает стек вызовов, но мы можем провести лучшую аналогию. Представьте себе группу почтовых ящиков, сложенных друг на друга. Каждый почтовый ящик может содержать только один элемент, и все почтовые ящики изначально пустые. Кроме того, каждый почтовый ящик прибивается к почтовому ящику под ним, поэтому количество почтовых ящиков не может быть изменено. Если мы не можем изменить количество почтовых ящиков, как мы можем добиться поведения, подобного стеку?
Во-первых, мы используем маркер (например, наклейку), чтобы отслеживать, где находится самый нижний пустой почтовый ящик. Вначале это будет самый нижний почтовый ящик (внизу стопки). Когда мы помещаем элемент в наш стек почтовых ящиков, мы помещаем его в отмеченный почтовый ящик (который является первым пустым почтовым ящиком) и перемещаем маркер на один ящик вверх. Когда мы извлекаем элемент из стека, мы перемещаем маркер на один почтовый ящик вниз так, чтобы он указывал на верхний непустой почтовый ящик, и удаляем элемент из этого почтового ящика. Всё, что ниже маркера, считается «в стеке». Всё, что находится на уровне маркера или над ним, – не в стеке.
Сегмент стека вызовов
Когда встречается вызов функции, эта функция помещается в стек вызовов. Когда текущая функция завершается, эта функция удаляется из стека вызовов. Таким образом, глядя на функции, помещенные в стек вызовов, мы можем увидеть все функции, которые были вызваны для перехода к текущей точке выполнения.
Приведенная выше аналогия с почтовыми ящиками в значительной степени похожа на то, как работает стек вызовов. Сам стек представляет собой блок адресов памяти фиксированного размера. Почтовые ящики – это адреса памяти, а «элементы», которые мы помещаем в стек, называются кадрами (фреймами) стека. Кадр стека отслеживает все данные, связанные с одним вызовом функции. Мы поговорим о стековых кадрах чуть позже. «Маркер» – это регистр (небольшой фрагмент памяти в CPU), известный как указатель стека (иногда сокращенно «SP», «stack pointer»). Указатель стека отслеживает текущее положение вершины стека вызовов.
Мы можем сделать еще одну оптимизацию: когда мы извлекаем элемент из стека вызовов, нам нужно только переместить указатель стека вниз – нам не нужно очищать или обнулять память, используемую извлекаемым кадром стека (эквивалент опустошению почтового ящика). Эта память больше не считается «в стеке» (указатель стека будет по этому адресу или ниже), поэтому к ней не будет доступа. Если мы позже поместим новый кадр стека в ту же самую память, он перезапишет старое значение, которое мы никогда не очищали.
Стек вызовов в действии
Давайте подробнее рассмотрим, как работает стек вызовов. Вот последовательность шагов, которые выполняются при вызове функции:
Когда функция завершается, происходят следующие шаги:
Возвращаемые значения могут обрабатываться разными способами в зависимости от архитектуры компьютера. Некоторые архитектуры включают возвращаемое значение как часть кадра стека. Другие используют регистры CPU.
Обычно неважно знать все подробности о том, как работает стек вызовов. Однако понимание того, что функции помещаются в стек при их вызове и удаляются при возврате, дает вам основы, необходимые для понимания рекурсии, а также некоторые другие концепции, полезные при отладке.
Техническое примечание: на некоторых архитектурах стек вызовов при увеличении изменяет адрес памяти в направлении от нуля. На других он при увеличении изменяет адрес в направлении нуля. Как следствие, новые добавленные кадры стека могут иметь более высокий или более низкий адрес памяти, чем предыдущие.
Пример стека вызовов
Рассмотрим следующее простое приложение:
Стек вызовов в отмеченных точках выглядит следующим образом:
Переполнение стека
Стек имеет ограниченный размер и, следовательно, может содержать только ограниченный объем информации. В Windows размер стека по умолчанию составляет 1 МБ. На некоторых Unix-машинах он может достигать 8 МБ. Если программа попытается поместить в стек слишком много информации, произойдет переполнение стека. Переполнение стека происходит, когда вся память в стеке была выделена – в этом случае дальнейшие размещения начинают переполняться в другие разделы памяти.
Переполнение стека обычно является результатом выделения слишком большого количества переменных в стеке и/или выполнения слишком большого количества вызовов вложенных функций (где функция A вызывает функцию B, вызывающую функцию C, вызывающую функцию D и т.д.). В современных операционных системах переполнение стека обычно приводит к тому, что ваша ОС выдаст нарушение прав доступа и завершит программу.
Вот пример программы, которая может вызвать переполнение стека. Вы можете запустить его на своей системе и посмотреть, как она завершится со сбоем:
Эта программа пытается разместить в стеке огромный массив (примерно 40 МБ). Поскольку стек недостаточно велик для обработки этого массива, размещение массива переполняется в части памяти, которые программе не разрешено использовать.
В Windows (Visual Studio) эта программа дает следующий результат:
-1073741571 – это c0000005 в шестнадцатеричном формате, что представляет собой код ОС Windows для нарушения прав доступа. Обратите внимание, что «hi» никогда не печатается, потому что программа завершается до этого момента.
Вот еще одна программа, которая вызовет переполнение стека, но по другой причине:
У стека есть достоинства и недостатки:
Принципы программирования: стек и куча: что это такое?
С каждым годом мы применяем для программирования всё более продвинутые языки, позволяющие писать меньше кода, но получать нужные нам результаты. Однако всё это не проходит даром для разработчиков. Так как программисты всё реже занимаются низкоуровневыми вещами, уже никого не удивляет ситуация, когда разработчик не вполне понимает, что означают такие понятия, как куча и стек. Что это такое, как происходит компиляция на самом деле, в чём разница между динамической и статической типизацией.
К сожалению, некоторые программисты, даже будучи «джуниорами» и работая на реальных проектах, не совсем чётко ориентируются в таких, казалось бы, олдскульных вещах. Именно поэтому в нашей сегодняшней статье мы вспомним, что же это такое — стек и куча, для чего они нужны и где применяются. Несмотря на то, что и стек, и куча связаны с управлением памятью, стратегия и принципы управления кардинально различаются.
Стек — что это такое?
Большое число задач, связанных с обработкой информации, поддаются типизированному решению. В результате совсем неудивительно, что многие из них решаются с помощью специально придуманных методов, терминов и описаний. Среди них нередко можно услышать и такое слово, как стек (стэк). Хоть и звучит этот термин, на первый взгляд, странно и даже сложно, всё намного проще, чем кажется.
Стек и простой жизненный пример
Представьте, что на столе в коробке лежит стопка бумажных листов. Чтобы получить доступ к самому нижнему листу, вам нужно достать самый первый лист, потом второй и так далее, пока не доберётесь до последнего. По схожему принципу и устроен стек: чтобы последний элемент стека стал верхним, нужно сначала вытащить все остальные.
Стек и особенности его работы
Перейдя к компьютерной терминологии, скажем, что стек — это область оперативной памяти, создаваемая для каждого потока. И последний добавленный в стек кусочек памяти и будет первым в очереди, то есть первым на вывод из стека. И каждый раз, когда функцией объявляется переменная, она, прежде всего, добавляется в стек. А когда данная переменная пропадает из нашей области видимости (к примеру, функция заканчивается), эта самая переменная автоматически удаляется из стека. При этом если стековая переменная освобождается, то и область памяти, в свою очередь, становится доступной и свободной для других стековых переменных.
Благодаря природе, которую имеет стек, управление памятью становится весьма простым и логичным для выполнения на центральном процессоре. Это повышает скорость и быстродействие ЦП, и в особенности такое происходит потому, что время цикла обновления байта весьма незначительно (данный байт, скорее всего, привязан к кэшу центрального процессора).
Тем не менее у данной довольно строгой формы управления имеются и свои недостатки. Например, размер стека — это величина фиксированная, в результате чего при превышении лимита памяти, выделенной на стеке, произойдёт переполнение стека. Как правило, размер задаётся во время создания потока, плюс у каждой переменной имеется максимальный размер, который зависит от типа данных. Всё это позволяет ограничивать размеры некоторых переменных (допустим, целочисленных).
Кроме того, это вынуждает объявлять размер более сложных типов данных (к примеру, массивов) заранее, так как стек не позволит потом изменить его. Вдобавок ко всему, переменные, которые расположены на стеке, являются всегда локальными.
Для чего нужен стек?
Главное предназначение стека — решение типовых задач, предусматривающих поддержку последовательности состояний или связанных с инверсионным представлением данных. В компьютерной отрасли стек применяется в аппаратных устройствах (например, в центральном процессоре, как уже было упомянуто выше).
Практически каждый, кто занимался программированием, знает, что без стека невозможна рекурсия, так как при любом повторном входе в функцию требуется сохранение текущего состояния на вершине, причём при каждом выходе из функции, нужно быстро восстанавливать это состояние (как раз наша последовательность LIFO).
Если же копнуть глубже, то можно сказать, что, по сути, весь подход к запуску и выполнению приложений устроен на принципах стека. Не секрет, что прежде чем каждая следующая программа, запущенная из основной, будет выполняться, состояние предыдущей занесётся в стек, чтобы, когда следующая запущенная подпрограмма закончит выполняться, предыдущее приложение продолжило работу с места остановки.
Стеки и операции стека
Если говорить об основных операциях, то стек имеет таковых две: 1. Push — ни что иное, как добавление элемента непосредственно в вершину стека. 2. Pop — извлечение из стека верхнего элемента.
Также, используя стек, иногда выполняют чтение верхнего элемента, не выполняя его извлечение. Для этого предназначена операция peek.
Как организуется стек?
Когда программисты организуют или реализуют стек, они применяют два варианта: 1. Используя массив и переменную, указывающую на ячейку вершины стека. 2. Используя связанные списки.
У этих двух вариантов реализации стека есть и плюсы, и минусы. К примеру, связанные списки считаются более безопасными в плане применения, ведь каждый добавляемый элемент располагается в динамически созданной структуре (раз нет проблем с числом элементов, значит, отсутствуют дырки в безопасности, позволяющие свободно перемещаться в памяти программного приложения). Однако с точки зрения хранения и скорости применения связанные списки не столь эффективны, так как, во-первых, требуют дополнительного места для хранения указателей, во-вторых, разбросаны в памяти и не расположены друг за другом, если сравнивать с массивами.
Подытожим: стек позволяет управлять памятью более эффективно. Однако помните, что если вам потребуется использовать глобальные переменные либо динамические структуры данных, то лучше обратить своё внимание на кучу.
Стек и куча
Куча — хранилище памяти, расположенное в ОЗУ. Оно допускает динамическое выделение памяти и работает не так, как стек. По сути, речь идёт о простом складе для ваших переменных. Когда вы выделяете здесь участок памяти для хранения, к ней можно обращаться как в потоке, так и во всём приложении в целом (именно так и определяются переменные глобального типа). По завершении работы приложения все выделенные участки освобождаются.
Размер кучи задаётся во время запуска приложения, однако, в отличие от того, как работает стек, в куче размер ограничен только физически, что позволяет создавать переменные динамического типа.
Если сравнивать, опять же, с тем, как работает стек, то куча функционирует медленнее, т. к. переменные разбросаны по памяти, а не находятся вверху стека. Тем не менее данный факт не уменьшает важности кучи, и если вам надо работать с глобальными либо динамическими переменными, она больше подходит. Однако управлять памятью тогда должен программист либо сборщик мусора.
Итак, теперь вы знаете и что такое стек, и что такое куча. Это довольно простые знания, больше подходящие для новичков. Если же вас интересуют более серьёзные профессиональные навыки, выбирайте нужный вам курс по программированию в OTUS!