Подтвердить что ты не робот

C strcpy() - зло?

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

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(), а третьи используют plain strncpy(). Есть ли какая-либо логика для предпочтения одного над тремя (без учета проверок буфера первых двух)?

4b9b3361

Ответ 1

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

Ответ 2

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

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

По этой причине вы не получите trailing '\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). Я бы использовал их также.

Ответ 3

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

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

Ответ 4

Честно говоря, если вы выполняете большую обработку строк в 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. Это сделало жизнь намного проще для нас. С другой стороны, такую ​​библиотеку достаточно просто написать, поэтому вы можете написать ее для себя, даже если это упражнение.

Ответ 5

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

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

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

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

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

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

Ответ 6

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

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

Возможно, было бы лучше:

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. В любом случае лучше, чем тот, который я быстро собрал изначально.

Ответ 7

Я согласен. Я бы рекомендовал против 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(), они будут интерпретировать их, открывая целые полки червей червей.

Ответ 8

Я думаю, что 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;

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

Ответ 9

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

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

Ответ 10

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

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

    strcpy(buffer, input);

    ...
}

Это ситуация, которая часто случается.

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

Ответ 11

strlen находит до последнего нулевого конечного места.

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

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

Ответ 12

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

Ответ 13

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

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

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

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

Ответ 14

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;
}

Ответ 15

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

Проблемы:

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

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

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

Ответ 16

Этот ответ использует 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 "Строка - это непрерывная последовательность символов, заканчивающихся и включающая первый нулевой символ..."

Ответ 17

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

Один раз в strlen().

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

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

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