Есть ли примеры современных целевых / компиляторов, где нарушение строгого алиасинга в C влияет на результат программы? - PullRequest
0 голосов
/ 17 мая 2018

Иногда вопросы и ответы о переполнении стека предполагают использование указателя в качестве правильного способа обозначения типа. Часто утверждается, что это нарушает строгий псевдоним и, следовательно, вызывает неопределенное поведение.

Действительно ли современные цели действительно заботятся о строгом алиасинге? Существуют ли какие-либо программы, которые будут показывать неожиданное поведение в этом случае?

Ответы [ 3 ]

0 голосов
/ 17 мая 2018

На более низких уровнях оптимизации или с -fno-strict-aliasing компилятором добавляет инструкцию movsbl (% rsi),% eax внутри тела цикла.

Не на более низких уровнях оптимизации.

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

Эта функция просто плохо написана. Автор, зная, что он подвержен побочным эффектам - просто проигнорировал это. Это неправильно с указателем или без него.

typedef struct
{
    char a;
} my_struct;

my_struct a, b[100];

void foo( int * a, my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}

void foo1( int * a, volatile my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}


void call1(void)
{
    int *v = (int *)b;

    foo(v, &b[0], 100);
}

void call2(void)
{


    foo((int *)b, b, 100);
}

void call3(void)
{
    int *v = (int *)b;

    foo1(v, &b[0], 100);
}

void call4(void)
{


    foo1((int *)b, b, 100);
}

И скомпилированный код.

foo: # @foo
  test edx, edx
  jle .LBB0_3
  movsx eax, byte ptr [rsi]
  mov ecx, edx
.LBB0_2: # =>This Inner Loop Header: Depth=1
  add dword ptr [rdi], eax
  add rdi, 4
  add rcx, -1
  jne .LBB0_2
.LBB0_3:
  ret
foo1: # @foo1
  test edx, edx
  jle .LBB1_3
  mov eax, edx
.LBB1_2: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rsi]
  add dword ptr [rdi], ecx
  add rdi, 4
  add rax, -1
  jne .LBB1_2
.LBB1_3:
  ret
call1: # @call1
  mov rax, -400
  movsx ecx, byte ptr [rip + b]
.LBB2_1: # =>This Inner Loop Header: Depth=1
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB2_1
  ret
call2: # @call2
  mov rax, -400
  movsx ecx, byte ptr [rip + b]
.LBB3_1: # =>This Inner Loop Header: Depth=1
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB3_1
  ret
call3: # @call3
  mov rax, -400
.LBB4_1: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rip + b]
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB4_1
  ret
call4: # @call4
  mov rax, -400
.LBB5_1: # =>This Inner Loop Header: Depth=1
  movsx ecx, byte ptr [rip + b]
  add dword ptr [rax + b+400], ecx
  add rax, 4
  jne .LBB5_1
  ret
0 голосов
/ 14 июня 2018

Компиляторы, такие как gcc и clang, либо слишком примитивны, либо слишком «умны» [или, может быть, оба - зависит от того, кого вы спрашиваете], чтобы распознавать ситуации, в которых к хранилищу, которое использовалось как один тип, нужно обращаться как к другому, дажев случаях, которые должны быть легко распознаны чем-либо, кроме самого примитивного компилятора.Например:

struct s1 { int x; };
struct s2 { int x; };
union s1s2 { struct s1 v1; struct s2 v2; };

int read_S1(struct s1 *p) { return p->x; }
void write_S2(struct s2 volatile *p, int v) { p->x = v; }

int test1(union s1s2 arr[], int i, int j)
{
    int temp;
    if (read_S1(&arr[i].v1))
    {
        temp = arr[i].v1.x;
        arr[i].v2.x = temp;
        write_S2(&arr[j].v2, 1);
        temp = arr[i].v2.x;
        arr[i].v1.x = temp;
    }
    return read_S1(&arr[i].v1);
}

int test2(union s1s2 arr[], int i, int j)
{
    int temp;
    { struct s1 *p = &arr[i].v1; temp = p->x; }
    if (temp)
        { struct s2 volatile *p = &arr[j].v2; p->x = 1; }
    { struct s1 *p = &arr[i].v1; temp = p->x; }
    return temp;
}

Ни clang, ни gcc не могут должным образом обрабатывать вероятность того, что в test1 или test2, arr[i] и arr[j] могут идентифицировать одно и то же хранилище, даже если на самом деле его нетлюбой псевдоним в приведенном выше коде, как написано.По любому разумному определению псевдонимов arr[i] и arr[j] не псевдоним, если i==j - они просто применяют один и тот же индекс к одному и тому же массиву.Кроме того, все хранилища, к которым осуществляется доступ с использованием каждого указателя или любого производного от него, в течение срока действия этого указателя будут доступны исключительно таким образом, и, таким образом, ни один из псевдонимов указателей тоже ничего не будет.

К сожалению, оба кланаи gcc слишком примитивны, чтобы заметить, что между временами жизни первого и второго указателей, сформированных путем взятия адреса arr[i].v1, код записал в arr[j], что вполне может быть таким же, как arr[i].Вместо этого они слепо предполагают, что, поскольку физический адрес arr[i].v1 будет одинаковым оба раза, и поскольку никакая операция, которая фактически не изменит какие-либо биты хранилища, связанные с этим местоположением, не делает это с использованием типа struct s1, им разрешеноигнорируй все остальное.

0 голосов
/ 17 мая 2018

Да, строгий псевдоним - это очень реальное явление, и его часто используют современные компиляторы для выполнения оптимизаций.

Рассмотрим следующий код -

typedef struct
{
    char a;
} my_struct;

void foo( int * a, my_struct * b, int count )
{
    int i;

    for (i=0;i<count;i++)
    {
        a[i] += b->a;
    }
}

При компиляции с clang 3.8.0-2 (это очень современный компилятор) для цели X64 (которая также является очень современной целью) с командой -

clang -m64 -S -O2 -std=c11 foo.c -fno-vectorize -fno-unroll-loops

Создает следующую сборку (упрощенно и в синтаксисе AT & T ) -

foo:
    testl   %edx, %edx
    jle     .LBB0_3
    movsbl  (%rsi), %eax   # Value loaded only once before start of loop
    .align  16, 0x90
.LBB0_2:
    addl    %eax, (%rdi)
    addq    $4, %rdi
    decl    %edx
    jne     .LBB0_2
.LBB0_3:
    retq

Видно, что значение из b->a загружается только один раз до начала цикла и добавляется ко всем целым числам в a.

Но если эта функция называется как -

my_struct a[100];
// initializaion of values in a;
...

foo((int*)a, a, 2);  // Breaking strict aliasing

Теперь легко увидеть, что результат будет не тем, что ожидается.

На более низких уровнях оптимизации или с -fno-strict-aliasing компилятор добавляет инструкцию movsbl (%rsi), %eax в тело цикла.

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

Ссылки : Пример был мотивирован из этого сообщения в блоге

...