为什么Windows64使用与x86-64上所有其他操作系统不同的调用约定?


110

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

有谁知道这种差异的技术,历史或政治原因,还是纯粹是NIHsyndrome问题?

我知道不同的操作系统可能对更高层次的东西有不同的需求,但这并不能解释为什么例如Windows上的register参数传递顺序却是rcx - rdx - r8 - r9 - rest on stack其他所有人都使用的原因rdi - rsi - rdx - rcx - r8 - r9 - rest on stack

PS:我知道这些调用约定通常有何不同,并且我知道在需要的地方可以找到详细信息。我想知道的是为什么

编辑:有关操作方法,请参见Wikipedia条目及其链接。


2
好吧,仅对于第一个寄存器:rcx:ecx是msvc __thiscall x86约定的“ this”参数。因此,可能只是为了简化将编译器移植到x64的工作,他们首先以rcx开始。那时其他一切也将不同,这仅仅是该最初决定的结果。
克里斯·贝克

@Chris:我在下面添加了对AMD64 ABI补充文件的引用(以及一些实际上的解释)。
FrankH。

1
我还没有从MS那里找到理由,但是我在这里
phuclv 2014年

Answers:


81

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

关于x86,要记住的一件事是“ reg number”编码的寄存器名称并不明显。就指令编码(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为首位。

牢记这一点,微软的选择RAX(返回值)和RCXRDXR8R9(ARG [0..3])是,如果你选择一个可以理解的选择4级为参数的寄存器。

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

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

在RISC架构上,UN * X传统上是在寄存器中传递参数-特别是对于前六个参数(至少在PPC,SPARC和MIPS中是如此)。这可能是AMD64(UN * X)ABI设计人员选择在该体系结构上也使用六个寄存器的主要原因之一。

所以,如果你想6个寄存器来传递参数,这是合乎逻辑的选择RCXRDXR8R9为他们四个,你应该选择哪两个其他?

“较高”的reg需要一个额外的指令前缀字节来选择它们,因此具有较大的指令大小占用空间,因此,如果您有选择的话,您就不想选择其中的任何一个。经典的寄存器,由于中隐含的意义RBPRSP这些都没有用,而且RBX传统上对联合国* 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指令的代码很有用,否则该代码会将此类源/目标地址参数加载到“正确的”寄存器。

因此,在某种程度上,联合国* X和Win64中是只有在UN * X“预规划”两个额外的参数,在特意挑选不同RSI/ RDI寄存器,为四个参数中的自然选择RCXRDXR8R9

除此之外 ...

UN * X和Windows x64 ABI之间的区别不只是参数到特定寄存器的映射。有关Win64的概述,请检查:

http://msdn.microsoft.com/zh-CN/library/7kcdt6fy.aspx

Win64和AMD64 UN * X在使用堆栈空间的方式上也有显着不同。例如,在Win64上,即使args 0 ... 3在寄存器中传递,调用方也必须为函数参数分配堆栈空间。另一方面,在UN * X上,如果叶函数(不调用其他函数)所需要的空间不超过128字节,则根本不需要分配叶空间(是的,您拥有并可以使用一定数量的堆栈而没有分配它...好吧,除非您是内核代码,否则就是一些令人讨厌的错误的来源)。所有这些都是特殊的优化选择,原始海报的Wikipedia参考所指向的完整ABI参考中对这些选择的大多数理由进行了解释。


1
关于寄存器名称:前缀字节可能是一个因素。但是,对于MS而言,选择rcx-rdx-rdi-rsi作为参数寄存器会更合乎逻辑。但是,如果您是从头开始设计ABI,则前八位的数值可以为您提供指导,但是如果已经存在完美的ABI,则没有理由更改它们,这只会导致更多的混乱。
JanKanis 2010年

2
在RSI / RDI上:通常将内联这些指令,在这种情况下,调用约定无关紧要。否则,该功能在系统范围内只有一个副本(或可能只有几个副本),因此总共只保存了一小部分字节。不值得。关于其他差异/调用堆栈:ABI参考资料中说明了特定选择的有用性,但没有进行比较。他们没有说为什么不选择其他优化方法-例如,为什么Windows没有128字节的红色区域,为什么AMD ABI没有额外的堆栈插槽供参数使用?
JanKanis 2010年

1
@cHao:不。但是他们还是改变了它。Win64 ABI与Win32 ABI不同(且不兼容),也与AMD ABI不同。
JanKanis 2010年

7
@Somejan:__fastcall对于不超过两个不大于32bit的参数并返回不大于32bit的值,Win64和Win32 是100%相同的。那不是一小类函数。在用于i386 / amd64的UN * X ABI之间根本没有这样的向后兼容性。
FrankH。

2
@szx:我刚刚找到了2000年11月以来的相关邮件列表线程,并发布了一个总结推理的答案。请注意,memcpy可以用这种方式实现,而不是strcpy
彼得·科德斯

42

IDK为什么Windows会做他们所做的事情。请参阅此答案的结尾进行猜测。我对如何确定SysV调用约定感到好奇,因此我钻研了邮件列表存档并找到了一些整洁的东西。

阅读AMD64邮件列表中的一些旧线程很有趣,因为AMD架构师一直很活跃。例如,选择寄存器名称是困难的部分之一:AMD考虑重命名原始的8个寄存器r0-r7,或者调用新的寄存器,例如UAX

另外,从内核开发者的反馈是由原始设计标识的事物syscallswapgs不可用。这就是AMD 在发布任何实际芯片之前更新指令以解决该问题的方式。有趣的是,在2000年末,人们以为英特尔可能不会采用AMD64。


SysV(Linux)调用约定以及关于应保留多少个寄存器的调用者保留与调用者保存的决定,最初是由Jan Hubicka(gcc开发人员)于2000年11月做出的。他编译了SPEC2000,并研究了代码大小和指令数量。该讨论线索围绕着与该SO问题的答案和评论相同的一些想法。在第二个线程中,他提出了当前序列为最佳序列,并希望是最终序列,从而生成了比某些替代方案更小的代码

他使用“全局”一词来表示保留呼叫的寄存器,如果使用则必须推送/弹出。

的选择rdirsirdx作为第三个参数的动机是:

  • 在调用的函数memset或其他arg函数上的其他C字符串函数中节省较小的代码大小(gcc内嵌rep字符串操作?)
  • rbx是保留呼叫的,因为可以访问两个没有REX前缀(rbx和rbp)的保留呼叫的regs是一个胜利。之所以选择它,是因为它是唯一一条未被任何指令隐式使用的其他reg。(代表字符串,移位计数和mul / div输出/输入涉及其他所有内容)。
  • 具有特殊用途的寄存器都不是保留调用的(请参见上一节),因此想要使用rep字符串指令或变量计数移位的函数可以将函数args移到其他地方,但不必保存/恢复呼叫者的价值。
  • 我们试图在序列的早期避免RCX,因为它通常用于特殊目的的寄存器,例如EAX,因此在序列中丢失它具有相同的目的。而且它不能用于系统调用,我们希望使系统调用序列尽可能地与函数调用序列匹配。

    (背景:syscall/ sysret不可避免地破坏了rcx(with rip)和r11(with RFLAGS),因此内核看不到运行rcx时的原始内容syscall。)

内核系统调用ABI选择相匹配的函数调用ABI,除了r10代替的rcx,所以包装的libc功能,如mmap(2)可以只mov %rcx, %r10/ mov $0x9, %eax/ syscall


请注意,与Window的32位__vectorcall相比,i386 Linux使用的SysV调用约定很糟糕。 它传递栈中的所有内容,并且只返回edx:eaxint64而不返回小结构。毫不费力地维护它的兼容性也就不足为奇了。当没有理由不这样做时,他们做了保持rbx呼叫保留的操作,因为他们认为原来的8个中不需要另一个(不需要REX前缀)是很好的。

使得ABI最佳是多少更重要的长期比任何其他考虑。我认为他们做得很好。我不确定返回包装在寄存器中的结构,而不是返回不同regs中的不同字段。我猜想在没有实际操作字段的情况下按值传递它们的代码会以这种方式获胜,但是拆包的额外工作似乎很愚蠢。他们可能有更多的整数返回寄存器,而不仅仅是rdx:rax,所以返回具有4个成员的结构可以以rdi,rsi,rdx,rax或其他形式返回它们。

他们考虑在向量reg中传递整数,因为SSE2可以对整数进行运算。幸运的是他们没有那样做。 整数经常被用作指针偏移量,往返堆栈内存非常便宜。同样,SSE2指令比整数指令占用更多的代码字节。


我怀疑Windows ABI的设计者可能一直试图将32位和64位之间的差异最小化,以使必须将asm相互移植的人受益,或者可以#ifdef在某些ASM中使用一对s,以便可以更轻松地构建同一源函数的32位或64位版本。

最小化工具链中的更改似乎不太可能。x86-64编译器需要一个单独的表,其中哪个寄存器用于什么以及调用约定是什么。与32bit的重叠很小,不太可能在工具链代码大小/复杂度方面节省大量资金。


1
我想我已经在Raymond Chen的博客上读到了有关从MS端进行基准测试之后选择这些寄存器的基本原理的信息,但我找不到了。然而对于归属区是一些原因解释这里blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/...
phuclv


@phuclv:另请参见在ESP之下编写是否有效?。雷蒙德(Raymond)对我的回答的评论指出了一些我不知道的SEH详细信息,这解释了为什么x86 32/64 Windows当前没有实际红色区域。他的博客文章针对我在该答案中提到的同一代码页入处理程序可能性提出了一些合理的案例:)是的,雷蒙德在解释它方面比我做得更好(不足为奇,因为我从对Windows的了解很少),非x86的红色区域大小表确实很整洁。
彼得·科德斯

13

请记住,微软最初是“官方不致力于早期AMD64的努力”(摘自Matthew Kerner和Neil Padgett的“现代64位计算的历史”),因为它们是Intel在IA64架构上的强大合作伙伴。我认为这意味着,即使他们本来愿意与ACC的GCC工程师一起在Unix和Windows上使用,他们也不会这样做,因为这意味着他们公开支持AMD64的努力。尚未正式这样做(可能会使英特尔不高兴)。

最重要的是,在那时,Microsoft绝对不愿意与开源项目保持友好。当然不是Linux或GCC。

那么为什么他们会在ABI上合作呢?我猜想,ABI之所以不同,仅仅是因为它们或多或少地同时设计并且是孤立的。

“现代64位计算的历史”中的另一句话:

在与微软合作的同时,AMD还与开源社区合作,为该芯片做准备。AMD与Code Sorcery和SuSE签订了工具链工作合同(Intel已经在IA64工具链端口上聘用了Red Hat)。Russell解释说SuSE生产了C和FORTRAN编译器,而Code Sorcery生产了Pascal编译器。Weber解释说,该公司还与Linux社区合作准备Linux端口。这项工作非常重要:它激励了微软继续投资AMD64 Windows计划,并且还确保了在芯片发布后可以使用当时正在成为重要操作系统的Linux。

Weber甚至说Linux的工作对AMD64的成功至关重要,因为Linux使AMD能够生产端到端系统,而无需其他任何公司的帮助。这种可能性确保了即使其他合作伙伴退出,AMD也会采取最坏情况的生存策略,这反过来又使其他合作伙伴因为担心自己落后而参与进来​​。

这表明即使AMD也不认为合作不一定是MS和Unix之间最重要的事情,但是拥有Unix / Linux支持非常重要。甚至试图说服一方或双方做出让步或合作都不值得激怒任何一方的努力或风险吗?也许AMD认为,即使建议使用通用的ABI也可能会延迟或破坏更重要的目标,即在芯片准备就绪时简单地准备好软件支持。

我的猜测是,但我认为ABI不同的主要原因是政治原因,即MS和Unix / Linux方面没有共同努力,而AMD也不认为这是问题。


对政治的看法不错。我同意这不是AMD的错或责任。我责怪微软选择了更糟糕的通话惯例。如果他们的调用约定变得更好,我会有些同情,但是他们必须从最初的ABI更改为,__vectorcall因为传递__m128堆栈很烂。对于某些矢量regs的低128b具有呼叫保留语义也很奇怪(部分原因是Intel最初没有设计使用SSE来设计可扩展的保存/恢复机制,而仍然没有使用AVX来解决问题。)
Peter Cordes

1
对于ABI的出色程度,我真的没有任何专业知识或知识。我只是偶尔需要知道它们是什么,这样我就可以在程序集级别理解/调试。
Michael Burr

1
一个好的ABI可以最大程度地减少代码大小和指令数量,并通过避免额外的内存往返来保持依赖链的低延迟。(对于args或需要溢出/重新加载的本地对象)。需要权衡。SysV的红色区域在一个位置(内核的信号处理程序调度程序)处接受了两个额外的指令,这对于叶函数(无需调整堆栈指针来获取暂存空间)带来了较大的好处。因此,这是显而易见的胜利,下行空间几乎为零。在为SysV提议之后,几乎没有讨论就采用了它。
彼得·科德斯

1
@dgnuff:是的,这就是为什么内核代码不能使用Red Zone的答案。中断使用内核堆栈,而不使用用户空间堆栈,即使它们在CPU运行用户空间代码时到达。内核不信任用户空间堆栈,因为同一用户空间进程中的另一个线程可以修改它,从而接管了内核的控制!
彼得·科德斯

1
@ DavidA.Gray:是的,ABI并没有说您必须使用RBP作为帧指针,因此通常不需要优化代码(在使用函数alloca或其他几种情况下除外)。如果您习惯gcc -fomit-frame-pointer在Linux上使用默认设置,这是正常的。ABI定义了堆栈展开元数据,该元数据允许异常处理仍然有效。(我假设它的工作方式类似于GNU / Linux x86-64 System V的CFI .eh_frame)。 gcc -fomit-frame-pointer自从在x86-64上永久使用以来,它一直是默认设置(启用优化功能),其他编译器(如MSVC)也做同样的事情。
彼得·科德斯

12

Win32对ESI和EDI有其自己的用途,并且要求不要修改它们(或至少在调用API之前将其还原)。我猜想64位代码对RSI和RDI的作用相同,这可以解释为什么它们不被用来传递函数参数。

不过,我无法告诉您为什么要切换RCX和RDX。


1
所有调用约定都有一些寄存器指定为临时寄存器,有些则保留为Win64上的ESI / EDI和RSI / RDI。但是这些是通用寄存器,Microsoft可以毫无疑问地选择使用它们。
JanKanis 2010年

1
@Somejan:当然,如果他们想重写整个API并拥有两个不同的OS。不过,我不会称其为“没有问题”。几十年来,MS已经对x86寄存器将做什么和不做什么做出了某些承诺,并且一直以来它们一直保持一致和兼容。他们不会因为AMD的某些命令而将所有事情扔到窗外,尤其是一个如此武断且超出“构建处理器”领域的命令。
cHao 2010年

5
@Somejan:AMD64 UN * X ABI始终就是这样- 特定UNIX的组件。由于某种原因,x86-64.org / documentation / abi.pdf文档的标题为System V Application Binary Interface,AMD64 Architecture Processor Supplement。(通用的)UNIX ABI(多卷集合,sco.com / developers / devspecs)为特定于处理器的第3章(补编)保留了一部分,这是针对特定处理器的函数调用约定和数据布局规则。
FrankH。

7
@Somejan:Microsoft Windows从未尝试过特别接近UN * X,并且在将Windows移植到x64 / AMD64时,他们只是选择扩展自己 __fastcall调用约定。您声称Win32 / Win64不兼容,但是请仔细观察:对于需要两个 32位args并返回32bit 的函数,Win64和Win32 __fastcall实际上 100%兼容的(传递两个32位args的相同规则,返回值相同)。甚至某些二进制代码(!)都可能在两种操作模式下工作。UNIX方面完全打破了“旧方法”。有充分的理由,但是休息就是休息。
FrankH。

2
@Olof:不仅仅是编译器。当我在NASM中做独立工作时,我遇到了ESI和EDI的问题。Windows绝对关心那些寄存器。但是可以,如果您先保存它们,然后再在Windows需要它们之前还原它们,则可以使用它们。
cHao 2011年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.