19 смертных грехов, угрожающих безопасности программ - [15]
Операции приведения
Есть несколько типичных ситуаций, приводящих к переполнению целого. Одна из самых частых – незнакомство с порядком приведений и неявными приведениями, которые осуществляют некоторые операторы. Рассмотрим, например, такой код:
const long MAX_LEN = 0x7fff;
short len = strlen(input);
if (len < MAX_LEN)
// что-то сделать
Если даже не обращать внимания на усечение, то вот вопрос: в каком порядке производятся приведения типов при сравнении len и MAX_LEN? Стандарт языка гласит, что повышающее приведение следует выполнять перед сравнением; следовательно, len будет преобразовано из 16–разрядного целого со знаком в 32–разрядное целое со знаком. Это простое приведение, так как оба типа знаковые. Чтобы сохранить значение числа, оно расширяется с сохранением знака до более широкого типа. В данном случае мог бы получиться такой результат:
len = 0x100;
(long)len = 0x00000100;
ИЛИ
len = 0xffff;
(long)len = 0xfffffffff;
Поэтому если противник сумеет добиться того, чтобы len превысило 32К, то len станет отрицательным и останется таковым после расширения до 32 битов. Следовательно, после сравнения с MAX_LEN программа пойдет по неверному пути.
Вот как формулируются правила преобразования в С и С++:
Целое со знаком в более широкое целое со знаком. Меньшее значение расширяется со знаком, например приведение (char)0x7f к int дает 0x0000007f, но (char)0x80 становится равно 0xffffff80.
Целое со знаком в целое без знака того же размера. Комбинация битов сохраняется, значение может измениться или остаться неизменным. Так, (char)0xff (-1) после приведения к типу unsigned char становится равно 0xff, но ясно, что–1 и 255 – это не одно и то же.
Целое со знаком в более широкое целое без знака. Здесь сочетаются два предыдущих правила. Сначала производится расширение со знаком до знакового типа нужного размера, а затем приведение с сохранением комбинации битов. Это означает, что положительные числа ведут себя ожидаемым образом, а отрицательные могут дать неожиданный результат. Например, (char) -1 (0xff) после приведения к типу unsigned long становится равно 4 294 967 295 (0xffffffff).
Целое без знака в более широкое целое без знака. Это простейший случай: новое число дополняется нулями, чего вы обычно и ожидаете. Следовательно, (unsigned char)0xff после приведения к типу unsigned long становится равно
0x000000ff.
Целое без знака в целое со знаком того же размера. Так же как при приведении целого со знаком к целому без знака, комбинация битов сохраняется, а значение может измениться в зависимости от того, был ли старший (знаковый) бит равен 1 или 0.
Целое без знака в более широкое целое со знаком. Так же как при приведении целого без знака к более широкому целому без знака, значение сначала дополняется нулями до нужного беззнакового типа, а затем приводится к знаковому типу. Значение не изменяется, так что никаких сюрпризов в этом случае не бывает.
Понижающее приведение. Если в исходном числе хотя бы один из старших битов был отличен от нуля, то мы имеем усечение, что вполне может привести к печальным последствиям. Возможно, что число без знака станет отрицательным или произойдет потеря информации. Если речь не идет о битовых масках, всегда проверяйте, не было ли усечения.
Преобразования при вызове операторов
Большинство программистов не подозревают, что одного лишь обращения к оператору достаточно для изменения типа результата. Обычно ничего страшного не происходит, но граничные случаи могут вас неприятно удивить. Вот код на С++, иллюстрирующий проблему:
template
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, тогда как операторы префиксного и постфиксного инкремента и декремента (++, – ) типа не меняют.
В практике разработки ПО зачастую встает задача динамической модификации программного кода в зависимости от текущих или настраиваемых значений параметров. Для решения этой задачи широко используются обратные вызовы. В языке C++ обратные вызовы реализуются различными способами, и далеко не всегда очевидно, какой из них лучший для конкретной ситуации. В книге рассмотрены теоретические и практические аспекты организации обратных вызовов, проанализированы достоинства и недостатки различных реализаций, выработаны рекомендации по выбору в зависимости от требований к проектируемому ПО.
Хватит тратить время на скучные академические фолианты! Изучение Computer Science может быть веселым и увлекательным занятием. Владстон Феррейра Фило знакомит нас с вычислительным мышлением, позволяющим решать любые сложные задачи. Научиться писать код просто — пара недель на курсах, и вы «программист», но чтобы стать профи, который будет востребован всегда и везде, нужны фундаментальные знания. Здесь вы найдете только самую важную информацию, которая необходима каждому разработчику и программисту каждый день. «Эта книга пригодится и для решения повседневных задач.
В учебно-методическом пособии рассматриваются основы языка программирования PL/SQL, реализованного в системе управления базами данных Oracle Database Server. Приводятся сведения о поддерживаемых типах данных, структуре программ PL/SQL и выполнении SQL-предложений в них. Отдельно рассмотрено создание хранимых в базах данных Oracle программ PL/SQL – процедур, функций, пакетов и триггеров.
В книге рассказывается история главного героя, который сталкивается с различными проблемами и препятствиями на протяжении всего своего путешествия. По пути он встречает множество второстепенных персонажей, которые играют важные роли в истории. Благодаря опыту главного героя книга исследует такие темы, как любовь, потеря, надежда и стойкость. По мере того, как главный герой преодолевает свои трудности, он усваивает ценные уроки жизни и растет как личность.
Книга известного специалиста по программированию (Югославия), содержащая основы языка Пролог и его приложения для решения задач искусственного интеллекта. Изложение отличается методическими достоинствами — книга написана в хорошем стиле, живым языком. Книга дополняет имеющуюся на русском языке литературу по языку Пролог.Для программистов разной квалификации, специалистов по искусственному интеллекту, для всех изучающих программирование.
РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN, НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.