所以我一直在练习使用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, ...);
,则CCD_2必须工作,因此printf
必须使用C实现的默认调用约定。
C实现可能提供printf
的多个实现,使得#include <stdio.h>
提供指定printf
的第二实现的printf
函数或宏的替代声明。然而,必须提供上述的主要实现方式。
此外,在x86 32位程序集中定义该函数也很好。
除非通过编译C和/或其他语言的实现,否则您不太可能在汇编语言中找到printf
的高质量现代实现。实际上,你不太可能在一个例程中找到一个定义;printf
是一个具有许多子部分的复杂例程,其实现通常分散在多个例程和源文件中。这个答案和这个答案都有一些实现的链接。前者包括指向vprintf
的GNU C库实现的链接(或其入口点),printf
的核心部分。
printf
在实际的C库中总是使用CDECL,因为STDCALL对可变函数非常不方便,并且在某些情况下ISO C要求它无法正确工作。
ISO C表示,传递额外的args是定义良好的行为,如printf("%dn", 1, 2, 3);
printf
必须安全地忽略它们,并像printf("%dn", 1);
一样行事。这排除了像STDCALL这样的被调用者pop约定。(无论如何,这对任何变元函数来说都是不方便的,因为在弹出[er]IP后递增[er]SP的ret imm16
只能用于立即操作数,而不能用于寄存器。因此,您必须弹出返回地址,将其复制到最高的4或8字节args上,然后从那里复制ret
,即使您可以准确地计算出位置。)
主流的调用约定不会将args的数量(或其在堆栈上的大小(以字节为单位))单独传递给可变函数,也不会使用任何类型的sentinel值,因此printf
的实现无法确定实际传递了多少args。对于与格式字符串引用一样多的args,args必须与格式字符串匹配,但不要求不传递超过此数量的args。
这就是为什么Windows C ABI/实现总是对变量函数使用类似CDECL的约定,即使它们对具有固定参数数的函数默认为STDCALL。(32位FASTCALL也被称为pops;Windows x64不是,当前的MS文档有时称之为x64快速调用或"快速调用约定"。)
此外,在x86 32位程序集中定义函数也很棒。
我这样做是为了教育目的。
由于您这样做是为了了解asm,而不一定是为了创建C实现,printf
是一个非常复杂的API实现,对于纯汇编项目来说可能不是一个好选择。(或者可以说,对于任何不必真正是ISO C的现代设计来说;解析文本格式字符串并浏览可变长度的参数列表对于简单性来说都有很大的缺点。C++人士认为,为你想要输出的每个对象单独调用函数对类型安全和其他方面来说要好得多)
处理单个type -> string
函数通常更容易,比如print_int
(十进制)与print_int_hex
与print_double
(实际上本身非常复杂)与print_c_string
(0终止)与CCD26(指针,长度)。
作为一个玩具项目,不要对I/O格式化功能目标过高
首先提供一些简单可用的,很容易从asm调用。Irvine32的WriteDec
(无符号)与WriteInt
(有符号)和WriteString
是玩具程序输出函数集的一个不错的例子。Irvin32特别使用了一种自定义调用约定,其中所有寄存器都保留调用(训练轮模式),并且arg在EAX或EDX中(这非常好;堆栈arg是哑的,尤其是对于只使用一个的函数)
另一个非常相似的例子是MARS系统调用MIPS模拟器。其中一些设计不好(或者故意给学生带来不便?),比如它的读取字符串没有在返回值寄存器中返回长度,只是将指向缓冲区中的字符留有一个终止的0字节(作为C字符串)。因此,如果你想知道你读了多少书,你必须循环浏览它们,寻找第一个0
,即strlen
。
这些玩具API没有光标移动、没有回声的输入,也没有任何使真正的终端和键盘处理更加复杂的东西。或者以任何方式指定格式,如printf
的%020d
,用前导零填充到20位长。
如果你想编写自己的输入/输出函数,你可以考虑是否希望它们能够轻松地与直接使用较低级别函数的代码混合,或者它们是否像C stdio一样进行自己的缓冲,并且应该被视为不透明的I/O层,这样程序就不应该同时使用它们和较低级别的操作系统调用。
根据您想要的复杂程度,可以使用args来指定宽度限制,或者根据具体情况为项目定制。(毕竟,如果你想要可维护性和易于重用的代码,你一开始就不会选择asm。所以,只需在执行操作的地方实现I/O细节,而不是为调用方构建一个灵活的机制来请求任何格式)