Странные результаты для условного оператора с GCC и указателями bool
В следующем коде я memset()
a stdbool.h
bool
значение переменной 123
. (Возможно, это поведение undefined?) Затем я передаю указатель на эту переменную функции-жертвы, которая пытается защитить от неожиданных значений с помощью условной операции. Однако 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;
}
[email protected]:~$ gcc -Wall -O0 test.c
[email protected]:~$ ./a.out
123
Что особенно неприятно, так это то, что функция victim()
фактически находится внутри библиотеки и сработает, если значение больше 1.
Воспроизводится в версиях GCC 4.8.2-19ubuntu1 и 4.7.2-5. Не воспроизводится на clang.
Ответы
Ответ 1
(Возможно, это поведение undefined?)
Не напрямую, но чтение с объекта после этого.
Цитата C99:
6.2.6 Представления типов
6.2.6.1 Общие сведения
5 Некоторые представления объектов не обязательно должны представлять значение типа объекта. Если сохраненный значение объекта имеет такое представление и считывается выражением lvalue, которое делает не имеют характера, поведение undefined. [...]
В основном, это означает, что если конкретная реализация решила, что единственными двумя действительными байтами для bool
являются 0
и 1
, то вам лучше убедиться, что вы не используете никаких обманов чтобы попытаться установить его на любое другое значение.
Ответ 2
Когда GCC компилирует эту программу, выход языка ассемблера включает последовательность
movzbl (%rax), %eax
movzbl %al, %eax
movl %eax, -4(%rbp)
который выполняет следующие действия:
- Скопируйте 32 бита из
*foo
(обозначенный (%rax)
в сборке) в регистр %eax
и заполните старшие разряды %eax
нулями (не так, чтобы их было, потому что %eax
является 32-разрядным регистром).
- Скопируйте 8 битов младшего порядка
%eax
(обозначается символом %al
) на %eax
и залейте старшие разряды %eax
нулями. Как программист на C, вы понимаете это как %eax &= 0xff
.
- Скопируйте значение
%eax
в 4 байта выше %rbp
, которое является местоположением bar
в стеке.
Таким образом, этот код является ассемблерным переводом
int bar = *foo & 0xff;
Ясно, что GCC оптимизировал линию, основанную на том, что a 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 его с собой и установки некоторых флагов в процессоре на основе результата. (Здесь AND здесь нет необходимости, но нет инструкции просто проверять регистр и устанавливать флаги.)
- Задайте 8 разрядов младшего порядка
%eax
(обозначается символом %al
) равным 1, если результат ANDing равен 0 или 0 в противном случае.
- Скопируйте 8 битов младшего порядка
%eax
(обозначается символом %al
) на %eax
и запишем младшие разряды %eax
нулями, как в первом фрагменте.
- Скопируйте значение
%eax
в 4 байта выше %rbp
, которое является местоположением bar
в стеке; также как и в первом фрагменте.
Это действительно верный перевод кода на C. И действительно, если вы добавите приведение в (int*)
и скомпилируете и запустите программу, вы увидите, что она выводит 1
.
Ответ 3
Сохранение значения, отличного от 0
или 1
в bool
, - это поведение undefined в C.
Итак, на самом деле это:
int bar = *foo ? 1 : 0;
оптимизирован с чем-то близким к этому:
int bar = *foo ? *foo : 0;