Превышает ли целочисленное переполнение поведение undefined из-за повреждения памяти?

Недавно я прочитал, что подписанное целочисленное переполнение в C и С++ вызывает поведение undefined:

Если во время оценки выражения результат не определен математически или нет в диапазоне отображаемых значений для его типа, поведение undefined.

В настоящее время я пытаюсь понять причину поведения undefined. Я думал, что здесь происходит поведение undefined, потому что целое число начинает манипулировать памятью вокруг себя, когда оно становится слишком большим, чтобы соответствовать базовому типу.

Итак, я решил написать небольшую тестовую программу в Visual Studio 2015, чтобы проверить эту теорию следующим кодом:

#include <stdio.h>
#include <limits.h>

struct TestStruct
{
    char pad1[50];
    int testVal;
    char pad2[50];
};

int main()
{
    TestStruct test;
    memset(&test, 0, sizeof(test));

    for (test.testVal = 0; ; test.testVal++)
    {
        if (test.testVal == INT_MAX)
            printf("Overflowing\r\n");
    }

    return 0;
}

Я использовал здесь структуру, чтобы предотвратить любые защитные функции Visual Studio в режиме отладки, такие как временное дополнение переменных стека и т.д. Бесконечный цикл должен вызывать несколько переполнений test.testVal, и это действительно так, хотя и без каких-либо последствий, кроме самого переполнения.

Я просмотрел дамп памяти во время выполнения тестов переполнения со следующим результатом (test.testVal имел адрес памяти 0x001CFAFC):

0x001CFAE5  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
0x001CFAFC  94 53 ca d8 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Переполнение целого числа с дампом памяти

Как вы видите, память вокруг int, которая постоянно переполняется, осталась "неповрежденной". Я тестировал это несколько раз с аналогичным выходом. Никогда не было памяти вокруг переполненного внутреннего повреждения.

Что здесь происходит? Почему нет повреждений для памяти вокруг переменной test.testVal? Как это может привести к поведению undefined?

Я пытаюсь понять свою ошибку и почему нет повреждения памяти во время переполнения целых чисел.

Ответы

Ответ 1

Вы неправильно понимаете причину поведения undefined. Причина не в повреждении памяти вокруг целого числа - она ​​всегда будет занимать тот же размер, который занимают целые числа, - но лежащая в основе арифметика.

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

И как и во всех действиях undefined, даже если ваше оборудование использует 2 дополнения для своей арифметики и имеет определенные правила для переполнения, компиляторы не связаны ими. Например, в течение долгого времени GCC оптимизировал любые проверки, которые будут реализованы только в среде с добавлением 2'-дополнений. Например, if (x > x + 1) f() будет удаляться из оптимизированного кода, так как подписанное переполнение - это поведение undefined, что никогда не происходит (из представления компилятора, программы никогда не содержат код, создающий поведение undefined), что означает x никогда больше x + 1.

Ответ 2

Авторы стандартного переполнения целочисленного значения undefined, потому что некоторые аппаратные платформы могут ловить ловушку способами, последствия которых могут быть непредсказуемыми (возможно, включая случайное выполнение кода и последующее повреждение памяти). Хотя аппаратное обеспечение с двумя дополнительными устройствами с предсказуемой обработкой переполнения без перерывов в значительной степени было установлено как стандарт к моменту опубликования стандарта C89 (из многих рассмотренных мною перепрограммируемых микрокомпьютерных архитектур, нулевого использования чего-либо еще), авторы стандарта не хочет препятствовать тому, чтобы кто-либо производил реализации C на старых машинах.

В реализациях, в которых реализована стандартная двусмысленная двойная семантика, добавляет код, например

int test(int x)
{
  int temp = (x==INT_MAX);
  if (x+1 <= 23) temp+=2;
  return temp;
}

на 100% надежно, вернет 3 при передаче значения INT_MAX, так как добавление 1 до INT_MAX приведет к INT_MIN, что, конечно, меньше 23.

В 1990-х годах компиляторы использовали тот факт, что целочисленное переполнение было undefined поведением, а не определялось как перенос с двумя дополнениями, чтобы обеспечить различные оптимизации, что означало, что точные результаты вычислений, которые переполнены, не были бы предсказуемыми, но аспекты поведения, которые не зависят от точных результатов, оставались бы на рельсах. Компилятор 1990-х годов, приведенный выше, может, вероятно, рассматривать его так, как если бы добавление 1 к INT_MAX приводило к численному значению, превышающему INT_MAX, что заставило функцию возвращать 1 а не 3, или он может вести себя как старые компиляторы, уступая 3. Обратите внимание, что в приведенном выше коде такое обращение могло бы сохранить инструкцию на многих платформах, поскольку (x + 1 <= 23) будет эквивалентно (x <; = 22). Компилятор может не будет последовательным в выборе 1 или 3, но сгенерированный код не будет делать ничего, кроме одного из этих значений.

С тех пор, однако, для компиляторов стало более модно использовать Стандартная неспособность навязывать любые требования к поведению программы в случае целочисленное переполнение (отказ, обусловленный наличием аппаратного обеспечения, при котором последствия могут быть действительно непредсказуемыми), чтобы оправдать наличие компиляторов полностью исключить код запуска с рельсов в случае переполнения. Современный компилятор может заметить, что программа будет вызывать undefined Behavior, если x == INT_MAX, и таким образом заключить, что функция никогда не будет передана это значение. Если функция никогда не передавала это значение, сравнение с INT_MAX может быть опущено. Если вышеупомянутая функция была вызвана из другой единицы перевода с x == INT_MAX, он может таким образом вернуть 0 или 2; если вызвано изнутри того же единица перевода, эффект может быть еще более причудливым, поскольку компилятор расширьте свои выводы о х обратно вызывающему.

Что касается переполнения, это может привести к повреждению памяти на каком-то старом оборудовании, которое может иметь. На старых компиляторах, работающих на современном оборудовании, этого не произойдет. В гиперсовременных компиляторах переполнение отменяет ткань времени и причинности, поэтому все ставки отключены. Переполнение при оценке x + 1 может эффективно испортить значение x, которое было замечено более ранним сравнением с INT_MAX, заставляя его вести себя так, как если бы значение x в памяти было повреждено. Кроме того, такое поведение компилятора часто устраняет условную логику, которая предотвратила бы другие виды повреждения памяти, что может привести к произвольному повреждению памяти.

Ответ 3

Undefined поведение undefined. Это может привести к сбою вашей программы. Он ничего не может сделать. Он может делать именно то, что вы ожидали. Он может вызвать носовых демонов. Он может удалить все ваши файлы. Компилятор может свободно генерировать любой код, который ему нравится (или вообще отсутствует), когда он встречает поведение undefined.

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

Есть много ответов и сообщений в блоге о поведении undefined, но следующие мои фавориты. Я предлагаю прочитать их, если вы хотите узнать больше о теме.

Ответ 4

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

  • Даже если вы знаете, что архитектура должна быть дополнена двумя (или что-то еще), переполненная операция может не устанавливать флаги, как ожидалось, поэтому оператор вроде if(a + b < 0) может взять неправильную ветвь: учитывая два больших положительных числа, поэтому при добавлении их переполнения и результата, поэтому утверждают, что пуристы с двумя дополнениями отрицательны, но инструкция добавления может фактически не устанавливать отрицательный флаг)

  • Многоступенчатая операция могла иметь место в более широком регистре, чем sizeof (int), без усечения на каждом шаге, и поэтому выражение, подобное (x << 5) >> 5, не может обрезать левые пять бит, поскольку вы предположим, что они будут.

  • Операции умножения и деления могут использовать вторичный регистр для дополнительных бит в продукте и дивидендах. Если умножить "не может" переполнение, компилятор может предположить, что вторичный регистр равен нулю (или -1 для отрицательных продуктов), а не reset перед делением. Таким образом, выражение типа x * y / z может использовать более широкий промежуточный продукт, чем ожидалось.

Некоторые из них звучат как дополнительная точность, но это дополнительная точность, которая не ожидаема, не может быть предсказана и не опираться, и нарушает вашу ментальную модель "каждая операция принимает N-битные операнды двойного дополнения и возвращает наименее значимые N бит результата для следующей операции"

Ответ 5

Целочисленное поведение переполнения не определяется стандартом С++. Это означает, что любая реализация С++ может делать все, что угодно.

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

Существует аргумент, согласно которому переполнение целого числа должно рассматриваться как ошибка, как целое деление на ноль. В архитектуре '86 даже есть команда INTO для создания исключения при переполнении. В какой-то момент этот аргумент может получить достаточный вес, чтобы превратить его в компиляторы основного потока, и в этот момент переполнение целых чисел может привести к сбою. Это также соответствует стандарту С++, который позволяет реализовать что-либо.

Вы могли бы представить себе архитектуру, в которой числа представлялись в виде строк с нулевым символом в стиле little-endian с нулевым байтом с надписью "end of number". Добавление может быть выполнено путем добавления байта байтом до достижения нулевого байта. В такой архитектуре целочисленное переполнение может переписать конечный ноль на один, что делает результат гораздо глубже и потенциально искажает данные в будущем. Это также соответствует стандарту С++.

Наконец, как указано в некоторых других ответах, большое количество генерации и оптимизации кода зависит от рассуждений компилятора о генерируемом им коде и способах его выполнения. В случае переполнения целых чисел полностью компилятор (а) полностью генерирует код для добавления, который дает отрицательные результаты при добавлении больших положительных чисел, и (б) сообщать о генерации кода с учетом того, что добавление больших положительных чисел дает положительный результат. Так, например,

if (a+b>0) x=a+b;

может, если компилятор знает, что оба a и b положительны, не нужно выполнять тест, но безоговорочно добавить a в b и поместить результат в x. На двухкомпонентной машине, которая может привести к отрицательному значению, помещенному в x, при явном нарушении намерения кода. Это будет полностью соответствовать стандарту.

Ответ 6

undefined какое значение представлено int. Там нет "переполнения" в памяти, как вы думали.