我正在研究一些遗留的C代码。原始代码是在90年代中期编写的,目标是那个时代的Solaris和Sun的C编译器。当前版本在GCC 4下编译(虽然有许多警告),它似乎有效,但我正在努力整理它 - 我想尽可能多地挤出潜在的错误,因为我确定了可能需要的东西。使其适应64位平台以及编译器以外的编译器。
我在这方面的主要活动之一是确保所有函数都有完整的原型(许多人都没有),在这种情况下,我发现了一些调用函数(以前没有原型)的代码,参数更少而不是函数定义声明。函数实现确实使用了缺少参数的值。
示例:
impl.c:
int foo(int one, int two) {
if (two) {
return one;
} else {
return one + 1;
}
}
client1.c:
extern foo();
int bar() {
/* only one argument(!): */
return foo(42);
}
client2.c:
extern int foo();
int (*foop)() = foo;
int baz() {
/* calls the same function as does bar(), but with two arguments: */
return (*foop)(17, 23);
}
问题:是否定义了缺少参数的函数调用的结果?如果是这样,函数将为未指定的参数接收什么值?否则,Sun C编译器的ca. 1996(对于Solaris,而不是VMS)展示了一种可预测的特定于实现的行为,我可以通过向受影响的调用添加特定的参数值来模拟它吗?
答案 0 :(得分:5)
问题:是否定义了缺少参数的函数调用的结果?
我会说不......原因在于我认为该函数将作为第二个参数运行,但如下所述,第二个参数可能只是垃圾。
如果是这样,函数会为未指定的参数接收什么值?
我认为收到的值未定义。这就是你可以拥有UB的原因。
我知道有两种通用的参数传递方式......(维基百科在calling conventions上有一个很好的页面)
因此,当你给一个模块定义一个带有“......未指定(但不是可变)数量的参数......”(extern def)的函数时,它不会放置你给它的那么多参数(在这种情况下1)在寄存器或堆栈位置,实际函数将查看以获取参数值。因此,错过的第二个参数的第二个区域基本上包含随机垃圾。
编辑:根据我发现的其他堆栈线程,我会推荐上面说的extern声明一个没有参数的函数来声明一个带有“未指定(但不是可变)的参数数量的函数”。当程序跳转到函数时,该函数假定参数传递机制已被正确遵守,因此要么在寄存器或堆栈中查找并使用它找到的任何值...将它们设置为正确。
否则,Sun C编译器的ca. 1996年(对于Solaris,而非VMS)展出了>>可预测的特定于实现的行为
您必须检查编译器文档。我怀疑它...外部定义将完全被信任所以我怀疑寄存器或堆栈,取决于参数传递机制,将被正确初始化...
答案 1 :(得分:4)
如果参数的数量或类型(在默认参数提升之后)与实际函数定义中使用的不匹配,则行为是未定义的。
实践中会发生什么取决于实施。丢失参数的值将无法有意义地定义(假设访问缺失参数的尝试不会出现段错误),即它们将保留不可预测且可能不稳定的值。
程序是否能够在不正确的调用中存活也取决于调用约定。一种“经典”C调用约定,其中调用者负责将参数放入堆栈并从那里移除它们,在出现此类错误时将更不容易崩溃。关于使用CPU寄存器传递参数的调用也可以这样说。同时,函数本身负责清理堆栈的调用约定几乎会立即崩溃。
答案 2 :(得分:1)
过去bar
函数不太可能产生一致的结果。我唯一可以想象的是它总是在新的堆栈空间上调用,并且在启动过程时清除堆栈空间,在这种情况下第二个参数将为0.或者返回one
和之间的差异one+1
对应用程序的更大范围没有太大影响。
如果它真的像你在你的例子中所描绘的那样,那么你正在看一个大胖子。在遥远的过去,有一种编码风格,其中通过指定比传递更多的参数来实现vararg
函数,但就像现代varargs
一样,您不应该访问任何未实际传递的参数。
答案 3 :(得分:1)
我假设此代码已在Sun SPARC体系结构上编译和运行。根据{{3}}古老的SPARC网页:“寄存器%o0
- %o5
用于传递给过程的前六个参数。”
在您的示例中,如果函数需要两个参数,并且未在调用站点指定第二个参数,那么调用时,寄存器%01
可能始终具有合理的值。
如果您可以访问原始可执行文件并且可以在不正确的呼叫站点周围反汇编代码,则可以推断出呼叫发生时%o1
的值。或者您可以尝试在SPARC仿真器上运行原始可执行文件,例如QEMU。无论如何,这不是一项微不足道的任务!