ChatGPT解决这个技术问题 Extra ChatGPT

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

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 我知道这些调用约定通常有何不同,并且如果需要,我知道在哪里可以找到详细信息。我想知道的是为什么。

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

好吧,仅对于第一个寄存器: rcx: ecx 是 msvc __thiscall x86 约定的“this”参数。所以可能只是为了方便将他们的编译器移植到 x64,他们从 rcx 作为第一个开始。其他一切都会有所不同,这只是最初决定的结果。
@Chris:我在下面添加了对 AMD64 ABI 补充文档的引用(以及一些解释它实际上是什么)。
我没有从 MS 中找到理由,但我发现了一些讨论 here

C
Community

在 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(返回值)和 RCXRDXR8R9 (arg[0..3]) 是可以理解的选择四个 注册参数。

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

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

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

因此,如果您希望 六个 寄存器传入参数,并且为其中四个选择 RCXRDXR8R9 是合乎逻辑的,你应该选择另外两个?

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

因此,如果您必须将 RSI / RDI 作为参数寄存器,它们应该是哪些参数?

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

因此,在某种程度上,UN*X 和 Win64 的不同之处仅在于 UN*X 在有意选择的 RSI/RDI 寄存器中“预先”添加了两个附加参数,以自然选择 RCX 中的四个参数,{ 4}、R8R9

除此之外 ...

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


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

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

阅读 AMD64 邮件列表中的一些旧线程很有趣,因为 AMD 架构师对此很活跃。例如,选择寄存器名称是困难的部分之一:AMD 考虑过 renaming the original 8 registers r0-r7, or calling the new registers UAX etc.

此外,来自内核开发人员的反馈确定了构成 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 是一种胜利。之所以选择它们,是因为它们是唯一未被任何通用指令隐式使用的“遗留”寄存器。 (rep 字符串、移位计数和 mul/div 输出/输入涉及其他所有内容)。

通用指令强制您使用的所有寄存器都不是调用保留的(请参阅上一点),因此想要使用可变计数移位或除法的函数可能必须将函数 args 移动到其他地方,但不必保存/恢复调用者的值。 cmpxchg16b 和 cpuid 需要 RBX,但很少使用,所以不是一个大因素。 (cmpxchg16b 不是原始 AMD64 的一部分,但 RBX 仍然是显而易见的选择。cmpxchg8b 存在,但已被 qword cmpxchg 淘汰)

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

(背景:syscall / sysret 不可避免地会破坏 rcx(with rip)和 r11(with RFLAGS),所以当 syscall 时内核无法看到最初在 rcx 中的内容跑了。)

选择内核系统调用 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 优化比任何其他考虑因素都重要。我认为他们做得很好。我不完全确定是否返回打包到寄存器中的结构,而不是不同注册表中的不同字段。我猜想通过值传递它们而不实际对字段进行操作的代码会以这种方式获胜,但是解包的额外工作似乎很愚蠢。他们可以有更多的整数返回寄存器,而不仅仅是 rdx:rax,所以返回一个有 4 个成员的结构可以在 rdi、rsi、rdx、rax 或其他东西中返回它们。

他们考虑在向量 regs 中传递整数,因为 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 以便相同的源可以更容易构建 32 位或 64 位版本的函数。

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


我想我已经在 Raymond Chen 的博客上读到了关于在 MS 端进行基准测试后选择这些寄存器的理由,但我再也找不到了。但是这里解释了有关 homezone 的一些原因 blogs.msdn.microsoft.com/oldnewthing/20160623-00/?p=93735 blogs.msdn.microsoft.com/freik/2006/03/06/…
@phuclv:另见Is it valid to write below ESP?。 Raymond 对我的回答的评论指出了一些我不知道的 SEH 细节,这解释了为什么 x86 32/64 Windows 目前没有事实上的红色区域。他的博客文章为我在那个答案中提到的相同代码页面处理程序可能性提供了一些似是而非的案例:) 所以是的,雷蒙德在解释它方面做得比我做得更好(不足为奇,因为我一开始对 Windows 知之甚少),非 x86 的红色区域大小表非常整洁。
@PeterCordes '大概是因为它是唯一没有被任何指令隐式使用的其他 reg' r0-r7 中的任何指令都没有隐式使用的寄存器是什么?我没想到,这就是为什么他们有特殊的名字,如 rax、rcx 等。
@SouravKannanthaB:是的,所有遗留寄存器都有一些隐含的用途。 (Why are rbp and rsp called general purpose registers?) 我真正的意思是说,没有您想要使用的通用指令用于其他原因(如shl rax, clmul ) 要求您使用 RBX 或 RBP。只有 cmpxchg16bcpuid 需要 RBX,并且 RBP 仅由 leave 隐式使用(以及不可用的慢 enter 指令)。因此,对于 RBP,唯一的隐含用途只是操纵 RBP,如果不将其用作帧指针,则不是您想要的
M
Michael Burr

请记住,Microsoft 最初“官方对 AMD64 的早期工作不置可否”(来自 Matthew Kerner 和 Neil Padgett 的 "A History of Modern 64-bit Computing"),因为他们是英特尔在 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 并不认为这是一个问题。


对政治的看法很好。我同意这不是 AMD 的错或责任。我责怪微软选择了更糟糕的调用约定。如果他们的调用约定变得更好,我会有些同情,但他们不得不从最初的 ABI 更改为 __vectorcall,因为在堆栈上传递 __m128 很糟糕。为某些向量 reg 的低 128b 保留调用语义也很奇怪(部分原因是英特尔最初没有使用 SSE 设计可扩展的保存/恢复机制,但仍然没有使用 AVX。)
对于 ABI 有多好,我真的没有任何专业知识或知识。我只是偶尔需要知道它们是什么,以便我可以在程序集级别理解/调试。
一个好的 ABI 可以最大限度地减少代码大小和指令数量,并通过避免额外的内存往返来保持依赖链的低延迟。 (对于 args,或对于需要溢出/重新加载的本地人)。有权衡。 SysV 的红色区域在一个地方(内核的信号处理程序调度程序)需要一些额外的指令,这对于叶函数来说具有相对较大的好处,即不必调整堆栈指针以获得一些暂存空间。所以这是一个明显的胜利,几乎为零的缺点。在为 SysV 提出后,几乎没有任何讨论就被采用了。
@dgnuff:对,这就是 Why can't kernel code use a Red Zone 的答案。中断使用内核堆栈,而不是用户空间堆栈,即使它们在 CPU 运行用户空间代码时到达。内核不信任用户空间堆栈,因为同一用户空间进程中的另一个线程可以修改它,从而接管内核的控制权!
@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)做同样的事情。
c
cHao

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

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


所有调用约定都有一些寄存器被指定为临时寄存器,一些被保留,如 Win64 上的 ESI/EDI 和 RSI/RDI。但这些是通用寄存器,微软本可以毫无问题地选择以不同方式使用它们。
@Somejan:当然,如果他们想重写整个 API 并拥有两个不同的操作系统。不过,我不会称其为“没有问题”。几十年来,MS 已经就 x86 寄存器将做什么和不做什么做出了某些承诺,并且它们一直或多或少地保持一致和兼容。他们不会仅仅因为 AMD 的某些法令而将所有这些都扔到窗外,尤其是一项如此武断且超出“构建处理器”领域的法令。
@Somejan:AMD64 UN*X ABI 始终就是这样 - 一个 UNIX-specific 部分。文档 x86-64.org/documentation/abi.pdf 的标题为 System V Application Binary Interface, AMD64 Architecture Processor Supplement 是有原因的。 (常见的)UNIX ABI(一个多卷集合,sco.com/developers/devspecs)为特定于处理器的第 3 章留下了一个部分 - 补充 - 这是特定的函数调用约定和数据布局规则处理器。
@Somejan:Microsoft Windows 从未尝试过特别接近 UN*X,在将 Windows 移植到 x64/AMD64 时,他们只是选择扩展他们的 自己的 __fastcall 调用约定。您声称 Win32/Win64 不兼容,但仔细观察:对于采用 两个 32 位参数并返回 32 位的函数,Win64 和 Win32 __fastcall 实际上 100% 兼容(传递两个 32 位参数的相同规则,相同的返回值)。甚至一些二进制(!)代码也可以在两种操作模式下工作。 UNIX 方面完全打破了“旧方式”。有充分的理由,但休息就是休息。
@Olof:这不仅仅是编译器的事情。当我在 NASM 中做独立的事情时,我遇到了 ESI 和 EDI 的问题。 Windows 绝对关心这些寄存器。但是,是的,如果您先保存它们并在 Windows 需要它们之前恢复它们,您就可以使用它们。

关注公众号,不定期副业成功案例分享
关注公众号

不定期副业成功案例分享

领先一步获取最新的外包任务吗?

立即订阅