Является ли опция GCC -O2 нарушением этой небольшой программы или у меня неопределенное поведение

Я нашел эту проблему в очень большом приложении, сделал из него SSCCE. Я не знаю, имеет ли код неопределенное поведение или -O2 нарушает его.

При компиляции с gcc a.c -o a.exe -O2 -Wall -Wextra -Werror выводится 5.

Но он печатает 25 при компиляции без -O2 (например, -O1) или раскомментирования одной из двух закомментированных строк (предотвращает встраивание).

#include <stdio.h>
#include <stdlib.h>
// __attribute__((noinline)) 
int f(int* todos, int input) {
    int* cur = todos-1; // fixes the ++ at the beginning of the loop
    int result = input;
    while(1) {
        cur++;
        int ch = *cur;
        // printf("(%i)\n", ch);
        switch(ch) {
            case 0:;
                goto end;
            case 1:;
                result = result*result;
            break;
        }
    }
    end:
    return result;
}
int main() {
    int todos[] = { 1, 0}; // 1:square, 0:end
    int input = 5;
    int result = f(todos, input);
    printf("=%i\n", result);
    printf("end\n");
    return 0;
}

Является ли опция GCC -O2 нарушением этой небольшой программы или у меня где-то есть неопределенное поведение?


person Bernd Elkemann    schedule 15.05.2014    source источник
comment
UB или нет, я бы рекомендовал избавиться от todos - 1 и использовать int ch = *cur++; вместо увеличения перед разыменованием.   -  person asveikau    schedule 15.05.2014
comment
Согласно GDB, при компиляции с -O2 todos имеет значение {-5632, 0}... но я не могу понять почему!   -  person Kevin    schedule 15.05.2014
comment
Valgrind предупреждает о том, что ch не инициализирован.. не могу понять, почему   -  person Mark Nunberg    schedule 15.05.2014
comment
вывод не «просто» =5, это =$input !   -  person Kevin    schedule 15.05.2014
comment
@ChrisStratton Кажется, ты что-то понял. Когда я делаю todo как { -1, 1, 0 }, а затем передаю &todos[1] в f(), программа дает желаемый результат   -  person Mark Nunberg    schedule 15.05.2014
comment
У меня нет gcc, но код выглядит нормально. Взгляните на вывод сборки, попробуйте поставить какие-нибудь printfs и попробовать GDB, хотя последнее может быть сложно с оптимизацией.   -  person Jabberwocky    schedule 15.05.2014
comment
Честно говоря, я не уверен, что должен был проголосовать за то, чтобы закрыть его как дубликат, даже если технически это может соответствовать требованиям ... Я думаю, что тот факт, что существует формальная проблема, подтверждается другим вопросом, но ни один из ответов не является чудесно удовлетворяет.   -  person Chris Stratton    schedule 15.05.2014
comment
так это из-за todos-1 даже при том, что он не разыменовывается? интересно!   -  person Bernd Elkemann    schedule 15.05.2014
comment
@MichaelWalz да. принял ответ ouah, который цитировал причину.   -  person Bernd Elkemann    schedule 15.05.2014
comment
Какую версию gcc вы используете на какой платформе?   -  person Jabberwocky    schedule 15.05.2014
comment
Если вы найдете стандарт ANSI C, это объясняется более подробно, но да, вы вызываете UB. Из стандартного приложения ISO к UB: Добавление или вычитание указателя в объект массива и целочисленный тип или сразу за ним приводит к результату, который не указывает на один и тот же объект массива или сразу за ним (6.5.6). Имейте в виду (N.B.), что 6.5.6 переопределяет объекты, не являющиеся массивами, как одноэлементные массивы. Действительно, стандарт ISO ужасен по сравнению со своим предшественником ANSI. В любом случае, только недавно версии GCC стали настолько драконовскими в отношении UB, что сломали так много кода C.   -  person Heath Hunnicutt    schedule 15.05.2014
comment
@HeathHunnicutt +1 за это. Более распространенным является строгое сглаживание.   -  person Mark Nunberg    schedule 15.05.2014


Ответы (2)


int* cur = todos-1;

вызывает неопределенное поведение. todos - 1 является недопустимым адресом указателя.

Акцент мой:

(C99, 6.5.6p8) «Если и операнд-указатель, и результат указывают на элементы одного и того же объекта-массива или один за последним элементом объекта-массива, оценка не должна приводить к переполнению; в противном случае поведение не определено."

person ouah    schedule 15.05.2014
comment
даже если он снова увеличивается перед разыменованием? интересно! - person Bernd Elkemann; 15.05.2014
comment
@eznme вам не нужно разыменовывать его, чтобы вызвать неопределенное поведение, C говорит, что добавление указателя и целого числа здесь вызывает неопределенное поведение. - person ouah; 15.05.2014

В дополнение к ответу @ouah это объясняет, что делает компилятор.

Сгенерированный ассемблер для справки:

  400450:       48 83 ec 18             sub    $0x18,%rsp
  400454:       be 05 00 00 00          mov    $0x5,%esi
  400459:       48 8d 44 24 fc          lea    -0x4(%rsp),%rax
  40045e:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%rsp)
  400465:       00 
  400466:       48 83 c0 04             add    $0x4,%rax
  40046a:       8b 10                   mov    (%rax),%edx

Однако, если я добавлю printf в main():

  400450:       48 83 ec 18             sub    $0x18,%rsp
  400454:       bf 84 06 40 00          mov    $0x400684,%edi
  400459:       31 c0                   xor    %eax,%eax
  40045b:       48 89 e6                mov    %rsp,%rsi
  40045e:       c7 04 24 01 00 00 00    movl   $0x1,(%rsp)
  400465:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%rsp)
  40046c:       00 
  40046d:       e8 ae ff ff ff          callq  400420 <printf@plt>
  400472:       48 8d 44 24 fc          lea    -0x4(%rsp),%rax
  400477:       be 05 00 00 00          mov    $0x5,%esi
  40047c:       48 83 c0 04             add    $0x4,%rax
  400480:       8b 10                   mov    (%rax),%edx

В частности (в версии printf) эти две инструкции заполняют массив todo.

  40045e:       c7 04 24 01 00 00 00    movl   $0x1,(%rsp)
  400465:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%rsp)

Это явно отсутствует в версии, отличной от printf, которая по какой-то причине назначает только элемент второй:

  40045e:       c7 44 24 04 00 00 00    movl   $0x0,0x4(%rsp)
person Mark Nunberg    schedule 15.05.2014
comment
-1. Это не отвечает на вопрос осмысленно. Кроме, конечно, людей, которые без проблем читают ассемблер. - person Daniel Kamil Kozar; 15.05.2014
comment
@DanielKamilKozar это, кроме того, ответ ouah, поэтому можно не повторять, а только добавить то, что не было сказано в этом ответе. +1 - person Bernd Elkemann; 15.05.2014
comment
Похоже, что это демонстрирует то, что проблема на самом деле не в том, что указатель функционально нарушается во время выполнения, а в том, что оптимизирующий компилятор решает, что первый элемент массива никогда не используется (таким образом, что соответствует правилам) и, таким образом, никогда не нуждается в инициализации. Объявление массива volatile может быть еще одним экспериментом, который стоит попробовать. - person Chris Stratton; 15.05.2014
comment
@ChrisStratton Это очень интересно. Поскольку я не хочу добавлять ответ на свой вопрос, может быть, вы захотите изучить это и написать ответ? - person Bernd Elkemann; 15.05.2014
comment
Я понятия не имею, что означает этот asm - person paulm; 15.05.2014
comment
%rsp — это указатель стека. Массив todo находится в (%rsp) или 0x0(%rsp) или в начале стека. todo[1] занимает четыре байта в стеке или 0x4(%rsp). В первом примере показано присвоение $0x0 (1) только 0x4(rsp) =› todo[1] = 0, а во втором примере присваиваются оба значения: $0x1(%rsp) (todo[0]=1) и 0x00x4(%rsp) (todo[1]=0) - person Mark Nunberg; 15.05.2014