Функция gets()

Функция gets(), входящая в состав стандартной библиотеки C, имеет следующий прототип:

char* gets(char* s);

Это определение содержится в stdio.h. Функция предназначена для ввода строки символов из файла stdin. Она возвращает s если чтение прошло успешно и NULL в обратном случае.

При всей простоте и понятности, эта функция уникальна. Все дело в том, что более опасного вызова, чем этот, в стандартной библиотеке нет… почему это так, а также чем грозит использование gets(), я как раз и попытаюсь объяснить в сегодняшней заметке.

Вообще говоря, для тех, кто не знает, почему использование функции gets() так опасно, будет полезно посмотреть еще раз на ее прототип, и подумать. Если догадаетесь самостоятельно, будет лишний повод немного погордиться 😉

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

char name[10];

// ...

puts("Enter you name:");
gets(name);

Если у пользователя будет имя больше, чем 9 символов, например, 10, то по адресу (name + 10) будет записан 0. Что там на самом деле находится, другие данные или просто незанятое место (возникшее, например, из-за того, что компилятор соответствующим образом выравнял данные), или этот адрес для программы недоступен, неизвестно.

Все эти ситуации ничего хорошего не сулят. Порча собственных данных означает то, что программа выдаст неверные результаты, а почему это происходит понять будет крайне трудно — первым делом программист будет проверять ошибки в алгоритме и только в конце заметит, что произошло переполнение внутреннего буфера. Я думаю, все знают как это происходит — несколько часов непрерывных “бдений” с отладчиком, а потом через день, “на свежую голову”, выясняется что где-то был пропущен один символ…

Опять же, для программиста самым удобным будет моментальное аварийное прекращение работы программы в этом месте — тогда он сможет заменить gets() на что-нибудь более “порядочное”.

У кого-то может возникнуть предложение просто взять и увеличить размер буфера. Но не надо забывать, что всегда можно ввести строку длиной, превышающий выделенный размер; если кто-то хочет возразить, что случаи имен длиной более чем, например, 1024 байта все еще редки, то я перейду к другому, несколько более интересному примеру возникающей проблемы при использовании gets().

Для это просто подчеркну контекст, в котором происходит чтение строки.

void foo()
{
  char name[10];

  // ...

  puts("Enter you name:");
  gets(name);

  // ...
}

Я имею в виду, что теперь name расположен в стеке. Надеюсь, что читающие эти строки люди имеют представление о том, как обычно выполняется вызов функции. Грубо говоря, сначала в стек помещается адрес возврата, а потом в нем же размещается память под массив name. Так что теперь, когда функция gets() будет писать за пределами массива, она будет портить адрес возврата из функции foo().

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

Немного отвлечемся, потому что это достаточно интересно. В операционной системе Unix есть возможность запускать программы, которые будут иметь привилегии пользователя, отличного от того, кто этот запуск произвел. Самый распространенный пример, это, конечно же, суперпользователь. Например, команды ps или passwd при запуске любым пользователем получают полномочия root’а. Сделано это потому, что копаться в чужой памяти (для ps) может только суперпользователь, так же как и вносить изменения в /etc/passwd. Понятно, что такие программы тщательнейшим образом проверяются на отсутствие ошибок — через них могут “утечь” полномочия к нехорошим “хакерам” (существуют и хорошие 😉 ). Размещение буфера в стеке некоторой функции, чей код выполняется с привилегиями другого пользователя, позволяет при переполнении этого буфера изменить на что-то осмысленное адрес возврата из функции. Как поместить по переданному адресу то, что требуется выполнить (например, запуск командного интерпретатора), это уже другой разговор и он не имеет прямого отношения к программированию на C или C++.

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

fgets(name, 10, stdin);

Резюме

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