Windows x64调用约定中使用了哪些R10-R15寄存器?

时间:2018-11-13 23:19:18

标签: assembly x86-64 calling-convention

从英特尔在https://software.intel.com/en-us/articles/introduction-to-x64-assembly的x64组装介绍,

  • RCX,RDX,R8,R9按从左到右的顺序用于整数和指针参数。
  • RAX,RCX,RDX,R8,R9,R10和R11寄存器被认为是易失性的,必须在函数调用时视为已销毁。
  • RBX,RBP,RDI,RSI,R12,R14,R14和R15必须使用它们保存在任何功能中。

虽然我了解如何将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函数不执行此操作,是否有原因?我想象寄存器中的快速调用参数比检查,偏移堆栈的效率更高。

1 个答案:

答案 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 strcmpmemcmp函数可以返回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位,可能会很好。