如何最好地记住x86-64 System V参数寄存器的顺序?

5
我经常忘记每个系统调用参数需要使用的寄存器,每次忘记时我都会访问this问题。

在x86_64用户空间函数调用中,整数/指针参数的正确顺序是:%rdi%rsi%rdx%rcx%r8%r9。(可变参数函数将取AL = FP参数数量,最多8个)

或者对于系统调用,%rax(系统调用号),以及相同的参数,除了%rcx之外使用%r10

除了每次都谷歌这个问题,有什么更好的方法来记住这些寄存器吗?
1个回答

9
如果您记得C语言中memcpy的参数顺序,以及rep movsb的工作原理,那么就可以大致了解x86-64 System V的使用方法。
设计使得memcpy(dst, src, size)易于实现,使用rep movsb即可,但是在更多的函数中,RCX没有被使用,因为它经常用于变量数量的移位,而不是像RDX一样需要。
然后,R8和R9是前两个“高”寄存器。使用它们需要一个REX前缀,在不需要额外字节的指令中会增加一个字节的代码大小。因此,它们是最后两个参数的明智选择。(Windows x64也使用R8、R9作为最后2个寄存器参数)。
实际的设计过程涉及将指令计数和编译某个东西(可能是SPECcpu)的代码大小成本权衡最小化,使用当时的AMD64端口GCC。我不知道将memcpy内联为“rep movsb”是否相关,或者当时的glibc是否以这种方式实现,或其他信息。
我的回答为什么Windows64使用与x86-64上所有其他操作系统不同的调用约定?引用了一些调用约定设计决策的来源。(来自GCC开发人员的早期x86-64.org邮件列表帖子,特别是Jan Hubicka在尝试了几个寄存器顺序后提出了这个顺序。)
特别值得注意的是记住RDX,RCX部分顺序的是这段引用:
“我们试图避免在序列中早期使用RCX,因为它是常用于特殊目的的寄存器,例如EAX,因此它有同样的目的,即在序列中缺失。此外,它不能用于系统调用,我们希望使系统调用序列尽可能地匹配函数调用序列。”

用户空间与系统调用的区别:

R10替代了RCX在系统调用约定中的位置,因为syscall指令本身会破坏RCX(使用它来保存RIP,避免使用用户空间堆栈,并且无法使用内核堆栈,因为它将堆栈切换留给软件处理)。就像它使用R11来保存RFLAGS一样。

尽可能保持相似性可以让libc包装器只需mov %rcx, %r10,而不需要移动多个参数以填补空缺。 R10是在R8和R9之后的下一个可用寄存器。


备选方案:一个助记符:

Diane的silk dressc售价$89

(建议来源于CS:APP博客)


如果r8和r9需要额外的一个字节来表示,为什么会选择它们呢?如果可能的话,难道不应该使所有6个寄存器都使用少一个字节来表示吗? - rayaantaneja
1
大多数函数没有六个参数,所以这是一种折衷。除了堆栈指针之外,只有7个寄存器不需要REX前缀进行访问。在循环中使用其中2个寄存器(RBP和RBX)作为调用保留是更好的选择,以便在调用函数的循环中可以更小的代码,如果任何循环变量不需要64位操作数大小(这将要求任何寄存器都需要一个REX前缀)。将RAX保持未使用用于传递参数也是有意义的,这样它可以立即成为非可变函数中的暂存寄存器。(如果可能有任何XMM参数,则可变函数必须在保存XMM参数之前检查AL)。 - Peter Cordes
如果我理解正确的话,这是因为没有剩余非REX前缀寄存器可用,因为它们正在被用于另一个特定/特殊目的。我是对的吗?此外,现在我想知道为什么选择将6个参数传递到寄存器中,而不是7或8个。选择6是否是随意的,还是有一个“最佳”选择的原因? - rayaantaneja
1
@rayaantaneja:没错,唯一一个在调用约定中没有任何用途的被调用破坏寄存器是R11。其余的要么是调用保留(在非叶子函数中进行多次调用时避免溢出/重新加载非常有用,例如在循环中),要么已经用于参数传递。几个堆栈参数并不是灾难性的。然而这并不是随机的;Jan Hubicka在设计它时对GCC输出进行了一些静态分析(指令计数),请参见本答案中的链接。(我的答案和对其他Q&A的评论提到了效率)。 - Peter Cordes
1
@rayaantaneja:另请参阅为什么不将函数参数存储在XMM向量寄存器中?,讨论调用约定的权衡。还有被调用者和调用者保存的寄存器是什么? - 在良好的约定中,您需要两者的混合。关于R11,有一个寄存器可以由“跳板”或包装器函数明确使用作为临时寄存器而不干扰args或返回值是很有用的。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接