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

dee*_*nkt 2 assembly x86-64 calling-convention

来自 Intel 对 x64 汇编的介绍https://software.intel.com/en-us/articles/introduction-to-x64-assemble

  • 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
Run Code Online (Sandbox Code Playgroud)

这是 IDA 的摘录,您可以看到 ShellExecute 的 nShowCmd 和 lpDirectory 参数位于堆栈上。为什么我们不能使用 R9 之后的额外寄存器来实现快速调用行为?

或者,如果我们可以在用户定义的函数中做到这一点,而系统 API 函数却不能做到这一点,那么有什么原因吗?我想寄存器中的快速调用参数比检查、偏移堆栈更有效。

Pet*_*des 6

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

鉴于这种设计限制,更多的寄存器参数将需要更大的影子空间,这对于没有大量参数的小函数浪费了更多的堆栈空间。

是的,更多的寄存器参数通常会更有效。但是,如果被调用者想要立即使用不同的参数进行另一个函数调用,则必须将其所有寄存器参数存储到堆栈或复制到它保存的调用保留寄存器,因此必须注册获得的参数有缺点当你有更多的时候,情况会更糟。

您需要调用保留寄存器和调用破坏寄存器的良好组合,无论有多少用于 arg-passing。R10 和 R11 是调用破坏的暂存寄存器。用 asm 编写的透明包装函数可以将它们用于暂存空间,而不会干扰 RCX、RDX、R8、R9 中的任何参数,并且不需要在任何地方保存/恢复调用保留的寄存器。

R12..R15 是调用保留寄存器,您可以将其用于任何您想要的用途,只要您在返回之前恢复它们即可。与 RSI、RDI、RBX 和 RBP 相同。


或者如果我们可以在用户定义的函数中做到这一点

是的,当从 asm 调用到 asm 时,您可以自由地制定自己的调用约定,但要受到操作系统的限制。但是,如果您希望异常能够通过此类调用展开堆栈(例如,如果某个子函数回调到某个可能抛出异常的 C++),则必须遵循更多限制,例如创建展开元数据。如果没有,您几乎可以做任何事情。

请参阅我的选择您的调用约定,将参数放在您想要的位置。回答 CodeGolf 问答“x86/x64 机器代码打高尔夫球的技巧”。

您还可以返回所需的任何寄存器,并返回多个值。(例如,asmstrcmpmemcmp函数可以返回 EAX 中不匹配的 -/0/+ 差异,返回 RDI 中的不匹配位置,因此调用者可以使用其中之一或两者。)


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

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

因此,其他主要的 x86-64 调用约定确实使用了更多的参数传递寄存器。它不使用阴影空间;它定义了一个低于 RSP 的红色区域,可以安全地避免被异步破坏。小叶子函数仍然可以避免操纵 RSP 来预留空间。

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

另请参阅为什么 Windows64 使用与 x86-64 上的所有其他操作系统不同的调用约定?了解有关 Microsoft 为什么在调用约定中做出设计选择的更多猜测和信息。

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

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

不过,x86-64 System V 的弱点之一是没有调用保留的 XMM 寄存器。因此任何函数调用都需要溢出/重新加载任何 FP 变量。拥有一对,例如 xmm6 和 xmm7 的低 128 或 64 位,可能会很好。

  • “是的,在 asm 之间调用时,您可以自由地制定自己的调用约定。” 在限制之内。Windows 平台确实施加了一些限制以支持异步异常。例如,您不能在红色区域之外使用低于“esp”的内存。(当然,如果您正在打代码,您可能不关心您的程序在面对异步异常时行为不正常。但如果您正在编写生产代码,则需要了解平台要求。) (4认同)