Почему у IA-32 есть неинтуитивное соглашение об удержании звонков и звонков?

Общие соглашения о вызовах для IA-32 говорят:

• Callee-save registers
%ebx, %esi, %edi, %ebp, %esp
Callee must not change these.  (Or restore the caller 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?

Ответ 1

Зачем вам писать код для сохранения регистров в каждой функции, которая вам может не понадобиться? Это добавит дополнительный код и дополнительную запись в память для каждого вызова функции. Сейчас это может показаться неважным, но еще в 80 году, когда было создано это соглашение, это, вероятно, имело значение.

И обратите внимание, что ia-32 не имеет соглашения о фиксированном вызове - то, что вы указываете, является только внешним соглашением - ia-32 не применяет его. Если вы пишете свой собственный код, вы используете регистры, как хотите.

Также см. обсуждение История соглашений о вызовах в блоге Old New Thing.

При определении того, какие регистры должны быть сохранены вызовом конвенции, вам необходимо сбалансировать потребности вызывающего потребностей вызываемого лица. Абонент предпочел бы, чтобы все регистры были поскольку это устраняет необходимость того, чтобы вызывающий беспокоился о сохранение/восстановление значения во время разговора. Вызов предпочел бы, чтобы регистры не сохраняются, поскольку это устраняет необходимость сохранения значение при входе и восстановить его при выходе.

Если вам требуется слишком мало регистров для сохранения, то вызывающие заполненный кодом сохранения/восстановления регистра. Но если вам нужно слишком много регистры должны быть сохранены, тогда призывы становятся обязательными для сохранения и восстановить регистры, о которых вызывающий абонент не мог действительно заботиться. Это особенно важно для функций листа (функции, которые делают не вызывать никаких других функций).

Ответ 2

Догадка:

Если вызывающий абонент сохраняет все регистры, он по-прежнему понадобится после вызова функции, он теряет время, когда вызываемая функция не изменяет все эти регистры.

Если вызываемый абонент сохраняет все регистры, он меняет время, когда вызывающему абоненту не нужны значения в этих регистрах.

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

Ответ 3

Если вы посмотрите немного глубже на регистры, вы можете понять, почему они не будут сохранены вызываемым пользователем:

  • EAX: используется для возврата функции, поэтому вполне очевидно, что ее нельзя сохранить.
  • EDX:EAX: используется для 64-битных возвратов функций, таких же, как EAX.
  • ECX: это регистр счетчика, а в предыдущие дни x86, когда LOOPcc был "крутым", этот регистр будет разбит как безумный, даже сегодня есть еще несколько инструкций, используя ECX как счетчик (например, префиксы REP). Однако, благодаря появлению __thiscall и __fastcall, он привык передавать аргументы, что означает, что он очень сильно изменится, поэтому почти нет смысла его сохранять.
  • ESP: это небольшое побочное исключение, так как оно не сохранилось, его изменение в соответствии с изменениями стека. Хотя он может быть сохранен, чтобы предотвратить повреждение/безопасность указателя стека или несбалансирование благодаря встроенной сборке (через фреймы стека).

Теперь это фактически становится интуитивным:)

Ответ 4

Короче говоря, сохранение вызывающего абонента связано с передачей аргументов. Все остальное - спасение.