关于汇编:Windows x64调用约定中使用的R10-R15寄存器是什么?

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

从英特尔介绍的x64汇编到https://software.intel.com/en-us/articles/introduction-to-x64-assembly,

  • 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位代码这样的堆栈。一个例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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个寄存器args转储到阴影空间中来轻松实现可变参数函数(如printf和scanf),从而创建所有args的连续数组。大于8字节的Args通过引用传递,因此每个arg总是采用1个arg传递槽。

鉴于这种设计约束,更多的寄存器args需要更大的阴影空间,这会为没有大量args的小函数浪费更多的堆栈空间。

是的,更多寄存器args通常会更有效率。但是如果被调用者想要使用不同的args立即调用另一个函数,则必须将其所有寄存器args存储到堆栈中,因此对有多少寄存器args有限制。

无论有多少用于arg传递,你都希望混合使用call-preserved和call-clobbered寄存器。 R10和R11是呼叫破坏的临时注册。用asm编写的透明包装函数可以将它们用于临时空间,而不会干扰RCX,RDX,R8,R9中的任何args,也无需在任何地方保存/恢复调用保留寄存器。

R12..R15是调用保留的寄存器,只要您在返回之前保存/恢复,就可以使用它们。

Or if we can do that in user-defined functions

Ok.

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

请参阅我的选择您的调用约定,将args放在您想要的位置。回答CodeGolf Q&A"x86 / x64机器码打高尔夫球的提示"。

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

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

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

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

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

另请参见为什么Windows64在x86-64上使用来自所有其他操作系统的不同调用约定?更多的猜测和信息,为什么微软做出了他们的调用惯例做出的设计选择。

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

在低8(rax..rdi原始寄存器,不需要REX前缀)中传递更多args,并且具有更多不需要REX前缀的调用寄存器寄存器,可能对代码中的代码大小有利内联足以不进行大量的函数调用。你可以说Window有更多非REX寄存器的选择被调用保存对于包含函数调用的循环的代码更好,但是如果你要对短callees进行大量的函数调用,那么他们会受益于更多call-clobbered临时寄存器,不需要REX前缀。我想知道MS在这方面有多少想法,或者当他们选择哪个低8位寄存器进行调用保存时,他们只是保持类似于32位调用约定的东西。

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

好。