Должны ли функции библиотеки C всегда ожидать длину строки?

15

В настоящее время я работаю над библиотекой, написанной на C. Многие функции этой библиотеки ожидают строку как char*или const char*в своих аргументах. Я начал с тех функций, которые всегда ожидали длину строки как size_tтак, чтобы нулевое завершение не требовалось. Однако при написании тестов это приводило к частому использованию strlen(), например:

const char* string = "Ugh, strlen is tedious";
libFunction(string, strlen(string));

Доверие пользователя передать правильно завершенные строки приведет к менее безопасному, но более лаконичному и (на мой взгляд) читаемому коду:

libFunction("I hope there's a null-terminator there!");

Итак, что толковая практика здесь? Сделать API более сложным в использовании, но заставить пользователя задуматься о вводе или задокументировать требование для строки с нулевым символом в конце и доверить вызывающей стороне?

Бенджамин Клостер
источник

Ответы:

4

Определенно и абсолютно нести длину вокруг . Таким образом, стандартная библиотека C печально нарушена, что не причиняет боли при переполнении буфера. Этот подход находится в центре такой ненависти и мучений, что современные компиляторы будут предупреждать, жаловаться и жаловаться при использовании стандартных библиотечных функций такого рода.

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

Оставляя эмоции всего этого в стороне, многое может пойти не так с этим NULL в конце вашей строки, как при чтении, так и при манипулировании ею - плюс это действительно является прямым нарушением современных концепций дизайна, таких как глубокоэшелонированная защита (не обязательно относится к безопасности, но к разработке API). Примеров API C, которые несут длину в изобилии - напр. Windows API.

Фактически, эта проблема была решена когда-то в 90-х, и сегодня складывается единодушное мнение, что вам не следует даже трогать свои строки .

Позже редактирование : это довольно живая дискуссия, поэтому я добавлю, что доверять всем ниже и выше вам хорошо и использовать библиотечные функции str * можно, пока вы не увидите классические вещи вроде output = malloc(strlen(input)); strcpy(output, input);или while(*src) { *dest=transform(*src); dest++; src++; }. Я почти слышу Лакримозу Моцарта на заднем плане.

vski
источник
1
Я не понимаю ваш пример Windows API, требующий, чтобы вызывающая сторона указала длину строки. Например, типичная функция Win32 API, такая как CreateFileпринимает LPTCSTR lpFileNameпараметр в качестве входных данных. Длина строки не ожидается от вызывающей стороны. Фактически, использование NUL-завершенных строк настолько укоренилось, что в документации даже не упоминается, что имя файла должно заканчиваться NUL-символами (но, конечно, это должно быть).
Грег Хьюгилл
1
На самом деле в Win32 LPSTRтип говорит, что строки могут заканчиваться NUL, и если нет , то это будет указано в соответствующей спецификации. Таким образом, если специально не указано иное, такие строки в Win32, как ожидается, будут заканчиваться NUL.
Грег Хьюгилл
Отличный момент, я был неточным. Учтите, что CreateFile и его связка существуют начиная с Windows NT 3.1 (начало 90-х); текущий API (т. е. с момента появления Strsafe.h в XP SP2 - с публичными извинениями Microsoft) явно осуждает все NULL-завершенные вещи, которые он может. В первый раз Microsoft действительно очень пожалела об использовании строк, заканчивающихся на NULL, на самом деле гораздо раньше, когда им пришлось ввести BSTR в спецификации OLE 2.0, чтобы каким-то образом привести VB, COM и старый WINAPI в одну лодку.
вс
1
Даже в, StringCbCatнапример, только у места назначения есть максимальный буфер, что имеет смысл. Источник по - прежнему обычный NUL с концевым C строка. Возможно, вы могли бы улучшить свой ответ, уточнив разницу между входным параметром и выходным параметром. Выходные параметры всегда должны иметь максимальную длину буфера; входные параметры обычно заканчиваются NUL (есть исключения, но по моему опыту редкие).
Грег Хьюгилл
1
Да. Строки являются неизменяемыми как в JVM / Dalvik, так и в .NET CLR на уровне платформы, а также во многих других языках. Я бы зашел так далеко и предположил, что родной мир еще не вполне может это сделать (стандарт C ++ 11) из-за а) наследства (вы на самом деле не получаете такого большого количества, если только часть ваших строк неизменна) и b ) вам действительно нужны сборщик мусора и таблица строк, чтобы сделать эту работу, распределители области действия в C ++ 11 не могут его полностью сократить.
вс
16

В Си идиома состоит в том, что символьные строки заканчиваются NUL, поэтому имеет смысл придерживаться общепринятой практики - на самом деле относительно маловероятно, что пользователи библиотеки будут иметь строки, не заканчивающиеся NUL (поскольку для их печати требуется дополнительная работа использование printf и использование в другом контексте). Использование любого другого типа строки неестественно и, вероятно, относительно редко.

Кроме того, в данных обстоятельствах ваше тестирование кажется мне немного странным, поскольку для правильной работы (с использованием strlen) вы предполагаете строку с NUL-оканчивающейся строкой. Вам следует протестировать случай строк, не заканчивающихся NUL, если вы намерены работать с ними в своей библиотеке.

Джеймс Маклеод
источник
-1 простите, это просто опрометчиво.
вс
В старые времена это не всегда было так. Я много работал с двоичными протоколами, которые помещали строковые данные в поля фиксированной длины, которые не заканчивались NULL. В таких случаях было очень удобно работать с функциями, которые занимали много времени. Я не делал C в течение десятилетия, хотя.
Gort Робот
4
@vski, как заставить пользователя вызывать 'strlen' перед вызовом целевой функции что-нибудь, чтобы избежать проблем переполнения буфера? По крайней мере, если вы сами проверите длину в целевой функции, вы можете быть уверены в том, какое значение длины используется (включая нулевое значение терминала или нет).
Чарльз И. Грант
@Charles E. Grant: см. Комментарий выше о StringCbCat и StringCbCatN в Strsafe.h. Если у вас просто есть char * и нет длины, то у вас действительно нет другого выбора, кроме как использовать функции str *, но смысл в том, чтобы переносить длину, поэтому это становится опцией между str * и strn * функции которых последние являются предпочтительными.
вс
2
@vski Нет необходимости передавать длину строки . Там является необходимость пройти вокруг буфера длины «s. Не все буферы являются строками, и не все строки являются буферами.
Джеймсдлин
10

Ваш аргумент "безопасности" на самом деле не верен. Если вы не доверяете пользователю вручать вам строку с нулевым символом в конце, когда это то, что вы задокументировали (и что является «нормой» для простого C), вы не можете доверять длине, которую они вам дают (которую они вероятно, используйте, strlenкак вы делаете, если у них нет этого под рукой, и что не получится, если «строка» изначально не была строкой).

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

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

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

Мат
источник
Категорически не согласен с таким подходом. Никогда не доверяйте своим абонентам, особенно за библиотечным API, делайте все возможное, чтобы подвергнуть сомнению то, что они вам дают, и изящно провалиться. Носите заштрихованную длину, работать со строками, оканчивающимися на NULL, - это не то, что означает «быть свободным с вызывающими и строгими с вызываемыми».
вс
2
Я согласен в основном с вашей позицией, но вы, похоже, очень доверяете этому аргументу длины - нет никаких причин, почему он должен быть надежнее, чем нулевой терминатор. Моя позиция заключается в том, что это зависит от того, что делает библиотека.
Мат
Есть намного больше, что может пойти не так с NULL-терминатором в строках, чем с длиной, переданной по значению. В C единственная причина, по которой можно было бы доверять длине, заключается в том, что она была бы неразумной и непрактичной, чтобы не переносить длину буфера, не является хорошим ответом, это просто лучший вариант с учетом альтернатив. Это одна из причин того, почему строки (и буферы в целом) аккуратно упакованы и инкапсулированы в языки RAD.
вс
2

Нет. По определению строки всегда заканчиваются нулем, длина строки избыточна.

Символьные данные с ненулевым символом в конце никогда не должны называться «строкой». Обработка этого (и отбрасывание длины) обычно должна быть заключена в библиотеку, а не часть API. Требование длины в качестве параметра просто для того, чтобы избежать одиночных вызовов strlen (), вероятно, является преждевременной оптимизацией.

Доверие к вызывающей функции API не является небезопасным ; неопределенное поведение вполне нормально, если задокументированные предварительные условия не выполняются.

Конечно, хорошо разработанный API не должен содержать подводных камней и должен облегчать правильное использование. И это просто означает, что это должно быть как можно более простым и понятным, избегая избыточностей и следуя языковым соглашениям.

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

Вы должны всегда держать свою длину вокруг. Например, ваши пользователи могут захотеть содержать в них значения NULL. И во-вторых, не забывайте, что strlenэто O (N) и требует касания всего кеша пока. И в-третьих, это облегчает передачу подмножеств - например, они могут дать меньше, чем фактическая длина.

DeadMG
источник
4
Должна ли быть очень хорошо задокументирована работа библиотечной функции со встроенными значениями NULL в строках. Большинство функций библиотеки C останавливаются на NULL или длине, в зависимости от того, что произойдет первым. (И если написано грамотно, те, кто не берет длину, никогда не используют strlenв циклическом тесте.)
Gort the Robot
1

Вы должны различать передачу строки и буфер .

В Си строки традиционно заканчиваются NUL. Вполне разумно ожидать этого. Поэтому обычно нет необходимости передавать длину строки; это может быть вычислено strlenпри необходимости.

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

Возможно, есть некоторая путаница, потому что могут быть как строки, так и буферы, char*а также потому, что многие строковые функции генерируют новые строки, записывая в целевые буферы. Некоторые люди тогда приходят к выводу, что строковые функции должны иметь длину строки. Однако это неточный вывод. Практика включения размера с буфером (будет ли этот буфер использоваться для строк, массивов целых чисел, структур и т. Д.) Является более полезной и более общей мантрой.

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

jamesdlin
источник
Это именно то, что вопрос и другие ответы пропустили.
Blrfl
0

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

void use_string(char *string, int length);

можно определить макрос:

#define use_strlit(x) use_string(x, sizeof ("" x "")-1)

и затем вызовите это как показано в:

void test(void)
{
  use_strlit("Hello");
}

Хотя может быть возможно придумать «творческие» вещи для передачи этого макроса, который будет компилироваться, но на самом деле не будет работать, использование ""по обе стороны строки в оценке «sizeof» должно отлавливать случайные попытки использовать символ указатели, отличные от разложенных строковых литералов [при их отсутствии ""попытка передать указатель на символ ошибочно даст длину в качестве размера указателя, минус один.

Альтернативный подход в C99 будет определять тип структуры «указатель и длина» и определять макрос, который преобразует строковый литерал в составной литерал этого структурного типа. Например:

struct lstring { char const *ptr; int length; };
#define as_lstring(x) \
  (( struct lstring const) {x, sizeof("" x "")-1})

Обратите внимание, что если кто-то использует такой подход, он должен передавать такие структуры по значению, а не передавать их адреса. В противном случае что-то вроде:

struct lstring *p;
if (foo)
{
  p = &as_lstring("Hello");
}
else
{
  p = &as_lstring("Goodbye!");
}
use_lstring(p);

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

Supercat
источник