Почему IA-32 имеет неинтуитивное соглашение о сохранении регистра звонящего и вызываемого абонента?
Общие соглашения о вызовах для IA-32 гласят:
• Callee-save registers
%ebx, %esi, %edi, %ebp, %esp
Callee must not change these. (Or restore the caller's values before returning.)
• Caller-save registers
%eax, %edx, %ecx, condition flags
Caller saves these if it wants to preserve them. Callee can freely clobber.
Почему существует это странное соглашение? Почему бы не сохранить все регистры перед вызовом другой функции? Или попросить вызываемого сохранить и восстановить все с pusha
/popa
?
4 ответа
Зачем вам писать код для сохранения регистров в каждой функции, которая вам может не понадобиться? Это добавило бы дополнительный код и дополнительную запись в память для каждого вызова функции. Сейчас это может показаться неважным, но в 80-е годы, когда была создана эта конвенция, это, вероятно, имело значение.
И обратите внимание, что у ia-32 нет фиксированного соглашения о вызовах - то, что вы перечислите, является лишь внешним соглашением - ia-32 не обеспечивает его соблюдение. Если вы пишете свой собственный код, вы используете регистры как хотите.
Также см. Обсуждение " История вызовов" в блоге Old New Thing.
При принятии решения о том, какие регистры следует сохранить в соответствии с соглашением о вызовах, необходимо сбалансировать потребности вызывающего абонента и потребности вызываемого абонента. Вызывающая сторона предпочла бы, чтобы все регистры были сохранены, поскольку это устраняет необходимость для вызывающей стороны беспокоиться о сохранении / восстановлении значения в вызове. Вызываемый объект предпочел бы, чтобы регистры не сохранялись, поскольку это устраняет необходимость сохранять значение при входе и восстанавливать его при выходе.
Если для сохранения требуется слишком мало регистров, то вызывающие абоненты заполняются кодом сохранения / восстановления регистров. Но если вам требуется сохранить слишком много регистров, то вызываемые абоненты обязаны сохранять и восстанавливать регистры, о которых вызывающий абонент мог на самом деле не заботиться. Это особенно важно для конечных функций (функций, которые не вызывают никаких других функций).
Догадка:
Если вызывающая сторона сохраняет все регистры, в которых она все еще нуждается после вызова функции, она теряет время, когда вызываемая функция не изменяет все эти регистры.
Если вызываемый объект сохраняет все регистры, которые он изменяет, он теряет время, когда вызывающему абоненту снова не нужны значения в этих регистрах.
Когда некоторые регистры сохраняются вызывающей стороной, а некоторые - вызываемой, компилятор (или программист сборки) может выбирать, какой тип использовать, в зависимости от того, нужно ли значение после следующего вызова функции.
Если вы посмотрите немного глубже в используемые регистры, вы поймете, почему они не будут сохранены вызываемым пользователем:
EAX
: используется для возврата функции, поэтому вполне очевидно, что она не может быть сохранена.EDX:EAX
: используется для возврата 64-битной функции, так же, какEAX
,ECX
: это регистр подсчета, и еще в старые времена x86, когдаLOOPcc
было "круто", этот регистр был бы разбит как сумасшедший, даже сегодня есть еще довольно много инструкций, использующихECX
как счетчик (какREP
префикс инструкции). Однако благодаря появлению__thiscall
а также__fastcall
, он привыкает передавать аргументы, что означает, что он очень вероятно изменится, так что нет смысла его сохранять.ESP
: это небольшое побочное исключение, так как оно на самом деле не сохраняется, оно изменяется в соответствии с изменениями стека. Хотя это может быть сохранено для предотвращения повреждения / безопасности или разбаланса указателя стека благодаря встроенной сборке (через кадры стека).
Теперь это на самом деле становится интуитивно понятным:)
Короче говоря, сохранение звонящего происходит из-за передачи аргумента. Все остальное сохранено.