为什么Windows64在x86-64架构中使用不同的调用约定?

137

AMD有一份ABI规范,描述了在x86-64上使用的调用约定。除了Windows使用自己的x86-64调用约定外,所有操作系统都遵循它。为什么?

有人知道这种差异的技术、历史或政治原因吗?还是纯粹是因为NIH综合症?

我明白不同的操作系统可能需要更高级的东西,但这并不能解释为什么例如Windows上的寄存器参数传递顺序是rcx-rdx-r8-r9-rest on stack,而其他所有人都使用rdi-rsi-rdx-rcx-r8-r9-rest on stack

P.S. 我知道这些调用约定通常有哪些区别,如果需要我也知道在哪里可以找到详细信息。我想知道的是为什么。

编辑:如需详细了解如何,请参见例如维基百科条目和从那里链接过去的网站。


4
对于第一个寄存器,rcx:对于MSVC __thiscall x86约定,ecx是“this”参数。因此,可能只是为了方便将编译器移植到x64,他们从rcx作为第一个寄存器开始。其他所有的不同之处都是这个初始决策的结果。 - Chris Becke
1
@Chris:我已经在下面添加了对AMD64 ABI补充文档的引用(以及一些关于它的解释)。 - FrankH.
1
我还没有从微软那里找到解释,但我在这里找到了一些讨论(链接:https://groups.google.com/forum/#!msg/altdevauthors/tMZQhyeZbBk/yy34T1EL8kMJ) - phuclv
4个回答

103

选择在x64通用的四个参数寄存器 - 适用于UN*X / Win64

x86需要注意的一点是,寄存器名称到“寄存器号”编码并不明显;就指令编码而言(MOD R/M字节,参见http://www.c-jump.com/CIS77/CPU/x86/X77_0060_mod_reg_r_m_byte.htm),寄存器号0...7分别对应?AX?CX?DX?BX?SP?BP?SI?DI

因此,将A / C / D(寄存器0..2)选为返回值和前两个参数(这是“传统”32位__fastcall约定)是一个合乎逻辑的选择。就64位而言,“更高”的寄存器被排序,Microsoft和UN*X / Linux都选择了R8 / R9作为第一个寄存器。

记住这一点,Microsoft选择使用RAX(返回值)和RCXRDXR8R9(arg[0..3])是一个可以理解的选择,如果你选择四个寄存器作为参数。

我不知道为什么AMD64 UN*X ABI在RCX之前选择了RDX

选择在x64上特定于UN*X的六个参数寄存器

在RISC架构上,UN*X传递参数通常使用寄存器 - 特别地,在前六个参数上(至少在PPC、SPARC、MIPS上如此)。这可能是AMD64(UN*X)ABI设计者选择在该架构上也使用六个寄存器的主要原因之一。

因此,如果您想要使用六个寄存器来传递参数,并且选择使用RCXRDXR8R9作为其中四个寄存器,则应该选择哪两个?

“高级”寄存器需要额外的指令前缀字节来选择它们,因此指令大小会更大,因此如果有其他选择,您不会选择它们。由于RBPRSP具有隐含的含义,因此这些寄存器不可用,而RBX在UN*X上传统上具有特殊用途(全局偏移表),似乎AMD64 ABI设计者不想让它与之相冲突。
因此,唯一的选择是RSI / RDI

因此,如果您必须将RSI / RDI作为参数寄存器,那么应该选择哪些参数?

将它们设为arg[0]arg[1]具有某些优点。请参见cHao的评论。

?SI?DI 是字符串指令的源操作数和目标操作数。正如 cHao 所提到的,它们作为参数寄存器的使用意味着在 AMD64 UN*X 调用约定中,最简单的 strcpy() 函数只包含两个 CPU 指令 repz movsb; ret,因为调用者已经将源/目标地址放入正确的寄存器中。特别是在低级别和编译器生成的“粘合”代码(例如,一些 C++ 堆分配器在构造时对对象进行零填充,或内核对sbrk()的堆页面进行零填充,或写时复制页错误),存在大量的块复制/填充,因此对于经常使用的代码来说,保存本来会加载这样的源/目标地址参数到“正确”寄存器的两三个 CPU 指令将非常有用。

因此,在某种程度上,UN*X 和 Win64 只有一个区别,即 UN*X 在有意选择的RSI/RDI寄存器中添加了两个额外的参数,来代替自然选择的四个参数RCXRDXR8R9

除此之外 ...

UN*X 和 Windows x64 ABIs 之间还有更多的差异,而不仅仅是将参数映射到特定的寄存器。有关 Win64 的概述,请查看:

http://msdn.microsoft.com/en-us/library/7kcdt6fy.aspx

Win64和AMD64的UN*X在使用栈空间方面也有显著的差异;例如,在Win64上,调用者必须为函数参数分配栈空间,即使参数0…3已经通过寄存器传递了。另一方面,在UN*X上,如果叶函数(即不调用其他函数的函数)所需的栈空间不超过128字节,甚至都不需要分配栈空间(是的,您拥有并可以使用一定量的栈而无需分配它…除非您是内核代码,那就是一个麻烦的错误源)。所有这些都是特定的优化选择,其中大部分理由都在原始帖子的维基百科参考中解释。


2
关于寄存器名称:前缀字节可能是一个因素。但如果这样做,微软选择 rcx - rdx - rdi - rsi 作为参数寄存器会更合理。但前八个寄存器的数值可以指导您在从头开始设计 ABI 时,但如果已经存在完全正常的 ABI,则没有理由更改它们,这只会导致更多的混乱。 - JanKanis
3
关于 RSI/RDI:这些指令通常会被内联,因此调用约定并不重要。否则,在整个系统中只会有一个(或者可能是几个)该函数的副本,因此总共只能节省少量字节。不值得这样做。关于其他差异/调用堆栈:具体选择的实用性在 ABI 参考文献中有解释,但它们并不进行比较。它们没有讲解为什么没有选择其他优化方式——例如,为什么 Windows 没有128字节的红色区域,以及为什么 AMD ABI 没有额外的堆栈插槽来传递参数? - JanKanis
10
Win64和Win32中的__fastcall在最多有两个参数且参数大小不超过32位并返回值不超过32位的情况下,完全相同。这是一类非常广泛的函数。在i386/amd64的UNIX ABI之间不存在任何向后兼容性。 - FrankH.
2
为什么在System V ABI中先传递RDX再传递RCX?这样strcpy就不是2个指令了,而是3个(加上一个mov rcx, rdx)? - szx
3
@szx:我刚刚找到了2000年11月的相关邮件列表线程,并发布了一个总结推理的答案。请注意,可以实现这种方式的是“memcpy”,而不是“strcpy”。 - Peter Cordes
显示剩余13条评论

63

我不知道为什么Windows会做他们所做的事情。猜测请见本答案结尾。我对SysV调用约定是如何确定的很感兴趣,因此我深入研究了邮件列表档案并发现了一些有趣的东西。

阅读AMD64邮件列表上的一些旧线程非常有趣,因为AMD架构师在其中活跃。例如,选择寄存器名称是其中的难点之一:AMD考虑重命名原始的8个寄存器r0-r7,或将新寄存器称为UAX等。

此外,内核开发人员的反馈指出了原始设计中存在的使syscallswapgs无法使用的问题。这就是AMD在发布任何实际芯片之前更新指令以解决这个问题的方式。有趣的是,在2000年末,人们认为英特尔可能不会采用AMD64。

SysV(Linux)调用约定以及关于保留多少个寄存器是被调用方保存还是调用方保存的决定,最初是由gcc开发人员Jan Hubicka在2000年11月做出的。他编译了SPEC2000并查看了代码大小和指令数量。该讨论主题围绕与此SO问题中的答案和评论相同的想法展开。在第二个主题中,他提议当前序列是最优和最终的,比某些替代方案生成更小的代码

他使用术语“全局”来表示需要在使用时进行推入/弹出的保留调用寄存器。
选择rdirsirdx作为前三个参数的动机是:
  • 在调用memset或其他C字符串函数的参数时,函数中对代码大小进行了微小优化(其中gcc内联了rep字符串操作?)
  • rbx是被调用保留的寄存器,因为有两个无需REX前缀即可访问的被调用保留寄存器(rbxrbp)是一个优势。它们被选中可能是因为它们是唯一没有被任何常见指令隐式使用的“遗产”寄存器。(rep字符串、位移计数和乘/除输出/输入会涉及到其他所有寄存器)。
  • 没有寄存器被常见指令强制使用是被调用保留的(参见上一点),所以想要使用变量计数移位或除法的函数可能需要将函数参数移动到其他位置,但不必保存/恢复调用者的值。cmpxchg16bcpuid需要RBX,但很少使用,因此不是一个重要因素。(cmpxchg16b不是原始AMD64的一部分,但RBX仍然是显而易见的选择。cmpxchg8b存在,但已被qword cmpxchg淘汰)
  • 我们试图在序列中尽早避免使用RCX,因为它是常用于特殊目的的寄存器,如EAX,因此它具有与EAX相同的目的,即缺失在序列中。此外,它不能用于系统调用,我们希望使系统调用序列尽可能匹配函数调用序列。

(background: syscall / sysret不可避免地破坏了rcx(带有rip)和r11(带有RFLAGS), 所以内核无法看到在syscall运行时rcx中最初的内容。)
内核系统调用ABI被选择为与函数调用ABI相匹配,除了使用r10代替rcx,因此像mmap(2)这样的libc包装器函数只需执行mov %rcx, %r10 / mov $0x9, %eax / syscall

注意,在i386 Linux中使用的SysV调用约定与Windows 32位的__vectorcall相比较差。它将所有内容都传递到堆栈上,并且仅对int64返回edx:eax,而不是对小结构体进行返回。毫不奇怪,很少有人会花费精力来维护其兼容性。当没有理由不这样做时,他们会保留rbx的调用保存,因为他们认为在原始的8个寄存器(不需要REX前缀)中拥有另一个寄存器是好的。
使ABI最佳化长期来看比任何其他考虑因素都更重要。我认为他们做得相当不错。我不确定将结构体打包到寄存器中而不是不同寄存器的不同字段。我猜以值传递方式传递它们的代码,而不实际操作字段的代码会获胜,但解包的额外工作似乎有些愚蠢。他们本可以有更多的整数返回寄存器,不仅仅是rdx:rax,所以返回具有4个成员的结构体可以在rdi、rsi、rdx、rax等寄存器中返回它们。
他们考虑过在向量寄存器中传递整数,因为SSE2可以操作整数。幸运的是他们没有这样做。整数经常用作指针偏移量,回到堆栈内存的往返代价非常便宜。此外,SSE2指令比整数指令需要更多的代码字节。
我怀疑Windows ABI设计者可能旨在最小化32位和64位之间的差异,以便那些需要从一个平台移植汇编代码的人受益,或者可以在某些汇编代码中使用一些#ifdef,以便相同的源代码更容易地构建一个函数的32位或64位版本。
最小化工具链中的更改似乎不太可能。x86-64编译器需要一个单独的表来确定哪个寄存器用于什么,以及调用约定是什么。与32位有较小的重叠不太可能产生工具链代码大小/复杂度方面的显着节省。

2
我记得在陈纳德的博客上读到过有关从微软方面进行基准测试后选择这些寄存器的原理,但我再也找不到了。然而,关于homezone的一些原因在这里得到了解释:https://blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 https://blogs.msdn.microsoft.com/freik/2006/03/06/x64-abi-vs-x86-abi-aka-calling-conventions-for-amd64-em64t/ - phuclv
2
另一篇来自雷蒙德·陈的博客文章:为什么我们需要定义红区?我不能把我的堆栈用于任何事情吗? - phuclv
1
@phuclv: 另请参见在ESP下方编写代码是否有效?。 Raymond在我的答案上的评论指出了一些我不知道的SEH细节,这解释了为什么x86 32/64 Windows目前没有事实上的红区。他的博客文章提供了一些可能性相同的代码页面处理程序,这也是我在那个答案中提到的:)所以是的,Raymond比我做得更好地解释了它(并不奇怪因为我从很少了解Windows开始),而非x86的红区大小表真的很棒。 - Peter Cordes
@PeterCordes “可能是因为它是唯一一个没有被任何指令隐式使用的寄存器” 那么在r0-r7中,哪些寄存器没有被任何指令隐式使用呢?我认为没有,这就是为什么它们有像rax、rcx等特殊名称的原因。 - Sourav Kannantha B
@SouravKannanthaB:是的,所有的传统寄存器都有一些隐含的用途。(为什么rbp和rsp被称为通用寄存器?) 我真正想说的是,没有常见的指令需要你使用RBX或RBP,比如shl rax, clmul等。只有cmpxchg16bcpuid需要RBX,而RBP只被leave(以及无法使用的缓慢的enter指令)隐式使用。因此,对于RBP,唯一的隐含用途就是操作RBP,如果不将其用作帧指针,则不需要这样做。 - Peter Cordes

21
请记住,微软最初对AMD64的努力“官方上不予置评”(来自Matthew Kerner和Neil Padgett的"现代64位计算的历史"),因为他们与Intel在IA64架构上是强大的合作伙伴。我认为这意味着即使他们本来愿意与GCC工程师合作,在Unix和Windows上使用ABI,他们也不会这样做,因为这意味着在他们尚未正式支持AMD64的情况下公开支持该项目(并且可能会惹恼英特尔)。
除此之外,在那个年代,微软绝对没有倾向于友好地与开源项目合作。当然不是Linux或GCC。
那么为什么他们要在ABI上合作呢?我猜ABIs之所以不同,只是因为它们是在相对隔离的时间内设计的。
另一段引自“现代64位计算的历史”的引言:
与微软的合作同时,AMD还与开源社区合作准备芯片。AMD与Code Sorcery和SuSE签订了工具链工作的合同(Red Hat已经与Intel在IA64工具链端口上合作)。Russell解释说,SuSE制作了C和FORTRAN编译器,Code Sorcery制作了Pascal编译器。Weber解释说,公司还与Linux社区合作准备进行Linux移植。这一努力非常重要:它激励了微软继续投资于AMD64 Windows,还确保了Linux在发布芯片后能够使用,而Linux在当时正在成为一个重要的操作系统。
Weber甚至认为Linux的工作对于AMD64的成功是至关重要的,因为它使AMD能够在必要时制作一个端到端的系统而不需要其他公司的帮助。这种可能性确保了AMD有一个最坏情况下的生存策略,即使其他合作伙伴退出,也会让其他合作伙伴感到担忧,因为他们自己也可能会被落在后面。
这表明即使AMD也认为在微软和Unix之间合作并不是最重要的事情,而是拥有Unix/Linux支持非常重要。也许试图说服双方妥协或合作并不值得冒犯他们中的任何一方?也许AMD认为,即使建议一个共同的ABI可能会延迟或使更重要的目标——当芯片准备好时只需具备软件支持——受到干扰。这是我个人的猜测,但我认为ABI不同的主要原因是MS和Unix/Linux双方在政治上无法合作,而AMD并不认为这是一个问题。

2
很好的政治观点。我同意这不是AMD的错或责任。我指责微软选择了一个更糟糕的调用约定。如果他们的调用约定能够更好,我会有些同情,但他们必须从初始ABI改为__vectorcall,因为在堆栈上传递__m128很麻烦。对于某些向量寄存器的低128b具有调用保留语义也很奇怪(部分归咎于英特尔最初没有设计可扩展的SSE保存/恢复机制,而AVX仍然没有)。 - Peter Cordes
2
我并没有关于ABIs有多好的专业知识或了解。我只是偶尔需要知道它们是什么,以便我可以在汇编级别理解/调试。 - Michael Burr
3
一种良好的 ABI 应该尽可能地减小代码大小和指令数,并通过避免额外通过内存的往返来保持依赖链低延迟,这适用于参数或需要溢出/重新加载的本地变量。这其中存在权衡。SysV 的红区在一个地方需要多出几条指令(内核的信号处理程序调度器),但对于叶子函数而言,获得不必调整堆栈指针以获取一些临时空间的相对较大收益,这是一个显而易见的胜利,几乎没有任何负面影响。提议将其应用于 SysV 后,几乎没有任何讨论,就被采纳了。 - Peter Cordes
2
@dgnuff:没错,这就是为什么内核代码不能使用红区的答案(https://dev59.com/9l8e5IYBdhLWcg3wwMhW)。中断使用内核堆栈,而不是用户空间堆栈,即使它们在CPU运行用户空间代码时到达。内核不信任用户空间堆栈,因为同一用户空间进程中的另一个线程可能会修改它,从而接管内核的控制! - Peter Cordes
2
@DavidA.Gray:是的,ABI并没有规定必须使用RBP作为帧指针,因此优化后的代码通常不会使用它(除了在使用alloca或其他几种情况下)。如果你习惯于在Linux上使用gcc -fomit-frame-pointer,那么这是正常的。ABI定义了堆栈展开元数据,使异常处理仍然可以工作。(我假设它的工作方式类似于GNU/Linux x86-64 System V的.eh_frame中的CFI内容)。自从x86-64问世以来,gcc -fomit-frame-pointer一直是启用优化时的默认设置,其他编译器(如MSVC)也是如此。 - Peter Cordes
显示剩余3条评论

14

Win32有自己使用ESI和EDI寄存器的方法,要求不修改它们(或者至少在调用API之前恢复)。我想64位代码也会对RSI和RDI做同样的要求,这就解释了为什么它们不用于传递函数参数。

但我无法告诉你为什么RCX和RDX被交换了。


2
@cHao:你在谈论x86寄存器。要在64位模式下运行代码,你需要重新编译,32位模式的代码无法在64位(长模式)中运行。64位和32位之间唯一有用的兼容方式是源代码级别的兼容性和在64位内核上运行32位用户程序的可能性。Linux等还在32位模式下为某些寄存器使用特定的用途,但对于它们来说,AMD ABI显然不是问题。 - JanKanis
2
@Somejan:其实它们的差别并没有你想象的那么大。大部分操作都是相同的,寄存器也一样(加上一些额外的),二进制兼容性也能达到32位或64位模式下等效且有效代码的相同字节表示(需要注意一些细节)。即使你忽略这些,仍然存在一个事实:调用约定是由编程语言、操作系统和/或编译器定义的。如果AMD将任何东西定义为“唯一正确的方法”(因为实际上并不存在这种方法),那么它就在越权了。微软有一种方式已经运行良好20多年了。因为它没有得到AMD的认可而宣布它无效,这是极度无知的表现。 - cHao
6
AMD64 UN*X ABI 一直都是一个UNIX特定的部分。文档http://www.x86-64.org/documentation/abi.pdf是有原因的被命名为“System V Application Binary Interface,AMD64 Architecture Processor Supplement”。(通用的)UNIX ABIs(一个多卷集合,http://www.sco.com/developers/devspecs/)留下了一个处理器特定第三章的部分-即“ 补充材料” - 这些是特定处理器的函数调用约定和数据布局规则。 - FrankH.
8
@Somejan说,微软的Windows从未试图特别接近UN*X,在将Windows移植到x64/AMD64上时,他们选择扩展了他们自己的__fastcall调用约定。你声称Win32/Win64不兼容,但是请仔细看:对于一个需要两个32位参数并返回32位结果的函数,Win64和Win32的__fastcall实际上是100%兼容的(传递两个32位参数的相同寄存器,返回值相同)。甚至某些二进制代码在两种操作模式下也可以工作。UNIX方面完全抛弃了“旧方法”。出于好的原因,但是一旦中断就是中断。 - FrankH.
3
@Olof:这不仅仅是编译器的问题。当我使用NASM独立编写代码时,我遇到了ESI和EDI的问题。Windows系统确实很在意这些寄存器。但是,如果你在使用它们之前保存并在Windows需要它们之前还原它们,你就可以使用它们。 - cHao
显示剩余10条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接