0

我正在阅读以下关于 C 中序列点的文章:https ://www.geeksforgeeks.org/sequence-points-in-c-set-1/

其中有几个未定义行为的示例,例如调用两个修改单个全局变量的函数的表达式,或多次递增同一变量的单个表达式。

理论上,我理解这个概念。但是,无论我尝试运行这些示例多少次,行为都是一样的,而且永远不会“令人惊讶”。

为了亲身体验未定义的行为,让示例“令人惊讶”的最简单方法是什么?

(如果重要的话,我正在使用 MINGW64。)

4

2 回答 2

0

测试 gcc 和 clang 时一个有用的模式是使用下标访问数组,下标的值将在范围内,但编译器不知道,并使用标准描述为等效于下标表示法的指针语法。使用以下内容测试 gcc 和 clang:

struct S1 {int x;};
struct S2 {int x;};

union foo { struct S1 arr1[8]; struct S2 arr2[8]; } u;

uint32_t test1(int i, int j)
{
  if (sizeof u.arr1 != sizeof u.arr2)
    return -99;
  if (u.arr1[i].x)
    u.arr2[j].x = 2;
  return u.arr1[i].x;
}
uint32_t test2(int i, int j)
{
  if (sizeof u.arr1 != sizeof u.arr2)
    return -99;
  if ((u.arr1+i)->x)
    (u.arr2+j)->x = 2;
  return (u.arr1+i)->x;
}

将揭示,尽管标准将 和 的行为分别定义为和,u.arr1[i].x但gcc 和 clang 在给定前者时错过了一个允许的优化机会,而在给定后者时它们利用了。这很可能是因为作者认识到利用前一个机会是允许的,但不可否认是愚蠢的,以至于不得不承认该标准从未打算鼓励它允许的所有优化。u.arr2[j].x(u.arr1+i)->x(u.arr2+j)->x

于 2019-03-17T18:18:47.120 回答
0

这是我能在短时间内想到的最好的:

源代码:

#include <stdio.h>

int undefined(int *a, short *b)
{
    *a = 1;
    b[0] = 0;
    b[1] = 0;
    return *a;
}

int main()
{
    int x;
    short *y = (short*) &x;
    int z = undefined(&x, y);
    printf("%d\n", z);
    return 0;
}

使用 gcc 8.3 -O3 生成的程序集

undefined(int*, short*):
    mov     DWORD PTR [rdi], 1
    mov     eax, 1
    mov     DWORD PTR [rsi], 0
    ret
.LC0:
    .string "%d\n"
main:
    sub     rsp, 8
    mov     esi, 1
    mov     edi, OFFSET FLAT:.LC0
    xor     eax, eax
    call    printf
    xor     eax, eax
    add     rsp, 8
    ret

查看实际操作:https ://godbolt.org/z/E0XDYt

特别是,它依赖于将 a 的地址强制转换int为 a所导致的short*未定义行为,这种行为违反了严格的别名规则,因此会导致未定义的行为。

从组装开始undefined()。这假设因为ab是不同的类型,它们不能重叠,因此它优化了return *a;into mov eax,1,即使如果它从内存中获取值实际上会返回零。它在优化关闭的情况下执行此操作,因此这是仅在优化的发布版本中出现的真正隐蔽问题之一,而不是在您尝试使用非优化的调试版本对其进行调试时。

但是,请注意 in 中的代码如何main()尝试使其正确:它内联,然后优化调用 to undefined(),而是假设它0在调用. 所以它忽略了上面几行刚刚计算出的返回值,而是使用不同的值。zxor eax,eaxprintf

总而言之,一个非常糟糕的程序。正是您因未定义行为而面临的风险。

于 2019-03-17T07:16:56.067 回答