Как вызываемые функции возвращаются к вызывающему абоненту после его вызова?

Я читал, что когда вызов функции выполняется программой, вызываемая функция должна знать, как вернуть ее вызывающему.

Мой вопрос: как вызываемая функция знает, как вернуться к ее вызывающему абоненту? Есть ли механизм, работающий за кулисами через компилятор?

Ответ 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

Это требует сотрудничества между вызывающим и вызывающим.

Абонент соглашается предоставить адрес, на который вызываемый должен вернуться к вызываемому лицу (обычно, нажимая его на стек или передавая его в регистр), и вызываемый соглашается вернуться к этому адресу, когда он закончил выполнение.