C strcpy () - зло? - PullRequest
       68

C strcpy () - зло?

22 голосов
/ 04 марта 2009

Некоторые люди думают, что функция C 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 ]

26 голосов
/ 04 марта 2009

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

18 голосов
/ 04 марта 2009

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

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

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

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

16 голосов
/ 04 марта 2009

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

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

11 голосов
/ 04 марта 2009

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

9 голосов
/ 05 марта 2009

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

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

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

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

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

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

8 голосов
/ 04 марта 2009

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

Тем не менее, вы предполагаете, что пока ваша функция выполняется, никакой другой поток не изменит строку, на которую указывает 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. В любом случае лучше, чем тот, который я быстро собрал.

5 голосов
/ 04 марта 2009

Я думаю, что 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, упомянутые в комментариях выше, нужны ли вам неизменяемые и / или строки копирования при записи, которые совместно используют буферы, когда создать подстроку и т. д.). Таким образом, хотя в теории вы должны просто взять один с полки, вы можете получить один на проект.

5 голосов
/ 04 марта 2009

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

4 голосов
/ 04 марта 2009

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

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

4 голосов
/ 04 марта 2009

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

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

    strcpy(buffer, input);

    ...
}

Какова ситуация, которая часто случается удивительно.

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

...