В информатике соглашение о вызовах — это схема уровня реализации (низкоуровневая) того, как подпрограммы или функции получают параметры от вызывающего объекта и как они возвращают результат. Когда некоторый код вызывает функцию, были приняты решения о том, где и как параметры передаются в эту функцию, а также где и как результаты возвращаются из этой функции, причем эти передачи обычно выполняются через определенные регистры или внутри кадра стека при вызове . куча . Существуют варианты дизайна того, как задачи подготовки к вызову функции и восстановления среды после завершения функции распределяются между вызывающим и вызываемым объектом. Некоторые соглашения о вызовах определяют способ вызова каждой функции. Для каждого вызова функции следует использовать правильное соглашение о вызовах, чтобы обеспечить правильное и надежное выполнение всей программы с использованием этих функций.
Соглашения о вызовах обычно считаются частью двоичного интерфейса приложения (ABI).
Имена или значения параметров и возвращаемых значений определяются в интерфейсе прикладного программирования (API, в отличие от ABI), который представляет собой отдельную, хотя и связанную концепцию с ABI и соглашением о вызовах. Имена членов переданных структур и объектов также будут считаться частью API, а не ABI. Иногда API включают ключевые слова для указания соглашения о вызове функций.
Соглашения о вызовах обычно не включают информацию о сроке службы динамически выделяемых структур и объектов. {{{1}}} в других документах указывается, где лежит ответственность за освобождение выделенной памяти.
Соглашения о вызовах вряд ли будут определять расположение элементов внутри структур и объектов, например порядок байтов или упаковку структур.
Для некоторых языков соглашение о вызовах включает детали обработки ошибок или исключений (например, Go , Java ), а для других — нет (например, C++ ).
Для удаленных вызовов процедур существует аналогичная концепция, называемая маршаллингом .
Соглашения о вызовах могут быть связаны со стратегией оценки конкретного языка программирования , но чаще всего не считаются ее частью (или наоборот), поскольку стратегия оценки обычно определяется на более высоком уровне абстракции и рассматривается как часть языка, а не как часть языка. как низкоуровневая деталь реализации компилятора конкретного языка .
Соглашения о вызовах могут отличаться:
Иногда на одной платформе появляется несколько соглашений о вызовах; данная платформа и языковая реализация могут предлагать выбор соглашений о вызовах. Причины этого включают производительность, адаптацию соглашений других популярных языков, а также ограничения или соглашения, налагаемые различными « вычислительными платформами ».
Во многих архитектурах имеется только одно широко используемое соглашение о вызовах, часто предлагаемое архитектором. Для RISC, включая SPARC, MIPS и RISC-V , часто используются имена регистров, основанные на этом соглашении о вызовах. Например, регистры MIPS $4
через $7
имеют «имена ABI» $a0
через $a3
, что отражает их использование для передачи параметров в стандартном соглашении о вызовах. (ЦП RISC имеют множество эквивалентных регистров общего назначения, поэтому обычно нет аппаратных причин давать им имена, кроме чисел.)
Соглашение о вызовах языка данной программы может отличаться от соглашения о вызовах базовой платформы, ОС или какой-либо библиотеки, с которой связана ссылка. Например, в 32-разрядной версии Windows вызовы операционной системы имеют соглашение о вызовах stdcall , тогда как многие программы C , которые там выполняются, используют соглашение о вызовах cdecl . Чтобы учесть эти различия в соглашении о вызовах, компиляторы часто допускают ключевые слова, определяющие соглашение о вызовах для данной функции. Объявления функций будут включать дополнительные ключевые слова, специфичные для платформы, которые указывают используемое соглашение о вызовах. При правильной обработке компилятор сгенерирует код для вызова функций соответствующим образом.
Некоторые языки позволяют указывать соглашение о вызове функции с помощью этой функции; у других будет некоторое соглашение о вызовах, но оно будет скрыто от пользователей этого языка и поэтому обычно не будет учитываться программистом.
32-разрядная версия архитектуры x86 используется со многими различными соглашениями о вызовах. Из-за небольшого количества архитектурных регистров и исторического акцента на простоте и небольшом размере кода многие соглашения о вызовах x86 передают аргументы в стек. Возвращаемое значение (или указатель на него) возвращается в регистр. В некоторых соглашениях для первых нескольких параметров используются регистры, что может повысить производительность, особенно для очень часто вызываемых коротких и простых листовых подпрограмм (т. е. подпрограмм, которые не вызывают другие подпрограммы).
Пример вызова:
нажать EAX ; передать результат регистра push dword [ EBP + 20 ] ; передать некоторую переменную памяти (синтаксис FASM/TASM) push 3 ; передать некоторый постоянный вызов Calc ; возвращенный результат теперь находится в EAX
Типичная структура вызываемого объекта: (некоторые или все (кроме ret) приведенные ниже инструкции могут быть оптимизированы с помощью простых процедур). Некоторые соглашения оставляют пространство параметров выделенным, используя Plain ret
вместо ret imm16
. В этом случае вызывающая сторона могла бы add esp,12
в этом примере или иным образом справиться с изменением ESP.
расчет: нажмите EBP ; сохранить указатель старого кадра mov EBP , ESP ; получить указатель нового кадра sub ESP , localsize ; зарезервируйте место в стеке для местных жителей . . ; выполнить вычисления, оставить результат в EAX . мов ESP , EBP ; свободное место для местных жителей поп EBP ; восстановить указатель старого кадра ret paramsize ; свободное пространство параметров и возврат.
В 64-битной версии архитектуры x86, известной как x86-64 , AMD64 и Intel 64, обычно используются две последовательности вызовов. В Windows используется одна последовательность вызовов, определенная Microsoft; другая последовательность вызовов, указанная в AMD64 System V ABI, используется Unix-подобными системами и, с некоторыми изменениями, OpenVMS . Поскольку x86-64 имеет больше регистров общего назначения, чем 16-битный x86, оба соглашения передают некоторые аргументы в регистрах.
Стандартное 32-битное соглашение о вызовах ARM распределяет 16 регистров общего назначения следующим образом:
Если тип возвращаемого значения слишком велик, чтобы поместиться в от r0 до r3, или его размер не может быть определен статически во время компиляции, то вызывающая сторона должна выделить место для этого значения во время выполнения и передать указатель на это пространство в r0.
Подпрограммы должны сохранять содержимое от r4 до r11 и указатель стека (возможно, сохраняя их в стек в прологе функции , затем используя их как рабочее пространство, а затем восстанавливая их из стека в эпилоге функции ). В частности, подпрограммы, вызывающие другие подпрограммы, должны сохранить адрес возврата в регистре связи r14 в стеке перед вызовом этих других подпрограмм. Однако таким подпрограммам не требуется возвращать это значение в r14 — им просто нужно загрузить это значение в r15, программный счетчик, чтобы вернуться.
Соглашение о вызовах ARM требует использования полного нисходящего стека. Кроме того, указатель стека всегда должен быть выровнен по 4 байтам и всегда должен быть выровнен по 8 байтам при вызове функции с открытым интерфейсом. [1]
Это соглашение о вызовах приводит к тому, что «типичная» подпрограмма ARM:
Соглашение о вызовах 64-битного ARM ( AArch64 ) распределяет 31 регистр общего назначения следующим образом: [2]
Все регистры, начинающиеся с x , имеют соответствующий 32-битный регистр с префиксом w . Таким образом, 32-битный x0 называется w0.
Аналогично, 32 регистра с плавающей запятой распределяются следующим образом: [3]
RISC-V имеет определенное соглашение о вызовах с двумя вариантами: с плавающей запятой или без нее. [4] По возможности он передает аргументы в регистры.
Архитектуры POWER , PowerPC и Power ISA имеют большое количество регистров, поэтому большинство функций могут передавать все аргументы в регистры для одноуровневых вызовов. Дополнительные аргументы передаются в стек, и место для аргументов на основе регистров также всегда выделяется в стеке для удобства вызываемой функции в случае, если используются многоуровневые вызовы (рекурсивные или иные) и регистры необходимо сохранить. Это также полезно в функциях с переменным числом аргументов , таких как printf()
, где к аргументам функции необходимо обращаться как к массиву. Для всех процедурных языков используется единое соглашение о вызовах.
Инструкции ветвления и связи сохраняют адрес возврата в специальном регистре связи , отдельном от регистров общего назначения; процедура возвращается вызывающей стороне с инструкцией ветвления, которая использует регистр связи в качестве адреса назначения. Листовым процедурам не требуется сохранять или восстанавливать регистр связи; неконечные подпрограммы должны сохранять адрес возврата перед вызовом другой подпрограммы и восстанавливать его перед возвратом, сохраняя его с помощью команды «Переместить из регистра специального назначения», чтобы переместить регистр связи в регистр общего назначения и, при необходимости, затем сохранить его в стек и восстановить, если оно было сохранено в стеке, загрузив сохраненное значение регистра связи в регистр общего назначения, а затем используя команду «Переместить в регистр специального назначения» для перемещения регистра, содержащего сохраненную информацию. значение регистра связи в регистр связи.
ABI O32 [5] является наиболее часто используемым ABI благодаря своему статусу исходного ABI System V для MIPS. [6] Он строго основан на стеке, и для передачи аргументов доступны только четыре регистра. Эта кажущаяся медлительность, а также устаревшая модель с плавающей запятой, состоящая всего из 16 регистров, способствовали распространению многих других соглашений о вызовах. ABI сформировался в 1990 году и никогда не обновлялся с 1994 года. Он определен только для 32-битного MIPS, но GCC создал 64-битный вариант под названием O64. [7]$a0-$a3
Для 64-разрядной версии чаще всего используется N64 ABI (не связанный с Nintendo 64 ) от Silicon Graphics. Самым важным улучшением является то, что теперь для передачи аргументов доступны восемь регистров; Это также увеличивает количество регистров с плавающей запятой до 32. Существует также версия ILP32 под названием N32, которая использует 32-битные указатели для меньшего кода, аналогично x32 ABI . Оба работают в 64-битном режиме процессора. [7]
Было предпринято несколько попыток заменить O32 32-битным ABI, который больше напоминает N32. На конференции 1995 года был предложен MIPS EABI, 32-битная версия которого была очень похожа. [8] EABI вдохновила MIPS Technologies предложить более радикальный ABI «NUBI», который дополнительно повторно использует регистры аргументов для возвращаемого значения. [9] MIPS EABI поддерживается GCC, но не LLVM; ни один из них не поддерживает NUBI.
Для всех O32 и N32/N64 адрес возврата хранится в $ra
регистре. Это устанавливается автоматически с использованием инструкций JAL
(переход и соединение) или JALR
(регистр перехода и соединение). Стек растет вниз.
Архитектура SPARC , в отличие от большинства RISC- архитектур, построена на окнах регистров . В каждом окне регистров имеется 24 доступных регистра: 8 — «входящие» регистры (%i0-%i7), 8 — «локальные» регистры (%l0-%l7) и 8 — «выходящие» регистры (% о0-%о7). Регистры «in» используются для передачи аргументов вызываемой функции, а любые дополнительные аргументы необходимо помещать в стек . Однако вызываемая функция всегда выделяет пространство для обработки потенциального переполнения окна регистра, локальных переменных и (в 32-битном SPARC) возврата структуры по значению. Чтобы вызвать функцию, аргументы вызываемой функции помещаются в «выходные» регистры; когда функция вызывается, «выходные» регистры становятся «входящими» регистрами, и вызываемая функция получает доступ к аргументам в своих «входящих» регистрах. Когда вызываемая функция завершается, она помещает возвращаемое значение в первый входной регистр, который становится первым выходным регистром при возвращении вызванной функции.
ABI System V , [10] которому следуют большинство современных Unix -подобных систем, передает первые шесть аргументов во входные регистры от %i0 до %i5, резервируя %i6 для указателя кадра и %i7 для адреса возврата.
IBM System/360 — еще одна архитектура без аппаратного стека. Приведенные ниже примеры иллюстрируют соглашение о вызовах, использовавшееся в OS/360 и его преемниках до появления 64-разрядной версии z/Architecture ; другие операционные системы для System/360 могут иметь другие соглашения о вызовах.
Вызов программы:
LA 1,ARGS Загрузить адрес списка аргументов L 15,=A(SUB) Загрузка адреса подпрограммы BALR 14,15 Переход к вызываемой подпрограмме 1 ...ARGS DC A(FIRST) Адрес первого аргумента Постоянный ток А (ВТОРОЙ) ... DC A(THIRD)+X'80000000' Последний аргумент 2
Вызываемая программа:
SUB EQU * Это точка входа в подпрограмму.
Стандартная последовательность ввода:
ИСПОЛЬЗОВАНИЕ *,15 3 STM 14,12,12(13) Сохранение регистров 4 ST 13,SAVE+4 Сохранить адрес области сохранения вызывающего абонента LA 12,SAVE Цепочка сохранений СТ 12,8(13) ЛР 13,12 ...
Стандартная последовательность возврата:
Л 13,СОХРАНИТЬ+4 5 ЛМ 14,12,12(13) Л 15, РЕТВАЛЬ 6 BR 14 Возврат к абонентуСОХРАНИТЬ DS 18F Сохранить область 7
Примечания:
BALR
сохраняет адрес следующей инструкции (адрес возврата) в регистре, указанном первым аргументом (регистр 14), и переходит к адресу второго аргумента в регистре 15.STM
сохраняет регистры 14, 15 и с 0 по 12 в 72-байтовой области, предоставленной вызывающей стороной, называемой областью сохранения, на которую указывает регистр 13. Вызываемая процедура предоставляет свою собственную область сохранения для использования подпрограммами, которые она вызывает; адрес этой области обычно хранится в регистре 13 на протяжении всей процедуры. Следующие инструкции STM
обновляют прямую и обратную цепочки, связывающие эту область сохранения с областью сохранения вызывающего абонента.savearea
в вызываемой подпрограмме делает ее нереентерабельной и нерекурсивной ; повторно входящая программа использует динамический объект savearea
, полученный либо из операционной системы и освобождаемый при возврате, либо в памяти, переданной вызывающей программой.В ABI System/390 [11] и ABI z/Architecture [12] используется в Linux:
Дополнительные аргументы передаются в стек.
Примечание: «сохраненные» резервы для сохранения вызываемого абонента; то же самое касается и «гарантированного».
Наиболее распространенное соглашение о вызовах для серии Motorola 68000 : [13] [14] [15] [16]
IBM 1130 представлял собой небольшую 16-битную машину с пословной адресацией. В нем было всего шесть регистров плюс индикаторы состояния и не было стека. Регистрами являются регистр адреса инструкции (IAR) , аккумулятор (ACC) , расширение аккумулятора (EXT) и три индексных регистра X1–X3. Вызывающая программа отвечает за сохранение ACC, EXT, X1 и X2. [17] Существуют две псевдооперации для вызова подпрограмм: CALL
для кодирования неперемещаемых подпрограмм, напрямую связанных с основной программой, и LIBF
для вызова перемещаемых библиотечных подпрограмм через вектор передачи . [18] Обе псевдооперации преобразуются в машинную инструкцию перехода и сохранения IAR ( BSI
), которая сохраняет адрес следующей инструкции по ее эффективному адресу (EA) и выполняет переход к EA+1.
Аргументы следуют за BSI
—обычно это адреса аргументов, состоящие из одного слова—вызываемая подпрограмма должна знать, сколько аргументов ожидать, чтобы она могла пропустить их при возврате. Альтернативно аргументы могут передаваться в регистрах. Подпрограммы функций возвращали результат в ACC для реальных аргументов или в ячейку памяти, называемую псевдоаккумулятором действительных чисел (FAC). Аргументы и адрес возврата были адресованы с использованием смещения значения IAR, хранящегося в первом месте подпрограммы.
* 1130 пример подпрограммы ENT SUB Объявить «SUB» внешней точкой входа. SUB DC 0 Зарезервированное слово в точке входа, обычно кодируемое «DC *-*» * Код подпрограммы начинается здесь * Если были аргументы, адреса можно загрузить косвенно из адреса возврата. LDX I 1 SUB Загрузить в X1 адрес первого аргумента (например) ... * Возвратная последовательность LD RES Загрузить целочисленный результат в ACC * Если аргументы не были предоставлены, выполняется косвенный переход к сохраненному адресу возврата. BI SUB Если аргументы не были предоставлены КОНЕЦ ПОДПИСКИ
Подпрограммы в IBM 1130, CDC 6600 и PDP-8 (все три компьютера были представлены в 1965 году) хранят обратный адрес в первом месте подпрограммы. [19]
Многопоточный код возлагает всю ответственность за настройку и очистку после вызова функции на вызываемый код. Вызывающий код ничего не делает, а лишь выводит список вызываемых подпрограмм. Это помещает весь код настройки и очистки функции в одно место — пролог и эпилог функции — а не во многих местах, где функция вызывается. Это делает многопоточный код наиболее компактным соглашением о вызовах.
Поточный код передает все аргументы в стек. Все возвращаемые значения возвращаются в стек. Это делает наивные реализации медленнее, чем соглашения о вызовах, которые сохраняют больше значений в регистрах. Однако реализации многопоточного кода, которые кэшируют в регистрах несколько значений верхнего уровня стека, в частности адрес возврата, обычно работают быстрее, чем соглашения о вызове подпрограмм, которые всегда помещают и извлекают адрес возврата в стек. [20] [21] [22]
Соглашение о вызовах по умолчанию для программ, написанных на языке PL/I , передает все аргументы по ссылке , хотя при желании можно указать и другие соглашения. Аргументы обрабатываются по-разному для разных компиляторов и платформ, но обычно адреса аргументов передаются через список аргументов в памяти. Может быть передан окончательный скрытый адрес, указывающий на область, содержащую возвращаемое значение. Из-за большого разнообразия типов данных, поддерживаемых PL/I, также может быть передан дескриптор данных , чтобы определить, например, длину символьных или битовых строк, размерность и границы массивов ( дополнительные векторы ) или макет и содержимое. структуры данных . Фиктивные аргументы создаются для аргументов, которые являются константами или не соответствуют типу аргумента, ожидаемого вызываемой процедурой.
все регистры, кроме d0, d1, a0, a1 и a7, должны сохраняться во время вызова.
На 6809 или Zilog Super8 DTC работает быстрее, чем STC.
Хотя известно, что интерпретаторы с прямым потоком имеют плохие свойства прогнозирования ветвления... задержка вызова и возврата может быть больше, чем при косвенном переходе.