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

В следующем коде я memset() stdbool.hbool переменная к значению 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.

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

3 ответа

Решение

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

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

Цитата C99:

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

6.2.6.1 Общие положения

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

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

Когда 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 с нулями. Как программист на С, вы бы поняли это как %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 против себя, что означает ANDing это с собой и установка некоторых флагов в процессоре в зависимости от результата. (ANDing здесь не нужен, но нет инструкции просто проверить регистр и установить флаги.)
  3. Установите младшие 8 бит %eax (обозначается %al) к 1, если результат ANDing был 0, или к 0 в противном случае.
  4. Скопируйте младшие 8 бит %eax (обозначается %al) чтобы %eax и заполните старшие биты %eax с нулями, как в первом фрагменте.
  5. Скопируйте значение %eax до 4 байтов выше %rbpгде находится bar в стеке; также как в первом фрагменте.

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

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

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

int bar = *foo ? 1 : 0;

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

int bar = *foo ? *foo : 0;
Другие вопросы по тегам