Если вы прочтёте спецификацию языка C, то увидите, что функции ...ncpy имеют очень странную семантику.
Функция strncpy копирует первые count символов из strSource в strDest и возвращает strDest. Если count меньше или равно длине strSource, то к строке не добавляется нулевой символ. Если же count больше длинны строки strSource, то целевая строка заполняется нулевыми символами до своего конца.На рисунках ниже показаны различные сценарии копирования строк:
strncpy(strDest, strSrc, 5) | |||||||||
strSource |
| ||||||||
strDest |
| заметьте: нет терминатора | |||||||
strncpy(strDest, strSrc, 5) | |||||||||
strSource |
| ||||||||
strDest |
| заметьте: нет терминатора | |||||||
strncpy(strDest, strSrc, 5) | |||||||||
strSource |
| ||||||||
strDest |
| заметьте: заполнение нулями до конца strDest |
Вернитесь назад во времени в ранние дни UNIX. Лично я заведу свою машину времени на времена System V. В System V имена файлов могли иметь длину до 14 символов. Любые имена длиннее усекались до 14-ти символов. А поле для хранения имени файла на диске было ровно 14 байт. Не 15. Терминатор только подразумевался. Это экономило один байт.
Вот несколько имён файлов и их представления в каталоге:
passwd |
| ||||||||||||||
newsgroups.old |
| ||||||||||||||
newsgroups.old.backup |
|
newsgroups.old
и newsgroups.old.backup
фактически являются одинаковыми файлами из-за усечения. Длинные имена усекались автоматически, втихую, без какого-либо сообщения об ошибке. Это исторически было источником множества ошибок непреднамеренной потери данных.Функция
strncpy
использовалась системой для хранения имён файлов в каталоге на диске. Это объясняет часть странного поведения этой функции, а именно: почему она не добавляет нуль-терминатор к результату. Потому что терминатор неявно подразумевается концом массива символов (это также объясняет молчаливое усекание имён файлов).Но зачем дополнять нулями короткие имена?
Потому что это упрощает поиск имён (делает его быстрее). Если вы гарантируете, что все "мусорные байты" будут нулями, то вы можете использовать функцию
memcmp
для их сравнения.По соображениям совместимости, комитет языка C решил сохранить это причудливое поведение
strncpy
.Так что насчёт заголовка сегодняшнего поста? Как может код для предотвращения переполнения буфера в итоге вызывать его?
Вот один пример (к сожалению, я не понимаю японский, так что я действую, исходя только из кода). Заметьте, что он использует
_tcsncpy
для заполнения lpstrFile
и lpstrFileTitle
, при этом осторожно следя за размерами буферов. Это прекрасно, но это также приводит к отсутствию нуль-терминатора, если строка слишком длинна. Вызывающий вполне может потом скопировать этот буфер в какой-то другой. Но в lstrFile
отсутствует терминатор, поэтому он превышает длину, указанную вызывающим. Результат: переполнение другого буфера.А вот ещё один пример. Заметьте, что функция использует
_tcsncpy
для копирования результата в выходной буфер. Автор был осведомлён о причудливом поведении семейства функций strncpy
, поэтому он вручную добавил терминатор в конец буфера.Но что если
ccTextMax
= 0? Тогда попытка записи терминатора обратится к символам до начала массива, затирая случайный символ.Какой можно сделать вывод из всего этого?
Ну, лично мой вывод такой: избегать
strncpy
и всех её друзей, если вы работаете с нуль-терминированными строками. Несмотря на наличие в их имени "str" - эти функции не создают нуль-терминированные строки. Они конвертируют нуль-терминированную строку в символьный raw-буфер. Использование их там, где на выходе ожидается нуль-терминированная строка, просто неверно. Вы не только не получите терминатора в конце строки, но вы также получите бесполезное для вас дополнение нулями, если строка слишком коротка.
Но иногда работа со строковым буфером вместо строки - это то, что вам нужно.
ОтветитьУдалить