Как вызываемые функции возвращаются вызывающему объекту после вызова?
Я прочитал, что когда вызов функции выполняется программой, вызываемая функция должна знать, как вернуться к вызывающему.
мой вопрос: как вызываемая функция знает, как вернуться к вызывающему объекту? Есть ли механизм, работающий за кулисами через компилятор?
4 ответов
компилятор подчиняется определенному "соглашению о вызове", определенному как часть ABI, на которую вы нацелены. Это соглашение о вызовах будет включать способ для системы узнать, на какой адрес вернуться. Соглашение о вызовах обычно использует поддержку аппаратного обеспечения для вызовов процедур. Например, в Intel обратный адрес помещается в стек:
...процессор помещает значение
EIPregister (который содержит смещение инструкции, следующей за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(удаляются локальные переменные.
когда методы рекурсируют слишком далеко, стек может стать слишком большим, и может произойти" переполнение стека".
Это требует сотрудничества между вызываемым и вызывающим абонентом.
вызывающий абонент соглашается дать адрес, который вызываемый абонент должен вернуться к вызываемому абоненту (обычно, нажав его в стеке или передав его в регистр), и вызываемый абонент соглашается вернуться к этому адресу, когда он закончит выполнение.