Странни резултати за условен оператор с 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. Не се възпроизвежда на clang.


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 документ гласи 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 BTW, не означава автоматично, че е 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