从英特尔在https://software.intel.com/en-us/articles/introduction-to-x64-assembly的x64组装介绍,
虽然我了解如何将RCX,RDX,R8,R9用作函数参数,但我看到采用4个以上参数的函数会还原为使用堆栈(如32位代码)。下面是一个示例:
sub_18000BF10 proc near
lpDirectory = qword ptr -638h
nShowCmd = dword ptr -630h
Parameters = word ptr -628h
sub rsp, 658h
mov r9, rcx
mov r8, rdx
lea rdx, someCommand ; "echo "Hello""...
lea rcx, [rsp+658h+Parameters] ; LPWSTR
call cs:wsprintfW
xor r11d, r11d
lea r9, [rsp+658h+Parameters] ; lpParameters
mov [rsp+658h+nShowCmd], r11d ; nShowCmd
lea r8, aCmdExe ; "cmd.exe"
lea rdx, Operation ; "open"
xor ecx, ecx ; hwnd
mov [rsp+658h+lpDirectory], r11 ; lpDirectory
call cs:ShellExecuteW
mov eax, 1
add rsp, 658h
retn
sub_18000BF10 endp
这是IDA的摘录,您可以看到ShellExecute的nShowCmd和lpDirectory参数在堆栈中。 为什么我们不能在R9之后使用额外的寄存器来实现快速呼叫行为?
或者如果我们可以在用户定义的函数中执行此操作,而系统API函数不执行此操作,是否有原因?我想象寄存器中的快速调用参数比检查,偏移堆栈的效率更高。
答案 0 :(得分:2)
Windows x64调用约定旨在通过将4个寄存器args转储到影子空间中,创建所有arg的连续数组,来轻松实现可变参数函数(如printf和scanf)。大于8个字节的args通过引用传递,因此每个arg始终恰好占用1个arg传递插槽。
鉴于此设计约束,更多的寄存器arg将需要较大的影子空间,这将浪费更多的堆栈空间用于没有大量arg的小型函数。
是的,更多的寄存器参数通常会更有效。但是,如果被调用方想立即使用不同的args进行另一个函数调用,则必须将其所有寄存器args存储到堆栈中,因此对多少个寄存器args有用有一个限制。
您想要混合使用保留呼叫和保留呼叫的寄存器,而不管用于arg传递的有多少。 R10和R11是呼叫密集型暂存器。用asm编写的透明包装函数可以将它们用于暂存空间,而不会干扰RCX,RDX,R8,R9中的任何args,并且无需在任何地方保存/恢复保留调用的寄存器。
R12..R15是保留呼叫的寄存器,您可以将其用于任何需要的寄存器,只要在返回之前保存/恢复它们即可。
或者如果我们可以在用户定义的函数中做到这一点
是的,在由OS调用asm时,您可以自由地制定自己的调用约定,但要遵守操作系统的限制。但是,如果您希望异常能够通过这样的调用 来展开堆栈(例如,如果其中一个子函数调用返回可以抛出的某些C ++),则必须遵循更多的限制,例如创建展开元数据。如果没有,您几乎可以做任何事情。
在CodeGolf问答“在x86 / x64机器代码中打高尔夫球的技巧”中查看我的Choose your calling convention to put args where you want them.答案。
您还可以返回所需的任何寄存器,并返回多个值。 (例如,asm strcmp
或memcmp
函数可以返回EAX中不匹配的-/ 0 / +差异,和返回RDI中的不匹配位置,因此调用者可以两者之一使用。)
通过比较,x86-64系统V ABI将寄存器中的前6个整数args传递给XMM0..7中的和前8个FP args 。 (Windows x64在堆栈上传递第5个arg,即使它是FP并且前4个arg都是整数。)
因此其他主要的x86-64调用约定确实使用了更多的arg传递寄存器。它不使用阴影空间;它在RSP下定义了一个红色区域,可以防止被异步破坏。小叶子函数仍然可以避免操纵RSP来保留空间。
有趣的事实:R10和R11还是x86-64 SysV中的非arg-pass调用对象寄存器。有趣的事实2:syscall
破坏了R11(和RCX),因此Linux使用R10而不是RCX来将参数传递给系统调用,但在其他方面则使用与用户空间函数调用相同的register-arg传递约定。 >
另请参阅Why does Windows64 use a different calling convention from all other OSes on x86-64?,以获取有关Microsoft为什么要根据调用约定做出设计选择的更多猜测和信息。
x86-64 System V使实现可变参数函数(为args索引的代码更多)变得更加复杂,但是它们通常很少见。大多数代码不会限制sscanf
的吞吐量。阴影空间通常比红色区域差。原始的Windows x64约定不按值传递向量args(__m128
),因此Windows上有一个名为vectorcall
的第二个64位调用约定,它允许有效的向量args。 (通常没什么大不了的,因为大多数使用向量args的函数都是内联的,但是SIMD数学库函数会有所帮助。)
在低8位中传递更多的args(不需要REX前缀的rax..rdi原始寄存器),并且有更多不需要REX前缀的调用密集寄存器,可能对代码有利-内联足以不进行大量函数调用的代码大小。您可以说Window选择让更多的非REX寄存器保持调用状态更适合带有包含函数调用的循环的代码,但是如果您要对短调用者进行很多函数调用,那么它们会从中受益不需要REX前缀的呼叫密集型暂存寄存器。我想知道MS对此有多大的想法,或者在选择低8位寄存器中的哪个将保留呼叫时,它们是否只是保持类似于32位调用约定的东西。
不过,x86-64 System V的弱点之一是没有调用保留的XMM寄存器。因此,任何函数调用都需要溢出/重新加载任何FP变量。拥有一对,例如xmm6和xmm7的低128位或64位,可能会很好。