我的理解是,对于cdecl调用约定,调用方负责清理堆栈,因此可以传递任意数量的参数。另一方面,stdcall被调用方会清理堆栈,因此无法接收不同数量的参数。我的问题有两个:
cdecl
stdcall
axr492tv1#
stdcall函数不能也得到一个有多少变量的参数,并做同样的事情吗?是的,当然。你可以发明 * 任何 * 调用约定。但那就不再是stdcall了。cdecl函数如何知道它们收到了多少个参数?它们不会。它们假定在调用约定指定的位置找到所需数量的参数。如果缺少这些参数,则这是代码无法观察到的bug。编译以下代码:
printf("%s");
即使它缺少一个参数。结果是未定义的。对于printf风格的函数,编译器通常会发出警告(如果可以的话),这是由于了解函数的内部结构,但这不是一个可以普遍应用的解决方案。如果调用方提供了错误的参数数量或类型,则该行为未定义。
printf
fjnneemd2#
stdcall函数不能也得到一个有多少变量的参数,并做同样的事情吗?如果调用者必须传递一个单独的arg,其中包含要弹出的字节数,那么在调用之后,要做的工作就比add esp, 16或其他操作要多(cdecl风格的caller-pops)。这将完全违背stdcall的目的,stdcall的目的是在每个调用位置保存几个字节的空间,特别是对于不会延迟跨几个调用弹出参数的简单代码生成,或者使用mov存储重新使用由推送分配的空间。(每个函数通常有多个调用站点,因此ret imm16与ret相比多出的2个字节将分摊到这些调用站点上。)
add esp, 16
mov
ret imm16
ret
更糟糕的是,被调用方无法在x86 / x86-64上有效地使用变量数。ret imm16只能处理立即数(常量嵌入在机器代码中),因此要在返回地址上方弹出一个可变字节数,函数将不得不复制堆栈中较高的返回地址,并从那里执行普通的ret。(或者通过将返回地址弹出到寄存器中,使分支返回地址分支预测失效.)
另请参阅:
cdecl函数如何知道它们收到了多少个参数?"他们不会“C语言的设计假设变量函数不知道它们接收了多少个参数,因此函数需要一个格式字符串或sentinel之类的东西来知道要迭代多少个参数。例如,POSIX execl(3)(execve(2)系统调用的 Package 器)接受一个以NULL结尾的char*参数列表。
execl(3)
execve(2)
NULL
char*
因此,调用约定通常不会在提供作为侧通道的计数上浪费代码大小和周期;函数需要的任何信息都将是真实的的C级参数的一部分。
有趣的事实:printf("%d", 1, 2, 3)在C语言中是well-defined behaviour,并且需要它来安全地忽略格式字符串引用的参数之外的参数。因此,使用stdcall并基于format-string进行计算是行不通的。你是对的,如果你想创建一个适用于变量函数的被调用方弹出约定,你需要在某个地方传递一个大小,比如在寄存器中。但是就像我前面所说的,调用方知道正确的数字,所以让调用方管理堆栈会容易得多。而不是让被调用者以后再去挖掘这个额外的参数。这就是为什么现实世界中没有调用约定是这样工作的。
printf("%d", 1, 2, 3)
vhipe2zx3#
在被调用方中传递参数的数目可以清除堆栈约定,但额外参数的额外开销超过了它的有用性。它会浪费额外参数的堆栈空间,并使被调用方的堆栈处理复杂化。发明stdcall的原因是因为它使代码更小。(在x86或其他体系结构上,当寄存器中传递的参数过多时)。x86甚至有一个retn #指令,其中#是要调整的字节数。Windows NT在其开发早期从cdecl切换到stdcall,据说它减少了大小并提高了速度(我相信Larry Osterman在博客上提到了这一点(迷你答案here))。cdecl函数不知道有多少个参数。您可以(在ABI级别上)传递比函数实际使用的参数更多的参数。printf样式函数将使用format参数作为“向导”来逐个访问参数。完成此操作后,还必须通知被调用方每个参数的类型(因此它知道大小,这进而以实现定义的方式允许它遍历参数列表。在Windowsx86上,参数在堆栈上,在遍历堆栈时,您只需要参数size来计算它们的偏移量)。va_list及其在stdarg.h中的宏为C函数访问这些参数提供了帮助。
retn #
#
va_list
pzfprimi4#
我的总结,基于@IInspectable的回答。stdcall函数也可以得到一个变量个数的参数,但这样就不再是stdcall了。cdecl不知道要读取多少个参数,我们假设函数能够根据预先确定的参数数量导出参数的数量,就像printf的格式字符串一样。如果调用者提供的参数少于可以派生的参数,或者是一个意外的类型,那么该行为是未定义的。
4条答案
按热度按时间axr492tv1#
stdcall
函数不能也得到一个有多少变量的参数,并做同样的事情吗?是的,当然。你可以发明 * 任何 * 调用约定。但那就不再是
stdcall
了。cdecl
函数如何知道它们收到了多少个参数?它们不会。它们假定在调用约定指定的位置找到所需数量的参数。如果缺少这些参数,则这是代码无法观察到的bug。编译以下代码:
即使它缺少一个参数。结果是未定义的。对于
printf
风格的函数,编译器通常会发出警告(如果可以的话),这是由于了解函数的内部结构,但这不是一个可以普遍应用的解决方案。如果调用方提供了错误的参数数量或类型,则该行为未定义。
fjnneemd2#
stdcall
函数不能也得到一个有多少变量的参数,并做同样的事情吗?如果调用者必须传递一个单独的arg,其中包含要弹出的字节数,那么在调用之后,要做的工作就比
add esp, 16
或其他操作要多(cdecl风格的caller-pops)。这将完全违背stdcall的目的,stdcall的目的是在每个调用位置保存几个字节的空间,特别是对于不会延迟跨几个调用弹出参数的简单代码生成,或者使用mov
存储重新使用由推送分配的空间。(每个函数通常有多个调用站点,因此ret imm16
与ret
相比多出的2个字节将分摊到这些调用站点上。)更糟糕的是,被调用方无法在x86 / x86-64上有效地使用变量数。
ret imm16
只能处理立即数(常量嵌入在机器代码中),因此要在返回地址上方弹出一个可变字节数,函数将不得不复制堆栈中较高的返回地址,并从那里执行普通的ret
。(或者通过将返回地址弹出到寄存器中,使分支返回地址分支预测失效.)另请参阅:
cdecl
函数如何知道它们收到了多少个参数?"他们不会“
C语言的设计假设变量函数不知道它们接收了多少个参数,因此函数需要一个格式字符串或sentinel之类的东西来知道要迭代多少个参数。例如,POSIX
execl(3)
(execve(2)
系统调用的 Package 器)接受一个以NULL
结尾的char*
参数列表。因此,调用约定通常不会在提供作为侧通道的计数上浪费代码大小和周期;函数需要的任何信息都将是真实的的C级参数的一部分。
有趣的事实:
printf("%d", 1, 2, 3)
在C语言中是well-defined behaviour,并且需要它来安全地忽略格式字符串引用的参数之外的参数。因此,使用
stdcall
并基于format-string进行计算是行不通的。你是对的,如果你想创建一个适用于变量函数的被调用方弹出约定,你需要在某个地方传递一个大小,比如在寄存器中。但是就像我前面所说的,调用方知道正确的数字,所以让调用方管理堆栈会容易得多。而不是让被调用者以后再去挖掘这个额外的参数。这就是为什么现实世界中没有调用约定是这样工作的。vhipe2zx3#
在被调用方中传递参数的数目可以清除堆栈约定,但额外参数的额外开销超过了它的有用性。它会浪费额外参数的堆栈空间,并使被调用方的堆栈处理复杂化。
发明
stdcall
的原因是因为它使代码更小。(在x86或其他体系结构上,当寄存器中传递的参数过多时)。x86甚至有一个retn #
指令,其中#
是要调整的字节数。Windows NT在其开发早期从cdecl
切换到stdcall
,据说它减少了大小并提高了速度(我相信Larry Osterman在博客上提到了这一点(迷你答案here))。cdecl
函数不知道有多少个参数。您可以(在ABI级别上)传递比函数实际使用的参数更多的参数。printf样式函数将使用format参数作为“向导”来逐个访问参数。完成此操作后,还必须通知被调用方每个参数的类型(因此它知道大小,这进而以实现定义的方式允许它遍历参数列表。在Windowsx86上,参数在堆栈上,在遍历堆栈时,您只需要参数size来计算它们的偏移量)。va_list
及其在stdarg.h中的宏为C函数访问这些参数提供了帮助。pzfprimi4#
我的总结,基于@IInspectable的回答。
stdcall
函数也可以得到一个变量个数的参数,但这样就不再是stdcall了。cdecl
不知道要读取多少个参数,我们假设函数能够根据预先确定的参数数量导出参数的数量,就像printf
的格式字符串一样。如果调用者提供的参数少于可以派生的参数,或者是一个意外的类型,那么该行为是未定义的。