Всего на сайте:
183 тыс. 477 статей

Главная | Информатика

Стрессовое тестирование  Просмотрен 211

 

Еще один эффективный прием тестирования — проверка программ большими объемами вводимых данных, сгенерированных компьюте­ром. Входные данные, сгенерированные машиной, оказывают на про­граммы несколько иное влияние, чем созданные вручную. Большие объемы сами по себе могут стать причиной сбоев, вызывая переполнение буферов ввода, массивов, счетчиков; они весьма эффективны для поиска неоправданно ограниченных в размерах структур данных. Кроме того, люди часто неосознанно избегают "невозможных" значений (вроде пус-гой строки ввода или ввода недопустимых значений) и нечасто вводят эчень длинные имена или очень большие значения. Компьютеры же ге­нерируют данные точно в соответствии с запрограммированными зада­ниями; у них нет никаких личных предпочтений или антипатий.

Вот для иллюстрации одна строка вывода, произведенного компиля­тором Microsoft Visual C++ версии 5.0 при компиляции нашей програм­мы markov (версия C++ с использованием STL):

 

xtree(114) : warning C4786: 'std::_Tree<std::deque<std::

basic_string<char,std: :char_traits<char>, std: :allocator

<char>> std: :allocator<std::basic string<char, std: :

... опущено 1420 символов

allocator<char>>>>> :iterator' : identifier was

truncated to '255' characters in the debug information

 

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

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

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

Некоторые виды тестов основаны на введении преднамеренно некорректных данных. При попытках взлома часто используют объемистый или некорректный ввод, который перезаписывает ценные данные: имеет смысл самому проверить свою программу на восприимчивость к такому вводу. Некоторые функции стандартных библиотек оказываются уязви­мы для подобных атак.

Например, в функции gets из стандартной библиотеки не предусмотрено никакого способа ограничения размера вводимой строки, поэтому ее нельзя использовать никогда; вместо нее нужно применять функцию fgets(buf, sizeof(buf), stdin). В обычном своем простейшем формате функция scanf("%s", buf) также не ограничивает размер вводимой строки, поэтому ее можно использовать, только указывая размер строки в явном виде: scanf ("%20s", buf). В разделе 3.3 мы показали, как решить эту проблему для буфера произвольного раз­мера.

Любой блок, который может получать данные извне программы (пря­мо или косвенно), должен проверять полученные значения перед тем, как их использовать. Следующая программа, взятая из учебника, по идее должна читать целое число, введенное пользователем, и, если это число слишком велико, выдавать предупреждение. Цель создания этой про­граммы — продемонстрировать, как можно справиться с проблемой gets, однако предложенное решение работает не всегда:

 

? #define MAXNUM 10 ?

? int main(void)

? {

? char num[MAXNUM];

?

? memset(num, 0, sizeof(num));

? printf("Введите число: ");

? gets(num);

? if (num[MAXNUM-1] != 0)

? printf("Число слишком велико!\n");

? /* ... ./

? }

 

Если вводимое число состоит из 10 символов, оно перепишет последний нуль массива num ненулевым значением, и по теории это может быть за­мечено после возврата gets. К сожалению, подобное решение нельзя признать удовлетворительным. Злонамеренный взломщик введет еще более длинную строку, которая перепишет какие-нибудь критические значения, — может быть, адрес возврата вызова, — и тогда программа ни­когда не вернется к выполнению условия if, а выполнит вместо этого инструкции взломщика. Вообще стоит запомнить, что любой неконтро­лируемый ввод есть, кроме всего прочего, еще и потенциальная лазейка для взлома системы.

Чтобы вы не думали, что описанные проблемы возможны только в про­граммах из плохих учебников, вспомните о том, как в июле 1998 года ошибка подобного рода обнаружилась в нескольких основных програм­мах электронной почты. Как писала New York Times:

«Лазейка в системе безопасности была вызвана тем, что принято на­зывать "ошибкой переполнения буфера". Программисты должны вклю­чать в свои программы код, который проверяет, что вводимые данные имеют нужный тип и размер. Если элемент данных слишком велик, он может выйти за границу "буфера" — кусок памяти, специально выделен­ный для его хранения. В этом случае программа электронной'почты даст сбой, и злоумышленник может заставить ваш компьютер выполнять его программу». Среди атак во время знаменитого инцидента "Internet Worm" ("Сетевой червь") 1988 года была и такая.

Программы, производящие разбор форм HTML, также могут быть чувствительны к атакам, основанным на хранении очень длинных строк Извода в маленьких массивах:

 

? static char query[1024];

?

? char *reac_form(void)

? {

? int qsize;

?

? qsize = atoi(getenv("CONTENT_LENGTH"));

? fread(query, qsize, 1, stdin);

? return query;

? }

 

В этом коде предполагается, что ввод никогда не будет длиннее 1024 байтов, поэтому он, как и gets, открыт для атак переполнения буфера.

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

 

? char *p;

? р = (char *) malloc(x * у * z);

 

Если результат перемножения х, у и z вызывает переполнение, вызов к malloc приведет к созданию массива приемлемого размера, но р[х] может ссылаться на раздел памяти вне выделенной области.

Предположим, что целые числа являются 16-битовыми, а каждая из переменных х, у и z равна 41. Тогда x*y*z равно 68 921, то есть 3385 по модулю 216. Так что вызов malloc выделит только 3385 байтов, а любая ссылка по индексу вне этого значения будет выходить за заданные границы.

Еще одной причиной переполнения может стать преобразование типов, и обработки таких ошибок не всегда возможны корректным образом. Ракета "Ariane 5" взорвалась во время своего первого запуска в июне 1996 года только из-за того, что навигационный пакет был унас­ледован от "Ariane 4" и не прошел тщательного тестирования. Новая ракета имела большую скорость, и, соответственно, навигационным про­граммам приходилось иметь дело с большими числами. Вскоре после запуска попытка преобразовать 64-битовое число с плавающей точкой в 16-битовое целое со знаком вызвала переполнение. Ошибка была от­ловлена, но коду, обработавшему ее, пришлось прервать работу подсис­темы. Ракета ушла с курса и взорвалась. Самое обидное, что код, в кото­ром произошел сбой, генерировал данные, необходимые только до момента запуска; если бы при запуске эта часть программы была отклю­чена, трагедии бы не произошло.

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

 

Хорошие тесты и тестовые случаи нередко могут быть использованы для большого количества программ. Так, каждая программа, читающая файлы, должна быть проверена вводом пустого файла. Каждая програм­ма, читающая текст, должна быть оттестирована двоичным вводом. Каждая программа, читающая строки текста, должна быть проверена вводом очень длинных строк, пустых строк и файлом без символов пере­вода строки вообще. Таким образом, полезно сохранять подобные общие тесты и всегда иметь их под рукой — тогда можно будет быстрее и с меньшими затратами тестировать любую программу. Полезно также раз и навсегда написать хорошую программу, которая бы создавала тесто­вые файлы в соответствии с запросами.

Когда Стив Борн (Steve Bourne) писал свою оболочку для Unix (которая теперь известна как Bourne shell), он создал каталог, содер­жащий 254 файла с именами из одного символа: по одному на каждое возможное значение байта, за исключением '\0' и косой черты двух символов, которые не могут встретиться в именах файлов Unix. Этот каталог он всячески использовал для тестирования поисков по шаблону и программ разбиения входного потока. (Надо ли говорить, что каталог этот был создан программно.) Через много лет этот ката­лог стал настоящим проклятием программ обхода дерева файлов - множество тестов с его участием приводили к плачевным для этих программ результатам.

Предыдущая статья:Тестовые оснастки. Тестирование, о котором мы вели разговор до этого момента, относило.. Следующая статья:Полезные советы
page speed (0.0111 sec, direct)