当谈到编译器、寄存器和函数调用时,您通常可以认为寄存器属于以下三类之一:“不干涉”、易失性和非易失性。
“放手”类别是那些编译器通常不会使用的类别,除非您明确告诉它(例如使用内联汇编)。这些可能包括调试寄存器和其他特殊用途的寄存器。该列表因平台而异。
易失性(或临时/调用破坏/调用者保存)寄存器集是函数可以在不需要保存的情况下使用的寄存器。也就是说,调用者明白在函数调用之后这些寄存器的内容可能不同。因此,如果调用者在其想要保留的那些寄存器中有任何数据,则它必须在调用之前保存该数据,然后再将其恢复。在 32 位 x86 平台上,这些易失性寄存器(有时称为暂存寄存器)通常是 EAX、ECX 和 EDX。
非易失性(或调用保留或被调用者保存)寄存器集是函数在使用它们之前必须保存并在返回之前恢复其原始值的寄存器。如果调用函数使用它们,它们只需要保存/恢复。在 32 位 x86 平台上,这些通常是剩余的通用寄存器:EBX、ESI、EDI、ESP、EBP。
希望这会有所帮助。
(我只是想添加一个小例子,但很快就被带走了。如果这个问题没有结束,我会添加我自己的答案,但我将把这个长部分留在这里,因为我认为它很有趣。如果您不想在答案中使用它,请压缩或完全编辑它——彼得)
举一个更具体的例子,SysV x86-64 ABI 是经过精心设计的(参数在寄存器中传递,调用保留与临时/参数 regs 之间取得了良好的平衡)。 x86 标签 wiki 中还有一些其他链接,解释了 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 的第二次调用中幸存,它可以使用一条指令(lea ebx, [rax+rax*4]) 存储5*foo(a+2)。此外,只需要一个调用保留寄存器,因为在第二个call 之后我们不需要a。这将删除一个 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) 之前。它在开始时保存了一条指令(因为我们可以将 arg 原封不动地传递给第一次调用 foo),但后来删除了潜在的保存(因为现在必须在第二次调用之后发生乘以 5,并且不能与移入呼叫保留寄存器结合使用)。
如果是5*foo(a) + foo(a+2),我可以去掉额外的mov。使用我写的表达式,我不能在每种情况下都将算术与数据移动(使用lea)结合起来。或者我需要在第一个 call 之前保存 a 并单独执行 add edi,2。