【问题标题】:Can undefined behavior which would follow a getc() alter program behavior if the getc() exits via SIGINT如果 getc() 通过 SIGINT 退出,getc() 之后的未定义行为能否改变程序行为
【发布时间】:2015-07-03 12:54:30
【问题描述】:

根据对“未定义行为”的现代解释,编译器有权假设不会发生导致未定义行为“不可避免”的事件链,并且可以消除仅适用于以下情况的代码将执行未定义的行为;这可能会导致未定义行为的影响及时向后工作,并使原本可以观察到的行为无效。另一方面,如果除非程序终止,否则未定义行为是不可避免的,但如果程序可以并且确实在调用未定义行为之前终止,则程序的行为将保持完全定义。

在做出此决定时,编译器需要考虑哪些终止原因?举几个例子:

在许多平台上,对“getc”之类的函数的调用通常会返回(至少最终会返回),但在编译器无法控制的某些情况下则不会。如果有人有这样的程序:

int main(int argc, char *argv[])
{
  if (argc != 3)
  {
    printf("Foo\n");
    return 0;
  }
  else
  {
    int ch;
    printf("You'd better type control-C!\n");
    int ch = getc();
    if (ch < 65)
      return (ch-33) / (argc-3);       
    else
      return INT_MAX + ch;
  }
}

如果程序被调用,argc 等于 3,但 SIGINT 阻止了 getc() 调用完全返回,是否会定义行为?当然,如果有任何 getc() 可以返回的 会导致定义的行为,那么在编译器可以确定不会接收到这样的输入之前,不会发生未定义的行为。如果没有值 getc() 可以返回,这将避免未定义的行为,但是,如果 getc() 被阻止返回任何值,整个程序是否会保持定义? getc() 的返回值和调用未定义行为的动作之间是否存在因果关系会影响事物(在上面的示例中,编译器无法知道会发生任何特定形式的未定义行为不知道输入了什么字符,但任何可能的输入都会触发某种形式)。

同样,如果在一个平台上存在地址,如果读取这些地址会导致程序立即终止,编译器会指定易失性读取将触发硬件读取请求,并且该平台上的某些外部库指定它将返回指向这样一个地址的指针,这些因素是否暗示bar 在这个单独的示例中的行为:

int foo(int x)
{
  char volatile * p = get_instant_quit_address();
  if (x)
    { printf("Hey"); fflush(stdout); }
  return *p / x; // Will cause UB if *p yields a value and x is zero
}
int bar(void)
{
  return foo(0);
}
如果尝试读取 *p 实际上会立即终止程序执行而不产生值,那么

是否会被定义(作为终止而不打印任何内容)?在返回值之前,除法不能继续;因此,如果没有返回值,就不会被零除。

允许 C 编译器通过什么方式确定给定操作是否可能导致程序执行以它不知道的方式终止,以及在什么情况下允许在此类操作之前重新安排未定义行为?

【问题讨论】:

  • 听起来你想让我们解决Halting Problem
  • 我不明白问题的重点。如果某些事情超出了编译器的控制范围,你希望它怎么做?
  • 这个问题最终归结为“C 编译器能否确定给定的操作是否会终止程序?” 这是停止问题。但接下来的问题是,“以它不知道的方式?” 答案是,“呃,不。”
  • @user3386109:问题基本上是编译器是否可以使用一个动作不会终止程序的假设作为声明程序执行未定义的基础,而实际上该动作将终止在调用未定义行为之前对程序进行排序。
  • @WeatherVane:我不是在试图“让编译器为我完成工作”——相反,我试图了解确保禁止编译器的必要条件按照 C 标准从做我希望它做的事情。

标签: c language-lawyer undefined-behavior


【解决方案1】:

这在 C++ 中的 [intro.execution] 中有很好的描述:

5 - 执行格式良好的程序的符合实现应产生与使用相同程序的抽象机器的相应实例的可能执行之一相同的可观察行为 和相同的输入。但是,如果任何此类执行包含未定义的操作,则本国际标准对使用该输入执行该程序的实现没有要求(甚至 关于第一个未定义操作之前的操作)。

普遍认为 C 具有相同的特性,因此 C 编译器可以在存在未定义行为的情况下类似地执行 "time travel"

重要的是,请注意问题是是否存在表现出未定义行为的抽象机实例;您可以通过首先终止程序执行来安排防止计算机上未定义的行为,这并不重要。

如果您使程序以抽象机器无法摆脱的完全定义的方式自行终止,您可以防止未定义的行为(以及由此产生的时间旅行)。例如,在您的第二个示例中,如果您用(exit(0), 0) 替换对*p 的访问,则不会发生未定义的行为,因为不可能执行exit 返回其调用者的抽象机器。但是无论您的平台的特性如何,抽象机都不必在访问 insta-kill 地址时终止您的程序(实际上,抽象机没有任何 insta-kill 地址)。

【讨论】:

  • 对于具有写入触发复位的平台上的嵌入式 C 场景,如果 FORCE_RESET 被声明为 volatilewhile(1) FORCE_RESET = 1; 是否会防蠕动?我认为如果无限循环包含副作用,则它们不是 UB,并且编写 volatile 应该符合条件。否则,如果一个人想要例如有一个方法,如果它会导致 SIGPIPE,它将输出数据并终止程序执行,这是唯一的方法来替换对标准库 I/O 的所有调用与对编译器无法知道的函数的调用。然后转发到标准库函数?
  • @supercat 是的,我认为应该可以;即使是后经典编译器也无权构造包含无限数量的操作后跟未定义操作的执行。我仍然倾向于在 while 循环之后输入abort()。关于替换调用,如果编译器能够通过链接时优化查看替换调用,您可能仍然处于危险之中。
  • 我不喜欢嵌入式系统中的abort(),因为不清楚它应该是什么意思;对于非托管平台,我认为 C 标准不会对 abort() 施加任何要求,这意味着将其称为未定义行为。否则,我不知道是否值得编辑原始问题,但我想知道 CHAR_BITS==8 机器的编译器是否可以将1/((getc(stdin)+1)&gt;&gt;9); 视为在传入字符到达之前调用UB? getc() 可以返回的任何值都会导致 UB,但是否强制 getc() 将返回?
  • @supercat 你是对的,abort&lt;stdlib.h&gt; 中,这不适用于严格遵守的独立程序 (4. Conformance/6)。同样,对于getc,它是否会返回并不重要;它可能足以使整个执行无效的事实。
  • 恕我直言,使优化有用的真正关键是提供程序员可以指定语义的方法,这些语义有些松散但仍然缺乏“任何事情都可以”。我反对提出基于 UB 的理念是,在许多情况下,它迫使程序员添加以前不需要的代码,以指定比他们实际需要的更严格的语义,纯粹是为了确保编译器不会使用 UB作为把因果关系抛到窗外的借口。顺便说一句,您的示例和我的示例之间的区别在于,在我的示例中,该除法将在 getc 返回后进行排序...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-09-19
  • 1970-01-01
  • 2014-01-12
相关资源
最近更新 更多