何时使用特定的调用约定
When to use a certain calling convention
x86-64 中是否有关于函数何时应遵守 System V 准则以及何时无关紧要的准则?这是对答案 的回应,其中提到使用其他调用约定来简化 internal/local 函数。
# gcc 32-bit regparm calling convention
is_even: # input in RAX, bool return value in AL
not %eax # 2 bytes
and , %al # 2 bytes
ret
# custom calling convention:
is_even: # input in RDI
# returns in ZF. ZF=1 means even
test , %dil # 4 bytes. Would be 2 for AL, 3 for DL or CL (or BL)
ret
请参阅 了解上下文。
例如,是否应该使用:
- 仅在被外部高级 C 函数调用时才需要。
- 仅当 label/function 为
globl
时才需要。
或者关于何时“随心所欲”使用寄存器以及何时根据 System V 约定使用它们的最佳指南是什么?
这取决于你用 asm 编写什么样的东西。如果您正在编写 纯 用 asm 编写的小型 self-contained asm 程序,例如 16 位引导加载程序,一定要继续为所有内容制定自定义调用约定(如果你做任何功能,而不仅仅是内联)。例如看看 @ecm's legacy BIOS bootloader 中的 disp_ax_hex
函数作为一个有趣的例子,并查看评论中关于让 disp_al
破坏更多寄存器的讨论。
我想说的是,大多数其他代码(包含 compiler-generated 代码的较大程序的一部分)通常都遵循标准调用约定; x86-64 System V 设计得很好。仅考虑对“私有”辅助函数使用自定义约定,尤其是那些仅从另一个函数的不同部分调用的辅助函数。通常这些调用者都在一个文件中,而不是 global
.
可以有用地 return 2 个独立值的函数绝对可以从自定义调用约定中受益,以利于 asm 调用者。
例如Cmemcmp
没有return第一个差的位置,只有-/0/+。这是really stupid and useless,剥夺了我们利用现有hand-optimized asm 找到不匹配位置的好方法。在 asm 中,我们可以很容易地只 return 两者,就像指向 RDI 中位置的指针和 cmp
导致 FLAGS.
在这种情况下,您可以编写一个与 x86-64 System V 调用约定 100% 兼容的 memcmp
函数(因此您需要 zero-extend 两个字节并执行dword sub
,而不仅仅是一个字节 cmp
),RDI 输出作为 asm 调用者的奖励。
您链接的我的回答部分是我决定提及的一种随机想法。这不是你通常做的事情(虽然一开始也不是手工编写 asm),你永远不想把 test
单独放在一个函数中,除非作为 code-golf 的解决方案锻炼。这就是它背后的真正想法:该函数的大部分“成本”只是因为你把它变成了一个函数,而在现实生活中你总是内联一些如此简单的东西。
通常你不会首先编写小函数。你只需在较大函数中间的几条指令中实现逻辑,就像编译器一样内联一个小的辅助函数。然后,为您的所有功能遵循平台 ABI(在本例中为 x86-64 System V)并不昂贵。
将逻辑优化为 return a 0 / 1 int
(不仅仅是 8 位 bool
), 和 坚持标准的调用约定,可能是一个有趣的练习,但通常没有用,除非你真正的 use-case 想要做类似 even_count += is_even(x);
的事情。但在那种情况下,你应该做odds += x&1;
,并在你需要的时候最后计算一次偶数,如even = total-odd
。除了消除 call/return 开销之外,内联还允许考虑将微小函数的逻辑优化为实际 use-case.
的一部分
私有辅助函数有一个use-case:
有时您想重复几个指令块作为一个更大函数的私有“辅助”函数,例如使用mov eax, 1
/ call func
/ 做点别的事 / mov eax 123
/ call func
。然后你可以把“函数”想成更像是一个循环体或更大函数内部的东西,而调用者更像是自定义迭代。
有时使用宏重复一段代码是有意义的,但如果序列有点长,会使您的代码膨胀。 (每次使用时宏都会扩展;不像 5 字节 call rel32
。)
需要说明的是,is_even
太简单了,将它放在自己的函数中是没有意义的。 调用一个函数而不仅仅是 运行 test , %reg
/ jz
或 jnz
对于某些寄存器来说将是完全疯狂和混淆的,并且更大和更慢。或者 and , %eax
从 reg 中得到一个 0/1 整数是奇数,你可以使用 add
来计算奇数。 (total-odd 最后算偶数)。大多数程序员也不会将其包装在宏中;理解二进制是汇编语言的标准,对 test
或 jcc 指令的简单注释来描述语义含义 (# if odd
) 就足够了。
理论上,对于一个纯粹的hand-written程序,你可以在每个函数的case-by-case基础上使用任何最方便的调用约定,记录评论。但通常情况下,与遵循标准调用约定相比,好处很小,并且跟踪哪些函数破坏了哪些注册并希望它的 args 很快就会成为 general-purpose 具有多个不同调用者的函数的维护噩梦除了被调用的函数之外彼此相关。
当然,出于同样的原因,我们使用 high-level 语言编写应用程序,并且很少真正手动编写 any asm。事实上你是提议在 asm 中手工编写函数意味着值得考虑“像编译器一样思考”是否过于局限。这就是我的 codegolf answer 的要点:如果从函数中挤出每个最后一个字节或循环是值得的,那么整个程序(或至少它的调用者)可能会以类似的方式编写。
现在用 asm 编写整个程序的唯一好理由是优化它们 machine-code 大小的废话,例如演示场景。 https://en.wikipedia.org/wiki/Demoscene。 (或者如果“程序”真的是一个引导加载程序,它在 OS 之前没有/运行。)
到那时,不要让 ABI 和调用约定限制您的优化。而且您的程序通常足够小,以至于可以跟踪不同的函数及其调用约定,特别是如果它们具有某种逻辑意义(或者大部分匹配它们的调用者恰好保留正确变量的寄存器)。
x86-64 中是否有关于函数何时应遵守 System V 准则以及何时无关紧要的准则?这是对答案
# gcc 32-bit regparm calling convention
is_even: # input in RAX, bool return value in AL
not %eax # 2 bytes
and , %al # 2 bytes
ret
# custom calling convention:
is_even: # input in RDI
# returns in ZF. ZF=1 means even
test , %dil # 4 bytes. Would be 2 for AL, 3 for DL or CL (or BL)
ret
请参阅
例如,是否应该使用:
- 仅在被外部高级 C 函数调用时才需要。
- 仅当 label/function 为
globl
时才需要。
或者关于何时“随心所欲”使用寄存器以及何时根据 System V 约定使用它们的最佳指南是什么?
这取决于你用 asm 编写什么样的东西。如果您正在编写 纯 用 asm 编写的小型 self-contained asm 程序,例如 16 位引导加载程序,一定要继续为所有内容制定自定义调用约定(如果你做任何功能,而不仅仅是内联)。例如看看 @ecm's legacy BIOS bootloader 中的 disp_ax_hex
函数作为一个有趣的例子,并查看评论中关于让 disp_al
破坏更多寄存器的讨论。
我想说的是,大多数其他代码(包含 compiler-generated 代码的较大程序的一部分)通常都遵循标准调用约定; x86-64 System V 设计得很好。仅考虑对“私有”辅助函数使用自定义约定,尤其是那些仅从另一个函数的不同部分调用的辅助函数。通常这些调用者都在一个文件中,而不是 global
.
可以有用地 return 2 个独立值的函数绝对可以从自定义调用约定中受益,以利于 asm 调用者。
例如Cmemcmp
没有return第一个差的位置,只有-/0/+。这是really stupid and useless,剥夺了我们利用现有hand-optimized asm 找到不匹配位置的好方法。在 asm 中,我们可以很容易地只 return 两者,就像指向 RDI 中位置的指针和 cmp
导致 FLAGS.
在这种情况下,您可以编写一个与 x86-64 System V 调用约定 100% 兼容的 memcmp
函数(因此您需要 zero-extend 两个字节并执行dword sub
,而不仅仅是一个字节 cmp
),RDI 输出作为 asm 调用者的奖励。
您链接的我的回答部分是我决定提及的一种随机想法。这不是你通常做的事情(虽然一开始也不是手工编写 asm),你永远不想把 test
单独放在一个函数中,除非作为 code-golf 的解决方案锻炼。这就是它背后的真正想法:该函数的大部分“成本”只是因为你把它变成了一个函数,而在现实生活中你总是内联一些如此简单的东西。
通常你不会首先编写小函数。你只需在较大函数中间的几条指令中实现逻辑,就像编译器一样内联一个小的辅助函数。然后,为您的所有功能遵循平台 ABI(在本例中为 x86-64 System V)并不昂贵。
将逻辑优化为 return a 0 / 1 int
(不仅仅是 8 位 bool
), 和 坚持标准的调用约定,可能是一个有趣的练习,但通常没有用,除非你真正的 use-case 想要做类似 even_count += is_even(x);
的事情。但在那种情况下,你应该做odds += x&1;
,并在你需要的时候最后计算一次偶数,如even = total-odd
。除了消除 call/return 开销之外,内联还允许考虑将微小函数的逻辑优化为实际 use-case.
私有辅助函数有一个use-case:
有时您想重复几个指令块作为一个更大函数的私有“辅助”函数,例如使用mov eax, 1
/ call func
/ 做点别的事 / mov eax 123
/ call func
。然后你可以把“函数”想成更像是一个循环体或更大函数内部的东西,而调用者更像是自定义迭代。
有时使用宏重复一段代码是有意义的,但如果序列有点长,会使您的代码膨胀。 (每次使用时宏都会扩展;不像 5 字节 call rel32
。)
需要说明的是,is_even
太简单了,将它放在自己的函数中是没有意义的。 调用一个函数而不仅仅是 运行 test , %reg
/ jz
或 jnz
对于某些寄存器来说将是完全疯狂和混淆的,并且更大和更慢。或者 and , %eax
从 reg 中得到一个 0/1 整数是奇数,你可以使用 add
来计算奇数。 (total-odd 最后算偶数)。大多数程序员也不会将其包装在宏中;理解二进制是汇编语言的标准,对 test
或 jcc 指令的简单注释来描述语义含义 (# if odd
) 就足够了。
理论上,对于一个纯粹的hand-written程序,你可以在每个函数的case-by-case基础上使用任何最方便的调用约定,记录评论。但通常情况下,与遵循标准调用约定相比,好处很小,并且跟踪哪些函数破坏了哪些注册并希望它的 args 很快就会成为 general-purpose 具有多个不同调用者的函数的维护噩梦除了被调用的函数之外彼此相关。
当然,出于同样的原因,我们使用 high-level 语言编写应用程序,并且很少真正手动编写 any asm。事实上你是提议在 asm 中手工编写函数意味着值得考虑“像编译器一样思考”是否过于局限。这就是我的 codegolf answer 的要点:如果从函数中挤出每个最后一个字节或循环是值得的,那么整个程序(或至少它的调用者)可能会以类似的方式编写。
现在用 asm 编写整个程序的唯一好理由是优化它们 machine-code 大小的废话,例如演示场景。 https://en.wikipedia.org/wiki/Demoscene。 (或者如果“程序”真的是一个引导加载程序,它在 OS 之前没有/运行。)
到那时,不要让 ABI 和调用约定限制您的优化。而且您的程序通常足够小,以至于可以跟踪不同的函数及其调用约定,特别是如果它们具有某种逻辑意义(或者大部分匹配它们的调用者恰好保留正确变量的寄存器)。