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

时间:2010-12-13 13:45:52

标签: windows x86-64 calling-convention

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

有没有人知道这种差异的技术,历史或政治原因,还是纯粹是NIH综合症的问题?

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

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

编辑:有关方法,请参阅例如wikipedia entry和那里的链接。

4 个答案:

答案 0 :(得分:74)

在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,{{1 }},?CX?DX?BX?SP?BP?SI

因此选择A / C / D(regs 0..2)作为返回值,前两个参数(即“经典”32bit ?DI约定)是一个合理的选择。就64位而言,订购了“更高”的注册表,Microsoft和UN * X / Linux都是__fastcall / R8的第一个。

记住这一点,微软选择R9(返回值)和RAXRCXRDXR8(arg [0 ..如果你为参数选择 四个 寄存器,那么这是一个可以理解的选择。

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

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

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

因此,如果您想要 六个 寄存器来传递参数,那么选择RCXRCXRDX是合乎逻辑的和R8其中四个,你应该选哪两个?

“更高”的regs需要一个额外的指令前缀字节来选择它们,因此具有更大的指令大小,所以如果你有选项,你不会想要选择其中任何一个。在经典寄存器中,由于R9RBP隐式含义不可用,RSP传统上对UN * X有特殊用途(全球抵消表)似乎AMD64 ABI设计师不希望不必要地与之不相容 因此,唯一选择RBX / RSI

所以如果你必须把RDI / RSI作为参数寄存器,它们应该是哪个参数?

让他们RDIarg[0]有一些优势。见cHao的评论 arg[1]?SI是字符串指令源/目标操作数,正如cHao所提到的,它们用作参数寄存器意味着使用AMD64 UN * X调用约定,最简单的?DI函数,例如,仅由两个CPU指令strcpy()组成,因为源/目标地址已被调用者放入正确的寄存器中。特别是在低级和编译器生成的“粘合”代码中(例如,想想一些C ++堆分配器在构造上填充零对象,或者repz movsb; ret上的内核零填充堆页面,或者复制-on-write pagefaults)大量的块复制/填充,因此它对于经常用于保存两个或三个CPU指令的代码非常有用,否则这些指令会将这些源/目标地址参数加载到“正确”中寄存器。

所以在某种程度上,UN * X和Win64的区别仅在于UN * X“预先”有两个额外的参数,在有目的地选择sbrk() / RSI寄存器中,自然选择四个RDIRCXRDXR8中的参数。

除此之外......

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参考文献中进行了解释。

答案 1 :(得分:31)

IDK为什么Windows会做他们所做的事情。请参阅此答案的结尾以进行猜测。我很好奇SysV调用约定是如何决定的,所以我挖到the mailing list archive并发现了一些简洁的东西。

阅读AMD64邮件列表中的一些旧线程很有意思,因为AMD架构师积极参与其中。例如选择注册名称是其中一个难点:AMD考虑renaming the original 8 registers r0-r7, or calling the new registers stuff like UAX

此外,来自内核开发人员的反馈确定了原始设计为syscall and swapgs unusable的内容。这就是AMD updated the instruction如何在发布任何实际芯片之前解决这个问题。同样有趣的是,2000年末,英特尔可能不会采用AMD64。

SysV(Linux)调用约定,以及关于应该保留被调用者数量与调用者保存的寄存器数量的决定是made initially in Nov 2000, by Jan Hubicka(gcc开发人员)。他compiled SPEC2000并查看了代码大小和指令数量。这个讨论主题围绕着一些与SO问题的答案和评论相同的想法反弹。在第二个帖子中,他是proposed the current sequence as optimal and hopefully final, generating smaller code than some alternatives

他使用的术语是"全球"表示调用保留的寄存器,如果使用则必须按下/弹出。

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

  • 在args上调用memset或其他C字符串函数的函数中的次要代码大小保存(其中gcc内联一个rep字符串操作?)
  • rbx是呼叫保留的,因为在没有REX前缀(rbx和rbp)的情况下可以访问两个呼叫保留的reg是一个胜利。据推测,因为它是唯一一个不被任何指令隐含使用的其他reg。 (rep字符串,移位计数和mul / div输出/输入触及其他所有内容)。
  • 具有特殊用途的寄存器都没有被调用保留(参见上一点),因此想要使用rep字符串指令或变量计数移位的函数可能必须将函数args移动到其他位置,但不能。必须保存/恢复来电者的价值。
  •   

    我们试图在序列的早期避免使用RCX,因为它是寄存器   通常用于特殊目的,如EAX,因此它具有相同的目的   在序列中缺失。   它也不能用于系统调用,我们想制作系统调用序列   尽可能匹配函数调用序列。

    (背景:syscall / sysret不可避免地会破坏rcx(使用rip)和r11(使用RFLAGS),因此内核在rcx运行时,无法查看syscall中的内容。

选择内核系统调用ABI来匹配函数调用ABI,r10而不是rcx除外,因此像mmap(2)这样的libc包装函数只能mov %rcx, %r10 } / mov $0x9, %eax / syscall

请注意,与Window的32位__vectorcall相比,i386 Linux使用的SysV调用约定很糟糕。 It passes everything on the stack, and only returns in edx:eax for int64, not for small structs。毫不奇怪,为保持与它的兼容性做了很少的努力。当没有理由不这样做的时候,他们做了保持rbx保留呼叫的事情,因为他们认为在原始的8(不需要REX前缀)中有另一个是好的。

使ABI最佳很多比任何其他考虑更重要。我认为他们做得很好。我不完全确定返回打包到寄存器中的结构,而不是不同的regs中的不同字段。我想在没有实际操作字段的情况下按值传递它们的代码会以这种方式获胜,但是解压缩的额外工作看起来很愚蠢。它们可能有更多的整数返回寄存器,不仅仅是rdx:rax,所以返回一个包含4个成员的结构可以在rdi,rsi,rdx,rax或其他东西中返回它们。

他们考虑在向量寄存器中传递整数,因为SSE2可以对整数进行操作。幸运的是,他们没有这样做。 Integers are used as pointer offsets very often, and a round-trip to stack memory is pretty cheap。 SSE2指令也比整数指令占用更多的代码字节。

我怀疑Windows ABI设计人员可能一直致力于最大限度地减少32位和64位之间的差异,以便那些必须将asm从一个移植到另一个的人的利益,或者可以在某些ASM中使用几个#ifdef s所以同一个源可以更容易地构建一个32或64位版本的函数。

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

答案 2 :(得分:12)

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

我无法告诉你为什么RCX和RDX会被切换。

答案 3 :(得分:12)

请记住,微软最初"正式对AMD64早期的努力不负责任" (来自"A History of Modern 64-bit Computing" Matthew Kerner和Neil Padgett)因为他们是英特尔在IA64架构上的强大合作伙伴。我认为这意味着即使他们原本愿意与ABCC的GCC工程师合作在Unix和Windows上使用它们,他们也不会这样做,因为它意味着公开支持AMD64的努力。还没有正式完成(并且可能会让英特尔感到不安)。

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

那么为什么他们会在ABI上合作?我猜测ABI是不同的,因为它们的设计或多或少是在同一时间并且是孤立的。

另一个引用来自"现代64位计算历史":

  

与微软合作的同时,AMD也参与其中   开源社区为筹码做准备。 AMD与之签约   用于工具链工作的Code Sorcery和SuSE(Red Hat已经是   英特尔参与IA64工具链端口)。拉塞尔解释说   SuSE制作了C和FORTRAN编译器,Code Sorcery制作了一个   Pascal编译器。韦伯解释说,该公司也参与其中   Linux社区准备一个Linux端口。这个努力非常   重要的是:它是微软继续努力的动力   投资AMD64 Windows的努力,也确保了Linux,其中   当时正在成为一个重要的操作系统,可用一次   筹码被释放。

     韦伯甚至说Linux的工作绝对至关重要   AMD64的成功,因为它使AMD能够实现端到端的生产   没有任何其他公司帮助的系统,如有必要。这个   可能性确保AMD甚至可以采用最坏情况的生存策略   如果其他合作伙伴退出,反过来又保留其他合作伙伴   因为害怕被抛在身后而订婚。

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

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