15

我正在处理一些遗留的 C 代码。原始代码是在 90 年代中期编写的,针对那个时代的 Solaris 和 Sun 的 C 编译器。当前版本在 GCC 4 下编译(尽管有很多警告),它似乎可以工作,但我正在尝试整理它——我想尽可能多地排除潜在的错误,因为我确定什么可能是必要的使其适应 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);
}

问题:是否定义了缺少参数的函数调用的结果?如果是这样,该函数将为未指定的参数接收什么值?否则,Sun C 编译器的 ca. 1996 年(对于 Solaris,而不是 VMS)展示了一种可预测的特定于实现的行为,我可以通过向受影响的调用添加特定的参数值来模拟这种行为?

4

4 回答 4

5

编辑:我发现了一个没有参数行为的堆栈线程C 函数,它给出了一个非常简洁、具体、准确的答案。PMG在答案末尾的评论是关于UB的。以下是我最初的想法,我认为这些想法是一致的,并解释了为什么这种行为是 UB..

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

我会说不...原因是我认为该函数将像具有第二个参数一样运行,但如下所述,第二个参数可能只是垃圾。

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

我认为收到的值是未定义的。这就是为什么你可以拥有UB。

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

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

因此,当您给一个模块定义具有“...未指定(但不是可变)参数数量...”(extern def)的函数时,它不会放置与您提供的参数一样多的参数(在这种情况下1) 在寄存器或堆栈位置中,真正的函数将查看以获取参数值。因此,被遗漏的第二个参数的第二个区域本质上包含随机垃圾。

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

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

否则,Sun C 编译器的 ca. 1996 年(针对 Solaris,而不是 VMS)展示了 >> 可预测的特定于实现的行为

您必须检查您的编译器文档。我对此表示怀疑... extern 定义将被完全信任,因此我怀疑寄存器或堆栈(取决于参数传递机制)是否会正确初始化...

于 2013-07-11T22:09:54.737 回答
4

如果参数的数量或类型(在默认参数提升之后)与实际函数定义中使用的不匹配,则行为未定义。

在实践中会发生什么取决于实施。缺失参数的值不会被有意义地定义(假设访问缺失参数的尝试不会出现段错误),即它们将包含不可预测且可能不稳定的值。

程序是否能在这种不正确的调用中幸存下来也取决于调用约定。“经典” C 调用约定,其中调用者负责将参数放入堆栈并从那里删除它们,在出现此类错误时不太容易崩溃。对于使用 CPU 寄存器传递参数的调用也是如此。同时,函数本身负责清理堆栈的调用约定几乎会立即崩溃。

于 2013-07-11T22:50:12.453 回答
1

bar过去的函数不太可能给出一致的结果。我唯一能想象的是,它总是在新的堆栈空间上调用,并且堆栈空间在进程启动时被清除,在这种情况下,第二个参数将为 0。或者返回oneone+1未创建之间的区别应用范围越大,差别越大。

如果它真的像您在示例中描述的那样,那么您正在寻找一个大胖子。在遥远的过去,有一种编码风格,其中vararg通过指定比传递更多的参数来实现函数,但就像现代一样,varargs您不应该访问任何未实际传递的参数。

于 2013-07-11T22:18:39.103 回答
1

我假设这段代码是在 Sun SPARC 架构上编译和运行的。根据这个古老的 SPARC 网页:“寄存器%o0-%o5用于传递给程序的前六个参数。”

在您的示例中,函数需要两个参数,而第二个参数未在调用站点指定,很可能 register%01在调用时总是碰巧具有合理的值。

如果您有权访问原始可执行文件并且可以在不正确的调用站点周围反汇编代码,则您可能能够推断出%o1调用时的值。或者您可以尝试在 SPARC 仿真器(如 QEMU)上运行原始可执行文件。无论如何,这都不是一件小事!

于 2013-07-11T23:02:15.270 回答