为什么Windows64在x86-64上使用与其他所有操作系统不同的调用约定?
AMD有一个ABI规范,描述了在x86-64上使用的调用约定。 除了拥有自己的x86-64调用约定的Windows,所有的操作系统都遵循它。 为什么?
有没有人知道这种差异的技术,历史或政治原因,还是纯粹是NIH综合征的问题?
我知道不同的操作系统可能对更高级别的东西有不同的需求,但是这并不能解释为什么例如Windows上的寄存器parameter passing顺序是rcx - rdx - r8 - r9 - rest on stack
人都使用rdi - rsi - rdx - rcx - r8 - r9 - rest on stack
。
PS我知道这些调用约定是如何不同的,我知道在哪里可以find细节。 我想知道的是为什么 。
编辑:如何,见例如维基百科条目和从那里的链接。
在x64上select四个参数寄存器 – 通用于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
。
因此,selectA / C / D(regs 0..2)作为返回值,前两个参数(这是“古典”32位__fastcall
约定)是合乎逻辑的select。 就64位而言,“更高”的regs是有序的,微软和UN * X / Linux都是以R8
/ R9
为第一。
牢记这一点,如果你select四个寄存器作为参数,微软selectRAX
(返回值)和RCX
, RDX
, R8
, R9
(arg [0..3])是一个可以理解的select。
我不知道为什么AMD64 UN * X ABI在RCX
之前select了RDX
。
在x64上select六个参数寄存器 – UN * X specific
在RISC体系结构上,UN * X传统上是通过在寄存器中传递参数来实现的,特别是前六个参数(PPC,SPARC,MIPS至less如此)。 这可能是AMD64(UN * X)ABIdevise人员select在该架构上使用六个寄存器的主要原因之一。
所以如果你想要六个寄存器来传递参数,那么selectRCX
, RDX
, R8
和R9
是合乎逻辑的,其中四个寄存器应该select哪个?
“较高”的寄存器需要一个额外的指令前缀字节来select它们,因此具有更大的指令大小的占用空间,所以如果你有选项,你不会select任何这些。 在经典寄存器中,由于RBP
和RSP
的隐含含义,这些都是不可用的, RBX
传统上在UN * X(全局偏移表)上有特殊用途,看起来AMD64 ABIdevise者似乎不想不必要地变得不兼容用。
艾尔戈, 唯一的select是RSI
/ RDI
。
所以如果你必须把RSI
/ RDI
作为参数寄存器,那么它们应该是什么参数呢?
使他们arg[0]
和arg[1]
有一些优点。 见cHao的评论。
?SI
和?DI
是string指令源/目的地操作数,正如cHao所提到的,它们用作参数寄存器意味着使用AMD64 UN * X调用约定,最简单的可能的strcpy()
函数,例如,只包含两个CPU指令repz movsb; ret
repz movsb; ret
因为源/目标地址已经被调用者放入正确的寄存器。 特别是在低级和编译器生成的“粘合”代码中(例如,想一些C ++堆分配器在构build时填零对象,或者在sbrk()
上内核填零堆页,或者copy-on – 写页面错误)大量的块复制/填充,因此对于经常用于保存两三个CPU指令的代码来说非常有用,否则这些指令会将这些源/目标地址参数加载到“正确的”寄存器中。
所以从某种意义上来说,UN * X和Win64的区别只在于,在有目的地selectRSI
/ RDI
寄存器的情况下,UN * X“前置”两个附加参数,自然selectRCX
, RDX
, R8
和R9
的四个参数。
除此之外 …
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字节(是的,你拥有并可以使用一定数量的堆栈没有分配它…好吧,除非你是内核代码,一个漂亮的错误来源)。 所有这些都是特定的优化select,大部分理论基础都是在原始海报的维基百科参考指向的完整ABI参考文献中解释的。
IDK为什么Windows做了他们做的。 看到这个答案的结尾猜测。 我很好奇SysV调用约定是如何决定的,所以我挖掘了邮件列表的存档 ,发现了一些整洁的东西。
阅读AMD64邮件列表中的一些旧线程很有意思,因为AMD架构师对此非常积极。 例如,select寄存器名称是困难的部分之一:AMD认为重命名原来的8个寄存器r0-r7,或者调用新的寄存器像UAX
。
此外,来自内核开发人员的反馈意见指出, syscall
和swapgs
的原始devise无法使用 。 这就是AMD在发布任何实际芯片之前更新了指令以便将其解决的原因。 另外值得一提的是,在2000年年底,英特尔可能不会采用AMD64。
SysV(Linux)调用约定,以及决定多less个寄存器应该被调用者保存还是调用者保存, 最初由Jan Hubicka (一位gcc开发人员) 在2000年11月做出 。 他编译SPEC2000并查看代码大小和指令数量。 这个讨论主题围绕着这个SO问题的答案和评论反驳了一些相同的想法。 在第二个线程中,他提出当前序列是最优的,希望是最终的,产生比一些替代scheme更小的代码 。
他使用术语“全局”来表示调用保存的寄存器,如果使用的话必须被推入/popup。
rdi
, rsi
, rdx
作为前三个参数的select是由以下因素驱动的:
- 在调用
memset
或其他Cstring函数的函数(gcc内嵌repstring操作? -
rbx
是保存的,因为有两个不需要REX前缀(rbx和rbp)的调用保存的reg是一个胜利。 大概select,因为它是唯一的不是任何指令隐含使用的其他章。 (代表string,移位计数和mul / div输出/input触摸其他所有内容)。 - 没有任何具有特殊用途的寄存器是保留的(请参阅上一点),所以想要使用repstring指令或可变计数移位的函数可能必须将函数args移到其他地方,但不必保存/恢复来电者的价值。
-
我们试图在序列的早期避免RCX,因为它是通常用于特殊目的的寄存器,比如EAX,所以它在序列中有相同的目的。 也不能用于系统调用,我们希望尽可能使系统调用序列匹配函数调用序列。
(后台:
syscall
/sysret
不可避免地破坏rcx
(带有rip
)和r11
(带有RFLAGS
),所以当syscall
运行时,内核看不到rcx
。
内核系统调用ABI被select为匹配函数调用ABI,除了r10
而不是rcx
,所以像mmap(2)
这样的libc包装函数可以只mov %rcx, %r10
/ mov $0x9, %eax
/ syscall
。
请注意,i386 Linux使用的SysV调用约定与Window的32位__vectorcall相比更为糟糕。 它传递堆栈中的所有内容,并且只返回edx:eax
for int64,而不是小结构 。 毫不奇怪,为保持与它的兼容性做了一点努力。 当没有理由不这样做的时候,他们做了一些事情,比如保持rbx
调用保留,因为他们决定在原来的8(不需要REX前缀)中有另一个是好的。
使ABI最优化比任何其他考虑更为重要。 我认为他们做得很好。 我不完全确定将返回的结构包含到寄存器中,而不是在不同的regs中的不同的字段。 我猜测代码是通过价值传递它们而不是在字段上实际运行的代码赢得这种方式,但是额外的拆包工作似乎很愚蠢。 他们可能有更多的整数返回寄存器,不仅仅是rdx:rax
,所以返回一个4个成员的结构可以返回它们在rdi,rsi,rdx,rax或其他东西。
他们考虑传递整数在向量regs,因为SSE2可以操作整数。 幸运的是,他们没有这样做。 整数常常用作指针偏移量,往返堆栈的内存相当便宜 。 另外SSE2指令比整数指令占用更多的代码字节。
我怀疑Windows ABIdevise人员可能一直致力于将32位和64位之间的差异降至最低,以利于必须将asm从一个端口移到另一个端口的用户,或者可以在某些ASM中使用几个#ifdef
,以便同一个源可以更多轻松构build一个32位或64位版本的function。
最小化工具链中的变化似乎不太可能。 一个x86-64编译器需要一个单独的表,哪个寄存器用于什么,调用约定是什么。 与32位有一个小的重叠不太可能产生工具链代码大小/复杂度的显着节省。
Win32对ESI和EDI有自己的用途,并且要求它们不被修改(或者至less在调用API之前被恢复)。 我想64位代码与RSI和RDI一样,这将解释为什么他们不被用来传递函数参数。
但是我不能告诉你为什么RCX和RDX被切换。
请记住,微软最初是“对AMD64早期的努力表示正式的不服,”(来自Matthew Kerner和Neil Padgett的“现代64位计算的历史” ),因为他们是Intel在IA64架构上的强有力的合作伙伴。 我认为这就意味着,即使他们本来可以与ABI的GCC工程师一起在Unix和Windows上使用,他们也不会这么做,因为这意味着公开支持AMD64的努力,但官方还是这么做的(而且可能会让英特尔感到不安)。
最重要的是,在那个时代,微软对于和开源项目友好绝对没有任何关系。 当然不是Linux或GCC。
那么他们为什么要在ABI上进行合作呢? 我猜测ABI是不同的,因为它们的devise或多或less是在同一时间和孤立的。
另外引用“现代64位计算的历史”:
与微软合作的同时,AMD也聘请了开源社区为芯片做准备。 AMD与Code Sorcery和SuSE签订了工具链合作协议(红帽公司已经在IA64工具链端口上使用了英特尔)。 Russell解释说SuSE生产C和FORTRAN编译器,而Code Sorcery生成一个Pascal编译器。 Weber解释说,该公司还与Linux社区合作,准备一个Linux端口。 这一努力是非常重要的:它激励微软继续投资于AMD64 Windows的努力,同时也确保了Linux成为当时重要的操作系统,一旦芯片发布,就可以使用。
Weber甚至说,Linux的工作对于AMD64的成功绝对是至关重要的,因为它使AMD能够在没有任何其他公司的帮助下生产端到端系统。 这种可能性确保了即使其他合作伙伴退出,AMD仍然有最糟糕的生存战略,这反过来又让其他合作伙伴因为害怕自己被抛弃而不得不参与其中。
这表明,即使AMD没有觉得合作必然是MS和Unix之间的最重要的事情,但是拥有Unix / Linux支持是非常重要的。 也许甚至试图说服一方或双方妥协或合作是不值得的努力或风险(?)的刺激他们之一? 也许AMD认为,甚至提出一个共同的ABI可能会延迟或破坏一个更重要的目标,那就是在芯片准备就绪时,只需准备好软件即可。
有人猜测,但我认为ABI不同的主要原因是MS和Unix / Linux方面并没有一起工作的政治原因,AMD并没有把这看作是一个问题。