Для простоты я возьму Система V ABI и ограничу это объяснение целочисленными параметрами и указателями .Для более подробного описания соглашений о вызовах для Unix см. этот пост .
Первые шесть аргументов передаются функции в регистрах rdi
, rsi
, rdx
, rcx
, r8
и r9
.Эти регистры сохраняются вызывающим абонентом , т. Е. Они могут не сохраняться между вызовами функций, так как они вызываемый могут их засорять (более подходящим термином для них будет call-Clobbered регистров).
Давайте объявим Q
как:
int Q(int, int, int, int, int, int);
То есть Q
принимает шесть целых чисел, так что сгенерированный компилятором код вынужден использовать всеэти шесть регистров, сохраненных вызывающими.
Теперь давайте определим P
следующим образом:
int P(int a, int b, int c, int d, int e, int f) {
return Q(a, b, c, d, e, f) + Q(f, e, d, c, b, a);
}
Таким образом, компилятор исчерпает регистры, сохраненные вызывающими, и это будетвынужден использовать сохраненных вызывающих регистров для кода функции выше.
Приведенный выше код генерирует следующую сборку:
P:
// save rbx, rbp, r12, r13, r14 and r15 onto the stack
// copy rdi, rsi, rdx, rcx, r8 and r9 into those registers
pushq %r15
movl %esi, %r15d
pushq %r14
movl %edx, %r14d
pushq %r13
movl %ecx, %r13d
pushq %r12
movl %r8d, %r12d
pushq %rbp
movl %r9d, %ebp
pushq %rbx
movl %edi, %ebx
subq $24, %rsp
call Q // <-- 1st call to Q (may clobber rdi, rsi, rdx, rcx, r8 and r9)
// prepare rdi, rsi, rdx, rcx, r8 and r9 for the 2nd call to Q
movl %ebx, %r9d
movl %r15d, %r8d
movl %r14d, %ecx
movl %r13d, %edx
movl %r12d, %esi
movl %ebp, %edi
movl %eax, 12(%rsp)
call Q // <-- 2nd call to Q
addl 12(%rsp), %eax
addq $24, %rsp
popq %rbx
popq %rbp
popq %r12
popq %r13
popq %r14
popq %r15
ret
Вызов Q
разрешено блокировать регистры rdi
, rsi
, rdx
, rcx
, r8
и r9
(среди прочих).Эти регистры содержат аргументы вызова P
(то есть a
, b
, c
, d
, e
и f
соответственно), и их значения все еще необходимы для второгопозвоните по номеру Q
.По этой причине регистры rbx
, rbp
, r12
, r13
, r14
и r15
используются для копирования этих регистров перед вызовом Q
, чтобы сохранить исходные аргументы P
был вызван с.
Q
не разрешено блокировать эти регистры, которые используются для сохранения регистров, сохраненных вызываемым абонентом, поскольку это регистры сохраненные вызывающим абонентом , т.е. их значениеДолжен быть сохранен между вызовами функций (другой термин для них будет сохраняемый вызов регистры).Поэтому код в функции P
сохраняет эти регистры в стек с помощью push
перед тем, как их забить, и восстанавливает их с помощью pop
из стека, прежде чем поток управления вернется из P
(P
- это абонент из Q
здесь).Чистый эффект, который увидит вызывающий P
, состоит в том, что эти регистры имеют свое первоначальное значение, то же значение, которое они имели непосредственно перед вызовом P
.
Обратите внимание, что второй вызов Q
может заглушить регистры rdi
, rsi
, rdx
, rcx
, r8
и r9
, но нет никакого интереса в сохранении их значений, так как они больше не сохраняются после Q
возвращает.
Для более глубокого понимания сбоев регистров посмотрите Какие регистры сохраняются посредством вызова функции linux x86-64 .