具有从未实际执行的未定义行为的表达式是否会导致程序错误?

时间:2014-06-12 14:15:19

标签: c++ language-lawyer undefined-behavior

在许多关于未定义行为(UB)的讨论中,已经提出了在程序中仅具有存在的任何构造中具有UB的程序中的存在要求符合要求的实现做任何事情(包括什么都没有)。我的问题是,即使在UB与代码的执行相关联的情况下,是否应该采取这种方式,而标准中规定的行为(否则)规定有问题的代码应该不执行(这可能是程序的特定输入;在编译时可能无法判断)。

更加非正式地说,UB的气味是否要求一致的实现来决定整个程序发臭,并且拒绝正确执行甚至行为完全明确定义的程序部分。一个示例程序是

#include <iostream>

int main()
{
    int n = 0;
    if (false)
      n=n++;   // Undefined behaviour if it gets executed, which it doesn't
    std::cout << "Hi there.\n";
}

为清楚起见, 我假设程序格式正确(因此特别是UB与预处理无关)。事实上,我愿意将UB限制在&#34;评估&#34;,这显然不是编译时实体。我认为,与给出的例子相关的定义(强调是我的):

  

之前排序的是由单个线程(1.10)执行的评估之间的非对称,传递,成对关系,这导致这些评估中的部分顺序

     

一个操作数的值计算   在运算符结果的值计算之前对运算符进行排序。如果标量对象上的副作用相对于...或使用相同标量对象的值的值计算未被排序,则行为未定义。

隐含地清楚地表明最后一句中的主语,&#34;副作用&#34;和&#34;值计算&#34;,是&#34;评估&#34;的实例,因为这是&#34;之前排序的关系&#34;是为。

定义的

我认为在上述程序中,标准规定不进行评价,以满足最后一句中的条件(相对于彼此和所描述的类型),并且该程序不具有UB;这不是错误的。

换句话说,我确信我的头衔问题的答案是否定的。但是我会很感激其他人对此事的(动机)意见。

对于那些主张肯定答案的人来说,或许还有一个问题,就是要求在编制错误的程序时可能会出现谚语重新格式化硬盘的行为?

本网站上的一些相关指示:

9 个答案:

答案 0 :(得分:9)

  

如果对标量对象的副作用相对于等没有排序

副作用是执行环境状态的变化(1.9 / 12)。更改是一个更改,而不是一个表达式,如果进行评估,可能会产生更改。如果没有变化,则没有副作用。如果没有副作用,那么相对于其他任何事物都没有副作用。

这并不意味着从未执行过的任何代码都是无UB的(虽然我很确定其中大部分都是)。标准中每次出现的UB都需要单独检查。(受损文本可能过于谨慎;见下文)。

标准也说

  

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

(强调我的)

据我所知,这是唯一的规范性引用,它说明了“未定义行为”的含义:程序执行中的未定义操作。没有执行,没有UB。

答案 1 :(得分:6)

没有。例如:

struct T {
    void f() { }
};
int main() {
    T *t = nullptr;
    if (t) {
        t->f(); // UB if t == nullptr but since the code tested against that
    }
}

答案 2 :(得分:5)

确定程序是否将整数除以0(即UB)通常等同于暂停问题。一般而言,编译器无法确定这一点。因此,仅仅存在可能的UB无法在逻辑上影响程序的其余部分:标准中对此影响的要求将要求每个编译器供应商在编译器中提供暂停问题解算器。

更简单,以下程序只有在用户输入0时才有UB:

#include <iostream>
using namespace std;

auto main() -> int
{
    int x;
    if( cin >> x ) cout << 100/x << endl;
}

保持此程序本身具有UB是荒谬的。

然而,一旦发生了未定义的行为,就会发生任何事情:程序中代码的进一步执行会受到损害(例如,堆栈可能已被污染)。

答案 3 :(得分:3)

在一般情况下,我们可以说这里最好的是它取决于。

答案为否的一种情况是在处理不确定的值时发生。最新的草案显然使undefined behavior to produce an indeterminate value during an evaluation有一些例外,但代码示例清楚地显示了它有多么微妙:

  

[示例:

int f(bool b) {
  unsigned char c;
  unsigned char d = c; // OK, d has an indeterminate value
  int e = d;           // undefined behavior
  return b ? d : 0;    // undefined behavior if b is true
}
     

- 结束示例]

所以这行代码:

return b ? d : 0;
如果btrue,则

仅为未定义。这似乎是一种直观的方法,如果我们阅读It’s Time to Get Serious About Exploiting Undefined Behavior,似乎是John Regehr也能看到它。

在这种情况下答案是肯定的,即使我们没有调用调用未定义行为的代码,代码也是错误的:

constexpr const char *str = "Hello World" ;      

constexpr char access()
{
    return str[100] ;
}

int main()
{
}

clang选择使access出错,即使它从未被调用过( see it live )。

答案 4 :(得分:3)

在固有的未定义行为(例如n = n ++)和可能具有已定义或未定义行为的代码之间存在明显的区别,具体取决于运行时的程序状态,例如int的x / y。在后一种情况下,除非y为0,否则程序必须工作,但在第一种情况下,编译器要求生成完全不合法的代码 - 它有权拒绝编译,它可能不是&#34;防弹&#34;针对此类代码,因此其优化器状态(寄存器分配,自读取后可能已修改其值的记录等)被破坏,导致该和周围源代码的伪机器代码。可能是早期分析认识到&#34; a = b ++&#34;前面的if跳过两个字节的指令的情况和生成的代码,但是当遇到n = n ++时,没有输出指令,因此if语句跳转到下面的操作码。无论如何,它只是游戏结束。放一个&#34; if&#34;在前面,甚至包装在一个不同的功能,没有记录为&#34;包含&#34;未定义的行为......代码的部分没有被定义为未定义的行为 - 标准一致地说&#34;程序具有未定义的行为&#34;。

答案 5 :(得分:1)

应该是,如果不是&#34; &#34;。

行为,根据ISO C的定义(在ISO C ++中没有相应的定义,但它应该仍然适用),是:

  

<强> 3.4

     

1行为

     

外观或行动

和UB:

WG21 / N4527

  

1.3.25 [defns.undefined]

     

未定义的行为

     

本国际标准没有要求的行为[注意:当本国际标准忽略任何明确的行为定义或程序使用错误的结构或错误数据时,可能会出现未定义的行为。允许的未定义行为包括完全忽略不可预测的结果,在翻译或程序执行期间以环境特征(有或没有发出诊断消息)的特定行为,终止翻译或执行(发布)一条诊断信息)。许多错误的程序结构不会产生未定义的行为;他们需要被诊断出来。    - 后注]

尽管&#34;在翻译过程中表现良好&#34;上面,单词&#34;行为&#34; ISO C ++使用的主要是关于程序的执行

WG21 / N4527

  

1.9程序执行[intro.execution]

     

1本国际标准中的语义描述定义了参数化的非确定性抽象机器。本国际标准对符合实施的结构没有要求。特别是,它们不需要复制或模拟抽象机器的结构。相反,需要符合实现来模拟(仅)抽象机器的可观察行为,如下所述.5

     

2抽象机的某些方面和操作在本国际标准中描述为实现定义的(例如,sizeof(int))。这些构成了抽象机器的参数。   每个实现应包括描述其在这些方面的特征和行为的文档.6此类文档应定义与该实现相对应的抽象机器的实例(以下称为“相应实例”)。

     

3抽象机器的某些其他方面和操作在本国际标准中描述为未指定(例如,如果分配函数无法分配内存,则评估 new-initializer 中的表达式)(5.3 。4))。在可能的情况下,本国际标准定义了一组允许的行为。   这些定义了抽象机器的非确定性方面。因此,抽象机器的实例可以为给定程序和给定输入提供多个可能的执行。

     

4本国际标准中将某些其他操作描述为未定义(例如,尝试修改const对象的效果)。 [注意:本国际标准对包含未定义行为的程序的行为没有要求。 - 后注]

     

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

     

5)此条款有时被称为“as-if”规则,因为只要结果,就好像要求一样,实施可以自由地忽略本国际标准的任何要求从程序的可观察行为可以确定。例如,实际实现不需要评估表达式的一部分,如果它可以推断出它的值没有被使用,并且没有产生影响程序的可观察行为的副作用。

     

6)此文档还包括有条件支持的构造和特定于语言环境的行为。见1.4。

很明显,未定义的行为是由错误使用的特定语言构造或以非可移植方式(不符合标准)引起的。但是,标准没有提及程序中哪些特定代码部分会导致它。换句话说,&#34;具有未定义的行为&#34;是正在执行的整个程序的属性(关于符合),而不是它的任何较小部分

标准本可以提供更强的保证,一旦某些特定代码没有被执行,就能很好地定义行为,只有在存在将C ++代码精确映射到相应行为的方法时 。如果没有关于执行的详细语义模型,这很难(如果不是不可能的话)。简而言之,上面的抽象机器模型给出的操作语义不足以实现更强的保证。但无论如何,ISO C ++永远不会是JVMS或ECMA-335。而且我不希望会有一套完整的描述语言的形式语义。

这里的一个关键问题是&#34;执行&#34;的含义。有些人认为&#34;执行一个程序&#34;意味着让程序运行。这不是真的。请注意,未指定在抽象机器中执行的程序的表示。 (另请注意&#34;本国际标准对符合实现的结构没有要求&#34;。)此处执行的代码可以是字面上的C ++代码(不一定是机器代码或其他形式的未指定的中间代码)完全按标准)。这有效地允许核心语言实现为解释器,在线部分评估器或其他一些动态地翻译C ++代码的怪物。因此,实际上没有办法在执行过程之前完全拆分翻译阶段(由ISO C ++ [lex.phases]定义),而无需了解具体实现。因此,有必要允许在翻译期间发生UB,因为很难指定便携式明确定义的行为。

除了上面的问题,也许对于大多数普通用户来说,一个(非技术)原因就足够了:根本没有必要提供更强的保证,允许不良的代码并且打败其中一个(可能最重要的)有用性方面。 UB本身:鼓励快速丢弃一些(不必要的)不便携的臭代码而不费力地修复&#34;他们最终将徒劳无功。

附加说明:

有些字词会从我对this comment的回复中复制和重建。

答案 6 :(得分:0)

一旦程序进入一个状态,允许程序在将来的某个时刻避免调用未定义的行为,那么AC编译器就可以做任何它喜欢的事情。没有任何副作用,并且没有编译器需要识别的退出条件,在其自身中调用未定义的行为)。 编译器在这种情况下的行为受到时间和因果关系的法则的约束。在结果从未使用的表达式中发生未定义行为的情况下,某些编译器不会为表达式生成任何代码(因此它永远不会“执行”),但这不会阻止编译器使用未定义的行为来创建其他关于程序行为的推论。

例如:

void maybe_launch_missiles(void)
{      
  if (should_launch_missiles())
  {
    arm_missiles();
    if (should_launch_missiles())
      launch_missiles();
  }
  disarm_missiles();
}
int foo(int x)
{
  maybe_launch_missiles();
  return x<<1;
}

在C当前C标准下,如果编译器可以确定disarm_missiles()将始终返回而不终止但上面调用的其他三个外部函数可能会终止,则语句{{1}的最有效的符合标准的替换(返回值被忽略)将是foo(-1);

只有在should_launch_missiles(); arm_missiles(); should_launch_missiles(); launch_missiles();调用终止而不返回,第一个调用返回非零并且should_launch_missiles()终止而不返回,或者两个调用都返回非零且{ {1}}终止而不返回。在这些情况下正常工作的程序将遵守标准,无论其在任何其他情况下做什么。如果从arm_missiles()返回会导致未定义的行为,则不需要编译器识别对launch_missiles()的调用可能返回零的可能性。

因此,一些现代编译器,左移负数的影响可能比任何更糟糕,这可能是由平台上典型的C99编译器上的任何类型的未定义行为引起的。单独的代码和数据空间以及陷阱堆栈溢出。即使代码涉及可能导致随机控制传输的未定义行为,也没有办法可以导致maybe_launch_missiles()should_launch_missiles()连续调用,而无需对arm_missiles()进行干预调用除非至少有一次对launch_missiles()的调用返回非零值。然而,超现代编译器可能会否定这种保护。

答案 7 :(得分:0)

在启用了完全优化的gcc处理的方言中,如果程序包含两个在定义了两者的情况下具有相同行为的构造,则可靠的程序操作要求仅在以下两个情况下执行的代码才能在它们之间进行切换被定义。例如,启用优化后,ARM gcc 9.2.1和x86-64 gcc 10.1都将处理以下源:

#include <limits.h>

#if LONG_MAX == 0x7FFFFFFF
typedef int longish;
#else
typedef long long longish;
#endif

long test(long *x, long *y)
{
    if (*x)
    {
        if (x==y)
            *y = 1;
        else
            *(longish*)y = 1;
    }
    return *x;
}

进入将测试xy是否相等的机器代码,如果*x*y不相等,则将*x设置为1,否则将*x设置为1。无论哪种情况,都返回if的先前值。为了确定是否有任何事物可能影响*x,gcc决定if的两个分支都是等效的,因此仅评估“假”分支。由于这不会影响*y,因此得出结论,*x总体上也不会。观察到在真实分支上对foobar的写操作可以替换为对Fruit的写操作,因此无法确定该决定。

答案 8 :(得分:-2)

在安全关键的嵌入式系统环境中,发布的代码将被视为有缺陷:

  1. 代码不应通过代码审查和/或标准合规性(MISRA等)
  2. 静态分析(lint,cppcheck等)应将此标记为缺陷
  3. 有些编译器可以将此标记为警告(也表示存在缺陷。)