C 中的 printf() 使用什么调用约定?

What calling convention does printf() in C use?

所以我一直在练习使用 CDECL 和 STDCALL 调用约定在 FASMW 中编写简单的子例程,这让我想知道 C 中的 printf 函数会使用什么。

此外,在 x86 32 位汇编中定义函数会很棒。如果这还不算过分的话。

根据 C 2018 7.1.4 2 和 7.21.6.3 1,如果程序将其声明为 int printf(const char * restrict format, ...);printf 必须工作,因此 printf 必须与默认调用一起工作C 实现的约定。

一个 C 实现可能提供 printf 的多个实现,因此 #include <stdio.h> 提供了 printf 函数或宏的替代声明,指定了 printf 的第二个实现printf。但是,必须提供上述主要实现。

Also, a definition of the function in x86 32 bit Assembly would be great.

您不太可能找到 high-quality 汇编语言 printf 的现代实现,除非是通过 C and/or 其他语言编译实现获得的。实际上,您不太可能在单个例程中找到定义。 printf 是一个包含许多子部分的复杂例程,通常其实现分散在多个例程和源文件中。 and this one 对某些实现有 link。前者包括 link 到 vprintf 的 GNU C 库实现(或其入口点),printf.

的核心部分

printf 总是在 real-world C 库中使用 CDECL,因为 STDCALL 对于可变函数非常不方便,并且无法在某些情况下 ISO C 需要它。


ISO C 说它是 well-defined 传递额外参数的行为,比如 printf("%d\n", 1, 2, 3);

printf 必须安全地忽略它们,并表现得像 printf("%d\n", 1);。这排除了像 STDCALL 这样的 callee-pops 约定。 (这对于任何可变参数函数来说都是不方便的,因为 ret imm16 在弹出 [er]IP 后递增 [er]SP 仅适用于立即操作数,而不是寄存器。所以你必须弹出 return 地址,将其复制到 args 的最高 4 或 8 个字节,并从那里 ret,即使您可以准确计算出位置。)

主流调用约定不会单独将 args 的数量(或它们在堆栈中的字节大小)传递给可变参数函数,或使用任何类型的标记值,因此无法实现 printf 可以找出实际传递了多少个参数。 args 必须匹配与格式字符串引用一样多的 args 的格式字符串,但没有要求不传递超出此范围的 args。

这就是为什么 Windows C ABI/实现总是对可变参数函数使用像 CDECL 这样的约定,即使它们默认为具有固定参数数量的函数使用 STDCALL。 (32 位 FASTCALL 也是 callee-pops;Windows x64 不是,当前的 MS 文档有时称它为 x64 fastcall 或“快速调用约定”。)


Also, a definition of the function in x86 32 bit Assembly would be great.

I'm doing this for educational purposes.

由于您这样做是为了了解 asm,不一定是为了创建 C 实现,printf 实现起来相当复杂 API,可能不是纯汇编的好选择项目。 (或者可以说对于任何实际上不必是 ISO C 的现代设计;解析文本格式字符串并通过 variable-length 参数列表对于简单性来说有很大的缺点。C++ 人认为单独的函数调用你想要输出的每个对象在类型安全和其他方面都要好得多)

处理单个 type -> string 函数通常更容易,例如 print_int(十进制)vs. print_int_hex vs. print_double(实际上它本身非常复杂) vs. print_c_string (0 终止) vs. print_buffer (指针, 长度).

作为一个玩具项目,不要对您的 I/O 格式化功能抱有太大的目标。

先提供一些简单可用的,方便asm调用。 Irvine32 及其 WriteDec(无符号)vs. WriteInt(有符号)vs. WriteString 是玩具程序的一组输出函数的一个不错的例子。 Irvine32 特别使用自定义调用约定,其中所有寄存器都是 call-preserved(training wheels 模式),arg 在 EAX 或 EDX 中(这非常好;堆栈 args 是愚蠢的,特别是对于只接受一个的函数)

另一个非常相似的例子是 MIPS 模拟器的 MARS system-calls。其中一些设计很糟糕(或者故意给学生带来不便?),比如它的 read-string 没有 return 写入 return-value 寄存器中的长度,只留下 [=83= 中的字符] 带有终止字节 0 的缓冲区(作为 C 字符串)。所以如果你想知道你读了多少,你必须遍历它们寻找第一个 0,即 strlen.

这些玩具 APIs 没有 cursor-movement,没有回声的输入,或任何使实际终端和键盘处理方式更加复杂的东西。 或者任何指定格式的方式,如 printf%020d 以前导零填充到 20 位长。

如果您想编写自己的 input/output 函数,您可以考虑是否希望它们能够轻松地与直接使用 lower-level 函数的代码混合,或者他们是否自己做像 C stdio 一样拥有缓冲,并且应该被视为不透明的 I/O 层,因此程序不应使用它们 and lower-level OS 系统调用同时。

根据您想要的复杂程度,可以使用 args 来指定宽度限制,或者根据具体情况为项目定制。 (毕竟,如果你想要可维护性和简单code-reuse,你不会首先选择asm。所以只是在做的地方实现I/O细节,而不是构建一个灵活的机制供来电者请求任何格式)