Действительно ли кадр стека помещается в стек при вызове функции?

В течение некоторого времени меня учили тому, что когда я запускаю программу, первое, что немедленно попадает в стек, — это кадр стека для основного метода. И если я вызову функцию foo () из main, то в стек также будет помещен кадр стека, равный размеру локальных переменных (автоматических объектов) и параметров.

Однако я столкнулся с парой вещей, которые противоречат этому. И я надеюсь, что кто-то может прояснить мою путаницу или объяснить, почему на самом деле нет никаких противоречий.

Первое противоречие:

В книге Бьярна Страуструпа «Язык программирования C ++», 3-е издание, на странице 244 говорится: «Именованный автоматический объект создается каждый раз, когда его объявление встречается при выполнении программы». Если это недостаточно ясно, на следующей странице написано: «Конструктор для локальной переменной выполняется каждый раз, когда поток управления проходит через объявление локальной переменной».

Означает ли это, что общая память для стекового фрейма выделяется не все сразу, а скорее блок за блоком, когда встречаются объявления переменных?
Кроме того, означает ли это, что кадр стека может не всегда иметь одинаковый размер, если объявление переменной не встречалось из-за оператора if?

Второе противоречие:

Я немного программировал на ассемблере (точнее, на ARM), и мой класс учили так, что при вызове функции мы немедленно использовали регистры и никогда не помещали какие-либо локальные переменные текущей функции в стек, если алгоритм не было возможно выполнить с ограниченным количеством регистров. И даже тогда мы только выдвинули оставшиеся переменные.

Означает ли это, что когда вызывается функция, кадр стека может вообще не создаваться?
Означает ли это также, что размер стека может отличаться по размеру из-за использования регистров?

1

Решение

По поводу вашего первого вопроса:

Создание объекта не имеет ничего общего с распределением самих данных. Чтобы быть более конкретным: тот факт, что объект имеет свое зарезервированное пространство в стеке, не подразумевает ничего, когда вызывается его конструктор.

Означает ли это, что общая память для стекового фрейма выделяется не все сразу, а скорее блок за блоком, когда встречаются объявления переменных?

Этот вопрос действительно зависит от компилятора. Указатель стека — это просто указатель, то, как он используется двоичным файлом, зависит от компилятора. На самом деле некоторые компиляторы могут зарезервировать всю запись активации, некоторые могут зарезервировать понемногу, другие могут зарезервировать ее динамически в соответствии с конкретным вызовом и так далее. Это даже тесно связано с оптимизацией, так что компилятор может расположить все так, как он считает лучше.

Означает ли это, что когда вызывается функция, кадр стека может вообще не создаваться? Означает ли это также, что размер стека может отличаться по размеру из-за использования регистров?

Опять же, здесь нет строгого ответа. Обычно компиляторы полагаются на распределение регистров алгоритмы, которые могут распределять регистры таким образом, чтобы минимизировать «разлитые» (по стеку) переменные. Конечно, если вы пишете на ассемблере вручную, вы можете решить назначить определенные регистры определенным переменным в вашей программе только потому, что по их содержимому вы знаете, как вы хотите, чтобы она работала.

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

3

Другие решения

  1. Создание объекта C ++ имеет очень мало общего с получением памяти для объекта. На самом деле было бы точнее сказать «резервирование памяти», поскольку в целом на компьютерах нет небольших групп разработчиков ОЗУ, которые запускаются в действие каждый раз, когда вы запрашиваете новый объект. Память более или менее постоянна (хотя мы могли бы спорить о ВМ). Конечно, компилятор должен организовать, чтобы его программа использовала только определенный диапазон памяти для одной вещи за один раз. Это может (и, вероятно, делает) требовать, чтобы оно зарезервировало диапазон памяти до существования объекта и избегало использования его для других объектов до некоторого времени после исчезновения объекта. Для эффективности компилятор может (даже в случае объектов с динамической длительностью хранения) оптимизировать резервирование, резервируя несколько блоков памяти одновременно, если он знает, что они понадобятся. В любом случае, когда C ++ говорит о «конструировании объекта», это означает только то, что: захват диапазона памяти с неопределенным содержимым и выполнение того, что необходимо для создания представления объекта (и всего остального в состоянии мира подразумевается созданием объекта, который не может быть ограничен конкретным фрагментом памяти.)

  2. Не существует требования для существования стековых фреймов. Не существует требования для существования стека. Это все зависит от компилятора. Конечно, большинство компиляторов генерируют код, который использует стек, и хорошие компиляторы выяснят, когда можно сократить или даже пропустить фрейм стека. Так что да, рамки могут отличаться по размеру.

1

Вы абсолютно правы, стековый фрейм не требуется. Кадры стека — это быстрое и грязное решение проблемы управления локальным пространством, которое легче отлаживать, чем управлять изменениями в указателе стека в ходе выполнения функции. Если в функции есть необходимость в стеке, проще настроить указатель стека при входе и восстановить его при возврате.

Это также не черно-белое изображение, компиляторы — это программы, подобные любой другой программе, и если вы еще не знаете, вы поймете, что при любом количестве программистов вы получите несколько решений одной и той же проблемы. Даже если число программистов равно одному, один человек может решить проблему снова и снова, пока они не будут удовлетворены и / или по любой причине не решат выпустить различные версии. Использование стека очень распространено для локальных переменных, это действительно то, как вы это делаете, но это не означает, что вы должны использовать кадр стека, созданный при входе и восстановленный при возврате.

И, как вы узнали в своих классах, и в ходе экспериментов очень легко увидеть (скомпилируйте несколько простых функций с различными уровнями оптимизации от отсутствия оптимизации до некоторого), которые, например, gcc не будет использовать стек без необходимости. Мы говорим здесь о руке прямо там, где обычное соглашение о вызовах основано на регистре (нет ничего, что говорит, что автор (ы) компилятора должны следовать этому соглашению, возможно использовать стек, основанный на руке, если компилятор решит это сделать). Процессоры, в которых обычное соглашение основано на стеке, так как код уже имеет дело со стеком, в любом случае могут использовать фрейм стека. Вполне вероятно, что в этих случаях используется соглашение на основе стека, потому что у процессора нет регистров общего назначения и больше полагается на стек, чем у других процессоров с большим количеством регистров, что означает, что процессору, скорее всего, нужен стек часто не только для соглашения о вызовах, но и для большая часть местного хранения.

1
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector