Целочисленное переполнение что это

Переполнение целых чисел

Переполнение целых чисел

В ы уже знаете, что при сложении целых может происходить переполнение. Загвоздка в том, что компьютер не выдаёт предупреждения при переполнении: программа продолжит работать с неверными данными. Более того, поведение при переполнении определено только для целых без знака.

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

1. Предварительная проверка данных. Мы знаем, из файла limits.h, максимальное и минимальное значение для чисел типа int. Если оба числа положительные, то их сумма не превысит INT_MAX, если разность INT_MAX и одного из чисел меньше второго числа. Если оба числа отрицательные, то разность INT_MIN и одного из чисел должна быть больше другого. Если же оба числа имеют разные знаки, то однозначно их сумма не превысит INT_MAX или INT_MIN.

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

Обратите внимание на явное приведение типов. Без него сначала произойдёт переполнение, и неправильное число будет записано в переменную c.

3. Третий способ проверки платформозависимый, более того, его реализация будет разной для разных компиляторов. При переполнении целых (обычно) поднимается флаг переполнения в регистре флагов. Можно на ассемблере проверить значение флага сразу же после выполнения суммирования.

Здесь переменная noOverflow равна 1, если нет переполнения. jno (jump if no overflow) выполняет переход к метке NO_OVERFLOW, если переполнения не было. Если же переполнение было, то выполняется

По адресу переменной noOverflow записывается нуль.

Работа с числами без знака гораздо проще: при переполнении происходит обнуление и известно, что получившееся число заведомо будет меньше каждого из слагаемых.

Источник

Русские Блоги

Целочисленная проблема языка Си

Что такое целочисленное переполнение

Я считаю, что целочисленная проблема языка Си не чужая. Для целочисленного переполнения оно делится на целочисленное переполнение без знака и целочисленное переполнение со знаком.

Кроме того, не думайте, что целочисленное переполнение со знаком отрицательно, это неопределенно. Например:

Код выше выведет: 123

Я верю, что все будут знакомы с этим.

Опасности целочисленного переполнения

Давайте поговорим о вреде целочисленного переполнения.

Пример 1: бесконечный цикл, вызванный переполнением пластика
Пример 2: переполнение при пластическом преобразовании
Пример 3: выделение памяти

Типичным примером целочисленного переполнения, вызывающего переполнение кучи, является уязвимость OpenSSH Challenge-Response SKEY / BSD_AUTH, связанная с удаленным переполнением буфера. Следующий проблемный код взят из функции input_userauth_info_response () в auth2-chall.c в коде OpenSSH:

Пример четвертый: переполнение буфера вызывает проблемы безопасности
Пример пятый: переполнение size_t

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

О поведении компилятора

Прежде чем говорить о том, как правильно проверять целочисленное переполнение, нам нужно кое-что узнать о компиляторе. Пожалуйста, не вините меня.

Оптимизация компилятора

Что касается оптимизации компилятора, вот еще один пример. Предположим, у нас есть следующий код (опять же довольно распространенный код):

Для этого вам нужно преобразовать вышеуказанный тип char * в uintptr_t или size_t. Проще говоря, это означает, что char * превращается в структуру данных без знака, и блок оператора if не может быть оптимизирован. Это выглядит так:

В связи с этим, вы можете посмотреть на спецификации C99 » ISO/IEC 9899:1999 C specification «Страница §6.5.6, пункт 8, я делаю снимок следующим образом: (Этот параграф означает поведение указателя +/- целое число определено, если поведение выходит за пределы, поведение не определено)

Целочисленное переполнение что это. Смотреть фото Целочисленное переполнение что это. Смотреть картинку Целочисленное переполнение что это. Картинка про Целочисленное переполнение что это. Фото Целочисленное переполнение что это

Викторины: пасхальное яйцо компилятора

Выше говорилось, что так называемое неопределенное поведение полностью делегировано компилятору. GCC также продемонстрировал неопределенное поведение в версии 1.17 (Смотрите Википедию)。

При обнаружении неопределенного поведения в версии gcc 1.17 ниже, исходный код яйца, который gcc воспроизводит в дистрибутиве unix. Мы видим, что он попытается выполнить некоторые игрыNetHack,Rogue или Emacs Towers of HanoiЕсли не найдено, выведите ошибку NB.

Правильное обнаружение целочисленного переполнения

Посмотрев на эти поведения компилятора, вы должны понять…Вы должны проверить, прежде чем целочисленные переполнения, в противном случае это слишком поздно”。

Давайте посмотрим на кусок кода:

Например, следующий код неверен:

В приведенном выше коде все должны обратить внимание(SIZE_MAX – m SIZE_MAX? Потому что, если m + n переполнится, оно будет усечено, поэтому выражение всегда будет истинным и не будет обнаружено. Кроме того, в этом выражении m и n переводятся в unsigned, соответственно.

Но приведенный выше код неверен, потому что:

1) Слишком поздно проверять, неопределенное поведение компилятора появилось до if (вы не знаете, что произойдет).

Источник

Целочисленное переполнение что это. Смотреть фото Целочисленное переполнение что это. Смотреть картинку Целочисленное переполнение что это. Картинка про Целочисленное переполнение что это. Фото Целочисленное переполнение что это

Для некоторых приложений, таких как таймеры и часы, может быть желательно перенос при переполнении. Стандарт C11 утверждает, что для беззнаковых целых чисел перенос по модулю является определенным поведением, и термин «переполнение» никогда не применяется: «вычисление с участием беззнаковых операндов никогда не может переполниться».

СОДЕРЖАНИЕ

Источник

Флаги

Большинство компьютеров имеют два выделенных флага процессора для проверки условий переполнения.

Варианты определения и неоднозначность

Для беззнакового типа, когда идеальный результат операции находится за пределами представимого диапазона типа и возвращаемый результат получается путем упаковки, это событие обычно определяется как переполнение. Напротив, стандарт C11 определяет, что это событие не является переполнением, и заявляет, что «вычисление с использованием беззнаковых операндов никогда не может переполниться».

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

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

Непоследовательное поведение

Методы решения проблем целочисленного переполнения

Обработка целочисленного переполнения в различных языках программирования

ЯзыкБеззнаковое целоеЗнаковое целое число
Адапо модулю модуля типаподнять Constraint_Error
C / C ++степень двойки по модулюнеопределенное поведение
C #степень по модулю 2 в непроверенном контексте; System.OverflowException поднимается в проверяемом контексте
ДжаваN / Aстепень двойки по модулю
JavaScriptвсе числа с плавающей запятой двойной точности, кроме нового BigInt
MATLABВстроенные целые числа насыщают. Целые числа с фиксированной запятой, настраиваемые для переноса или насыщения
Python 2N / Aпреобразовать в длинный тип (bigint)
Семя7N / Aподнять OVERFLOW_ERROR
СхемаN / Aпреобразовать в bigNum
Simulinkнастраивается для обертывания или насыщения
БолтовняN / Aпреобразовать в LargeInteger
БыстрыйВызывает ошибку, если не используются специальные операторы переполнения.

Обнаружение

Избегание

Умение обращаться

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

У ЦП обычно есть способ обнаружения этого для поддержки добавления чисел, превышающих размер их регистра, обычно с использованием бита состояния; этот метод называется арифметикой с высокой точностью. Таким образом, можно добавить два числа каждые два байта шириной, используя только добавление байтов по шагам: сначала добавьте младшие байты, затем добавьте старшие байты, но если необходимо выполнить младшие байты, это арифметическое переполнение добавление байтов, и становится необходимым обнаруживать и увеличивать сумму старших байтов.

Явное распространение

Поддержка языков программирования

В отличие от более старых языков, таких как C, некоторые новые языки, такие как Rust, например, предоставляют встроенные функции, которые позволяют легко обнаруживать и выбирать пользователю, как следует обрабатывать переполнение в каждом конкретном случае. В Rust, хотя использование основных математических операторов, естественно, лишено такой гибкости, пользователи могут альтернативно выполнять вычисления с помощью набора методов, предоставляемых каждым из целочисленных примитивных типов. Эти методы предоставляют пользователям несколько вариантов выбора между выполнением «проверенной» (или «переполненной») операции (которая указывает, произошло ли переполнение через возвращаемый тип); «неконтролируемая» операция; операция, которая выполняет упаковку, или операцию, которая выполняет насыщение на числовых границах.

Насыщенная арифметика

Примеры

Целочисленное переполнение что это. Смотреть фото Целочисленное переполнение что это. Смотреть картинку Целочисленное переполнение что это. Картинка про Целочисленное переполнение что это. Фото Целочисленное переполнение что это

Microsoft / IBM Macro Assembler (MASM) версии 1.00 и, вероятно, все другие программы, созданные одним и тем же компилятором Pascal, имели целочисленное переполнение и ошибку подписи в коде настройки стека, что не позволяло им запускаться на новых машинах DOS или эмуляторах в некоторых общих конфигурации с объемом памяти более 512 КБ. Программа либо зависает, либо отображает сообщение об ошибке и выходит в DOS.

В августе 2016 года автомат в казино Resorts World распечатал призовой билет на 42 949 672,76 долларов в результате ошибки переполнения. Казино отказалось выплатить эту сумму, назвав это неисправностью, используя в свою защиту то, что в автомате четко указано, что максимальная выплата составляет 10 000 долларов, поэтому любой приз, превышающий эту сумму, должен быть результатом ошибки программирования. Верховный суд штата Айова вынес решение в пользу казино.

Источник

Целочисленное переполнение что это. Смотреть фото Целочисленное переполнение что это. Смотреть картинку Целочисленное переполнение что это. Картинка про Целочисленное переполнение что это. Фото Целочисленное переполнение что это

Для некоторых приложений, таких как таймеры и часы, может быть желательно перенос при переполнении. Стандарт C11 утверждает, что для беззнаковых целых чисел перенос по модулю является определенным поведением, и термин «переполнение» никогда не применяется: «вычисление с участием беззнаковых операндов никогда не может переполниться».

СОДЕРЖАНИЕ

Источник

Флаги

Большинство компьютеров имеют два выделенных флага процессора для проверки условий переполнения.

Варианты определения и неоднозначность

Для беззнакового типа, когда идеальный результат операции находится за пределами представимого диапазона типа и возвращаемый результат получается путем упаковки, это событие обычно определяется как переполнение. Напротив, стандарт C11 определяет, что это событие не является переполнением, и заявляет, что «вычисление с использованием беззнаковых операндов никогда не может переполниться».

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

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

Непоследовательное поведение

Методы решения проблем целочисленного переполнения

Обработка целочисленного переполнения в различных языках программирования

ЯзыкБеззнаковое целоеЗнаковое целое число
Адапо модулю модуля типаподнять Constraint_Error
C / C ++степень двойки по модулюнеопределенное поведение
C #степень по модулю 2 в непроверенном контексте; System.OverflowException поднимается в проверяемом контексте
ДжаваN / Aстепень двойки по модулю
JavaScriptвсе числа с плавающей запятой двойной точности, кроме нового BigInt
MATLABВстроенные целые числа насыщают. Целые числа с фиксированной запятой, настраиваемые для переноса или насыщения
Python 2N / Aпреобразовать в длинный тип (bigint)
Семя7N / Aподнять OVERFLOW_ERROR
СхемаN / Aпреобразовать в bigNum
Simulinkнастраивается для обертывания или насыщения
БолтовняN / Aпреобразовать в LargeInteger
БыстрыйВызывает ошибку, если не используются специальные операторы переполнения.

Обнаружение

Избегание

Умение обращаться

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

У ЦП обычно есть способ обнаружения этого для поддержки добавления чисел, превышающих размер их регистра, обычно с использованием бита состояния; этот метод называется арифметикой с высокой точностью. Таким образом, можно добавить два числа каждые два байта шириной, используя только добавление байтов по шагам: сначала добавьте младшие байты, затем добавьте старшие байты, но если необходимо выполнить младшие байты, это арифметическое переполнение добавление байтов, и становится необходимым обнаруживать и увеличивать сумму старших байтов.

Явное распространение

Поддержка языков программирования

В отличие от более старых языков, таких как C, некоторые новые языки, такие как Rust, например, предоставляют встроенные функции, которые позволяют легко обнаруживать и выбирать пользователю, как следует обрабатывать переполнение в каждом конкретном случае. В Rust, хотя использование основных математических операторов, естественно, лишено такой гибкости, пользователи могут альтернативно выполнять вычисления с помощью набора методов, предоставляемых каждым из целочисленных примитивных типов. Эти методы предоставляют пользователям несколько вариантов выбора между выполнением «проверенной» (или «переполненной») операции (которая указывает, произошло ли переполнение через возвращаемый тип); «неконтролируемая» операция; операция, которая выполняет упаковку, или операцию, которая выполняет насыщение на числовых границах.

Насыщенная арифметика

Примеры

Целочисленное переполнение что это. Смотреть фото Целочисленное переполнение что это. Смотреть картинку Целочисленное переполнение что это. Картинка про Целочисленное переполнение что это. Фото Целочисленное переполнение что это

Microsoft / IBM Macro Assembler (MASM) версии 1.00 и, вероятно, все другие программы, созданные одним и тем же компилятором Pascal, имели целочисленное переполнение и ошибку подписи в коде настройки стека, что не позволяло им запускаться на новых машинах DOS или эмуляторах в некоторых общих конфигурации с объемом памяти более 512 КБ. Программа либо зависает, либо отображает сообщение об ошибке и выходит в DOS.

В августе 2016 года автомат в казино Resorts World распечатал призовой билет на 42 949 672,76 долларов в результате ошибки переполнения. Казино отказалось выплатить эту сумму, назвав это неисправностью, используя в свою защиту то, что в автомате четко указано, что максимальная выплата составляет 10 000 долларов, поэтому любой приз, превышающий эту сумму, должен быть результатом ошибки программирования. Верховный суд штата Айова вынес решение в пользу казино.

Источник

Почему перенос при целочисленном переполнении — не очень хорошая идея

Эта статья посвящена неопределённому поведению и оптимизациям компилятора, особенно в контексте знакового целочисленного переполнения.

Примечание от переводчика: в русском языке нет четкого соответствия в употребляемом контексте слова «wrap»/«wrapping». Существует математический термин «перенос», который близок к описываемому явлению, а термин «флаг переноса» (carry flag) — механизм выставления флага в процессорах при целочисленном переполнении. Другим вариантом перевода может быть фраза «вращение/переворот/оборот вокруг нуля». Она лучше отображает смысл «wrap» по сравнению с «перенос», т.к. показывает переход чисел при переполнении из положительного в отрицательный диапазон. Однако, как оказалось, эти слова смотрятся в тексте непривычно для тестовых читателей. Для упрощения в дальнейшем примем в качестве перевода термина «wrap» слово «перенос».

Компиляторы языка C (и C++) в своей работе всё чаще руководствуются понятием неопределённого поведения — представлением о том, что поведение программы при некоторых операциях не регламентировано стандартом и что, генерируя объектный код, компилятор вправе исходить из предположения, что программа таких операций не производит. Немало программистов возражало против такого подхода, поскольку сгенерированный код в этом случае может вести себя не так, как задумывал автор программы. Эта проблема становится всё острее, так как компиляторы применяют всё более хитроумные методы оптимизации, которые наверняка будут опираться на понятие неопределённого поведения.

В этом контексте показателен пример со знаковым целочисленным переполнением. Большинство разработчиков на C пишут код для машин, в которых для представления целых чисел используется дополнительный код, а сложение и вычитание в таком представлении реализованы точно так же, в беззнаковой арифметике. Если сумма двух положительных целых чисел со знаком переполнится — то есть станет больше, чем вмещает тип, — процессор выдаст значение, которое, будучи интерпретировано как двоичное дополнение знакового числа, будет считаться отрицательным. Это явление называется «переносом», поскольку результат, дойдя до верхней границы диапазона значений, «переносится» и начинается с нижней границы.

По этой причине можно иногда увидеть вот такой код на C:

Задача оператора if — обнаружить состояние переполнения (в данном случае оно возникает после прибавления 1000 к значению переменной a) и сообщить об ошибке. Проблема в том, что в C знаковое целочисленное переполнение является одним из случаев неопределённого поведения. Компиляторы с некоторых пор считают такие условия всегда ложными: если прибавить 1000 (или любое другое положительное число) к другому числу, результат не может быть меньше начального значения. Если же происходит переполнение, значит, возникает неопределённое поведение, и не допускать этого — уже (по-видимому) забота самого программиста. Поэтому компилятор может решить, что условный оператор можно целиком удалить в целях оптимизации (ведь условие всегда ложно, оно ни на что не влияет, значит, можно обойтись без него).

Проблема в том, что этой оптимизацией компилятор убрал проверку, которую программист добавил специально, чтобы выявить неопределённое поведение и обработать его. Вот тут можно посмотреть, как это происходит на практике. (Примечание: сайт godbolt.org, на котором размещён пример, очень классный! Можно редактировать код и сразу же смотреть, как его обрабатывают разные компиляторы, а их там представлено множество. Поэкспериментируйте!). Обратите внимание, что компилятор не убирает проверку на переполнение, если изменить тип на беззнаковый, поскольку поведение при беззнаковом переполнении в языке С определено (точнее, при беззнаковой арифметике результат переносится, поэтому переполнения на самом деле не происходит).

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

Перенос при переполнении — полезное поведение?

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

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

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

Учитывая всё это, я нахожу неубедительным аргумент, будто перенос полезен в большинстве случаев.

Перенос — это поведение, которого ожидают программисты?

С этим доводом сложнее спорить, поскольку очевидно, что код по крайней мере некоторых программистов на C предполагает семантику переноса при знаковом целочисленном переполнении. Но одного только этого факта недостаточно, чтобы считать такую семантику предпочтительной (заметьте, что некоторые компиляторы позволяют включить её, если необходимо).

Очевидное решение проблемы (ожидание программистами именно этого поведения) — сделать так, чтобы компилятор выдавал предупреждение, когда он оптимизирует код, предполагая отсутствие неопределённого поведения. К сожалению, как мы видели в примере на сайте godbolt.org по ссылке выше, компиляторы не всегда поступают таким образом (Gcc версии 7.3 — да, а версии 8.1 — нет, так что налицо шаг назад).

Семантика неопределённого поведения при переполнении не даёт заметного преимущества?

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

Я допускаю, что конкретно этой оптимизацией (удаление проверок математически противоречивых условий) в обычных программах на C зачастую можно пренебречь, так как их авторы стремятся к наилучшей производительности и всё равно оптимизируют код вручную: то есть, если очевидно, что данный оператор if содержит условие, которое никогда не будет истинным, программист, скорее всего, уберёт его сам. В самом деле, я выяснил, что в нескольких исследованиях эффективность такой оптимизации была поставлена под вопрос, протестирована и признана практически несущественной в рамках контрольных тестов. Однако, хотя эта оптимизация почти никогда не даёт преимущества в языке C, генераторы кода и оптимизации компиляторов в большинстве своём универсальны и могут использоваться в других языках — и для них данный вывод может быть неверен. Возьмём язык C++ с его, скажем так, традицией полагаться на оптимизатор, чтобы тот убирал избыточные конструкции в коде шаблонов, а не делать это вручную. А ведь существуют и языки, которые преобразуются транспайлером в C, и при этом избыточный код в них также оптимизируется C-компиляторами.

Кроме того, даже если сохранить проверки на переполнение, вовсе не факт, что прямая стоимость переноса целочисленных переменных будет минимальной даже на машинах, использующих дополнительный код. Архитектура Mips, например, может выполнять арифметические операции только в регистрах фиксированного размера (32 бита). Тип short int, как правило, имеет размер 16 бит, а char — 8 бит; при хранении в регистре переменной одного из этих типов её размер расширится, и, чтобы корректно перенести её, потребуется выполнить по крайней мере одну дополнительную операцию и, возможно, задействовать дополнительный регистр (чтобы вместить соответствующую битовую маску). Должен признать, что я уже давно не имел дела с кодом для Mips, так что я не уверен насчёт точной стоимости этих операций, но я уверен, что она ненулевая и что на других архитектурах RISC могут возникнуть такие же проблемы.

Стандарт языка запрещает избегать переноса переменных, если оно предполагается архитектурой?

Если разобраться, этот аргумент особенно слаб. Его суть в том, что стандарт якобы позволяет реализации (компилятору) трактовать «неопределённое поведение» лишь в ограниченных пределах. В самом же тексте стандарта — в том его фрагменте, к которому апеллируют сторонники переноса, — сказано следующее (это часть определения понятия «неопределённое поведение»):

Идея в том, что слова «полное игнорирование ситуации» не позволяют предполагать, что событие, приводящее к неопределённому поведению, — например, переполнение при сложении — не может произойти, а скорее, что если оно случается, то компилятор должен продолжить работу как ни в чем не бывало, но при этом также учитывать результат, который получится, если он пошлёт процессору запрос на выполнение такой операции (другими словами, как если бы исходный код транслировался в машинный прямолинейно и наивно).

Прежде всего, следует заметить, что этот текст дан как «примечание», а потому не является нормативным (т.е. не может что-то предписывать), согласно директиве ISO, упомянутой в предисловии к стандарту:

В соответствии с Частью 3 Директив ISO/IEC, данное предисловие, введение к тексту, примечания, сноски и примеры также носят исключительно информационный характер.

Поскольку этот фрагмент о «неопределённом поведении» является примечанием, он ничего не предписывает. Обратите внимание, что настоящее определение понятия «неопределённое поведение» звучит так:

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

Я выделил главную мысль: к неопределённому поведению не предъявляется никаких требований; список «возможных видов неопределённого поведения» в примечании содержит лишь примеры и не может быть окончательным предписанием. Фразу «не предъявляет никаких требований» невозможно истолковать как-то иначе.

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

Доводы в защиту переноса по большей части несостоятельны. Пожалуй, самый сильный аргумент получается, если их объединить: на перенос иногда рассчитывают менее опытные программисты (которые не знают тонкостей языка C и неопределённого поведения в нем), и оно не снижает производительность — хотя последнее верно не во всех случаях, а первая часть неубедительна, если рассматривать её отдельно.

Лично я предпочёл бы, чтобы переполнения блокировались (trapping), а не переносились. То есть, чтобы программа падала, а не продолжала работать — с неопределённым ли поведением или потенциально некорректным результатом, ведь и в том, и в другом случае появляется уязвимость. Такое решение, конечно, немного снизит производительность на большинстве (?) архитектур, особенно на x86, но, с другой стороны, ошибки, связанные с переполнением, будут сразу выявлены и ими не получится воспользоваться или получить с их помощью некорректные результаты дальше по ходу выполнения программы. Кроме того, в теории компиляторы при таком подходе могли бы безопасно удалять избыточные проверки на переполнение, поскольку оно точно не случится, хотя, как я вижу, ни Clang, ни GСС этой возможностью не пользуются.

К счастью, и прерывание, и перенос реализованы в компиляторе, которым я пользуюсь чаще всего, — GCC. Для переключения между режимами используются аргументы командной строки -ftrapv и -fwrapv соответственно.

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

Дополнение (от 24 августа 2018 года)

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

Дополнительные ссылки по теме от команды PVS-Studio:

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *