Почему 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?


person Bruce    schedule 01.12.2011    source источник
comment
Какая часть является «неинтуитивной»? Конкретный выбор того, какие регистры следует сохранять вызываемому, а какой вызывающему, или тот факт, что существуют разные регистры сохранения вызываемого и вызывающего абонентов?   -  person Gian    schedule 01.12.2011


Ответы (4)


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

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

См. также обсуждение History of Calling Conventions по адресу Блог Старой Новой Вещи.

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

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

person shf301    schedule 01.12.2011
comment
Я хотел бы знать, применим ли этот ответ в равной степени к функциональному коду с большим количеством хвостовых вызовов. Я думаю, что caller-preserve-all и не нужно ничего сохранять в случае хвостового вызова. - person Rich Remer; 17.07.2014
comment
@RichRemer: Как правило, в функциональном коде не так много вызовов не хвостовых? Те по-прежнему выигрывают от некоторых регистров, сохраняемых вызовами. И это не отменяет оптимизации хвостового вызова для перехода вместо call/ret: просто восстановите регистры, сохраненные вызываемым пользователем, перед переходом к функции tailcall. При входе в функцию, к которой вы перешли, регистры, сохраненные вызываемым пользователем, будут иметь значения, которые они должны иметь, когда она в конечном итоге вернется. Вы правы в том, что если большинство вызовов являются хвостовыми вызовами, сохранение/восстановление регистров в каждой функции в основном является напрасной работой. - person Peter Cordes; 06.02.2016

Догадка:

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

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

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

person Baffe Boyois    schedule 01.12.2011
comment
Это верно. Обратите внимание, что иногда вызывающим абонентам нужно только восстановить, потому что значение в регистре уже было в памяти, и они просто кэшировали его в регистре. Наличие, возможно, еще одного регистра с затиранием вызовов может привести к улучшению кода, по крайней мере, для современных компиляторов, которые встраивают очень маленькие функции. (К сожалению, ABI был разработан задолго до современных компиляторов.) OTOH, многие библиотечные функции маленькие, но не встраиваются. Код сохранения/восстановления необходим на каждом сайте вызова, и многие функции имеют несколько вызывающих объектов, поэтому иметь код сохранения/восстановления один раз в вызываемом объекте — это выигрыш. - person Peter Cordes; 06.02.2016

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

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

Теперь это действительно становится интуитивно понятным :)

person Necrolis    schedule 01.12.2011
comment
Почему вызываемый объект должен сохранять %ebx, %esi, %edi? Есть ли этому интуитивное объяснение? - person Bruce; 01.12.2011
comment
@Bruce: чтобы его можно было вызвать чем угодно, то есть: вызывающему абоненту не нужно ничего знать о внутренних компонентах вызываемого, поскольку данные вызывающего абонента будут сохранены (ESI и EDI - это регистры src/dest данных, поэтому это сохраняется строка с отделением данных вызывающего абонента от вызываемого). - person Necrolis; 01.12.2011
comment
Поскольку это ваши базовые регистры и регистры смещения, они используются в цикле по строкам и т. д. Скорее всего, они сохраняются вызываемым пользователем, поэтому вызовы могут выполняться внутри этих циклов без необходимости каждый раз вталкивать/выталкивать их. - person rich remer; 07.07.2014

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

person Brian Knoblauch    schedule 01.12.2011
comment
Это неправильно. Даже соглашения о вызовах с чистыми аргументами стека, такие как соглашение SysV, используемое в 32-битном Linux, рассматривают eax, ecx и edx как затирание вызовов. edx:eax используется для возврата 64-битных целых чисел, но ecx является вспомогательным регистром, потому что полезно иметь приличное количество регистров. Однако наличие еще одного или двух регистров с затиранием вызовов, которые можно свободно использовать в качестве временных регистров, вероятно, приведет к улучшению кода, по крайней мере, для современных компиляторов, которые встраивают очень маленькие функции. (К сожалению, ABI был разработан задолго до современных компиляторов.) - person Peter Cordes; 06.02.2016
comment
На самом деле, даже при встраивании большая часть кода по-прежнему вызывает вызовы функций из внутренних циклов с большим количеством живого состояния, поэтому, вероятно, только три временных регистра — хороший выбор для 32-битной x86, чтобы уменьшить выбросы / перезагрузки в вызывающей программе. push и pop дешевы и малы (каждый по одному байту). Но этот ответ по-прежнему неверен ›.‹ - person Peter Cordes; 31.05.2016