- Путешествие по Стеку. Часть 1
- Стек вызовов — Call stack
- СОДЕРЖАНИЕ
- Описание
- Функции стека вызовов
- Состав
- Указатели стека и фрейма
- Сохранение адреса во фрейме звонящего
- Лексически вложенные подпрограммы
- Перекрывать
- Использовать
- Обработка звонков на сайт
- Обработка записи подпрограммы
- Обработка возврата
- Размотка
- Осмотр
- Безопасность
Путешествие по Стеку. Часть 1
В предыдущих материалах мы рассмотрели размещение программы в памяти – одну из центральных концепций, касающихся выполнения программ на компьютерах. Теперь обратимся к стеку вызовов – рабочей лошадке большинства языков программирования и виртуальных машин. Нас ожидает знакомство с удивительными вещами вроде функций-замыканий, переполнений буфера и рекурсии. Однако всему свое время – в начале нужно составить базовое представление о том, как работает стек.
Стек имеет такое важное значение, потому что благодаря ему любая функция «знает» куда возвращать управление после завершения; функция же, в свою очередь — это базовый строительный блок программы. Вообще, программы внутренне устроены довольно просто. Программа состоит из функций, функции могут вызывать другие функции, в процессе своей работы любая функция помещает данные в стек и снимает их оттуда. Если нужно, чтобы данные продолжили существовать после завершения функции, то место под них выделяется не в стеке, а в куче. Вышесказанное в равной степени относится как к программам, написанным на относительно низкоуровневом C, так и к интерпретируемым языкам вроде JavaScript и C#. Знание данных вещей обязательно пригодится — и если придется отлаживать программу, и если доведется заниматься тонкой подстройкой производительности, да и просто для того, чтобы понимать, что же там, все-таки творится внутри программы.
Итак, начнем. Как только мы вызываем функцию, в стеке для нее создается стековый кадр. Стековый кадр содержит локальные переменные, а также аргументы, которые были переданы вызывающей функцией. Помимо этого кадр содержит служебную информацию, которая используется вызванной функцией, чтобы в нужный момент возвратить управление вызвавшей функции. Точное содержание стека и схема его размещения в памяти могут быть разными в зависимости от процессорной архитектуры и используемой конвенции вызова. В данной статье мы рассматриваем стек на архитектуре x86 с конвенцией вызова, принятой в языке C (cdecl). На рисунке вверху изображен стековый кадр, разместившийся у верхушки стека.
Сразу бросаются в глаза три процессорных регистра. Указатель стека, esp, предназначается для того, чтобы указывать на верхушку стека. Вплотную к верхуше всегда находится объект, который был добавлен в стек, но еще оттуда не снят. Точно также в реальной жизни обстоят дела со стопкой тарелок или пачкой 100-долларовых банкнот.
Хранимый в регистре esp адрес изменяется по мере того, как объекты добавляются и снимаются со стека, однако он всегда указывает на последний добавленный и еще не снятый со стека объект. Многие процессорные инструкции изменяют значение регистра esp как побочный результат своего выполнения. Реализовать работу со стеком без регистра esp было бы проблематично.
В случае с процессорами Intel, ровно как и со многими другими архитетурами, стек растет в направлении меньших адресов памяти. Поэтому верхушка, в данном случае, соответствует наименьшему адресу в стеке, по которому хранятся валидные используемые данные: в нашем случае это переменная local_buffer. Думаю, должно быть понятно, что означает стрелка от esp к local_buffer. Здесь все, как говорится, по делу – стрелка указывает точно на первый байт, занимаемый local_buffer, и это соответствует тому адресу, который хранится в регистре esp.
Далее на очереди еще один регистр, используемый для отслеживания позиций в стеке – регистр ebp – базовый указатель или указатель базы стекового кадра. Данный регистр предназначен для того, чтобы указывать на позицию в стековом кадре. Благодаря регистру ebp текущая функция всегда имеет своего рода точку отсчёта для доступа к аргументам и локальным переменным. Хранимый в регистре адрес изменяется, когда функция начинает или прекращает выполнение. Мы можем довольно просто адресовать любой объект в стековом кадре как смещение относительно ebp, что и показано на рисунке.
В отличии от esp, манипуляции с регистром ebp осуществляется в основном самой программой, а не процессором. Иногда можно добиться выигрыша в производительности просто отказавшись от страндартного использования регистра ebp – за это отвечают некоторые флаги компилятора. Ядро Linux – пример того, где используется такой прием.
Наконец, регистр eax традиционно используется для хранения данных, возвращаемых вызвавшей функции — это высказывание справедливо для большинства поддерживаемых в языке C типов.
Теперь давайте разберем данные, содержащиеся в стековом кадре. Рисунок показывает точное побайтовое содержимое кадра, c направлением роста адресов слево-направо – это то, что мы обычно видим в отладчике. А вот и сам рисунок:
Локальная переменная local_buffer – это массив байт, представляющий собой нуль-терминированную ASCII-строку; такие строки — неизменный атрибут всех программ на C. Размер строки — 7 байт, и, скорее всего, она была получена в результате клавиатурного ввода или чтения из файла. В нашем массиве может храниться 8 байт и, следовательно, один байт остается неиспользуемым. Значение этого байта неизвестно. Дело в том, что, данные то и дело добавлются и снимаются со стека, и в этом «бесконечном танце операции добавления и снятия» никогда нельзя знать заранее, что содержит память, пока не осуществишь в нее запись. Компилятор языка C не обременяет себя тем, чтобы иницилизировать стековый кадр нулями. Поэтому содержащиеся там данные заранее неизвестны и являются в некоторой степени случайными. Уж сколько крови попило такое поведение компилятора у программистов!
Идем далее. local1 – 4-байтовое целое число, и на рисунке видно содержимое каждого байта. Кажется, что это большое число – только взгляните на все эти нули после восьмерки, однако здесь наша интуиция сослужила нам дурную службу.
Процессоры Intel используют прямой порядок байтов (дословно «остроконечный»), и это значит, что числа хранятся в памяти начиная с младшего байта. Иными словами, самый младший значащий байт хранится в ячейке памяти с наименьшим адресом. На рисунках и схемах байты многобайтовых чисел традиционно изображаются в порядке слева-направо. В случае с прямым порядком байт, самый младший значащий байт будет изображен в крайней левой позиции, что отличается от привычного нам способа представления и записи чисел.
Неплохо знать о том, что вся эта «остроконечная / тупоконечная» терминология восходит к произведению Джонатана Свифта «Путешествия Гулливера». Подобно тому, как жители Лилипутии чистили яйцо с острого конца, процессоры Intel тоже обрабатывают числа начиная с младшего байта.
Таким образом, переменная local1 в действительности хранит число 8 (да-да, прям как количество щупалец у осьминога). Что касается param1, то там во втором от начала октете изображена двойка, поэтому в результате получаем число 2 * 256 = 512 (мы умножаем на 256, потому что каждый октет – это диапазон от 0 до 255). param2 хранит число 1 * 256 * 256 = 65536.
Служебная информация стекового кадра включает в себя два компонента: адрес стекового кадра вызвавшей функции (на рисунке — saved ebp) и адрес инструкции, куда необходимо передать управление по завершении данной функции (на рисунке – return address). Эта информация делает возможным возвращение управления, и следовательно, дальнейшее выполнение программы как будто никакого вызова и не было.
Теперь давайте рассмотрим процесс «рождения» стекового кадра. Стек растет не в том направлении, которое обычно ожидают увидеть, и сначала это может сбивать с толку. Например, чтобы увеличить стек на 8 байт, программист вычитает 8 из значения, хранимого в регистре esp. Вычитание – странный способ что-либо увеличить. Забавно, не правда ли!
Возьмем для примера простенькую программу на C:
Предположим, программу запустили без параметров в командной строке. При выполнении «сишной» программы в Linux, первым делом управление получает код, содержащийся в стандартной библиотеке C. Этот код вызовет функцию main() нашей программы, и, в данном случае, переменная argc будет равна 0 (на самом деле, переменная будет равна «1», что соответствует параметру — названию, под которым запущена программа, но давайте для простоты это момент сейчас опустим). При вызове функции main() происходит следующее:
Шаг 2 и 3, а также 4 (описан ниже) соответствуют последовательности инструкций, которая называется «прологом» и встречается практически в любой функции: текущее значение регистра ebp помещается в стек, затем значение регистра esp копируется в регистр ebp, что фактически приводит к созданию нового стекового кадра. Пролог функции main() такой же, как и других функций, с той лишь разницей, что при начале выполнения программы регистр ebp содержит нули.
Если взглянуть на то, что располагается в стеке под argc, то будут видны еще некоторые данные – указатель на строку-название, под которым программа была запущена, указатели на строки-параметры, переданные через командную строку (традиционный C-массив argv), а также указатели на переменные среды и непосредствено сами эти переменные. Однако, на данном этапе нам это не особо важно, так что продолжаем двигаться по направлению к вызову функции add():
Функция main() сначала вычетает 12 из текущего значения в регистре esp для выделения нужного ей места и затем присваивает значения переменным a и b. Значения, хранимые в памяти, изображены на рисунке в шестнадцатеричной форме и с прямым порядком байтов – как и в любом отладчике. После присвоения значений, функция main() вызывает функцию add(), и та начинает выполняться:
Чем дальше, тем интересней! Перед нами еще один пролог, однако теперь уже четко видно как последовательность стековых кадров в стеке оказывается организованной в связный список, и регистр ebp хранит ссылку на первый элемент этого списка. Вот с опорой на это и реализованы трассировка стека в отладчиках и Exception-объекты высокоуровневых языков. Обратим внимание на типичную для начала выполнения функции ситуацию, когда регистры ebp и esp указывают в одно и то же место. И еще раз вспомним, что для наращивания стека осуществляется вычитание из значения, хранящегося в регистре esp.
Важно заметить следующее — при копировании данных из регистра ebp в память происходит непонятное на первый взгляд изменение порядка хранения байтов. Дело в том, что для регистров такого понятия как «порядок байтов» не существует. Иными словами, рассматривая регистр, мы не можем говорить о том, что в нем есть «старшие или младшие адреса». Поэтому отладчики показывают значения, хранимые в регистрах, в наиболее удобном для человеческого восприятия виде: от более значимых к менее значимым цифрам. Таким образом, имея стандартную нотацию «слева-направо» и «little-endian» машину, создается обманчивое впечатление, что в результате операции копирования из регистра в память байты поменяли порядок на обратный. Я хотел, чтобы картина, показанная на рисунках была максимально приближена к реальности – отсюда и такие рисунки.
Теперь, когда самая сложная часть у нас позади, осуществляем сложение:
Здесь у нас появляется неизвестный регистр, чтобы помочь со сложением, но в целом ничего особенного или удивительного. Функция add() выполняет вою работу и, начиная с этого момента все действия в стеке будут осуществляться в обратном порядке. Но об этом расскажем как-нибудь в другой раз.
Все, кто дочитал до этих строк, заслуживает подарок за стойкость, поэтому я с огромной гиковской гордостью презентую Вам вот эту единую схемку, на которой изображены все вышеописанные шаги.
Не так уж все и сложно, стоит только разложить все по полочкам. Кстати, маленькие квадратики очень сильно помогают понимаю. Без «маленьких квадратиков» в информатике вообще никуда. Надеюсь, мои рисунки позволили составить ясную картину происходящего, на которой интуитивно просто показан и рост стека, и изменения содержимого памяти. При ближайшем рассмотрении, наше программное обеспечение не так уж и сильно отличается от простой машины Тьюринга.
На этом завершается первая часть нашего путешествия по стеку. В будущих статьях нас ждут новые погружения в «байтовые» дебри, после чего посмотрим, что же на этом фундаменте способны выстроить высокоуровневые языки программирования. Увидимся на следующей неделе.
Источник
Стек вызовов — Call stack
В информатике , стек вызовов представляет собой стек структура данных , которая хранит информацию о всех подпрограммах одного компьютерной программы . Этот вид стеки также известен как стек исполнение , программный стек , стек управлений , стек время выполнения , или стек машины , и часто сокращается до просто « стек ». Хотя обслуживание стека вызовов важно для правильного функционирования большинства программ , детали обычно скрыты и автоматические в языках программирования высокого уровня . Многие компьютерные наборы команд содержат специальные инструкции для работы со стеками.
Стек вызовов используется для нескольких связанных целей, но основная причина его наличия — отслеживать точку, в которую каждая активная подпрограмма должна вернуть управление после завершения выполнения. Активная подпрограмма — это подпрограмма, которая была вызвана, но еще не завершила выполнение, после чего управление должно быть возвращено точке вызова. Такие активации подпрограмм могут быть вложенными на любой уровень (рекурсивный как частный случай), отсюда и структура стека. Например, если подпрограмма DrawSquare вызывает подпрограмму DrawLine из четырех разных мест, DrawLine необходимо знать, куда вернуться после завершения ее выполнения. Для этого адрес, следующий за инструкцией , на которую DrawLine выполняется переход, адрес возврата помещается в верхнюю часть стека вызовов при каждом вызове.
СОДЕРЖАНИЕ
Описание
Поскольку стек вызовов организован как стек , вызывающая сторона помещает адрес возврата в стек, а вызываемая подпрограмма, когда она завершается, извлекает или выталкивает адрес возврата из стека вызовов и передает управление этому адресу. Если вызываемая подпрограмма вызывает еще одну подпрограмму, она помещает другой адрес возврата в стек вызовов и так далее, при этом информация накапливается и распаковывается в соответствии с требованиями программы. Если нажатие занимает все пространство, выделенное для стека вызовов, возникает ошибка, называемая переполнением стека , что обычно приводит к сбою программы . Добавление записи подпрограммы в стек вызовов иногда называют «намоткой»; и наоборот, удаление записей «раскручивает».
Существует, как правило точно один стек вызовов , связанные с запущенной программой (или более точно, с каждой задачей или нитью из в процессе ), хотя дополнительные блоки могут быть созданы для сигнала обработки или кооперативной многозадачности (как с SetContext ). Поскольку существует только один в этом важном контексте, он может быть передан как в стеке (неявно, «задачи»); однако, в языке программирования Forth стек данных или параметры стека доступна более явно , чем в стеке вызовов и обычно упоминается как в стеке (см ниже).
В языках программирования высокого уровня особенности стека вызовов обычно скрыты от программиста. Им предоставляется доступ только к набору функций, а не к памяти самого стека. Это пример абстракции . С другой стороны, для большинства языков ассемблера требуется участие программистов в управлении стеком. Фактические детали стека на языке программирования зависят от компилятора , операционной системы и доступного набора инструкций .
Функции стека вызовов
Как отмечалось выше, основная цель стека вызовов — хранить адреса возврата . Когда вызывается подпрограмма, место (адрес) инструкции, с которой вызывающая подпрограмма может позже возобновиться, необходимо где-то сохранить. Использование стека для сохранения адреса возврата имеет важные преимущества перед альтернативными соглашениями о вызовах . Во-первых, каждая задача может иметь свой собственный стек, и, таким образом, подпрограмма может быть поточно-ориентированной , то есть может быть активной одновременно для разных задач, выполняющих разные действия. Другим преимуществом является то, что, предоставляя Реентерабельность , рекурсия автоматически поддерживается. Когда функция вызывает себя рекурсивно, необходимо сохранять адрес возврата для каждой активации функции, чтобы впоследствии его можно было использовать для возврата из активации функции. Структуры стека предоставляют эту возможность автоматически.
В зависимости от языка, операционной системы и машинной среды стек вызовов может служить дополнительным целям, в том числе, например:
Локальное хранилище данных
Вместо статической ссылки ссылки на включающие статические кадры могут быть собраны в массив указателей, известный как отображение, которое индексируется для определения местоположения желаемого кадра. Глубина лексической вложенности подпрограммы является известной константой, поэтому размер отображения подпрограммы является фиксированным. Также известно количество содержащихся прицелов, которые необходимо пересечь, индекс в отображении также фиксирован. Обычно отображение процедуры располагается в собственном стековом фрейме, но Burroughs B6500 реализовал такое отображение аппаратно, которое поддерживало до 32 уровней статической вложенности. Записи отображения, обозначающие содержащие области, берутся из соответствующего префикса отображения вызывающего абонента. Внутренняя процедура, которая рекурсивно создает отдельные кадры вызова для каждого вызова. В этом случае все статические ссылки внутренней подпрограммы указывают на один и тот же контекст внешней подпрограммы. Другое состояние возврата Помимо адреса возврата, в некоторых средах могут быть другие состояния машины или программного обеспечения, которые необходимо восстановить при возврате подпрограммы. Сюда могут входить такие вещи, как уровень привилегий, информация об обработке исключений, арифметические режимы и так далее. При необходимости его можно сохранить в стеке вызовов, как и адрес возврата.
Типичный стек вызовов используется для адреса возврата, локальных переменных и параметров (известных как кадр вызова ). В некоторых средах стеку вызовов может быть назначено больше или меньше функций. В языке программирования Forth , например, обычно только адрес возврата, подсчитываемые параметры цикла и индексы и, возможно, локальные переменные хранятся в стеке вызовов (который в этой среде называется стеком возврата ), хотя любые данные могут быть временно помещены там используется специальный код обработки стека возврата, если соблюдаются требования вызовов и возвратов; параметры обычно сохраняются в отдельном стеке данных или параметров стека , как правило , называют стек в терминологии Forth , даже если есть стек вызовов , поскольку он, как правило , доступ к более явно. Некоторые форты также имеют третий стек для параметров с плавающей запятой .
Состав
Стек вызовов состоит из кадров стека (также называемых записей активации или кадров активации ). Это машинно-зависимые и зависимые от ABI структуры данных, содержащие информацию о состоянии подпрограммы. Каждый кадр стека соответствует вызову подпрограммы, которая еще не завершилась возвратом. Например, если подпрограмма с именем DrawLine в настоящее время выполняется, будучи вызванной подпрограммой DrawSquare , верхняя часть стека вызовов может быть размещена, как на соседнем рисунке.
Подобную диаграмму можно нарисовать в любом направлении, если понятно расположение вершины и, следовательно, направление роста стопки. Кроме того, независимо от этого, архитектуры различаются в зависимости от того, растут ли стеки вызовов в сторону более высоких адресов или в сторону более низких адресов. Логика схемы не зависит от выбора адресации.
Фрейм стека наверху стека предназначен для выполняющейся в данный момент процедуры. Кадр стека обычно включает как минимум следующие элементы (в порядке отправки):
- аргументы (значения параметров), переданные в подпрограмму (если есть);
- адрес возврата обратно вызывающему подпрограмме (например, в DrawLine кадре стека, адрес в DrawSquare коде); а также
- место для локальных переменных процедуры (если есть).
Указатели стека и фрейма
Когда размеры кадра стека могут различаться, например, между разными функциями или между вызовами конкретной функции, извлечение кадра из стека не является фиксированным декрементом указателя стека . При возврате функции указатель стека вместо этого восстанавливается до указателя кадра , значения указателя стека непосредственно перед вызовом функции. Каждый кадр стека содержит указатель стека на верхнюю часть кадра непосредственно под ним. Указатель стека — это изменяемый регистр, совместно используемый всеми вызовами. Указатель кадра данного вызова функции является копией указателя стека, как это было до вызова функции.
Расположение всех других полей в кадре может быть определено либо относительно верха кадра, как отрицательные смещения указателя стека, либо относительно верха нижнего кадра, как положительные смещения указателя кадра. Местоположение самого указателя кадра должно быть определено как отрицательное смещение указателя стека.
Сохранение адреса во фрейме звонящего
В большинстве систем стековый фрейм имеет поле, содержащее предыдущее значение регистра указателя фрейма, значение, которое он имел во время выполнения вызывающей стороны. Например, кадр стека DrawLine будет иметь ячейку памяти, содержащую значение указателя кадра, которое DrawSquare использует (не показано на диаграмме выше). Значение сохраняется при входе в подпрограмму и восстанавливается при возврате. Наличие такого поля в известном месте в фрейме стека позволяет коду последовательно обращаться к каждому фрейму под фреймом выполняющейся в данный момент подпрограммы, а также позволяет подпрограмме легко восстанавливать указатель фрейма на фрейм вызывающей стороны непосредственно перед его возвратом.
Лексически вложенные подпрограммы
Языки программирования, поддерживающие вложенные подпрограммы, также имеют поле в кадре вызова, которое указывает на кадр стека последней активации процедуры, которая наиболее точно инкапсулирует вызываемого, то есть непосредственную область действия вызываемого. Это называется ссылкой доступа или статической ссылкой (поскольку она отслеживает статическое вложение во время динамических и рекурсивных вызовов) и предоставляет подпрограмме (а также любым другим подпрограммам, которые она может вызывать) доступ к локальным данным своих инкапсулирующих подпрограмм при каждом вложении. уровень. Некоторые архитектуры, компиляторы или варианты оптимизации хранят по одной ссылке для каждого уровня включения (а не только непосредственно включающего), так что глубоко вложенные подпрограммы, которые обращаются к поверхностным данным, не должны пересекать несколько ссылок; эту стратегию часто называют «демонстрацией».
Ссылки доступа можно оптимизировать, когда внутренняя функция не обращается к каким-либо (непостоянным) локальным данным в инкапсуляции, как, например, в случае с чистыми функциями, общающимися только через аргументы и возвращаемые значения. Некоторые исторические компьютеры, такие как большие системы Берроуза , имели специальные «регистры отображения» для поддержки вложенных функций, в то время как компиляторы для большинства современных машин (таких как вездесущий x86) просто резервируют несколько слов в стеке для указателей по мере необходимости.
Перекрывать
Для некоторых целей кадр стека подпрограммы и кадр ее вызывающей стороны можно рассматривать как перекрывающиеся, причем перекрытие состоит из области, в которой параметры передаются от вызывающей стороны к вызываемой. В некоторых средах вызывающий помещает каждый аргумент в стек, таким образом расширяя его фрейм стека, а затем вызывает вызываемого. В других средах вызывающий имеет предварительно выделенную область в верхней части кадра стека для хранения аргументов, которые он передает другим вызываемым им подпрограммам. Эту область иногда называют областью исходящих аргументов или областью выноски . При таком подходе размер области вычисляется компилятором как самый большой, необходимый для любой вызываемой подпрограммы.
Использовать
Обработка звонков на сайт
Обычно манипуляции со стеком вызовов, необходимые на месте вызова подпрограммы, минимальны (что хорошо, поскольку может быть много узлов вызова для каждой вызываемой подпрограммы). Значения фактических аргументов оцениваются на сайте вызова, поскольку они специфичны для конкретного вызова, и либо помещаются в стек, либо помещаются в регистры, как определено используемым соглашением о вызовах . Фактическая инструкция вызова, такая как «переход и ссылка», затем обычно выполняется для передачи управления коду целевой подпрограммы.
Обработка записи подпрограммы
В вызываемой подпрограмме первый выполненный код обычно называется прологом подпрограммы , так как он выполняет необходимые служебные действия до того, как будет запущен код для операторов подпрограммы.
Для архитектур с набором команд, в которых инструкция, используемая для вызова подпрограммы, помещает адрес возврата в регистр, а не помещает его в стек, пролог обычно сохраняет адрес возврата, помещая значение в стек вызовов, хотя, если вызываемый подпрограмма не вызывает никаких других подпрограмм, она может оставить значение в регистре. Точно так же могут быть переданы значения текущего указателя стека и / или указателя кадра.
Если используются указатели кадра, пролог обычно устанавливает новое значение регистра указателя кадра из указателя стека. Затем можно выделить пространство в стеке для локальных переменных путем постепенного изменения указателя стека.
Язык программирования Forth допускает явную намотку стека вызовов (называемого там «стеком возврата»).
Обработка возврата
Когда подпрограмма готова к возврату, она выполняет эпилог, отменяющий шаги пролога. Обычно это восстанавливает сохраненные значения регистров (например, значение указателя фрейма) из фрейма стека, выталкивает весь фрейм стека из стека, изменяя значение указателя стека, и, наконец, выполняет переход к инструкции по адресу возврата. Согласно многим соглашениям о вызовах элементы, извлекаемые из стека эпилогом, включают в себя исходные значения аргументов, и в этом случае обычно нет дополнительных манипуляций со стеком, которые должны выполняться вызывающей стороной. Однако с некоторыми соглашениями о вызовах ответственность за удаление аргументов из стека после возврата лежит на вызывающей стороне.
Размотка
Возврат из вызванной функции вытолкнет верхний фрейм из стека, возможно, оставив возвращаемое значение. Более общий акт выталкивания одного или нескольких кадров из стека для возобновления выполнения в другом месте программы называется раскручиванием стека и должен выполняться, когда используются нелокальные управляющие структуры, такие как те, которые используются для обработки исключений . В этом случае кадр стека функции содержит одну или несколько записей, определяющих обработчики исключений. Когда генерируется исключение, стек разворачивается до тех пор, пока не будет найден обработчик, готовый обработать (перехватить) тип выброшенного исключения.
В некоторых языках есть другие управляющие структуры, требующие общей раскрутки. Паскаль позволяет глобальному оператору goto передавать управление из вложенной функции в ранее вызванную внешнюю функцию. Эта операция требует, чтобы стек был размотан, удалив столько кадров стека, сколько необходимо для восстановления правильного контекста, чтобы передать управление целевому оператору внутри включающей внешней функции. Аналогично, С имеет setjmp и longjmp функцию , которые действуют как нелокальный GOTOS. Common Lisp позволяет контролировать то, что происходит при разворачивании стека, с помощью unwind-protect специального оператора.
При применении продолжения стек (логически) разматывается, а затем перематывается вместе со стеком продолжения. Это не единственный способ реализовать продолжения; например, используя несколько явных стеков, приложение продолжения может просто активировать свой стек и намотать значение, которое нужно передать. Язык программирования Scheme позволяет выполнять произвольные переходы в определенных точках при «раскручивании» или «перемотке» стека управления при вызове продолжения.
Осмотр
Стек вызовов иногда можно проверить во время работы программы. В зависимости от того, как программа написана и скомпилирована, информация в стеке может использоваться для определения промежуточных значений и трассировки вызовов функций. Это использовалось для генерации детализированных автоматических тестов, а в таких случаях, как Ruby и Smalltalk, для реализации первоклассных продолжений. Например, GNU Debugger (GDB) реализует интерактивную проверку стека вызовов работающей, но приостановленной программы C.
Регулярное взятие выборок из стека вызовов может быть полезно при профилировании производительности программ, потому что, если указатель подпрограммы появляется в данных выборки стека вызовов много раз, это, вероятно, узкое место кода и должно быть проверено на наличие проблем с производительностью.
Безопасность
В языке со свободными указателями или записью непроверенных массивов (например, в C) смешивание данных потока управления, влияющих на выполнение кода (адреса возврата или сохраненные указатели кадров) и простых данных программы (параметры или возвращаемые значения) ) в стеке вызовов представляет собой угрозу безопасности, которая может быть использована путем переполнения буфера стека как наиболее распространенного типа переполнения буфера .
Одна из таких атак включает заполнение одного буфера произвольным исполняемым кодом, а затем переполнение того же или другого буфера для перезаписи некоторого адреса возврата значением, которое указывает непосредственно на исполняемый код. В результате, когда функция возвращается, компьютер выполняет этот код. Этот вид атаки может быть легко блокирован W ^ X . Подобные атаки могут быть успешными даже при включенной защите W ^ X, включая атаку возврата к libc или атаки, исходящие от программирования, ориентированного на возврат . Были предложены различные меры по снижению рисков, такие как хранение массивов в полностью отдельном месте от стека возврата, как в случае с языком программирования Forth.
Источник