C strcpy() - зло?

Некоторые люди думают, что С strcpy() функция плохая или злая. Хотя я признаю, что обычно лучше использовать strncpy() во избежание переполнения буфера, следующее (реализация strdup() функция для тех, кому не повезло иметь ее) безопасно использует strcpy() и никогда не должен переполняться:

char *strdup(const char *s1)
{
  char *s2 = malloc(strlen(s1)+1);
  if(s2 == NULL)
  {
    return NULL;
  }
  strcpy(s2, s1);
  return s2;
}

*s2 гарантированно достаточно места для хранения *s1и используя strcpy() избавляет нас от необходимости хранить strlen() привести к другой функции, которая будет использоваться позже как ненужный (в данном случае) параметр длины для strncpy(), Тем не менее, некоторые люди пишут эту функцию с strncpy(), или даже memcpy(), которые оба требуют параметра длины. Я хотел бы знать, что люди думают об этом. Если вы думаете strcpy() скажем так, безопасно в определенных ситуациях. Если у вас есть веская причина не использовать strcpy() в этой ситуации, пожалуйста, дайте это - я хотел бы знать, почему это может быть лучше использовать strncpy() или же memcpy() в таких ситуациях Если вы думаете strcpy() все в порядке, но не здесь, пожалуйста, объясните.

В основном, я просто хочу знать, почему некоторые люди используют memcpy() когда другие используют strcpy() а третьи используют равнину strncpy(), Есть ли логика в предпочтении одного из трех (не учитывая проверки буфера первых двух)?

17 ответов

memcpy может быть быстрее, чем strcpy а также strncpy потому что он не должен сравнивать каждый скопированный байт с \0, и потому что он уже знает длину скопированного объекта. Он может быть реализован аналогичным образом с устройством Даффа, или использовать инструкции ассемблера, которые копируют несколько байтов за раз, например, movsw и movsd.

Я следую правилам здесь. Позвольте мне процитировать это

strncpy изначально был введен в библиотеку C для работы с полями имен фиксированной длины в таких структурах, как записи каталога. Такие поля не используются так же, как строки: завершающий нуль не нужен для поля максимальной длины, а задание байтов для более коротких имен равным нулю обеспечивает эффективное сравнение по полю. strncpy по происхождению не является "ограниченным strcpy", и Комитет предпочел признать существующую практику, а не изменять функцию, чтобы лучше приспособить ее для такого использования.

По этой причине вы не получите трейлинг '\0' в строке, если вы нажмете n не найти '\0' из исходной строки до сих пор. Его легко использовать неправильно (конечно, если вы знаете об этой ловушке, вы можете избежать ее). Как говорится в цитате, он не был задуман как ограниченный strcpy. И я бы предпочел не использовать его, если не нужно. В вашем случае явно его использование не является необходимым, и вы это доказали. Зачем тогда это использовать?

И вообще говоря, программный код также направлен на уменьшение избыточности. Если вы знаете, что у вас есть строка, содержащая символы "n", зачем указывать функции копирования максимально копировать n персонажи? Вы делаете избыточную проверку. Дело не столько в производительности, сколько в согласованности кода. Читатели спросят себя, что strcpy мог сделать это мог пересечь n символов и что делает необходимым ограничить копирование, просто чтобы прочитать в руководствах, что это не может произойти в этом случае. И тут начинаются путаницы среди читателей кода.

Для рационального использования mem-, str- или же strn- Я выбрал среди них, как в приведенном выше документе:

mem- когда я хочу скопировать сырые байты, как байты структуры.

str- при копировании строки с нулевым символом в конце - только когда 100% переполнения быть не может.

strn- при копировании строки с нулевым символом в конце до некоторой длины, заполнение оставшихся байтов нулем. Вероятно, не то, что я хочу в большинстве случаев. Легко забыть этот факт с помощью завершающего нулевого заполнения, но это так, как объясняет приведенная выше цитата. Итак, я бы просто написал свой собственный маленький цикл, который копирует символы, добавляя завершающий '\0':

char * sstrcpy(char *dst, char const *src, size_t n) {
    char *ret = dst;
    while(n-- > 0) {
        if((*dst++ = *src++) == '\0')
            return ret;
    }
    *dst++ = '\0';
    return ret;
}

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

Позже C получил функции для работы с широкими символами, названные wcs- а также wcsn- (за C99). Я бы использовал их аналогично.

Причина, по которой люди используют strncpy, а не strcpy, заключается в том, что строки не всегда заканчиваются нулем, и очень легко переполнить буфер (пространство, выделенное для строки с помощью strcpy) и перезаписать некоторый несвязанный бит памяти.

С strcpy это может произойти, с strncpy это никогда не произойдет. Вот почему strcpy считается небезопасным. Зло может быть немного сильным.

Честно говоря, если вы делаете много обработки строк в C, вы не должны спрашивать себя, следует ли вам использовать strcpy или же strncpy или же memcpy, Вы должны найти или написать библиотеку строк, которая обеспечивает абстракцию более высокого уровня. Например, тот, который отслеживает длину каждой строки, выделяет память для вас и предоставляет все необходимые вам строковые операции.

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

Библиотека может иметь такие функции:

typedef struct MyString MyString;
MyString *mystring_new(const char *c_str);
MyString *mystring_new_from_buffer(const void *p, size_t len);
void mystring_free(MyString *s);
size_t mystring_len(MyString *s);
int mystring_char_at(MyString *s, size_t offset);
MyString *mystring_cat(MyString *s1, ...); /* NULL terminated list */
MyString *mystring_copy_substring(MyString *s, size_t start, size_t max_chars);
MyString *mystring_find(MyString *s, MyString *pattern);
size_t mystring_find_char(MyString *s, int c);
void mystring_copy_out(void *output, MyString *s, size_t max_chars);
int mystring_write_to_fd(int fd, MyString *s);
int mystring_write_to_file(FILE *f, MyString *s);

Я написал один для проекта Kannel, смотрите файл gwlib/octstr.h. Это сделало жизнь намного проще для нас. С другой стороны, такую ​​библиотеку довольно просто написать, так что вы можете написать ее для себя, даже если только в качестве упражнения.

Никто не упомянул strlcpy разработанный Тоддом К. Миллером и Тео де Раадтом. Как говорится в их статье:

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

Есть контраргументы для использования strlcpy; страница Википедии отмечает, что

Дрэппер утверждает, что strlcpy а также strlcat упростит игнорирование ошибок усечения для программиста и, следовательно, может привести к появлению большего количества ошибок, чем их устранение. *

Однако я считаю, что это просто заставляет людей, которые знают, что они делают, добавить ручное NULL-завершение, в дополнение к ручной настройке аргумента для strncpy, Использование strlcpy значительно упрощает предотвращение переполнения буфера, потому что вы не смогли NULL завершить ваш буфер.

Также обратите внимание, что отсутствие strlcpy в библиотеках glibc или Microsoft не должно быть препятствий для использования; Вы можете найти источник для strlcpy и друзей в любом дистрибутиве BSD, и лицензия, вероятно, подходит для вашего коммерческого / некоммерческого проекта. Смотрите комментарий в верхней части strlcpy.c,

Я лично придерживаюсь мнения, что если код может быть доказан, что он действителен - и сделан так быстро, - это вполне приемлемо. То есть, если код простой и, следовательно, очевидно правильный, то это нормально.

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

Следующее может быть лучше:

char *
strdup(const char *s1) {
  int s1_len = strlen(s1);
  char *s2 = malloc(s1_len+1);
  if(s2 == NULL) {
    return NULL;
  }

  strncpy(s2, s1, s1_len);
  return s2;
}

Теперь струна может расти не по вашей вине, и вы в безопасности. Результат не будет дуплом, но не будет и безумных переполнений.

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

ETA: Вот немного лучшая реализация:

char *
strdup(const char *s1, int *retnum) {
  int s1_len = strlen(s1);
  char *s2 = malloc(s1_len+1);
  if(s2 == NULL) {
    return NULL;
  }

  strncpy(s2, s1, s1_len);
  retnum = s1_len;
  return s2;
}

Там количество символов возвращается. Вы также можете:

char *
strdup(const char *s1) {
  int s1_len = strlen(s1);
  char *s2 = malloc(s1_len+1);
  if(s2 == NULL) {
    return NULL;
  }

  strncpy(s2, s1, s1_len);
  s2[s1_len+1] = '\0';
  return s2;
}

Который прекратит это с NUL байт. В любом случае лучше, чем тот, который я быстро соединил изначально.

Я думаю, что strncpy тоже зло.

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

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

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

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

string *string_dup(const string *s1) {
    string *s2 = string_alloc(string_len(s1));
    if (s2 != NULL) {
        string_set(s2,s1);
    }
    return s2;
}

static inline size_t string_len(const string *s) {
    return strlen(s->data);
}

static inline void string_set(string *dest, const string *src) {
    // potential (but unlikely) performance issue: strncpy 0-fills dest,
    // even if the src is very short. We may wish to optimise
    // by switching to memcpy later. But strncpy is better here than
    // strcpy, because it means we can use string_set even when
    // the length of src is unknown.
    strncpy(dest->data, src->data, dest->capacity);
}

string *string_alloc(size_t maxlen) {
    if (maxlen > SIZE_MAX - sizeof(string) - 1) return NULL;
    string *self = malloc(sizeof(string) + maxlen + 1);
    if (self != NULL) {
        // empty string
        self->data[0] = '\0';
        // strncpy doesn't NUL-terminate if it prevents overflow, 
        // so exclude the NUL-terminator from the capacity, set it now,
        // and it can never be overwritten.
        self->capacity = maxlen;
        self->data[maxlen] = '\0';
    }
    return self;
}

typedef struct string {
    size_t capacity;
    char data[0];
} string;

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

Согласен. Я бы рекомендовал против strncpy() тем не менее, поскольку он всегда будет дополнять ваш вывод до указанной длины. Это какое-то историческое решение, которое, на мой взгляд, было действительно неудачным, поскольку оно серьезно ухудшает производительность.

Рассмотрим код, подобный следующему:

char buf[128];
strncpy(buf, "foo", sizeof buf);

Это не будет писать ожидаемые четыре символа в buf, но вместо этого напишет "foo", за которым следуют 125 нулевых символов. Например, если вы собираете много коротких строк, это будет означать, что ваша реальная производительность намного хуже, чем ожидалось.

Если доступно, я предпочитаю использовать snprintf(), написав выше, как:

snprintf(buf, sizeof buf, "foo");

Если вместо этого копировать неконстантную строку, это делается так:

snprintf(buf, sizeof buf, "%s", input);

Это важно, так как если input содержит% символов snprintf() будет интерпретировать их, открывая целые полки банок с червями.

Я склонен использовать memcpy если я уже рассчитал длину, хотя strcpy обычно оптимизирован для работы с машинными словами, он чувствует, что вы должны предоставить библиотеке как можно больше информации, чтобы она могла использовать наиболее оптимальный механизм копирования.

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

Зло приходит, когда люди используют его вот так (хотя приведенное ниже очень упрощено):

void BadFunction(char *input)
{
    char buffer[1024]; //surely this will **always** be enough

    strcpy(buffer, input);

    ...
}

Это ситуация, которая часто бывает удивительной.

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

strlen находит последнее завершающее нулевое место.

Но в действительности буферы не заканчиваются нулем.

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

char *strdup(const char *s1)
{
  char *s2 = malloc(strlen(s1)+1);
  if(s2 == NULL)
  {
    return NULL;
  }
  strcpy(s2, s1);
  return s2;
}

Проблемы:

  1. s1 не определен, strlen вызывает доступ к нераспределенной памяти, происходит сбой программы.
  2. s1 не определен, strlen, но не вызывает доступа к нераспределенной памяти для доступа к памяти из другой части вашего приложения. Он возвращается пользователю (проблема безопасности) или анализируется другой частью вашей программы (появляется heisenbug).
  3. s1 не определен, strlen приводит к malloc, который система не может удовлетворить, возвращает NULL. strcpy передается NULL, программа вылетает.
  4. s1 не определен, strlen приводит к очень маленькому malloc, система выделяет слишком много памяти для выполнения поставленной задачи, становится нестабильной.
  5. В лучшем случае код неэффективен, strlen требует доступа к каждому элементу в строке.

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

Имеет ли смысл писать код общего назначения, например, бизнес-логику? Нет.

Ну, strcpy() не такой злой, как strdup() - по крайней мере, strcpy() является частью стандарта C.

char* dupstr(char* str)
{
   int full_len; // includes null terminator
   char* ret;
   char* s = str;

#ifdef _DEBUG
   if (! str)
      toss("arg 1 null", __WHENCE__);
#endif

   full_len = strlen(s) + 1;
   if (! (ret = (char*) malloc(full_len)))
      toss("out of memory", __WHENCE__);
   memcpy(ret, s, full_len); // already know len, so strcpy() would be slower

   return ret;
}

Этот ответ использует size_t а также memcpy() быстро и просто strdup(),

Лучше всего использовать тип size_t так как это тип, возвращаемый из strlen() и используется malloc() а также memcpy(), int неправильный тип для этих операций.

memcpy() редко медленнее, чем strcpy() или же strncpy() и часто значительно быстрее.

// Assumption: `s1` points to a C string.
char *strdup(const char *s1) {
  size_t size = strlen(s1) + 1;
  char *s2 = malloc(size);
  if(s2 != NULL) {
    memcpy(s2, s1, size);
  }
  return s2;
} 

§7.1.1 1 " Строка - это непрерывная последовательность символов, оканчивающаяся и включающая первый нулевой символ. ..."

В ситуации, которую вы описываете, strcpy - хороший выбор. Этот strdup попадет в неприятности только в том случае, если s1 не заканчивался символом '\0'.

Я хотел бы добавить комментарий, указывающий, почему нет проблем с strcpy, чтобы другие (и вы сами через год) не задавались вопросом о его правильности слишком долго.

strncpy часто кажется безопасным, но может привести к неприятностям. Если исходная "строка" короче, чем count, она дополняет цель символом "\ 0", пока не достигнет значения count. Это может быть плохо для производительности. Если исходная строка длиннее, чем count, strncpy не добавляет '\ 0' к цели. Это неизбежно приведет к неприятностям позже, когда вы ожидаете завершившуюся '\ 0' "строку". Поэтому strncpy также следует использовать с осторожностью!

Я бы использовал memcpy только в том случае, если бы я не работал с завершенными символами '\ 0', но это, кажется, дело вкуса.

Ваш код ужасно неэффективен, потому что он проходит через строку дважды, чтобы скопировать его.

Однажды в Стрлен ().

Затем снова в strcpy().

И вы не проверяете s1 для NULL.

Сохранение длины в некоторой дополнительной переменной не требует никаких затрат, а повторное выполнение каждой строки дважды для ее копирования является кардинальным грехом.

Другие вопросы по тегам