Когда речь идет о компиляторах, регистрах и функциональных вызовах, вы можете, как правило, думать о том, что регистры попадают в одну из трех категорий: «руки», неустойчивые и нестабильные.
Категория «руки в выключенном состоянии» - это те, которые компилятор обычно не использует, если вы явно не указали это (например, с встроенной сборкой). Они могут включать регистры отладки и другие регистры специального назначения. Список будет варьироваться от платформы к платформе.
Неустойчивый (или скремблированный/сбрасываемый/вызываемый звонком) набор регистров - это те, с которыми функция может работать без необходимости сохранения. То есть, вызывающий абонент понимает, что содержимое этих регистров может не совпадать после вызова функции. Таким образом, если у вызывающего есть какие-либо данные в тех регистрах, которые он хочет сохранить, он должен сохранить эти данные перед выполнением вызова, а затем восстановить его после. На 32-разрядной платформе x86 эти изменчивые регистры (иногда называемые регистры царапин) обычно являются EAX, ECX и EDX.
Энергонезависимый (или сохраненный или сохраненный пользователем) набор регистров - это те, которые функция должна сохранять перед использованием и восстановить их исходные значения перед возвратом.Они должны быть сохранены/восстановлены вызываемой функцией, если она их использует. На 32-разрядной платформе x86 обычно это остальные регистры общего назначения: EBX, ESI, EDI, ESP, EBP.
Надеюсь, это поможет.
(я имел в виду просто добавить небольшой пример, но быстро увлекся. Я хотел бы добавить свой собственный ответ, если этот вопрос не был закрыт, но я собираюсь покинуть этот длинный отрезок здесь, потому что я подумайте, что это интересно. Конденсируйте это или отредактируйте полностью, если вы не хотите этого в своем ответе - Питер)
Для более конкретного примера SysV x86-64 ABI хорошо спроектирован (с аргументами, переданными в регистрах, и хороший баланс по-разному - по отношению к царапинам/arg regs). Есть и другие ссылки в вики-файле x86, объясняющие, какие соглашения об ABI/вызовах существуют.
Рассмотрим простой пример с вызовами функции, которые не могут быть встраиваемыми (потому что определение не доступен):
int foo(int);
int bar(int a) {
return 5 * foo(a+2) + foo (a) ;
}
It compiles (on godbolt with gcc 5.3 for x86-64 with -O3
к следующему:
## gcc output
# AMD64 SysV ABI: first arg in e/rdi, return value in e/rax
# the call-preserved regs used are: rbp and rbx
# the scratch regs used are: rdx. (arg-passing/return regs are not call-preserved)
push rbp # save a call-preserved reg
mov ebp, edi # stash `a` in a call-preserved reg
push rbx # save another call-preserved reg
lea edi, [rdi+2] # edi=a+2 as an arg for foo. `add edi, 2` would also work, but they're both 3 bytes and little perf difference
sub rsp, 8 # align the stack to a 16B boundary (the two pushes are 8B each, and call pushes an 8B return address, so another 8B is needed)
call foo # eax=foo(a+2)
mov edi, ebp # edi=a as an arg for foo
mov ebx, eax # stash foo(a+2) in ebx
call foo # eax=foo(a)
lea edx, [rbx+rbx*4] # edx = 5*foo(a+2), using the call-preserved register
add rsp, 8 # undo the stack offset
add eax, edx # the add between the to function-call results
pop rbx # restore the call-preserved regs we saved earlier
pop rbp
ret # return value in eax
Как обычно, компиляторы могли бы сделать лучше: вместо того, чтобы спрятать foo(a+2)
в ebx
, чтобы выжить во втором звонке до foo
, он мог бы спрятать 5*foo(a+2)
с одной инструкцией (lea ebx, [rax+rax*4]
). Кроме того, необходим только один регистр с сохранением вызова, поскольку нам не нужно a
после второго call
. Это удаляет пару push/pop, а также пару sub rsp,8
/add rsp,8
. (gcc bug report already filed for this missed optimization)
## Hand-optimized implementation (still ABI-compliant):
push rbx # save a call-preserved reg; also aligns the stack
lea ebx, [rdi+2] # stash ebx=a+2
call foo # eax=foo(a)
mov edi, ebx # edi=a+2 as an arg for foo
mov ebx, eax # stash foo(a) in ebx, replacing `a+2` which we don't need anymore
call foo # eax=foo(a+2)
lea eax, [rax+rax*4] #eax=5*foo(a+2)
add eax, ebx # eax=5*foo(a+2) + foo(a)
pop rbx # restore the call-preserved regs we saved earlier
ret # return value in eax
Обратите внимание, что вызов foo(a)
происходит перед foo(a+2)
в этой версии. Он сохранил инструкцию с самого начала (так как мы можем передать наш аргумент без изменений до первого вызова foo
), но позже удалили потенциальную экономию (поскольку умножение на 5 теперь должно произойти после второго вызова и может " t быть объединенным с перемещением в регистр, сохраняемый вызовом).
Я мог бы избавиться от дополнительного mov
, если это было 5*foo(a) + foo(a+2)
. С выражением, которое я написал, я не могу комбинировать арифметику с перемещением данных (используя lea
) в каждом случае. Или мне нужно будет сэкономить a
и сделать отдельный add edi,2
перед первым call
.
Ваше понимание неверно. Нет причин подталкивать все регистры, и они, конечно же, этого не делают. Некоторые регистры являются входами, некоторые выходы и некоторые из них никогда не используются, поэтому нет причин для их сохранения. Просмотрите соглашения о вызовах, и вы увидите, как это делается. Компилятор, безусловно, знает, какие регистры он использует, и может сохранить свое состояние, если это необходимо. –
@SamiKuhmonen О, хорошо, спасибо. – bss36504
Слишком широкий. Это включает в себя CPU, API, оптимизацию компилятора, операционную систему/библиотеку, компоновщик и т. Д. Если вам требуется конкретное использование регистров, вам также может понадобиться определенная структура кода, что означает Assembler (возможно, встроенный). Также некоторый компилятор уже позволяет зарезервировать глобальные регистры, но часто показывает, что это хуже, чем принятие некоторых других инструкций сохранения/восстановления в коде. И это от MCU-view, а не (просто) больших утюгов. – Olaf