Я несколько смущен относительно того, когда именно удаляется контекст вызываемой функции. Я читал, что стековый фрейм вызываемой функции выталкивается, когда он возвращается. Я пытаюсь применить эти знания к следующему сценарию, где функция foo
вызывает функцию bar
, которая возвращает структуру. Код может выглядеть примерно так:
//...
struct Bill
{
float amount;
int id;
char address[100];
};
//...
Bill bar(int);
//...
void foo() {
// ...
Bill billRecord = bar(56);
//...
}
Bill bar(int i) {
//...
Bill bill = {123.56, 2347890, "123 Main Street"};
//...
return bill;
}
Память для объекта bill
в функции bar
берется из стекового фрейма bar
, который извлекается при возврате bar
. Похоже, что он все еще действителен, когда присваивание возвращаемой структуры выполняется billRecord
в foo
.
Значит ли это, что кадр стека для bar
удаляется не в тот момент, когда он возвращается, а только после того, как значение, возвращаемое bar
, используется в foo
?
Время жизни локальной переменной bill
заканчивается, когда функция возвращается. Но возвращаемое значение доступно вызывающей стороне. Если бы это было не так, это разрушило бы идею возвращаемого значения/объекта. Вполне возможно, что подразумеваемая копия не имеет места. Но это между прочим.
Концептуально return bill
делает копию bill
и передает ее вызывающей стороне. Для вызывающей стороны эта копия является временной. bill
сам удаляется, а звонящий делает с копией, что хочет.
Кадр стека "удаляется" непосредственно перед возвратом из функции. Обычно возвращаемое значение сохраняется из стека в регистр, а затем функция возвращается. Давайте заглянем немного под капот, чтобы увидеть, что происходит на самом деле. Я собираюсь опустить фактическую дизассемблирование вашего кода, так как это немного утомительно, но я подытожу здесь ключевые моменты. Обычно функция, написанная на C, делает следующее (в качестве примера я использую x86 Assembly, процесс аналогичен для других архитектур, но имена регистров будут другими)
Во-первых, чтобы использовать функцию, ее нужно ВЫЗВАТЬ.
CALL bar
При этом содержимое регистра rip
помещается в стек (что можно рассматривать как представление того, «какую строку кода мы собираемся запустить следующей»).
bar:
push rbp
mov rbp,rsp
Большинство функций, написанных компилятором C, начинаются так. Целью этого является создание кадра стека. Содержимое rbp
хранится в стеке для сохранности. Затем мы копируем значение указателя стека (rsp
) в rbp
. Причина, по которой C делает это, проста: rsp
можно изменить с помощью определенных инструкций, таких как push
, pop
, call
и ret
, где rbp
нет. Кроме того, свободное пространство над стеком, которое еще не использовалось, может быть использовано вызывающей функцией.
Затем наши локальные переменные сохраняются в стеке. Один из них был 56, которому мы передали bar
. C решил сохранить значение 56 в регистре esi
перед вызовом функции.
mov DWORD PTR [rbp-12], esi
В основном это означает «взять содержимое регистра esi
и сохранить его за 12 байтов до адреса, на который указывает rbp
. Это гарантированно будет свободным пространством, благодаря последовательности push rbp mov rbp,rsp
из ранее.
Как только функция делает то, что ей нужно, возвращаемое значение сохраняется в rax
, а затем мы выполняем последовательность выхода.
pop rbp
ret
Что касается кадра стека, то он фактически не был «удален» как таковой. Эти значения временно сохраняются до тех пор, пока они не будут перезаписаны другой функцией. Однако во всех смыслах и целях они считаются удаленными, так как это пространство стека теперь считается «свободным» и может использоваться чем угодно (например, аппаратными прерываниями и т. д.). его локальных значений все еще там, если вы попытаетесь получить к ним доступ. (Не то чтобы C позволил вам получить к ним доступ без встроенного ассемблера, но я говорю, что вам не следует даже пытаться.)
Вы правы, есть эта «дыра» между тем, когда bar
возвращает и foo
копирует возвращаемое значение куда-то с операцией присваивания. Это работает так, что теоретически существует некоторое пространство «возвращаемого значения», в котором возвращаемое значение живет во время возврата. Таким образом, из модели выполнения есть две копии возвращаемого значения — из локального в bar
в пространство возврата и из пространства возврата в billRecord
в foo.
Как именно это работает, зависит от соглашений о вызовах. В x86_64 «пространство возвращаемых значений» находится в регистрах для небольших возвращаемых значений и в некоторой памяти, контролируемой вызывающей стороной, для больших возвращаемых значений. Если возвращаемое значение больше двух регистров, вызывающая сторона должна передать «скрытый» дополнительный аргумент с указателем на место, где должно храниться возвращаемое значение. bar
затем скопирует свою локальную переменную в это пространство, прежде чем удалить кадр стека и вернуться.
Поэтому при компиляции foo
компилятор знает, что ему нужно предоставить этот дополнительный скрытый аргумент, и знает, что ему нужно выделить для него некоторое пространство. Если он умен (и вы включили оптимизацию), он просто повторно использует пространство для billRecord
для этого (передавая указатель на billRecord
в качестве скрытого аргумента), а присваивание в foo
тогда будет нулевым оператором (как известно, bar
будет сделать всю работу)1`.
Если компилятор умен при компиляции bar
, он может выполнить «оптимизацию возвращаемого значения» и, понимая, что он просто собирается вернуть локальную переменную bill
, выделить эту локальную переменную в пространстве возвращаемых значений, которое он получил от своего вызывающего, а не в своем собственном стековый каркас.
1Of course, it can only do this if it knows there's no way for bar
to access billRecord
directly. This requires what is known as "escape analysis" -- if the location of billRecord
"escapes" from foo
(for example, by taking its address and storing it somewhere or passing it as an argument somewhere), this optimization can't be done and it will need to allocate additional space in its stack frame for the return space in addition to that used by billRecord
Отвечает ли это на ваш вопрос? Возврат `struct` из функции в C