Как стимулировать неопределенное поведение / неупорядоченное выполнение в программах на C? - PullRequest
0 голосов
/ 17 марта 2019

Я читаю следующую статью о точках последовательности в C: https://www.geeksforgeeks.org/sequence-points-in-c-set-1/

В ней есть несколько примеров неопределенного поведения, таких как выражения, вызывающие две функции, которые изменяют одну глобальную переменную, илиодно выражение, которое увеличивает одну и ту же переменную более одного раза.

Теоретически я понимаю эту концепцию.Однако, независимо от того, сколько раз я пытаюсь запустить примеры, поведение остается неизменным и никогда не бывает «удивительным».

Чтобы получить практическое представление о неопределенном поведении, какой самый простой способ заставить примеры быть «удивительными»?

(Если это имеет значение, я использую MINGW64.) * +1010 *

Ответы [ 2 ]

0 голосов
/ 17 марта 2019

Полезный шаблон при тестировании 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 и u.arr2[j].x как эквивалентное (u.arr1+i)->x и (u.arr2+j)->x, соответственно, gcc и clang упускают допустимую возможность оптимизации, если им дают первое, которое они используют, когда дано. последний. Скорее всего, это происходит потому, что авторы признают, что эксплуатирую прежнюю возможность была бы допустимо, но было бы так глупо, что, несомненно, чтобы заставить признание, что стандарт не был предназначен, чтобы поощрить все оптимизации это позволяет.

0 голосов
/ 17 марта 2019

Это лучшее из того, что я могу придумать в короткие сроки:

Исходный код:

#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

В частности, он опирается на неопределенное поведение, вызванное приведением адреса int к short*, действие, которое нарушает правило строгого наложения имен и, следовательно, вызывает неопределенное поведение.

Начните со сборки undefined(). Это предполагает, что, поскольку a и b являются разными типами, они не могут перекрываться, поэтому он оптимизирует return *a; в mov eax,1, даже если он на самом деле вернет ноль, если выберет значение из памяти. Что и происходит при выключенной оптимизации, так что это одна из тех действительно коварных проблем, которая проявляется только в оптимизированной сборке релиза, а не при попытке отладки его с неоптимизированной сборкой отладки.

Однако обратите внимание, как код в main() пытается и делает его правильно: он встроен, а затем оптимизирует вызов до undefined() и вместо этого принимает 0 в z, когда он делает xor eax,eax чуть выше звонка на printf. Поэтому он игнорирует то, что он вычислил как возвращаемое значение несколькими строками выше, и вместо этого использует другое значение.

В общем, очень сильно сломанная программа. Именно то, что вы рискуете с неопределенным поведением.

...