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