繁体   English   中英

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,如果程序将printf声明为int printf(const char * restrict format, ...);则它必须工作。 ,因此printf必须使用 C 实现的默认调用约定。

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

此外,在 x86 32 位汇编中定义函数会很棒。

您不太可能在汇编语言中找到高质量的现代printf实现,除非通过编译 C 和/或其他语言的实现获得。 实际上,您不太可能在单个例程中找到定义; printf是一个包含许多子部分的复杂例程,通常其实现分散在多个例程和源文件中。 这个答案这个答案有一些实现的链接。 前者包含一个指向vprintf的 GNU C 库实现(或它的入口点)的链接,它是printf的核心部分。

printf在实际的 C 库中总是使用 CDECL ,因为 STDCALL 对于可变参数函数非常不方便,并且在某些情况下 ISO C 要求它无法正常工作。


ISO C 表示传递额外参数是定义明确的行为,例如printf("%d\n", 1, 2, 3);

printf必须安全地忽略它们,并且表现得像printf("%d\n", 1); . 这排除了像 STDCALL 这样的被调用者弹出约定。 (无论如何这对于任何可变参数函数都是不方便的,因为ret imm16在弹出 [er]IP 后增加 [er]SP 只能用于立即操作数,而不是注册。所以你必须弹出返回地址,复制它超过 args 的最高 4 或 8 个字节,然后从那里ret ,即使您可以准确地计算出位置。)

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

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


此外,在 x86 32 位汇编中定义函数会很棒。

我这样做是出于教育目的。

由于您这样做是为了了解 asm,而不一定是为了创建 C 实现,因此printf是一个非常复杂的 API 实现,对于纯汇编项目可能不是一个好的选择。 (或者可以说,对于实际上不必是 ISO C 的任何现代设计;解析文本格式字符串并通过可变长度的参数列表具有简单性的主要缺点。C++ 人认为每个对象都有一个单独的函数调用你想要输出的类型安全性和东西要好得多)

处理单个type -> string函数通常更容易,例如print_int (十进制)与print_int_hexprint_double (实际上非​​常复杂)与print_c_string (0 终止)与print_buffer (指针,长度)。

作为一个玩具项目,不要对 I/O 格式化功能目标太高。

首先提供一些简单可用的,易于从 asm 调用的。 Irvine32及其WriteDec (unsigned) vs. WriteInt (signed) vs. WriteString是玩具程序的一组输出函数的一个很好的例子。 Irvine32 尤其使用自定义调用约定,其中所有寄存器都保留调用(训练轮模式),并且 arg 在 EAX 或 EDX 中(这非常好;堆栈 args 是愚蠢的,特别是对于只需要一个的函数)

另一个非常相似的例子是 MIPS 模拟器的MARS 系统调用 其中一些设计不佳(或故意给学生带来不便?),比如它的读取字符串没有返回返回值寄存器中的长度,只是将字符留在指向缓冲区中的终止 0 字节(作为 C细绳)。 所以如果你想知道你读了多少,你必须遍历它们寻找第一个0 ,即strlen

这些玩具 API 没有光标移动、没有回声的输入或任何使真实终端和键盘处理方式更加复杂的东西。 或任何指定格式的方法,如printf%020d用前导零填充到 20 位长。

如果你想编写自己的输入/输出函数,你可以考虑是否希望它们能够与直接使用低级函数的代码轻松混合,或者它们是否像 C stdio 那样自己做缓冲,应该是被视为不透明的 I/O 层,因此程序不应同时使用它们较低级别的 OS 系统调用。

根据您想要的复杂程度,可能使用 args 来指定宽度限制,或者根据具体情况为项目定制。 (毕竟,如果你想要可维护性和易于代码重用,你不会一开始就选择 asm。所以只需在执行它的地方实现 I/O 细节,而不是为调用者构建一个灵活的机制来请求任何格式)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM