Как вызываемые функции возвращаются к вызывающему абоненту после его вызова?
Я читал, что когда вызов функции выполняется программой, вызываемая функция должна знать, как вернуть ее вызывающему.
Мой вопрос: как вызываемая функция знает, как вернуться к ее вызывающему абоненту? Есть ли механизм, работающий за кулисами через компилятор?
Ответы
Ответ 1
Компилятор подчиняется определенному "соглашению о вызове", определенному как часть настроенного вами ABI. Эта конвенция о вызове будет включать в себя способ для системы узнать, какой адрес для возврата. Вызывающее соглашение обычно использует аппаратную поддержку для вызовов процедур. Например, на Intel адрес возврата переносится в стек:
... процессор подталкивает значение регистра EIP
(который содержит смещение команды, следующей за инструкцией CALL
) в стеке ( для использования позже в качестве указателя команды возврата).
Возврат из функции выполняется с помощью команды ret
:
... процессор выводит указатель (смещение) команды возврата из верхней части стека в регистр EIP
и начинает выполнение программы с помощью нового указателя инструкции.
Чтобы контрастировать, в ARM обратный адрес помещается в регистр ссылок:
Команды BL
и BLX
копируют адрес следующей команды в lr
( r14
, реестр ссылок).
Возвраты обычно выполняются путем выполнения movs pc, lr
для копирования адреса из регистра ссылок обратно в регистр счетчика программ.
Литература:
Ответ 2
-
Компилятор знает, как вызвать функцию и какое соглашение вызова используется. Например, в C аргументы для функции помещаются в стек. Вызывающий отвечает за очистку стека, поэтому вызываемой функции не нужно удалять аргументы. Другие соглашения о вызовах могут включать нажатие аргументов в стеке, и вызываемая функция должна очистить его. В этом случае сгенерированный код таков, что функция исправляет стек до его возврата. Соглашения вызова Ohter могут передавать аргументы в регистры, поэтому в этом случае вызываемая функция также не должна заботиться.
-
У процессора есть механизм для вызова подпрограммы. Это сохранит текущий адрес выполнения в стеке, а затем перенесет обработку на новый адрес. Когда функция выполнена, она выполняет оператор return, который будет извлекать адрес вызывающего абонента и возобновлять выполнение там.
Если адрес возврата уничтожен, потому что стек неправильно очищен uo или память перезаписана, вы получаете поведение undefined. Конечно, детали конкретной реализации различаются в зависимости от используемой платформы.
Ответ 3
Это стало возможным благодаря стеку (особенно в системах на базе Intel). Скажем, что у нас есть метод caller
, который включает, скажем, int
, что он локально сохраняется.
Когда caller(
вызывает target(
, что int должен быть сохранен. Он помещается в стек, а также адрес, из которого сделан вызов. target(
может выполнять свою логику, создавать свои собственные локальные переменные и вызывать другие методы. Его локальные переменные будут помещены в стек вместе с адресом вызова.
Когда target(
завершается, стек "разворачивается". Верхняя часть стека, содержащая локальные переменные target(
, удаляется.
Когда методы рекурсируют слишком далеко, стек может стать слишком большим, и может произойти "переполнение стека".
Ответ 4
Это требует сотрудничества между вызывающим и вызывающим.
Абонент соглашается предоставить адрес, на который вызываемый должен вернуться к вызываемому лицу (обычно, нажимая его на стек или передавая его в регистр), и вызываемый соглашается вернуться к этому адресу, когда он закончил выполнение.