C函数调用参数过少

15

我正在处理一些老旧的C代码。原始代码编写于90年代中期,针对Solaris和当时的Sun C编译器。当前版本可以在GCC 4下编译(虽然有许多警告),并且似乎可以工作,但我正在尝试整理它 - 我想尽可能排除潜在的bug,以确定适应64位平台和其他编译器所需的内容。

在这方面,我的主要活动之一是确保所有函数都具有完整的原型(其中许多没有),在这个过程中,我发现了一些调用函数的代码(之前未进行原型声明),其参数比函数定义声明的更少。函数的实现确实使用了缺少的参数值。

例如:

impl.c:

int foo(int one, int two) {
  if (two) {
      return one;
  } else {
      return one + 1;
  }
}

客户端1.c:

extern foo();
int bar() {
  /* only one argument(!): */
  return foo(42);
}

客户端2.c:

extern int foo();
int (*foop)() = foo;
int baz() {
  /* calls the same function as does bar(), but with two arguments: */
  return (*foop)(17, 23);
}

问题:如果函数调用缺少参数,那么结果是否定义?如果是,则函数将为未指定的参数接收什么值?否则,1996年左右(针对Solaris而非VMS),Sun C编译器是否会展现出我可以通过向受影响的调用添加特定参数值来模拟的可预测的实现特定行为?


1
如果缺少的参数对函数的结果没有不良影响,为什么不用空白、0或NULL等填充它们呢? - Kninnug
3
你有没有忽略预处理器宏的可能性?我可以帮你梳理一下内容,让它更加易懂,但不会改变原意。 - Robert Harvey
1
@kninnug:如果不了解为什么会发生这种情况,那这并不是一个可行的选择。 - Robert Harvey
@Robert Harvey:好问题,但据我所知,我没有忽略宏。 - John Bollinger
1
@Jimbo:你提到的问题涉及相同的主题,但情况是相反的:在函数调用中传递参数,而函数定义没有声明任何参数。我只能希望那是我的问题。 - John Bollinger
显示剩余4条评论
4个回答

5

编辑:我找到了一个栈线程C function with no parameters behavior,它给出了非常简洁、具体、准确的答案。PMG在答案结尾处的评论提到了UB。以下是我的原始想法,我认为它们沿着同样的思路,并解释了为什么行为是UB。

问题:缺少参数的函数调用的结果是否定义?

我会说不是...原因是我认为该函数将操作,就好像它有第二个参数一样,但正如下面所解释的那样,第二个参数可能只是垃圾。

如果是这样,未指定参数的函数将收到什么值?

我认为接收到的值是未定义的。这就是为什么你可能会有UB的原因。

我知道有两种一般的参数传递方式...(维基百科有一个关于调用约定的好页面)

  1. 通过寄存器传递。即,平台的ABI(应用程序二进制接口)会说,例如,寄存器x和y用于传递参数,任何更多的参数都通过堆栈传递...
  2. 所有东西都通过堆栈传递...

因此,当你给一个模块一个函数的定义,它有“...未指定(但不是可变)数量的参数...”(外部def),它不会放置与你给它的参数(在这种情况下为1)一样多的参数,无论是在寄存器还是堆栈位置中,真正的函数将查找以获取参数值。因此,第二个参数的第二个区域被省略了,实质上包含随机垃圾。

编辑:基于我发现的另一个栈线程,我会修改上面的内容,声明一个没有参数的函数为声明一个具有“未指定(但不是可变)数量的参数”的函数。

当程序跳转到函数时,该函数假定参数传递机制已正确遵守,因此查找寄存器或堆栈,并使用它找到的任何值... 假设它们是正确的。

否则,大约1996年的Sun C编译器(针对Solaris而非VMS)是否会表现出可预测的实现特定行为

你需要检查你的编译器文档。我怀疑...外部定义将被完全信任,因此我怀疑寄存器或堆栈,具体取决于参数传递机制,是否会得到正确的初始化...


谢谢。看起来 SPARC ABI 指定了最多使用六个寄存器进行参数传递,所以这就是代码最初处理的内容。虽然我还没有找到编译器文档,但我倾向于同意编译器不太可能修改它认为与函数参数不对应的寄存器。 - John Bollinger
我知道!我只需要调用 random() 来获取第二个参数的值! :) - John Bollinger

4
如果实际函数定义中的参数数量或类型(在默认参数提升之后)与使用的参数不匹配,则行为是未定义的。
实际情况取决于实现。缺少参数的值将没有意义地定义(假设尝试访问缺少的参数不会导致段错误),即它们将持有不可预测和可能不稳定的值。
程序是否能够在这种错误调用中保持运行也将取决于调用约定。采用“经典”C调用约定的调用方法,调用方负责将参数放入堆栈并从中移除它们,将在存在此类错误时更少崩溃。同样,使用CPU寄存器传递参数的调用也是如此。与此同时,一个调用约定,在其中函数本身负责清理堆栈,将几乎立即崩溃。

谢谢,这正是我所想的。看起来SPARC ABI要求前六个参数通过寄存器传递,但我没有找到任何关于触及不对应于参数的那组寄存器的信息。因此,如果调用者没有指定所有参数,则被调用函数可能会得到随机垃圾。 - John Bollinger

1
很不可能bar函数以前会给出一致的结果。我能想象的唯一可能是它总是在新的堆栈空间上调用,并且堆栈空间在进程启动时被清除,这种情况下第二个参数将为0。或者,在应用程序的更大范围内,返回oneone+1之间的差异并不大,这是我所能想到的。
如果真的像您在示例中描述的那样,那么您正在面对一个严重的错误。在遥远的过去,有一种编码风格,使用指定比传递的参数更多的参数来实现vararg函数,但与现代varargs一样,您不应访问任何未实际传递的参数。

它确实非常像我描述的那样。有时未传递的参数的值在被调用函数的条件控制表达式中进行测试(但是它在其他情况下没有使用)。我同意它看起来像一个错误,但我想要(1)确认一下,并且(2)获得一些关于可能正确修复的指导。 - John Bollinger
@JohnBollinger (1) 我认为我们都同意这是一个错误。(2) 恐怕你必须通过理解缺失参数的值来修复这个错误。 - Bryan Olivier

1
我假设这段代码是在Sun SPARC架构上编译和运行的。根据this古老的SPARC网页:“寄存器%o0-%o5用于传递给过程的前六个参数。”
在您的示例中,函数需要两个参数,但在调用时未指定第二个参数,很可能寄存器%01在调用时总是具有合理的值。
如果您可以访问原始可执行文件并反汇编错误调用站点周围的代码,则可以推断出调用时%o1的值。或者您可以尝试在SPARC模拟器(如QEMU)上运行原始可执行文件。无论哪种情况,这都不是一项简单的任务!

谢谢。我还找到了这个:http://www.sparc.com/standards/FpsABI3rd.pdf,其中包含更详细(也更技术性)的ABI描述。它与您所说的一致。 - John Bollinger

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接