Windows x64 调用约定中的 R10-R15 寄存器有什么用?

What are R10-R15 registers used for in the Windows x64 calling convention?

来自英特尔在 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 函数不这样做,有什么理由吗?我想寄存器中的快速调用参数会比检查、偏移堆栈更有效。

Windows x64 调用约定旨在通过将 4 个寄存器参数转储到影子 space 中来轻松实现可变参数函数(如 printf 和 scanf),创建一个连续数组所有参数。大于 8 字节的参数通过引用传递,因此每个参数总是恰好占用 1 个参数传递槽。

鉴于此设计限制,更多的寄存器 args 需要更大的影子 space,这会浪费更多的堆栈 space 对于没有很多 args 的小函数.

是的,更多的寄存器参数通常会更有效。但是,如果被调用方想立即使用不同的 args 进行另一个函数调用,则它必须将其所有寄存器 args 存储到堆栈,因此有多少寄存器 args 是有用的。

您需要保留调用和破坏调用的寄存器的良好组合,而不管有多少用于 arg 传递。 R10 和 R11 是 call-clobbered scratch regs。用 asm 编写的透明包装函数可以将它们用于临时 space 而不会干扰 RCX、RDX、R8、R9 中的任何参数,并且不需要 save/restore 任何地方的调用保留寄存器。

R12..R15 是调用保留寄存器,您可以随意使用它们,只要在 returning.

之前 save/restore 它们

Or if we can do that in user-defined functions

是的,在从 asm 调用到 asm 时,您可以自由制定自己的调用约定,但要遵守 OS 施加的限制。但是,如果您希望异常能够通过 通过 这样的调用展开堆栈(例如,如果其中一个子函数回调到可以抛出的某些 C++ 中),则必须遵循更多限制,例如创建展开元数据。如果没有,您几乎可以做任何事情。

查看我在 CodeGolf 问答 Choose your calling convention to put args where you want them. 上的回答 "Tips for golfing in x86/x64 machine code"。

您还可以 return 在任何您想要的寄存器中,并且 return 多个值。 (例如,asm strcmpmemcmp 函数可以 return EAX 中不匹配的 -/0/+ 差异, return RDI 中的不匹配位置,因此调用者可以使用其中一个或两个。)


评估设计的一个有用练习是将其与其他实际或可能的设计进行比较

相比之下,x86-64 System V ABI 在寄存器中传递前 6 个整数参数, 在 XMM0..7 中传递前 8 个 FP 参数。 (Windows x64 传递堆栈上的第 5 个参数,即使它是 FP 并且前 4 个参数都是整数。)

所以其他主要的 x86-64 调用约定确实使用了更多的参数传递寄存器。它不使用 shadow-space;它在 RSP 下面定义了一个红色区域,可以安全地避免被异步破坏。小叶函数仍然可以避免操纵RSP保留space.

有趣的事实:R10 和 R11 也是 x86-64 SysV 中的非参数传递调用破坏寄存器。有趣的事实 #2:syscall 破坏了 R11(和 RCX),因此 Linux 使用 R10 而不是 RCX 将参数传递给系统调用,但在其他方面使用与 user-[= 相同的寄存器参数传递约定79=] 函数调用。

另请参阅 Why does Windows64 use a different calling convention from all other OSes on x86-64?,了解更多关于 Microsoft 做出调用约定设计选择的原因的猜测和信息。

x86-64 System V 使可变参数函数的实现变得更加复杂(更多代码来索引 args),但它们通常很少见。大多数代码不会对 sscanf 吞吐量造成瓶颈。阴影 space 通常比红区差。原始的 Windows x64 约定不会按值传递向量参数 (__m128),因此在 Windows 上有一个名为 vectorcall 的第二个 64 位调用约定,它允许高效的向量参数。 (通常没什么大不了的,因为大多数采用矢量参数的函数都是内联的,但 SIMD 数学库函数会受益。)

在低位 8 中传递更多的参数(不需要 REX 前缀的 rax..rdi 原始寄存器),并且有更多不需要 REX 前缀的调用破坏寄存器,可能有利于代码中的代码大小足以内联到不会进行大量函数调用。您可以说 Window 选择保留更多非 REX 寄存器的调用对于包含函数调用的循环的代码更好,但是如果您对短被调用者进行大量函数调用,那么他们将从更多不需要 REX 前缀的调用破坏的临时寄存器中受益。我想知道 MS 对此投入了多少心思,或者他们在选择要保留调用的低 8 位寄存器时是否主要保留类似于 32 位调用约定的内容。

不过,x86-64 System V 的一个弱点是没有调用保留的 XMM 寄存器。所以任何函数调用都需要 spilling/reloading 任何 FP 变量。有一对,比如 xmm6 和 xmm7 的低 128 位或 64 位,可能会很好。