Why does Windows64 use a different calling convention from all other OSes on x86-64?

Jan*_*nis 97 windows x86-64 calling-convention

AMD has an ABI specification that describes the calling convention to use on x86-64. All OSes follow it, except for Windows which has it's own x86-64 calling convention. Why?

Does anyone know the technical, historical, or political reasons for this difference, or is it purely a matter of NIHsyndrome?

I understand that different OSes may have different needs for higher level things, but that doesn't explain why for example the register parameter passing order on Windows is rcx - rdx - r8 - r9 - rest on stack while everyone else uses rdi - rsi - rdx - rcx - r8 - r9 - rest on stack.

PS我知道这些调用约定一般如何不同,如果需要,我知道在哪里可以找到详细信息.我想知道的是为什么.

编辑:有关如何,请参阅维基百科条目和那里的链接.

Fra*_*kH. 77

在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(regs 0..2)作为返回值,前两个参数(即"经典"32位__fastcall约定)是一个逻辑选择.至于要64位来讲,"更高"的REG是有序的,而微软和UN*X/Linux的去了R8/ R9为第一人.

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

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

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

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

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

"更高"的regs需要一个额外的指令前缀字节来选择它们,因此具有更大的指令大小,所以如果你有选项,你不会想要选择其中任何一个.在经典寄存器中,由于隐含的含义RBP而且RSP这些寄存器不可用,并且RBX传统上在UN*X(全局偏移表)上有特殊用途,似乎AMD64 ABI设计者不希望不必要地变得不兼容.
Ergo,唯一的选择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寄存器,为四个参数中的自然选择RCX,RDX,R8R9.

除此之外 ...

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

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

Win64和AMD64 UN*X在使用堆栈空间的方式上也有惊人的不同; 例如,在Win64上,调用者必须为函数参数分配堆栈空间,即使args 0 ... 3在寄存器中传递.另一方面,在UN*X上,如果一个叶子函数需要不超过128个字节,那么它甚至根本不需要分配堆栈空间(是的,你拥有并且可以使用它)一定数量的堆栈而不分配它...好吧,除非你是内核代码,一个漂亮的bug的来源).所有这些都是特别的优化选择,大多数理由都在原始海报的维基百科参考指向的完整ABI参考文献中进行了解释.

  • @Somejan:Win64和Win32`__fastcall`在不超过两个参数不大于32位且返回不大于32位的情况下是100%相同的.这不是一小部分功能.对于i386/amd64,UN*X ABI之间根本没有这种向后兼容性. (7认同)
  • @szx:我刚刚找到了 2000 年 11 月的相关邮件列表线程,并发布了一个总结推理的答案。请注意,可以通过这种方式实现的是 `memcpy`,而不是 `strcpy`。 (3认同)
  • 关于寄存器名称:该前缀字节可能是一个因素。但是 MS 选择 rcx - rdx - rdi - rsi 作为参数寄存器会更合乎逻辑。但是,如果您从头开始设计 ABI,前八位的数值可以指导您,但如果已经存在完美的 ABI,则没有理由更改它们,这只会导致更多的混乱。 (2认同)
  • 在RSI/RDI上:这些指令通常是内联的,在这种情况下,调用约定无关紧要.否则,系统范围内只有一个副本(或者可能是少数几个),因此它只保存了一小部分字节_in total_.不值得.在其他差异/调用堆栈上:特定选择的有用性在ABI参考中进行了解释,但它们没有进行比较.他们没有说明为什么没有选择其他优化 - 例如为什么Windows没有128字节的红色区域,为什么AMD ABI没有额外的堆栈插槽用于参数? (2认同)
  • 为什么在 System V ABI 中“RDX”在“RCX”之前传递?`strcpy` 不是 2 条指令而是 3 条指令(加上一个 `mov rcx, rdx`)? (2认同)

Pet*_*des 33

IDK为什么Windows做了他们做的事情.请参阅此答案的结尾以进行猜测.我很好奇SysV调用约定是如何决定的,所以我挖掘了邮件列表存档并发现了一些简洁的东西.

阅读AMD64邮件列表中的一些旧线程很有意思,因为AMD架构师积极参与其中.例如,选择寄存器名称是其中一个难点:AMD考虑重命名原来的8个寄存器r0-r7,或者调用新的寄存器UAX.

此外,来自内核开发人员的反馈也确定了原始设计syscallswapgs无法使用的内容.这就是AMD 在发布任何实际芯片之前更新指令以解决这个问题的方法.同样有趣的是,在2000年末,英特尔可能不会采用AMD64.


SysV(Linux)调用约定,以及关于应该保留被调用者数量的寄存器与调用者保存的决定,最初是在2000年11月由Jan Hubicka(gcc开发人员)制作的.他编译了SPEC2000并查看了代码大小和指令数量.这个讨论主题围绕着一些与SO问题的答案和评论相同的想法.在第二个线程中,他提出当前序列是最优的并且希望是最终的,产生比一些替代品更小的代码.

他使用术语"全局"来表示调用保留寄存器,如果使用则必须按下/弹出.

的选择rdi,rsi,rdx作为第三个参数的动机是:

  • memset在其args上调用或其他C字符串函数的函数中的次要代码大小保存(其中gcc内联一个rep字符串操作?)
  • rbx是保留呼叫保留,因为有两个呼叫保留的regs可以访问没有REX前缀(rbx和rbp)是一个胜利.大概选择因为它是唯一没有被任何指令隐含使用的其他reg.(rep字符串,移位计数和mul/div输出/输入触摸其他所有内容).
  • 没有具有特殊用途的寄存器是调用保留的(参见上一点),因此想要使用rep字符串指令或变量计数移位的函数可能必须将函数args移动到其他地方,但不必保存/恢复来电者的价值.
  • 我们试图在序列的早期避免RCX,因为它通常用于特殊目的的寄存器,如EAX,因此它在序列中有相同的目的.它也不能用于系统调用,我们希望使syscall序列尽可能地匹配函数调用序列.

    (后台:syscall/ sysret不可避免地破坏rcx(带rip)和r11(带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或其他东西中返回它们.

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


我怀疑Windows ABI设计人员可能一直致力于最大限度地减少32位和64位之间的差异,这对于那些必须将asm从一个端口移植到另一个端口的人来说是有益的,或者可以#ifdef在某些ASM中使用一些s,因此相同的源可以更容易地构建32或64位版本的功能.

最小化工具链的变化似乎不太可能.x86-64编译器需要一个单独的表,其中哪个寄存器用于什么,以及调用约定是什么.与32位的小重叠不太可能显着节省工具链代码大小/复杂性.

  • 我想我已经在 Raymond Chen 的博客上读到过关于在从 MS 方面进行基准测试后选择这些寄存器的基本原理的文章,但我再也找不到了。但是,这里解释了有关 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/ (2认同)
  • Raymond Chen 的另一篇博文:[为什么我们甚至需要定义一个红色区域?我不能只使用我的堆栈吗?](https://blogs.msdn.microsoft.com/oldnewthing/20190111-00/?p=100685) (2认同)

cHa*_*Hao 12

Win32有自己的ESI和EDI用途,并且要求它们不被修改(或者至少在调用API之前它们被恢复).我想象64位代码对RSI和RDI的作用是一样的,这可以解释为什么它们不用于传递函数参数.

我不知道为什么RCX和RDX会被切换.

  • @Somejan:Microsoft Windows从未尝试过特别接近UN*X,当涉及将Windows移植到x64/AMD64时,他们只是选择扩展他们的_own_`__fastcall`调用约定.您声称Win32/Win64不兼容,但仔细观察:对于一个需要_two_ 32位args并返回32位的函数,Win64和Win32`__fastcall`实际上_are_ 100%兼容(相同的regs用于传递两个32位args,相同的返回值).甚至一些二进制(!)代码也可以在两种操作模式下工作.UNIX方面完全打破了"老路".有充分的理由,但休息是休息. (6认同)
  • @Somejan:AMD64 UN*X ABI总是那个 - 特定于_UNIX的片段.该文档名为http://www.x86-64.org/documentation/abi.pdf,标题为_System V Application Binary Interface,**AMD64 Architecture Processor Supplement**_.(常见的)UNIX ABI(多卷集合,http://www.sco.com/developers/devspecs/)为特定于处理器的第3章留下了一节--_Supplement_ - 它们是函数调用约定和数据布局特定处理器的规则. (5认同)
  • @Olof:它不仅仅是一个编译器。当我在 NASM 中做独立的东西时,我遇到了 ESI 和 EDI 的问题。Windows 肯定关心这些寄存器。但是,是的,如果您先保存它们并在 Windows 需要它们之前恢复它们,您就可以使用它们。 (3认同)
  • 所有调用约定都有一些寄存器被指定为临时寄存器,有些则被保留,如 Win64 上的 ESI/EDI 和 RSI/RDI。但这些是通用寄存器,微软本可以毫无问题地选择以不同方式使用它们。 (2认同)
  • @Somejan:当然,如果他们想重写整个 API 并拥有两个不同的操作系统。不过,我不会称之为“没有问题”。几十年以来,MS 已经就 x86 寄存器会做什么和不会做什么做出了一定的承诺,并且它们一直或多或少保持一致和兼容。他们不会仅仅因为 AMD 的一些法令,特别是在“构建处理器”的领域之外如此武断的法令而将所有这些都扔出窗外。 (2认同)
  • 顺便说一句,?SI 和?DI 是*半* 通用寄存器。就像一直存在的大多数寄存器一样,它们具有内置的特定于寄存器的用途;一些指令(字符串指令:MOVS?、INS?、OUTS? 等)是硬编码的,以使用这些寄存器来移动数据。除非 x86-64 提供了其他一些方法,否则它需要的不仅仅是使用不同的寄存器。 (2认同)
  • @cHao:您说的是 x86 寄存器。要以 64 位模式运行代码,无论如何都需要重新编译,32 位模式代码不能以 64 位(长模式)运行。64 位和 32 位之间兼容性的唯一有用方法是源代码级兼容性以及在 64 位内核上运行 32 位用户程序的可能性。Linux 等人也对 32 位模式下的某些寄存器有特定用途,但对他们来说,AMD ABI 显然不是问题。 (2认同)
  • @cHao: ?SI 和 ?DI 确实也有特殊用途(虽然我不知道它在 64 位中是否完全相同),但显然这对于​​除 Windows 之外的所有其他操作系统都不是问题。这些特殊用途也与函数调用无关。 (2认同)
  • @Somejan:如果从“x86-64”的名称中看不出来,它*是* x86,扩展到64位。许多不能直接处理 32 位值的代码(即:推送和弹出寄存器而不是加载值)将在这两种模式下工作。当然,除非你去重新定义所有这些寄存器的用途并说“你不能再使用这些了”。当然,如果 MS 想牺牲兼容性和一致性,他们可以做到。他们没有。考虑到 Win32 与 Win16 具有相同的规则,这并不奇怪。对他们来说,一致性胜过向 AMD 磕头。 (2认同)
  • @Somejan:不,它们与函数调用无关。事实上,这些特殊目的可能是让它们在 AMD 的调用约定中占有一席之地的一部分——在 RSI 中传递一个字符串可能非常有用。但是,Windows 对这些寄存器有自己的用途,这可能与其内置功能相关,也可能不相关。坦率地说,为什么无关紧要——MS 已经说过寄存器以某种方式使用,*这是它作为操作系统设计者的特权*。AMD 无权决定如何使用寄存器;这是操作系统制造商的领域。 (2认同)
  • @cHao:x86-64 是 x86 的直接扩展,但它仍然是一种不同的架构。为 32 位模式编译的二进制代码不能在 64 位模式下未经修改地运行。它可以在兼容模式下运行,但 64 位 ABI 与此无关。无论如何,Win64 调用约定 _is_ 不同于 Win32 调用约定。当然 MS 可以定义他们自己的 ABI 并且他们已经这样做了,但这让我回到我最初的问题:除了“我们不关心其他人做什么”之外,还有什么理由不遵循其他人使用的 ABI。(与 32bits 模式的兼容性不是一个) (2认同)
  • @Somejan:没有你想象的那么不同。大多数相同的操作、相同的寄存器(加上额外的)和二进制兼容性,以至于相同的字节可以表示 32 位或 64 位模式下的等效和有效代码(有一些警告)。即使您忽略了大部分内容,事实仍然存在:语言、操作系统和/或编译器定义了调用约定。如果 AMD 将任何东西定义为“一种真正的方式”(因为没有一种方式),那么它就是越界了。MS 有一种对他们有效 20 多年的方法。宣布它无效,因为它不受 AMD 的祝福是无知的高度。 (2认同)
  • @FrankH:您提出了一个很好的观点,请考虑将您的评论扩展为完整的答案。我不知道 AMD ABI 文档是为了“插入”这个常见的 unix ABI,它显然试图为所有支持 unix 的架构定义 ABI。我认为它更像是提供一个通用标准以避免 x86-32 混乱。我仍然想知道选择一个而不是另一个的技术原因,但您的评论确实揭示了非技术背景。 (2认同)

Mic*_*urr 12

请记住,微软最初"对AMD64早期的努力正式不承认"(来自Matthew Kerner和Neil Padgett的"现代64位计算历史"),因为他们是英特尔在IA64架构上的强大合作伙伴.我认为这意味着即使他们原本愿意与ABCC的GCC工程师合作在Unix和Windows上使用它们,他们也不会这样做,因为它意味着当他们没有公开支持AMD64的努力时尚未正式完成(并且可能会让英特尔感到不安).

最重要的是,在那些日子里,微软绝对没有倾向于与开源项目保持友好关系.当然不是Linux或GCC.

那他们为什么要在ABI上合作呢?我猜这些ABI之所以不同,仅仅是因为它们的设计或多或少是在同一时间并且是孤立的.

另一句话来自"现代64位计算史":

在与微软合作的同时,AMD还与开源社区合作筹备该芯片.AMD与Code Sorcery和SuSE签订了工具链合同(红帽已经被英特尔公司用于IA64工具链端口).Russell解释说SuSE生成了C和FORTRAN编译器,Code Sorcery生成了一个Pascal编译器.Weber解释说,该公司还与Linux社区合作准备一个Linux端口.这项工作非常重要:它激励微软继续投资AMD64 Windows的工作,同时也确保了当时正在成为重要操作系统的Linux在芯片发布后即可使用.

韦伯甚至说Linux工作对AMD64的成功至关重要,因为它使得AMD能够在没有任何其他公司帮助的情况下生产端到端系统.这种可能性确保AMD即使其他合作伙伴退出也有最坏情况的生存策略,这反过来又让其他合作伙伴保持参与,因为他们害怕被抛在身后.

这表明即使AMD也不认为合作必然是MS和Unix之间最重要的事情,但是拥有Unix/Linux支持非常重要.甚至试图说服一方或双方妥协或合作也不值得努力或冒险(?)激怒他们中的任何一方?也许AMD认为即使建议一个共同的ABI可能会延迟或破坏更简单的目标,即在芯片准备就绪时准备好软件支持.

我个人的猜测,但我认为ABI不同的主要原因是MS和Unix/Linux方面没有合作的政治原因,AMD没有把这看作是一个问题.

  • 对政治的看法很好。我同意这不是 AMD 的错或责任。我责怪微软选择了更糟糕的调用约定。如果他们的调用约定变得更好,我会有些同情,但他们必须从最初的 ABI 更改为“__vectorcall”,因为在堆栈上传递“__m128”很糟糕。对某些向量寄存器的低 128b 进行调用保留的语义也很奇怪(部分是英特尔的错误,因为最初没有使用 SSE 设计可扩展的保存/恢复机制,但仍然没有使用 AVX。) (2认同)
  • 我对 ABI 的*好*没有任何专业知识或知识。我只是偶尔需要知道它们是什么,以便我可以在程序集级别理解/调试。 (2认同)
  • 一个好的 ABI 可以最大限度地减少代码大小和指令数量,并通过避免额外的内存往返来保持依赖链的低延迟。(对于 args,或者对于需要溢出/重新加载的本地人)。有取舍。SysV 的红色区域在一个地方(内核的信号处理程序调度程序)接受了一些额外的指令,这对于叶函数来说相对较大的好处是不必调整堆栈指针来获得一些临时空间。所以这是一个明显的胜利,几乎为零的缺点。它在被提议用于 SysV 后几乎没有讨论就被采用。 (2认同)
  • @dgnuff:是的,这就是[为什么内核代码不能使用红色区域](/sf/ask/1805118591/) 的答案。中断使用内核堆栈,而不是用户空间堆栈,即使它们在 CPU 运行用户空间代码时到达。内核不信任用户空间堆栈,因为同一用户空间进程中的另一个线程可以修改它,从而接管内核的控制权! (2认同)
  • @DavidA.Gray:是的,ABI 并没有说你 *必须* 使用 RBP 作为帧指针,因此优化的代码通常不会(除了使用 `alloca` 的函数或其他一些情况)。如果您习惯将“gcc -fomit-frame-pointer”作为 Linux 上的默认设置,那么这是正常的。ABI 定义了堆栈展开元数据,允许异常处理仍然有效。(我认为它的工作方式类似于“.eh_frame”中的 GNU/Linux x86-64 System V 的 CFI 内容)。`gcc -fomit-frame-pointer` 一直是 x86-64 上的默认设置(启用优化),其他编译器(如 MSVC)也做同样的事情。 (2认同)