Странные результаты для условного оператора с указателями 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.
Воспроизводится в версиях 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)
который делает следующее:
- Скопировать 32 бита из
*foo
(обозначается(%rax)
в сборе) в реестр%eax
и заполните старшие биты%eax
с нулями (не то что есть, потому что%eax
это 32-битный регистр). - Скопируйте младшие 8 бит
%eax
(обозначается%al
) чтобы%eax
и заполните старшие биты%eax
с нулями. Как программист на С, вы бы поняли это как%eax &= 0xff
, - Скопируйте значение
%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)
который делает следующее:
- Скопировать 32 бита из
*foo
(обозначается(%rax)
в сборе) в реестр%eax
, - Тест 32 бит
%eax
против себя, что означает ANDing это с собой и установка некоторых флагов в процессоре в зависимости от результата. (ANDing здесь не нужен, но нет инструкции просто проверить регистр и установить флаги.) - Установите младшие 8 бит
%eax
(обозначается%al
) к 1, если результат ANDing был 0, или к 0 в противном случае. - Скопируйте младшие 8 бит
%eax
(обозначается%al
) чтобы%eax
и заполните старшие биты%eax
с нулями, как в первом фрагменте. - Скопируйте значение
%eax
до 4 байтов выше%rbp
где находитсяbar
в стеке; также как в первом фрагменте.
Это на самом деле точный перевод кода Си. И действительно, если вы добавите приведение к (int*)
и скомпилируйте и запустите программу, вы увидите, что она выводит 1
,
Сохранение значения, отличного от 0
или же 1
в bool
неопределенное поведение в C.
Так на самом деле это:
int bar = *foo ? 1 : 0;
оптимизирован с чем-то близким к этому:
int bar = *foo ? *foo : 0;