В информатике, стек вызовов - это стек структура данных, в которой хранится информация об активных подпрограммах компьютерной программы. Этот тип стека также известен как стек выполнения, стек программ, стек управления, стек времени выполнения или машинный стек, и часто сокращается до «стек ». Хотя обслуживание стека вызовов важно для правильного функционирования большей части программного обеспечения, детали обычно скрыты и автоматические в языках программирования высокого уровня. Многие компьютерные наборы инструкций предоставляют специальные инструкции для работы со стеками.
Стек вызовов используется для нескольких связанных целей, но основная причина его наличия - отслеживать точку, в которую каждая активная подпрограмма должна вернуть управление после завершения выполнения. Активная подпрограмма - это подпрограмма, которая была вызвана, но еще не завершила выполнение, после чего управление должно быть возвращено точке вызова. Такие активации подпрограмм могут быть вложенными на любой уровень (рекурсивный как частный случай), отсюда и структура стека. Например, если подпрограмма DrawSquare
вызывает подпрограмму DrawLine
из четырех разных мест, DrawLine
должна знать, куда вернуться после завершения ее выполнения. Для этого адрес , следующий за инструкцией , которая переходит к DrawLine
, адрес возврата, помещается в начало вызова. стек с каждым вызовом.
Поскольку стек вызовов организован как стек, вызывающая сторона помещает адрес возврата в стек и вызываемая подпрограмма, когда она завершается, извлекает или выталкивает адрес возврата из стека вызовов и передает управление по этому адресу. Если вызываемая подпрограмма вызывает еще одну подпрограмму, она помещает другой адрес возврата в стек вызовов и так далее, при этом информация накапливается и распаковывается в соответствии с требованиями программы. Если нажатие занимает все пространство, выделенное для стека вызовов, возникает ошибка, называемая переполнением стека, обычно вызывая сбой программы. Добавление записи подпрограммы в стек вызовов иногда называют «намоткой»; и наоборот, удаление записей "раскручивает".
Обычно существует ровно один стек вызовов, связанный с запущенной программой (точнее, с каждой задачей или потоком процесса ), хотя дополнительные стеки могут быть созданы для обработки сигнала или совместной многозадачности (как с setcontext ). Поскольку в этом важном контексте есть только один, его можно назвать стеком (неявно «задачи»); однако в языке программирования Forth доступ к стеку данных или стеку параметров осуществляется более явно, чем к стеку вызовов, и обычно его называют стеком (см. ниже).
В языках программирования высокого уровня особенности стека вызовов обычно скрыты от программиста. Им предоставляется доступ только к набору функций, а не к памяти самого стека. Это пример абстракции . С другой стороны, большинство языков ассемблера требуют участия программистов в управлении стеком. Фактические детали стека в языке программирования зависят от компилятора, операционной системы и доступного набора команд.
Как отмечалось выше, основная цель стека вызовов - хранить адреса возврата. Когда вызывается подпрограмма, место (адрес) инструкции, с которой вызывающая подпрограмма может позже возобновиться, необходимо где-то сохранить. Использование стека для сохранения адреса возврата имеет важные преимущества по сравнению с альтернативными соглашениями о вызовах. Во-первых, каждая задача может иметь свой собственный стек, и, таким образом, подпрограмма может быть поточно-ориентированной, то есть может быть активной одновременно для разных задач, выполняющих разные действия. Еще одно преимущество заключается в том, что при обеспечении повторного входа автоматически поддерживается рекурсия. Когда функция рекурсивно вызывает себя, необходимо сохранять адрес возврата для каждой активации функции, чтобы впоследствии его можно было использовать для возврата из активации функции. Структуры стека предоставляют эту возможность автоматически.
В зависимости от языка, операционной системы и среды компьютера стек вызовов может служить дополнительным целям, в том числе, например:
Типичный стек вызовов используется для адреса возврата, локальных переменных и параметров (известных как кадр вызова). В некоторых средах стеку вызовов может быть назначено больше или меньше функций. В языке программирования Forth, например, обычно только адрес возврата, подсчитываемые параметры цикла и индексы и, возможно, локальные переменные хранятся в стеке вызовов (который в этой среде называется стеком возврата), хотя любые данные могут быть временно помещены туда с использованием специального кода обработки стека возврата, если соблюдаются требования вызовов и возвратов; параметры обычно хранятся в отдельном стеке данных или стеке параметров, обычно называемом стеком в терминологии Forth, даже если существует стек вызовов, поскольку к нему обычно обращаются более явно. Некоторые форты также имеют третий стек для параметров с плавающей запятой.
A стек вызовов состоит из кадров стека (также называемых записями активации или кадрами активации). Это машинно-зависимые и ABI -зависимые структуры данных, содержащие информацию о состоянии подпрограммы. Каждый кадр стека соответствует вызову подпрограммы, которая еще не завершилась возвратом. Например, если подпрограмма с именем DrawLine
в настоящее время выполняется, будучи вызванной подпрограммой DrawSquare
, верхняя часть стека вызовов может быть размещена, как на соседнем рисунке.
Подобную диаграмму можно нарисовать в любом направлении, если понятно расположение верха и, следовательно, направление роста стопки. Кроме того, независимо от этого, архитектуры различаются в зависимости от того, растут ли стеки вызовов в сторону более высоких адресов или в сторону более низких адресов. Логика схемы не зависит от выбора адресации.
Фрейм стека наверху стека предназначен для текущей выполняющейся процедуры. Кадр стека обычно включает в себя, по крайней мере, следующие элементы (в порядке отправки):
DrawLine
- адрес в коде DrawSquare
); иКогда размеры фрейма стека могут различаться, например, между разными функциями или между вызовами конкретной функции, извлечение кадра из стека не является фиксированным декрементом указателя стека . При возврате функции указатель стека вместо этого восстанавливается до указателя кадра, значения указателя стека непосредственно перед вызовом функции. Каждый кадр стека содержит указатель стека на верхнюю часть кадра непосредственно под ним. Указатель стека - это изменяемый регистр, совместно используемый всеми вызовами. Указатель кадра при данном вызове функции является копией указателя стека, каким он был до вызова функции.
Расположение всех других полей в кадре может быть определено относительно верха фрейм, как отрицательные смещения указателя стека, или относительно верха нижнего фрейма, как положительные смещения указателя фрейма. Расположение самого указателя кадра должно быть определено как отрицательное смещение указателя стека.
В большинстве систем фрейм стека имеет поле, содержащее предыдущее значение регистра указателя фрейма, значение, которое он имел во время выполнения вызывающего. Например, кадр стека DrawLine
будет иметь ячейку памяти, содержащую значение указателя кадра, которое использует DrawSquare
(не показано на диаграмме выше). Значение сохраняется при входе в подпрограмму и восстанавливается при возврате. Наличие такого поля в известном месте в фрейме стека позволяет коду обращаться к каждому фрейму последовательно под фреймом выполняющейся в данный момент подпрограммы, а также позволяет подпрограмме легко восстанавливать указатель фрейма на фрейм вызывающей стороны непосредственно перед его возвратом.
Языки программирования, поддерживающие вложенные подпрограммы, также имеют поле в кадре вызова, которое указывает на кадр стека последней активации процедуры, точно инкапсулирует вызываемый объект, то есть непосредственную область действия вызываемого объекта. Это называется ссылкой доступа или статической ссылкой (поскольку она отслеживает статическое вложение во время динамических и рекурсивных вызовов) и обеспечивает подпрограмме (а также любым другим подпрограммам, которые она может вызывать) доступ к локальным данным своих инкапсулирующих подпрограмм при каждом вложении. уровень. Некоторые архитектуры, компиляторы или варианты оптимизации хранят по одной ссылке для каждого уровня включения (а не только непосредственно включающего), так что глубоко вложенные подпрограммы, которые обращаются к неглубоким данным, не должны проходить через несколько ссылок; эту стратегию часто называют "отображением".
Ссылки доступа могут быть оптимизированы, когда внутренняя функция не обращается к каким-либо (непостоянным) локальным данным в инкапсуляции, как в случае с чистыми функциями, взаимодействующими только через аргументы и возвращаемые значения, например. Некоторые исторические компьютеры, такие как большие системы Берроуза, имели специальные «регистры отображения» для поддержки вложенных функций, в то время как компиляторы для большинства современных машин (таких как вездесущий x86) просто резервируют несколько слов в стеке для указатели по мере необходимости.
Для некоторых целей кадр стека подпрограммы и кадр ее вызывающей стороны можно рассматривать как перекрывающиеся, причем перекрытие состоит из области, в которой параметры передаются от вызывающей стороны к вызываемый. В некоторых средах вызывающий помещает каждый аргумент в стек, таким образом расширяя его фрейм стека, а затем вызывает вызываемого. В других средах вызывающий имеет предварительно выделенную область в верхней части кадра стека для хранения аргументов, которые он передает другим вызываемым им подпрограммам. Эту область иногда называют областью исходящих аргументов или областью выноски. В соответствии с этим подходом размер области вычисляется компилятором как самый большой, необходимый для любой вызываемой подпрограммы.
Обычно манипуляции со стеком вызовов, необходимые на месте вызова подпрограммы, минимальны (что хорошо, поскольку может быть много сайтов вызова для каждой вызываемой подпрограммы). Значения фактических аргументов оцениваются на месте вызова, поскольку они специфичны для конкретного вызова, и либо помещаются в стек, либо помещаются в регистры, как определено используемым соглашением о вызовах . Фактическая инструкция вызова, такая как «ветвь и ссылка», затем обычно выполняется для передачи управления коду целевой подпрограммы.
В вызываемой подпрограмме первый выполненный код обычно называется прологом подпрограммы, так как он выполняет необходимые служебные действия перед кодом для операторов рутина началась.
Для архитектур с набором команд, в которых инструкция, используемая для вызова подпрограммы, помещает адрес возврата в регистр, а не помещает его в стек, пролог обычно сохраняет адрес возврата, помещая значение в вызов стек, хотя, если вызываемая подпрограмма не вызывает никаких других подпрограмм, она может оставить значение в регистре. Точно так же могут быть переданы значения текущего указателя стека и / или указателя кадра.
Если используются указатели фрейма, пролог обычно устанавливает новое значение регистра указателя фрейма из указателя стека. Затем можно выделить пространство в стеке для локальных переменных, постепенно изменяя указатель стека.
язык программирования Forth допускает явную намотку стека вызовов (называемого там «стеком возврата»).
Когда подпрограмма готова к возврату, она выполняет эпилог, отменяющий шаги пролога. Обычно это восстанавливает сохраненные значения регистров (например, значение указателя кадра) из кадра стека, выталкивает весь кадр стека из стека, изменяя значение указателя стека, и, наконец, выполняет переход к инструкции по адресу возврата. Согласно многим соглашениям о вызовах элементы, извлекаемые из стека эпилогом, включают в себя исходные значения аргументов, и в этом случае обычно нет дополнительных манипуляций со стеком, которые должны выполняться вызывающим. Однако с некоторыми соглашениями о вызовах ответственность за удаление аргументов из стека после возврата лежит на вызывающей стороне.
При возврате из вызванной функции верхний фрейм выталкивается из стека, возможно, при этом остается возвращаемое значение. Более общий акт выталкивания одного или нескольких кадров из стека для возобновления выполнения в другом месте программы называется раскручиванием стека и должен выполняться, когда используются нелокальные управляющие структуры, такие как те, которые используются для обработка исключений. В этом случае кадр стека функции содержит одну или несколько записей, определяющих обработчики исключений. Когда генерируется исключение, стек разворачивается до тех пор, пока не будет найден обработчик, готовый обработать (перехватить) тип выброшенного исключения.
В некоторых языках есть другие управляющие структуры, требующие общей раскрутки. Паскаль позволяет глобальному оператору goto передавать управление из вложенной функции в ранее вызванную внешнюю функцию. Эта операция требует, чтобы стек был размотан, удалив столько кадров стека, сколько необходимо для восстановления правильного контекста, чтобы передать управление целевому оператору во внешней функции. Точно так же в C есть функции setjmp
и longjmp
, которые действуют как нелокальные переходы. Common Lisp позволяет контролировать то, что происходит при раскручивании стека, с помощью специального оператора unwind-protect
.
При применении продолжения стек (логически) раскручивается, а затем перематывается вместе со стеком продолжения. Это не единственный способ реализовать продолжения; например, используя несколько явных стеков, приложение продолжения может просто активировать свой стек и намотать значение для передачи. Язык программирования схемы позволяет произвольным преобразователям выполняться в указанных точках при «раскручивании» или «перемотке» стека управления при вызове продолжения.
Стек вызовов иногда можно проверить во время работы программы. В зависимости от того, как программа написана и скомпилирована, информация в стеке может использоваться для определения промежуточных значений и трассировки вызовов функций. Это использовалось для генерации детализированных автоматических тестов, а в таких случаях, как Ruby и Smalltalk, для реализации первоклассных продолжений. В качестве примера, GNU Debugger (GDB) реализует интерактивную проверку стека вызовов работающей, но приостановленной программы C.
Регулярный выборка стека вызовов может быть полезно при профилировании производительности программ, потому что, если указатель подпрограммы появляется в данных выборки стека вызовов много раз, это, вероятно, узкое место кода и должно быть проверено на наличие проблем с производительностью.
В языке со свободными указателями или непроверенными записями в массиве (например, в C) смешивание данных потока управления, которое влияет на выполнение кода (адреса возврата или сохраненные указатели кадров) и простые программные данные (параметры или возвращаемые значения) в стеке вызовов представляют собой угрозу безопасности, возможно эксплуатируемые - переполнения буфера стека как наиболее распространенный тип переполнение буфера.
Одна из таких атак включает заполнение одного буфера произвольным исполняемым кодом, а затем переполнение того же или другого буфера для перезаписи некоторого адреса возврата значением, которое указывает непосредственно на исполняемый код. В результате, когда функция возвращается, компьютер выполняет этот код. Этот вид атаки можно легко заблокировать с помощью W ^ X. Подобные атаки могут быть успешными даже при включенной защите W ^ X, включая атаку return-to-libc или атаки, исходящие от программирования, ориентированного на возврат. Были предложены различные меры по снижению рисков, такие как хранение массивов в полностью отдельном месте от стека возврата, как в случае с языком программирования Forth.