Странные результаты для условного оператора с указателями GCC и bool

В следующем коде я memset() stdbool.h bool переменную со значением 123. (Возможно, это неопределенное поведение?) Затем я передаю указатель на эту переменную функции-жертве, которая пытается защитить себя от неожиданных значений с помощью условной операции. Однако GCC почему-то вроде бы вообще убирает условную операцию.

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

void victim(bool* foo)
{
    int bar = *foo ? 1 : 0;
    printf("%d\n", bar);
}

int main()
{
    bool x;
    bool *foo = &x;
    memset(foo, 123, sizeof(bool));
    victim(foo);
    return 0;
}
user@host:~$ gcc -Wall -O0 test.c
user@host:~$ ./a.out 
123

Что особенно раздражает, так это то, что функция victim() на самом деле находится внутри библиотеки и выйдет из строя, если значение больше 1.

Воспроизводится в версиях GCC 4.8.2-19ubuntu1 и 4.7.2-5. Не воспроизводится на лязгах.


person jpa    schedule 26.12.2014    source источник
comment
Из стандарта C99: 6.5.2 2 Объект, объявленный как тип _Bool, достаточно велик, чтобы хранить значения 0 и 1.   -  person Severin Pappadeux    schedule 26.12.2014
comment
Определив x как bool, вы пообещали компилятору, что вы сохраните в нем только 0 или 1. Сохраняя 123 в x1, вы солгали компилятору. Если вы солгаете компилятору, он отомстит. - Генри Спенсер   -  person Keith Thompson    schedule 26.12.2014
comment
@SeverinPappadeux: Да, но поскольку любой объект, не являющийся битовым полем, должен быть не менее CHAR_BIT бит (и CHAR_BIT >= 8), он также достаточно велик, чтобы содержать значение 123. Вам не запрещается хранить 123 в объекте bool из-за его размера, но это неопределенное поведение.   -  person Keith Thompson    schedule 26.12.2014
comment
Поскольку у вас включен заголовок <stdbool.h>, вы можете рассмотреть возможность использования только значений true и false (как в Pascal с типом Boolean). Это сделает ваш код более читабельным и избавит вас от других значений.   -  person Grzegorz Szpetkowski    schedule 27.12.2014
comment
@KeithThompson: Я не думаю, что стандарт запрещает компилятору сохранять true как битовый шаблон 01111011. Что еще более важно, он может выбрать 11111111 в качестве битового шаблона для true. Преобразование true в int должно давать 1, но это не ограничивает битовый шаблон true. (Сравните: от 1.0f до int.)   -  person MSalters    schedule 27.12.2014
comment
@MSalters: bool - это целочисленный тип, и он должен соответствовать тем же правилам, что и другие целочисленные типы. true - это макрос, определенный в <stdbool.h>, который расширяется до 1. Если присвоение значения 1 объекту bool устанавливает его битовую комбинацию в 11111111, тогда 7 битов старшего разряда должны быть битами заполнения. (И все нулевые биты должны быть представлением для 0, но не обязательно единственным представлением.)   -  person Keith Thompson    schedule 27.12.2014
comment
@KeithThompson: не может ли реализация ограничить значения битов заполнения? Поскольку в моем примере было показано 7 битов заполнения 0111101 перед битом значения 1? И в основном комментарий, на который я ответил, предполагал, что эти биты обязательно должны быть равны нулю, что я считаю особенно неверным.   -  person MSalters    schedule 27.12.2014
comment
@MSalters: Некоторые битовые шаблоны могут быть представлениями-ловушками; доступ к объекту с таким представлением (через lvalue соответствующего типа) вызывает неопределенное поведение. Правила для _Bool сговорились, чтобы запутать вещи способами, которые мне пока лень исследовать. Итог: следует избегать хранения значений, отличных от 0 и 1, в объекте _Bool.   -  person Keith Thompson    schedule 27.12.2014


Ответы (3)


(Возможно, это неопределенное поведение?)

Не напрямую, но чтение с объекта впоследствии.

Цитата C99:

6.2.6 Представления типов

6.2.6.1 Общие

5 Некоторые представления объектов не обязательно должны представлять значение типа объекта. Если сохраненное значение объекта имеет такое представление и читается выражением lvalue, не имеющим символьного типа, поведение не определено. [...]

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

person Community    schedule 26.12.2014
comment
Gcc doc читает GCC поддерживает только два дополнительных целочисленных типа, и все битовые шаблоны являются обычными значениями. Это означает, что _Bool также не имеет представления ловушек. Не уверен, что это небрежность в документации или есть что-то еще в стандарте, позволяющее такую ​​оптимизацию. - person mafso; 27.12.2014
comment
@mafso Думаю, это корявость в формулировках. Беззнаковые целочисленные типы (включая _Bool) никогда не могут использовать дополнение до двух, потому что у них вообще нет знакового бита. - person ; 27.12.2014
comment
Да, два дополнения не применяются, но это не меняет, все битовые шаблоны являются обычными значениями, а _Bool должен иметь не менее 8 бит. Другими словами, нужно ли, строго говоря, Gcc задокументировать _Bool как имеющие CHAR_BIT - 1 биты заполнения, чтобы сделать оптимизацию в вопросе возможной? Я не уверен, есть ли в стандарте еще одна часть, требующая этого. [...] - person mafso; 27.12.2014
comment
[...] То, что _Bool должен иметь возможность представлять 0 и 1, автоматически не означает, что UB хранит в нем что-то другое. То, что char должен содержать каждый символ из базового набора символов выполнения, не означает, что вы не можете хранить в нем что-либо еще (например, 127 вполне допустимо, независимо от того, входит ли он в базовый набор символов выполнения). - person mafso; 27.12.2014
comment
@mafso Я пытался указать на то, что все это предложение предназначено для охвата только целочисленных типов со знаком: это объяснение определяемого реализацией аспекта целочисленных типов со знаком. Я согласен с тем, что это плохо сформулировано, но если мы действительно примем его применимым только к целочисленным типам со знаком, тогда, если в другой части документации не будут сформулированы плохо сформулированные утверждения о представлении _Bool, GCC не требуется документировать сколько из возможных представлений _Bool действительны. - person ; 27.12.2014
comment
@mafso Кстати, это автоматически не означает, что UB хранить в нем что-то другое правильно. Также я начал свой ответ. Недействителен не магазин. Этого не может быть, так как это делается с использованием символьного типа. Это следующее чтение недействительно. - person ; 27.12.2014
comment
Насколько я понимаю, биты заполнения определяются реализацией. И _Bool имеет (по крайней мере) один бит значения, без знакового бита и всего не менее CHAR_BIT битов. Если рассматриваемая оптимизация разрешена предоставленной вами цитатой, это означает, что представления, отличные от 0 и 1, являются представлениями ловушки, что означает (опять же, как я это понимаю) как минимум 7 битов заполнения. Иными словами: чтобы сделать вашу цитату применимой, вы должны показать, что 123 действительно является представлением ловушки для _Bool (в данном случае для Gcc, если он действительно определен реализацией). - person mafso; 27.12.2014
comment
s/store...in/store or read...in or from/g для этого предложения (мои рассуждения все еще применимы). - person mafso; 27.12.2014
comment
@mafso В стандарте для реализации нет требования документировать, имеет ли какой-либо тип биты заполнения, за исключением косвенного сравнения sizeof(T) * CHAR_BIT со значением T_MAX, как определено в <limits.h>. Если тип имеет биты заполнения, в стандарте для реализации нет требования документировать, являются ли эти биты заполнения значимыми (могут ли неправильные значения битов заполнения создавать представления ловушек). - person ; 27.12.2014
comment
Я понимаю. Я думал, что все биты заполнения должны быть задокументированы, но это только для значения знакового бита и таких представлений ловушки, как отрицательный ноль или единица меньше -T_MAX. Спасибо за терпеливость. - person mafso; 27.12.2014

Когда GCC компилирует эту программу, вывод на ассемблере включает последовательность

movzbl (%rax), %eax
movzbl %al, %eax
movl %eax, -4(%rbp)

который делает следующее:

  1. Скопируйте 32 бита из *foo (обозначенного (%rax) в сборке) в регистр %eax и заполните старшие биты %eax нулями (не то чтобы они были, потому что %eax - 32-битный регистр).
  2. Скопируйте 8 младших битов %eax (обозначенных %al) в %eax и заполните старшие биты %eax нулями. Как программист на C вы понимаете это как %eax &= 0xff.
  3. Скопируйте значение %eax на 4 байта выше %rbp, где находится bar в стеке.

Итак, этот код представляет собой перевод на язык ассемблера

int bar = *foo & 0xff;

Очевидно, что GCC оптимизировал строку, основываясь на том факте, что bool никогда не должен содержать никаких значений, кроме 0 или 1.

Если вы измените соответствующую строку в источнике C на эту

int bar = *((int*)foo) ? 1 : 0;

тогда сборка изменится на

movl (%rax), %eax
testl %eax, %eax
setne %al
movzbl %al, %eax
movl %eax, -4(%rbp)

который делает следующее:

  1. Скопируйте 32 бита из *foo (обозначается (%rax) в сборке) в регистр %eax.
  2. Протестируйте 32 бита %eax против самого себя, что означает операцию AND с самим собой и установку некоторых флагов в процессоре в зависимости от результата. (И здесь нет необходимости, но нет инструкции просто проверить регистр и установить флаги.)
  3. Установите 8 младших битов %eax (обозначается %al) в 1, если результат операции И был 0, или в 0 в противном случае.
  4. Скопируйте 8 младших битов %eax (обозначенных %al) в %eax и заполните старшие биты %eax нулями, как в первом фрагменте.
  5. Скопируйте значение %eax на 4 байта выше %rbp, где находится bar в стеке; также как в первом фрагменте.

На самом деле это точный перевод кода C. И действительно, если вы добавите приведение к (int*), скомпилируете и запустите программу, вы увидите, что она выводит 1.

person David Z    schedule 27.12.2014
comment
Это, безусловно, лучший ответ. Думаю, у тебя меньше голосов за опоздание на вечеринку. - person Alex; 27.12.2014
comment
Это говорит о том, что на самом деле делает GCC, что является хорошим дополнением к ответу hvd, почему GCC разрешено это делать. Я был больше заинтересован в последнем, поэтому я принял его вместо этого. - person jpa; 27.12.2014
comment
@jpa да, я подумал, что это будет хорошим дополнением к другому ответу. Хотя я хотел бы отметить, что ваш вопрос на самом деле ничего не задает (он просто говорит: «Посмотрите на это странное поведение»), что затрудняет определение того, какой ответ вы хотите получить. - person David Z; 27.12.2014

Сохранение значения, отличного от 0 или 1 в bool, является неопределенным поведением в C.

Итак, на самом деле это:

int bar = *foo ? 1 : 0;

оптимизирован чем-то близким к этому:

int bar = *foo ? *foo : 0;
person ouah    schedule 26.12.2014
comment
Вы можете пойти еще дальше и сказать, что, поскольку x ? x : 0 является идентификатором в этом случае, он дополнительно оптимизируется только до x, откуда и будет получен результат. - person Iwillnotexist Idonotexist; 27.12.2014