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

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

另外,我知道 怎么做这些呼叫惯例大体上是不同的,如果需要,我知道在哪里可以找到细节。我想知道的是 为什么

编辑: 有关如何编辑,请参阅例如 维基百科词条和从那里的链接。

29466 次浏览

Win32对于 ESI 和 EDI 有自己的用途,并且要求不修改它们(或者至少在调用 API 之前还原它们)。我想64位代码对 RSI 和 RDI 也是如此,这就解释了为什么它们不用来传递函数参数。

我不能告诉你为什么 RCX 和 RDX 被调换了。

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

关于 x86,需要记住的一点是,“ reg number”编码的寄存器名称并不明显; 在指令编码(国防部 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位而言,“更高”的规则是有序的,微软和 UN * X/Linux 都把 R8/R9作为首选。

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

所以在某种程度上,UN * X 和 Win64的不同之处在于 UN * X“预置”了两个额外的参数,在有意选择的 RSI/RDI寄存器中,与自然选择的 RCXRDXR8R9中的四个参数不同。

除此之外..。

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

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

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

我不知道 Windows 为什么这么做。看看这个答案的结尾,猜一猜。我很好奇 SysV 调用大会是如何决定的,所以我深入研究了 邮件列表存档,发现了一些简洁的东西。

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

此外,来自内核开发人员的反馈确定了最初设计 ABC0和 swapgs无法使用的原因。这就是 AMD更新指令如何在释放任何实际芯片之前解决这个问题。同样有趣的是,在2000年末,假设英特尔可能不会采用 AMD64。


SysV (Linux)调用约定,以及应该保留多少寄存器还是保存多少寄存器的决定,是 最初由 Jan Hubicka 于2000年11月制作(一个 gcc 开发人员)。他查看了代码的大小和指令的数量。这个讨论线程围绕着一些与这个 SO 问题的答案和评论相同的观点进行反弹。在第二个线程中,他 提出当前序列作为最优和希望最终,生成较小的代码比一些替代方案

他使用术语“全局”来表示 保存呼叫寄存器,如果使用这个寄存器,就必须进行推送/弹出操作。

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

  • 在 args 上调用 memset或其他 C 字符串函数的函数(其中 gcc 内嵌 rep 字符串操作?)
  • rbx是调用保留的,因为拥有两个不带 REX 前缀(rbxrbp)的可访问的调用保留的 regs 是一种胜利。选择这些寄存器可能是因为它们是唯一没有被任何通用指令隐式使用的“遗留”寄存器。(rep 字符串、 shift count 和 mul/div 输出/输入触及其他所有内容)。
  • 没有一个寄存器 普通的指令迫使你使用是调用保留的(参见上一点) ,所以一个想要使用变量计数移位或除法的函数可能需要将函数参数移到其他地方,但是不需要保存/恢复调用者的值。cmpxchg16bcpuid需要 RBX,但很少使用,所以不是一个很大的因素。(cmpxchg16b不是原 AMD64的一部分,但是 RBX 仍然是显而易见的选择。cmpxchg8b存在,但被 qword cmpxchg淘汰)
  • 我们试图在序列的早期避免 RCX,因为它是寄存器 通常用于特殊目的,例如 EAX,因此,它的目的是 在序列中丢失了。 而且它也不能用于 syscalls,我们希望制作 syscall 序列 尽可能匹配函数调用序列。

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

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


注意,与 Window 的32bit _ _ vectorcall 相比,i386Linux 使用的 SysV 调用约定糟糕透顶。它传递堆栈上的所有内容,并且只为 int64在 edx:eax中返回,而不为小结构返回.毫不奇怪,为了保持与它的兼容性做出了很少的努力。当没有理由不这样做时,他们会做一些事情,比如保留 rbx调用,因为他们认为在原来的8中保留另一个(不需要 REX 前缀)是好的。

从长远来看,很多比其他任何考虑因素都更重要。我觉得他们干得不错。我不完全确定是否应该返回打包到寄存器中的结构,而不是不同规则中的不同字段。我猜测,通过值传递它们而不实际操作字段的代码通过这种方式获胜,但是解压缩的额外工作似乎很愚蠢。它们可以有更多的整数返回寄存器,而不仅仅是 rdx:rax,所以返回一个包含4个成员的结构可以用 rdi、 rsi、 rdx、 rax 或其他方式返回它们。

他们考虑在向量规则中传递整数,因为 SSE2可以对整数进行操作。幸运的是他们没有这么做。整数经常被用作指针偏移量,并且到堆栈内存的往返是相当便宜的.另外,SSE2指令比整数指令占用更多的代码字节。


我怀疑 Windows ABI 设计者可能是为了让32位和64位之间的差异最小化,以便人们能够从一个移植到另一个,或者在某些 ASM 中使用两个 #ifdef,这样同一个源可以更容易地构建一个32位或64位版本的函数。

尽量减少工具链中的更改似乎不太可能。X86-64编译器需要一个单独的表,列出哪个寄存器用于什么,以及调用约定是什么。与32位有一个小的重叠不太可能在工具链代码大小/复杂性方面产生显著的节省。

请记住,微软最初“对早期 AMD64的努力没有正式承诺”(来自 Matthew Kerner 和 Neil Padgett 的 《现代64位元史》) ,因为他们是英特尔在 IA64架构上的强大合作伙伴。我认为这意味着即使他们愿意与 GCC 的工程师合作,在 Unix 和 Windows 上同时使用 ABI,他们也不会这么做,因为这意味着在他们还没有正式这么做的时候就公开支持 AMD64的努力(可能会让英特尔不高兴)。

最重要的是,在那些日子里,微软对开源项目完全没有友好的倾向。当然不是 Linux 或者 GCC。

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

引自《现代64位元史》的另一段话:

与微软合作的同时,AMD 也参与了 开源社区为芯片做准备 代码巫术和工具链工作的 SuSE (红帽子已经 英特尔公司在 IA64工具链端口上的合作) ,罗素解释说, SuSE 生成了 C 和 FORTRAN 编译器,而 Code Sorcery 生成了一个 韦伯解释说,该公司还从事 Linux 社区准备一个 Linux 端口 重要的是: 它作为一个激励微软继续 投资于 AMD64 Windows 的努力,同时也确保了 Linux 正在成为当时一个重要的操作系统 芯片被释放了。

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

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

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