C strcpy() - 邪恶?

时间:2009-03-04 11:54:30

标签: c memcpy strcpy

有些人似乎认为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 个答案:

答案 0 :(得分:26)

memcpy可能比strcpystrncpy更快,因为它不必将每个复制的字节与'\ 0'进行比较,因为它已经知道复制对象的长度。它可以用与Duff's device类似的方式实现,或者使用一次复制几个字节的汇编程序指令,如movsw和movsd

答案 1 :(得分:18)

我遵守here中的规则。让我引用它

  最初将

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)。我会同样使用它们。

答案 2 :(得分:16)

人们使用strncpy而不是strcpy的原因是因为字符串并不总是以null结尾,并且很容易溢出缓冲区(用strcpy为字符串分配的空间)并覆盖一些不相关的内存。

使用strcpy,可以发生,使用strncpy,永远不会发生。这就是为什么strcpy被认为是不安全的原因。邪恶可能有点强大。

答案 3 :(得分:11)

坦率地说,如果你在C中做了很多字符串处理,你不应该问自己是否应该使用strcpystrncpymemcpy。您应该找到或编写一个提供更高级抽象的字符串库。例如,跟踪每个字符串的长度,为您分配内存,并提供您需要的所有字符串操作。

这几乎肯定会保证你很少发生通常与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 project写了一个,请参阅gwlib / octstr.h文件。它让我们的生活变得更加简单。另一方面,这样的库写起来相当简单,所以你可以自己写一个,即使只是作为练习。

答案 4 :(得分:9)

没人提及strlcpydeveloped by Todd C. Miller and Theo de Raadt。正如他们在论文中所说:

  

最常见的误解是   strncpy() NUL终止了   目标字串。这只是真的,   但是,如果源的长度   string小于大小   参数。这可能有问题   复制可能的用户输入时   任意长度变成固定大小   缓冲。最安全的使用方式   strncpy()在这种情况下即可通过   它比一个小的大小   目标字符串,然后终止   手工制作的字符串。那样你就是   保证永远有一个   NUL终止的目标字符串。

使用strlcpy存在反驳论据;维基百科页面记下了

  

Drepper辩称strlcpy和。{   strlcat使截断错误更容易   让程序员忽略,从而   可以引入比他们更多的错误   除去。*

但是,我认为除了手动调整strncpy的参数外,我认为这只会迫使人们知道他们正在做什么来添加手动NULL终止。使用strlcpy可以更容易避免缓冲区溢出,因为您无法使NULL终止缓冲区。

另请注意,glibc或Microsoft库中缺少strlcpy不应成为使用障碍;您可以在任何BSD发行版中找到strlcpy和朋友的来源,并且该许可证可能对您的商业/非商业项目很友好。请参阅strlcpy.c顶部的评论。

答案 5 :(得分:8)

我个人认为,如果代码可以被证明是有效的 - 并且如此快速地完成 - 这是完全可以接受的。也就是说,如果代码很简单,因此显然是正确的,那么就可以了。

但是,您的假设似乎是在您的函数执行时,没有其他线程会修改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字节终止它。无论哪种方式都比我最初快速组合的方式更好。

答案 6 :(得分:5)

我同意。我建议不要使用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()字符会解释它们,就会打开一大堆虫子。

答案 7 :(得分:5)

我认为strncpy也是邪恶的。

为了真正保护自己免受此类编程错误的影响,您需要编写(a)看起来不错的代码,并且(b)超出缓冲区。

这意味着你需要一个真正的字符串抽象,它不透明地存储缓冲区和容量,将它们永久地绑定在一起,并检查边界。否则,你最终会在整个商店里传递字符串和容量。一旦你得到真正的字符串操作,比如修改字符串的中间部分,将错误的长度传递给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的特性是好是坏,是否需要不可变和/或写时复制字符串,当你共享缓冲区时创建子字符串等)。因此,虽然理论上你应该只拿一个现成的,但每个项目最终只能拿一个。

答案 8 :(得分:4)

如果我已经计算了长度,我倾向于使用memcpy,虽然strcpy通常经过优化以处理机器词,但是您觉得应该为库提供尽可能多的信息。你可以,所以它可以使用最佳的复制机制。

但是对于你给出的例子,没关系 - 如果它会失败,它将在最初的strlen,所以strncpy不会在安全方面给你任何东西(并且是不公平的{ {1}}速度较慢,因为它必须同时检查边界和nul),并且strncpymemcpy之间的任何差异都不值得为推测性地更改代码。

答案 9 :(得分:4)

当人们像这样使用它时,邪恶就来了(尽管下面是超简化的):

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

    strcpy(buffer, input);

    ...
}

这种情况经常令人惊讶。

但是,在任何你为目标缓冲区分配内存并且已经使用strlen来查找长度的情况下,strcpy和strncpy一样好。

答案 10 :(得分:1)

strlen找到最后一个空终止位置。

但实际上缓冲区不会以空值终止。

这就是人们使用不同功能的原因。

答案 11 :(得分:0)

此答案使用size_tmemcpy()来快速简单地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.11“ string 是由第一个空字符终止并包含第一个空字符的连续字符序列....”

答案 12 :(得分:0)

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需要访问字符串中的每个元素。
  6. 可能存在其他问题......看,null终止并不总是一个坏主意。在某些情况下,为了提高计算效率或降低存储要求,这是有道理的。

    用于编写通用代码,例如业务逻辑有意义吗?否。

答案 13 :(得分:0)

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

答案 14 :(得分:0)

在你描述的情况下,strcpy是一个不错的选择。如果s1没有以'\ 0'结束,那么这个strdup只会遇到麻烦。

我会添加一条注释,说明为什么strcpy没有问题,以防止其他人(和你自己一年后)对它的正确性进行太长时间的疑问。

strncpy经常看似安全,但可能会让你陷入困境。如果源“字符串”短于计数,则用“\ 0”填充目标,直到达到计数。这可能对性能不利。如果源字符串长于count,则strncpy不会向目标附加“\ 0”。当你期望'\ 0'终止“字符串”时,这肯定会让你遇到麻烦。所以strncpy也应谨慎使用!

如果我不使用'\ 0'终止字符串,我只会使用memcpy,但这似乎是一种品味问题。

答案 15 :(得分:0)

嗯,strcpy()并不像strdup()那样邪恶 - 至少strcpy()是标准C的一部分。

答案 16 :(得分:-1)

您的代码非常低效,因为它会在字符串中运行两次以复制它。

进入strlen()。

然后再次使用strcpy()。

并且您不检查s1是否为NULL。

将长度存储在一些额外的变量中会使你无所事事,而在每个字符串中运行两次以复制它是一个重大的罪过。