122

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.

PS 我知道这些调用约定通常有何不同,并且如果需要,我知道在哪里可以找到详细信息。我想知道为什么

编辑:关于如何,参见例如维基百科条目和那里的链接。

4

4 回答 4

94

在 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 是有序的,Microsoft 和 UN*X/Linux 都选择R8/R9作为第一个。

请记住,如果您选择四个寄存器作为参数,Microsoft 选择RAX(return value) 和RCX, RDX, R8, (arg[0..3]) 是可以理解的选择。R9

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

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

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

因此,如果您想要六个寄存器来传递参数,并且选择RCX,和其中四个是合乎逻辑的,那么您应该选择另外两个RDXR8R9

“更高”的 regs 需要一个额外的指令前缀字节来选择它们,因此具有更大的指令大小占用空间,因此如果您有选项,您不会想要选择其中的任何一个。在经典寄存器中,由于和这些不可用的隐含含义,并且传统上对 UN*X(全局偏移表)有特殊用途,似乎 AMD64 ABI 设计人员不想不必要地与之不兼容。 因此,唯一的选择是/ 。RBPRSPRBX
RSIRDI

因此,如果您必须将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 ABI 之间的区别不仅仅是将参数映射到特定寄存器。有关 Win64 的概述,请查看:

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

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

于 2010-12-14T11:11:09.233 回答
53

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

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

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


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

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

选择rdi,作为前三个参数rsirdx动机是:

  • memset在其 args 上调用或其他 C 字符串函数的函数中节省了少量代码大小(其中 gcc 内联了 rep 字符串操作?)
  • rbx是保留呼叫的,因为在没有 REX 前缀 (rbxrbp) 的情况下可以访问两个保留呼叫的 reg 是一种胜利。之所以选择它们,是因为它们是唯一未被任何通用指令隐式使用的“遗留”寄存器。(rep 字符串、移位计数和 mul/div 输出/输入涉及其他所有内容)。
  • 通用指令强制您使用的所有寄存器都不是调用保留的(请参阅上一点),因此想要使用可变计数移位或除法的函数可能必须将函数 args 移动到其他地方,但不必保存/恢复调用者的值。 cmpxchg16b并且cpuid需要RBX,但很少使用所以不是一个大因素。(cmpxchg16b不是原始 AMD64 的一部分,但 RBX 仍然是显而易见的选择。cmpxchg8b存在但已被 qword 淘汰cmpxchg
  • 我们试图在序列的早期避免 RCX,因为它是通常用于特殊目的的寄存器,如 EAX,因此在序列中丢失它具有相同的目的。它也不能用于系统调用,我们希望使系统调用序列尽可能匹配函数调用序列。

(背景:syscall/sysret不可避免地破坏rcx(with rip)和r11(with ),所以内核在运行时RFLAGS看不到最初的内容。)rcxsyscall

选择内核系统调用 ABI 以匹配函数调用 ABI,除了r10代替rcx,因此 libc 包装器函数(mmap(2)mov %rcx, %r10// mov $0x9, %eaxsyscall


请注意,与 Window 的 32 位 __vectorcall 相比,i386 Linux 使用的 SysV 调用约定很糟糕。 它传递堆栈上的所有内容,并且只返回edx:eaxint64,而不是小的 structs。毫不奇怪,几乎没有努力保持与它的兼容性。当没有理由不这样做时,他们会做一些事情,比如保持rbx呼叫保留,因为他们认为在原始 8 中拥有另一个(不需要 REX 前缀)是好的。

从长远来看,使 ABI 达到最优比任何其他考虑因素都重要得多。我认为他们做得很好。我不完全确定是否返回打包到寄存器中的结构,而不是不同注册表中的不同字段。我猜想通过值传递它们而不实际对字段进行操作的代码会以这种方式获胜,但是解包的额外工作似乎很愚蠢。他们可能有更多的整数返回寄存器,而不仅仅是rdx:rax,因此返回一个具有 4 个成员的结构可以在 rdi、rsi、rdx、rax 或其他内容中返回它们。

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


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

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

于 2016-02-25T06:00:07.993 回答
17

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

最重要的是,在那些日子里,微软绝对没有对开源项目友好的倾向。当然不是 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 并不认为这是一个问题。

于 2016-02-25T07:50:04.280 回答
14

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

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

于 2010-12-13T14:31:08.200 回答