未定义的行为或未定义的行为

时间:2017-02-20 13:44:46

标签: c language-lawyer undefined-behavior

考虑以下代码:

#include <stdio.h>

int main()
{
    char A = A ? 0[&A] & !A : A^A;
    putchar(A);
}

我想问一下,是否在其中观察到任何未定义的行为。

修改

请注意:代码故意使用0[&A] & !A而非A & !A(请参阅下面的回复)

结束修改

从g ++ 6.3(https://godbolt.org/g/4db6uO)获取输出ASM得到(没有使用优化):

main:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     BYTE PTR [rbp-1], 0
    movzx   eax, BYTE PTR [rbp-1]
    movsx   eax, al
    mov     edi, eax
    call    putchar
    mov     eax, 0
    leave
    ret

然而clang为同一件事提供了更多代码(没有再次优化):

main:                                   # @main
    push    rbp
    mov     rbp, rsp
    sub     rsp, 16
    mov     dword ptr [rbp - 4], 0
    cmp     byte ptr [rbp - 5], 0
    je      .LBB0_2
    movsx   eax, byte ptr [rbp - 5]
    cmp     byte ptr [rbp - 5], 0
    setne   cl
    xor     cl, -1
    and     cl, 1
    movzx   edx, cl
    and     eax, edx
    mov     dword ptr [rbp - 12], eax # 4-byte Spill
    jmp     .LBB0_3
.LBB0_2:
    movsx   eax, byte ptr [rbp - 5]
    movsx   ecx, byte ptr [rbp - 5]
    xor     eax, ecx
    mov     dword ptr [rbp - 12], eax # 4-byte Spill
.LBB0_3:
    mov     eax, dword ptr [rbp - 12] # 4-byte Reload
    mov     cl, al
    mov     byte ptr [rbp - 5], cl
    movsx   edi, byte ptr [rbp - 5]
    call    putchar
    mov     edi, dword ptr [rbp - 4]
    mov     dword ptr [rbp - 16], eax # 4-byte Spill
    mov     eax, edi
    add     rsp, 16
    pop     rbp
    ret

Microsoft VC编译器给出:

EXTRN   _putchar:PROC
tv76 = -12                                          ; size = 4
tv69 = -8                                         ; size = 4
_A$ = -1                                                ; size = 1
_main   PROC
    push     ebp
    mov      ebp, esp
    sub      esp, 12              ; 0000000cH
    movsx    eax, BYTE PTR _A$[ebp]
    test     eax, eax
    je       SHORT $LN5@main
    movsx    ecx, BYTE PTR _A$[ebp]
    test     ecx, ecx
    jne      SHORT $LN3@main
    mov      DWORD PTR tv69[ebp], 1
    jmp      SHORT $LN4@main
$LN3@main:
    mov      DWORD PTR tv69[ebp], 0
$LN4@main:
    mov      edx, 1
    imul     eax, edx, 0
    movsx    ecx, BYTE PTR _A$[ebp+eax]
    and      ecx, DWORD PTR tv69[ebp]
    mov      DWORD PTR tv76[ebp], ecx
    jmp      SHORT $LN6@main
$LN5@main:
    movsx    edx, BYTE PTR _A$[ebp]
    movsx    eax, BYTE PTR _A$[ebp]
    xor      edx, eax
    mov      DWORD PTR tv76[ebp], edx
$LN6@main:
    mov      cl, BYTE PTR tv76[ebp]
    mov      BYTE PTR _A$[ebp], cl
    movsx    edx, BYTE PTR _A$[ebp]
    push     edx
    call     _putchar
    add      esp, 4
    xor      eax, eax
    mov      esp, ebp
    pop      ebp
    ret      0
_main   ENDP

但是通过优化,我们可以获得更清晰的代码(gcc和clang):

main:                                   # @main
    push    rax
    mov     rsi, qword ptr [rip + stdout]
    xor     edi, edi
    call    _IO_putc
    xor     eax, eax
    pop     rcx
    ret

还有一种神秘的VC代码(似乎VC编译器无法理解一个笑话......它只是不会预先计算右侧)。

EXTRN   _putchar:PROC
_A$ = -1                                                ; size = 1
_main   PROC                                      ; COMDAT
    push     ecx
    mov      cl, BYTE PTR _A$[esp+4]
    test     cl, cl
    je       SHORT $LN3@main
    mov      al, cl
    xor      al, 1
    and      cl, al
    jmp      SHORT $LN4@main
$LN3@main:
    xor      cl, cl
$LN4@main:
    movsx    eax, cl
    push     eax
    call     _putchar
    xor      eax, eax
    pop      ecx
    pop      ecx
    ret      0
_main   ENDP

一些警告:

  1. 你不应该写这样的代码。这绝对是错误的编码风格,永远不应该进入一个严肃的应用程序。只是为了好玩。
  2. 一些解释:

    1. 我寻找未定义的行为,因为A的值在其初始化中使用。再说一遍:你不应该这样做。
    2. 然而,构建表达式的方式,代码的两个部分将产生0,作为编译器
    3. 所以我现在处于这种困境,无论是UB还是UB。

3 个答案:

答案 0 :(得分:7)

首先,如果char对应unsigned char,则char不能有陷阱表示; however if char corresponds to signed char it can have trap representations。由于使用陷阱表示具有未定义的行为,因此修改代码以使用unsigned char更为有趣:

unsigned char A = A ? 0[&A] & !A : A^A;
putchar(A);

最初我认为在C中没有任何未定义的行为。问题是A未初始化的方式有未定义的行为,答案是“否”,因为虽然它是一个具有自动存储持续时间的局部变量,但是它的地址是有的,所以它必须驻留在内存中,并且它的类型是char,因此它的值是未指定的,但具体来说它不能是陷阱表示。

C11附录J.2。指定以下内容具有未定义的行为:

  

指定可以使用寄存器存储类声明的自动存储持续时间的对象的左值在需要指定对象的值的上下文中使用,但该对象未初始化。 (6.3.2.1)。

用6.3.2.1p2说

  

如果左值指定了一个可以使用寄存器存储类声明的自动存储持续时间的对象(从未使用过其地址),并且该对象未初始化(未使用初始化程序声明,并且未对其进行任何赋值)在使用之前),行为是未定义的。

由于A的地址已被采用,因此无法使用register存储类声明它,因此根据此6.3.2.1p2,它的使用没有未定义的行为;相反,它会有一个未指定但有效的char值; char没有陷阱表示。

然而,问题是没有任何要求A必须全部产生相同的未指定值,因为未指定的值是

  

相关类型的有效值,其中本国际标准未对任何实例中选择的值

进行选择

C11 Defect Report 451的答案似乎认为这有未定义的行为,说是使用不确定值的结果(即使是没有陷阱表示的类型,例如算术表达式中的unsigned char}也意味着结果将具有不稳定的值,并且在库函数中使用此类值将具有未定义的行为

因此:

unsigned char A = A ? 0[&A] & !A : A^A;

不会像这样调用未定义的行为,但A仍然使用不确定的值进行初始化,并且在调用库函数putchar(A)时使用这样的不确定值应被视为具有未定义的行为:

  

委员会的建议回应

     
      
  • 问题1的答案是“是”,在所述条件下的未初始化值似乎会改变其价值。
  •   
  • 问题2的答案是,对不确定值执行的任何操作都会产生不确定的值。
  •   
  • 问题3的答案是,当用于不确定的值时,库函数将表现出未定义的行为。
  •   
  • 这些答案适用于所有没有陷阱表示的类型。
  •   
  • 这一观点重申了C99 DR260的立场。
  •   
  • 委员会同意这个领域将受益于类似于“摇摇欲坠”的价值的新定义,这应该   在本标准的任何后续修订中予以考虑。
  •   
  • 委员会还指出,结构中的填充字节可能是“摇摆”表示的一种不同形式。
  •   

答案 1 :(得分:1)

这是一种行为类别,其中标准强烈暗示行为,标准中的任何内容都不会邀请实现跳转轨道,但官方的“解释”仍然允许编译器以任意方式行事。因此,将行为描述为“未定义”是不准确的[因为标准的文本确实暗示了一种行为,并且没有任何暗示它不应该适用]也不准确简单地说它是“定义的” “[因为委员会说编制者可能会以任意方式行事]。相反,有必要识别中间条件。

因为不同的应用领域(数字运算,系统编程等)受益于不同类型的行为保证,并且因为某些平台可能比其他平台更便宜地维护某些保证,所以迄今为止每个C标准的作者都有通常试图避免对各种担保的相对成本和收益作出判断。相反,他们已经显示出对实施者在什么实施中应该提供什么保证的判断的显着尊重。

如果提供某些特定的行为保证在某些应用领域没有任何价值(即使它在其他应用领域可能至关重要)似乎是合理的,并且放弃该保证有时可能会使某些实施更有效(即使在大多数情况下)如果不是这样的话,标准的作者通常不会要求保证。相反,它们让实现者根据实现的目标平台和预期的应用程序字段决定实现是否应始终支持该保证,从不支持该保证,或允许程序员选择(通过命令行选项或其他是否支持担保。

用于任何特定目的的质量实现(例如系统编程)将支持使编译器适合于该目的的各种行为保证(例如,读取程序所拥有的无符号字符将不会产生任何影响,除非产生一个标准是否要求它这样做可能毫无意义的价值。 C标准的作者并不要求也不打算所有实现都适用于系统编程等领域,因此不要求针对数字运算等其他领域的实现支持这种保证。针对其他领域的编译器可能无法维护系统编程所需的各种保证,这意味着系统程序员确保使用适合其目的的工具非常重要。知道一个工具有望支持所需的保证远比知道当前对标准的解释支持这样的保证更重要,因为如果编译器编写者可以建议放弃它,那么今天被认为是明确无误的保证可能会消失。有时是有益的。

答案 2 :(得分:1)

右侧首先评估A

在C ++中,由于A此时尚未初始化,因此代码为causes undefined behaviour

在C11中,由于A此时未初始化,因此其值可能是陷阱表示,因此代码会导致未定义的行为。

在C11中,如果我们所处的系统已知没有陷阱表示(或者我们将char更改为unsigned char),那么A具有不确定的值,然后{ {1}}通过将不确定的值传递给库函数来导致未定义的行为。

Further reading for C11 uninitialized variable use