Чтение онлайн

на главную - закладки

Жанры

19 смертных грехов, угрожающих безопасности программ

Виега Джон

Шрифт:
...

template <typename T>

void WhatIsIt(T value)

{

if((T)-1 < 0)

printf("Со знаком");

else

printf("Без знака");

printf(" – %d бит\n", sizeof(T)*8);

}

Для простоты оставим в стороне случай смешанных операций над целыми и числами с плавающей точкой. Правила формулируются так:

□ если хотя бы один операнд имеет тип unsigned long, то оба операнда приводятся к типу unsigned long. Строго говоря, long и int – это два разных типа, но на современных машинах тот и другой имеют длину 32 бита, поэтому компилятор считает их эквивалентными;

□ во всех остальных

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

Как правило, ничего неожиданного при этом не происходит, и неявное приведение в результате применения операторов может даже помочь избежать некоторых переполнений. Но бывают и сюрпризы. Во–первых, в системах, где имеется тип 64–разрядного целого, было бы логично ожидать, что коль скоро unsigned short и signed short приводятся к int, а операторное приведение не нарушает корректность результата (по крайней мере, если вы потом не выполняете понижающего приведения до 16 битов), то unsigned int и signed int будут приводиться к 64–разрядному типу (_int64). Если вы думаете, что все так и работает, то вынуждены вас разочаровать – по крайней мере, до той поры, когда стандарт C/C++ не станет трактовать 64–разрядные целые так же, как остальные.

Вторая неожиданность заключается в том, что поведение изменяется еще и в зависимости от оператора. Все арифметические операторы (+, – ,*,/,%) подчиняются приведенным выше правилам. Но им же подчиняются и поразрядные бинарные операторы (&,|,А); поэтому (unsigned short) | (unsigned short) дает int! Те же правила в языке С распространяются на булевские операторы (&&,|| и !), тогда как в С++ возвращается значение встроенного типа bool. Дополнительную путаницу вносит тот факт, что одни унарные операторы модифицируют тип, а другие–нет. Оператор дополнения до единицы (~) изменяет тип результата, поэтому -((unsigned short)0) дает int, тогда как операторы префиксного и постфиксного инкремента и декремента (++, – ) типа не меняют.

Один программист с многолетним стажем работы предложил следующий код для проверки того, возникнет ли переполнение при сложении двух 16–разрядных целых без знака:

...

bool IsValidAddition(unsigned short x, unsigned short y)

{

if(x + y < x)

return false;

return true;

}

Вроде бы должно работать. Если результат сложения двух положительных чисел оказывается меньше какого–то слагаемого, очевидно, что–то не в порядке. Точно такой же код должен работать и для чисел типа unsigned long. Увы, программист не учел, что компилятор оптимизирует всю функцию так, что она будет возвращать true.

Вспомним из предыдущего обсуждения, какой тип имеет результат операции unsigned short + unsigned short. Это int. Каковы бы ни были значения целых без знака, результат никогда не может переполнить тип int, поэтому сложение всегда выполняется корректно. Далее int сравнивается с unsigned short. Значение х приводится к типу int и, стало быть, никогда не будет больше х + у. Чтобы исправить код, нужно лишь привести результат обратно к unsigned short:

...

if((unsigned short)(x + y) < x)

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

Арифметические операции

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

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

Сложение и вычитание. Очевидная проблема при выполнении этих операций – возможность перехода через верхнюю и нижнюю границы объявленного типа. Например, если речь идет о 8–разрядных числах без знака, то 255 + 1 = 0. Или: 2 – 3 = 255. В случае 8–разрядных чисел со знаком 127 + 1 = -128. Менее очевидная ошибка возникает, когда числа со знаком используются для представления размеров. Если кто–то подсунет вам число–20, вы прибавите его к 50, получите 30, выделите буфер длиной 30 байтов, а затем попытаетесь скопировать в него 50 байтов. Все, вы стали жертвой хакера. Помните, особенно при программировании на языке, где переполнить целое трудно или невозможно, – что вычитание из положительного числа, в результате которого получается число, меньшее исходного, – это допустимая операция, и никакого исключения вследствие переполнения не будет, но поток исполнения программы может отличаться от ожидаемого. Если вы предварительно не проверили, что входные данные попадают в положенный диапазон, и не уверены на сто процентов, что переполнение невозможно, контролируйте каждую операцию.

Умножение, деление и вычисление остатка. Умножение чисел без знака не вызывает трудностей: любая операция, где а * b > MAX_INT, дает некорректный результат. Правильный, но не очень эффективный способ контроля заключается в том, чтобы проверить, что b > MAX_INT/a. Эффективнее сохранить результат в следующем по ширине целочисленном типе (если такой существует) и посмотреть, не возникло ли переполнение. Для небольших целых чисел это сделает за вас компилятор. Напомним, что short * short дает int. При умножении чисел со знаком нужно еще проверить, не оказался ли результат отрицательным вследствие переполнения.

Ну а может ли вызвать проблемы операция деления, помимо, конечно, деления на нуль? Рассмотрим 8–разрядное целое со знаком: MIN_INT = -128. Разделим его на–1. Это то же самое, что написать -(-128). Операцию дополнения можно записать в виде ~х+1. Дополнение–128 (0x80) до единицы равно 127 или 0x7f. Прибавим 1 и получим 0x80! Итак, минус–128 снова равно–128! То же верно для деления на–1 минимального целого любого знакового типа. Если вы еще не уверены, что контролировать операции над числами без знака проще, надеемся, что этот пример вас убедил.

Оператор деления по модулю возвращает остаток от деления одного числа на другое, поэтому мы никогда не получим результат, который по абсолютной величине больше числителя. Ну и как тут может возникнуть переполнение? Переполнения как такового и не возникает, но результат может оказаться неожиданным из–за правил приведения. Рассмотрим 32–разрядное целое без знака, равное MAX_INT, то есть 0xffffffff, и 8–разрядное целое со знаком, равное–1. Остаток от деления–1 на 4 294 967 295 равен 1, не так ли? Не торопитесь. Компилятор желает работать с похожими числами, поэтому приведет–1 к типу unsigned int. Напомним, как это происходит. Сначала число расширяется со знаком до 32 битов, поэтому из 0xff получится 0xffffffff. Затем (int)(0xffffffff) преобразуется в (unsigned int)(0xffffffff). Как видите, остаток от деления–1 на 4 млрд равен нулю, по крайней мере, на нашем компьютере! Аналогичная проблема возникает при смешанной операции над любыми 32–или 64–разрядными целыми без знака и отрицательными целыми со знаком, причем это относится также и к делению, так что–1/4 294 967 295 равно 1, что весьма странно, ведь вы ожидали получить 0.

Поделиться:
Популярные книги

Печать Пожирателя

Соломенный Илья
1. Пожиратель
Фантастика:
попаданцы
аниме
сказочная фантастика
фэнтези
5.00
рейтинг книги
Печать Пожирателя

Привет из Загса. Милый, ты не потерял кольцо?

Лисавчук Елена
Любовные романы:
современные любовные романы
5.00
рейтинг книги
Привет из Загса. Милый, ты не потерял кольцо?

Мастер 2

Чащин Валерий
2. Мастер
Фантастика:
фэнтези
городское фэнтези
попаданцы
технофэнтези
4.50
рейтинг книги
Мастер 2

Нечто чудесное

Макнот Джудит
2. Романтическая серия
Любовные романы:
исторические любовные романы
9.43
рейтинг книги
Нечто чудесное

Клан

Русич Антон
2. Долгий путь домой
Фантастика:
боевая фантастика
космическая фантастика
5.60
рейтинг книги
Клан

Имя нам Легион. Том 3

Дорничев Дмитрий
3. Меж двух миров
Фантастика:
боевая фантастика
рпг
аниме
5.00
рейтинг книги
Имя нам Легион. Том 3

Запасная дочь

Зика Натаэль
Фантастика:
фэнтези
6.40
рейтинг книги
Запасная дочь

Убивать чтобы жить 7

Бор Жорж
7. УЧЖ
Фантастика:
героическая фантастика
космическая фантастика
рпг
5.00
рейтинг книги
Убивать чтобы жить 7

У врага за пазухой

Коваленко Марья Сергеевна
5. Оголенные чувства
Любовные романы:
остросюжетные любовные романы
эро литература
5.00
рейтинг книги
У врага за пазухой

Кодекс Охотника. Книга XXI

Винокуров Юрий
21. Кодекс Охотника
Фантастика:
фэнтези
попаданцы
аниме
5.00
рейтинг книги
Кодекс Охотника. Книга XXI

Генерал Скала и ученица

Суббота Светлана
2. Генерал Скала и Лидия
Любовные романы:
любовно-фантастические романы
6.30
рейтинг книги
Генерал Скала и ученица

Оцифрованный. Том 1

Дорничев Дмитрий
1. Линкор Михаил
Фантастика:
боевая фантастика
попаданцы
аниме
5.00
рейтинг книги
Оцифрованный. Том 1

Его маленькая большая женщина

Резник Юлия
Любовные романы:
современные любовные романы
эро литература
8.78
рейтинг книги
Его маленькая большая женщина

Хуррит

Рави Ивар
Фантастика:
героическая фантастика
попаданцы
альтернативная история
5.00
рейтинг книги
Хуррит