Понимание того, как работает обратный звонок

Я несколько смущен относительно того, когда именно удаляется контекст вызываемой функции. Я читал, что стековый фрейм вызываемой функции выталкивается, когда он возвращается. Я пытаюсь применить эти знания к следующему сценарию, где функция 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?

Отвечает ли это на ваш вопрос? Возврат `struct` из функции в C

Progman 19.11.2022 14:27

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

Persixty 21.11.2022 18:58

Концептуально return bill делает копию bill и передает ее вызывающей стороне. Для вызывающей стороны эта копия является временной. bill сам удаляется, а звонящий делает с копией, что хочет.

n. m. 21.11.2022 19:20
Шаблоны Angular PrimeNg
Шаблоны Angular PrimeNg
Как привнести проверку типов в наши шаблоны Angular, использующие компоненты библиотеки PrimeNg, и настроить их отображение с помощью встроенной...
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Создайте ползком, похожим на звездные войны, с помощью CSS и Javascript
Если вы веб-разработчик (или хотите им стать), то вы наверняка гик и вам нравятся "Звездные войны". А как бы вы хотели, чтобы фоном для вашего...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Начала с розового дизайна
Начала с розового дизайна
Pink Design - это система дизайна Appwrite с открытым исходным кодом для создания последовательных и многократно используемых пользовательских...
Шлюз в PHP
Шлюз в PHP
API-шлюз (AG) - это сервер, который действует как единая точка входа для набора микросервисов.
14 Задание: Типы данных и структуры данных Python для DevOps
14 Задание: Типы данных и структуры данных Python для DevOps
проверить тип данных используемой переменной, мы можем просто написать: your_variable=100
1
3
61
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Кадр стека "удаляется" непосредственно перед возвратом из функции. Обычно возвращаемое значение сохраняется из стека в регистр, а затем функция возвращается. Давайте заглянем немного под капот, чтобы увидеть, что происходит на самом деле. Я собираюсь опустить фактическую дизассемблирование вашего кода, так как это немного утомительно, но я подытожу здесь ключевые моменты. Обычно функция, написанная на 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

Другие вопросы по теме