Будет ли gcc пропустить эту проверку для целочисленного переполнения?

Например, учитывая следующий код:

int f(int n)
{
 if (n < 0)
 return 0;
 n = n + 100;
 if (n < 0)
 return 0;
 return n;
}

Предполагая, что вы передаете число, очень близкое к переполнению целых чисел (менее 100 прочь), будет ли компилятор генерировать код, который даст вам отрицательный доход?

Вот выдержка из этой проблемы от "Спуска на C" Саймона Татхама:

"Компилятор GNU C (gcc) генерирует код для этой функции, который может возвращать отрицательное целое число, если вы передаете (например) максимальное значение, представляющее способное значение int. Поскольку компилятор знает после первого оператора if, что n является положительным, и тогда он предполагает, что целочисленное переполнение не происходит и использует это предположение, чтобы заключить, что значение n после добавления должно быть положительным, поэтому оно полностью удаляет второй оператор if и возвращает результат неконтролируемого добавления".

Мне стало интересно, существует ли такая же проблема в компиляторах на С++, и если я должен быть осторожен, что мои проверки переполнения целочисленного значения не пропущены.

1 ответ

Краткий ответ

Будет ли компилятор определенно оптимизировать проверку в вашем примере, мы не можем сказать для всех случаев, но мы можем сделать тест против gcc 4.9 с помощью интерактивный компилятор godbolt со следующим кодом (посмотреть его в прямом эфире):

int f(int n)
{
 if (n < 0) return 0;
 n = n + 100;
 if (n < 0) return 0;
 return n;
}
int f2(int n)
{
 if (n < 0) return 0;
 n = n + 100;
 return n;
}

и мы видим, что он генерирует идентичный код для обеих версий, что означает, что он действительно повторяет вторую проверку:

f(int): 
 leal 100(%rdi), %eax #, tmp88 
 testl %edi, %edi # n
 movl $0, %edx #, tmp89
 cmovs %edx, %eax # tmp88,, tmp89, D.2246
 ret
f2(int):
 leal 100(%rdi), %eax #, tmp88
 testl %edi, %edi # n
 movl $0, %edx #, tmp89 
 cmovs %edx, %eax # tmp88,, tmp89, D.2249
 ret

Длинный ответ

Когда ваш код демонстрирует undefined поведение или полагается на потенциальное поведение undefined (в этом примере встречное целочисленное переполнение), то да, компилятор может делать предположения и оптимизировать вокруг них. Например, он может предположить, что поведение undefined отсутствует и, таким образом, оптимизируется в соответствии с этим допущением. Самый позорный пример - это, вероятно, удаление нулевой проверки в ядре Linux. Код был следующим:

struct foo *s = ...;
int x = s->f;
if (!s) return ERROR;
... use s ..

Используемая логика заключалась в том, что, поскольку s был разыменован, он не должен быть нулевым указателем, иначе это будет поведение undefined, и поэтому оно оптимизировало проверку if (!s). В связанной статье говорится:

Проблема заключается в том, что разыменование s в строке 2 допускает компилятор сделать вывод, что s не является нулевым (если указатель имеет значение null, то функция undefined; компилятор может просто игнорировать этот случай). Таким образом нулевая проверка в строке 3 бесшумно оптимизируется, и теперь ядро содержит уязвимость, если злоумышленник может найти способ вызова этот код с нулевым указателем.

Это относится как к C, так и к С++, которые имеют одинаковый язык в отношении поведения undefined. В обоих случаях стандарт сообщает нам, что результаты поведения undefined непредсказуемы, хотя то, что конкретно undefined на любом языке может отличаться. проект стандарта С++ определяет поведение undefined следующим образом:

для которого настоящий международный стандарт не предъявляет требований

и включает следующее примечание (выделение мое):

undefined можно ожидать, когда этот международный стандарт опускает любое явное определение поведения или когда программа использует ошибочной конструкции или ошибочных данных. <span> Допустимое поведение undefinedварьируется от полного игнорирования ситуации с непредсказуемым результаты</span>, чтобы вести себя во время перевода или выполнения программы в характерный для окружающей среды (с или без выдача диагностического сообщения), прекращение перевода или выполнение (с выдачей диагностического сообщения). Многие ошибочные программные конструкции не порождают поведение undefined; они есть необходимо диагностировать.

Проект стандарта C11 имеет схожий язык.

Надлежащая проверка переполнения подписей

Ваша проверка не является надлежащим способом защиты от подписанного целочисленного переполнения, вам нужно проверить, прежде чем выполнять операцию, а не выполнять операцию, если это вызовет переполнение. У Cert есть хорошая ссылка о том, как предотвратить непрерывное переполнение целых чисел для различных операций. Для случая добавления он рекомендует следующее:

#include <limits.h>
void f(signed int si_a, signed int si_b) {
 signed int sum;
 if (((si_b > 0) && (si_a > (INT_MAX - si_b))) ||
 ((si_b < 0) && (si_a < (INT_MIN - si_b)))) {
 /* Handle error */
 } else {
 sum = si_a + si_b;
 }
</limits.h>

Если мы вставляем этот код в godbolt, мы можем видеть, что проверки устранены, что это поведение, которого мы ожидаем.

licensed under cc by-sa 3.0 with attribution.