22

有些人似乎认为 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,并且 using 使strcpy()我们不必将strlen()结果存储在另一个函数中,以便以后用作strncpy(). 然而有些人用strncpy(),甚至编写这个函数memcpy(),它们都需要一个长度参数。我想知道人们对此有何看法。如果您认为strcpy()在某些情况下是安全的,请说出来。如果您有充分的理由不在strcpy()这种情况下使用,请给出 - 我想知道为什么在这种情况下使用它可能会strncpy()更好memcpy()。如果您认为strcpy()还可以,但不在这里,请解释。

基本上,我只是想知道为什么有些人memcpy()在其他人使用而其他人使用时strcpy()使用 plain strncpy()。是否有任何逻辑比三个更喜欢一个(忽略前两个的缓冲区检查)?

4

17 回答 17

25

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

于 2009-03-04T17:08:36.973 回答
18

我在这里遵守规则。让我引用它

strncpy最初被引入 C 库以处理结构中的固定长度名称字段,例如目录条目。此类字段的使用方式与字符串不同:对于最大长度字段,尾随 null 是不必要的,并且将较短名称的尾随字节设置为 null 可确保有效的逐字段比较。strncpy 的起源并不是“有界 strcpy”,委员会更愿意承认现有的做法,而不是改变功能以更好地适应这种用途。

'\0'出于这个原因,如果您点击nnot found a'\0'从源字符串到目前为止,您将不会在字符串中得到尾随。很容易误用它(当然,如果你知道这个陷阱,你可以避免它)。正如引用所说,它不是设计为有界的strcpy。如果没有必要,我宁愿不使用它。在您的情况下,显然没有必要使用它,并且您证明了这一点。那为什么要使用它?

而且一般来说,编程代码也是为了减少冗余。如果你知道你有一个包含“n”个字符的字符串,为什么要告诉复制函数复制最大n字符?你做冗余检查。它与性能无关,但更多的是关于一致的代码。读者会问自己,有什么strcpy办法可以跨越n字符,这使得有必要限制复制,只是为了阅读手册,在这种情况下不会发生这种情况。代码的读者之间开始出现混乱。

为了合理使用mem-, str-or 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-and wcsn-(for C99)。我也会使用它们。

于 2009-03-04T12:24:09.637 回答
16

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

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

于 2009-03-04T12:10:09.437 回答
11

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

这几乎肯定会保证您很少犯通常与 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 文件。它让我们的生活变得更加简单。另一方面,这样的库编写起来相当简单,因此您可以为自己编写一个,即使只是作为练习。

于 2009-03-04T12:52:34.500 回答
9

没有人提到strlcpy由 Todd C. Miller 和 Theo de Raadt 开发。正如他们在论文中所说:

最常见的误解是 strncpy()NUL 终止目标字符串。但是,这仅在源字符串的长度小于 size 参数时才成立。在将可能具有任意长度的用户输入复制到固定大小的缓冲区中时,这可能会出现问题。在这种情况下使用最安全的方法 strncpy()是传递它比目标字符串的大小小一,然后手动终止该字符串。这样你就可以保证总是有一个以 NUL 结尾的目标字符串。

strlcpy使用;有反对意见。维基百科页面注意到

Drepper 认为,这strlcpy使得 strlcat截断错误更容易被程序员忽略,因此可能引入的错误多于删除的错误。*

但是,我相信这只会迫使知道自己在做什么的人除了手动调整strncpy. 使用strlcpy可以更容易地避免缓冲区溢出,因为您未能 NULL 终止您的缓冲区。

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

于 2009-03-05T05:10:02.730 回答
8

我个人的心态是,如果代码可以被证明是有效的——并且做得这么快——那是完全可以接受的。也就是说,如果代码很简单,因此显然是正确的,那就没问题了。

但是,您的假设似乎是,当您的函数正在执行时,没有其他线程会修改s1. 如果此函数在成功分配内存(并因此调用strlen)后被中断,字符串会增长,并且由于复制到 NULL 字节出现缓冲区溢出情况,会发生什么情况。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字节终止它。无论哪种方式都比我最初快速组合的方式要好。

于 2009-03-04T12:06:49.647 回答
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()会解释它们,打开整个货架的蠕虫罐头。

于 2009-03-04T12:15:29.550 回答
5

我认为 strncpy 也是邪恶的。

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

这意味着您需要一个真正的字符串抽象,它不透明地存储缓冲区和容量,将它们永远绑定在一起,并检查边界。否则,您最终会在整个商店中传递字符串及其容量。一旦你得到真正的字符串操作,比如修改字符串的中间,几乎很容易将错误的长度传递给 strncpy(尤其是 strncat),就像用太小的目标调用 strcpy 一样容易。

当然,您可能仍然会问在实现该抽象时是否使用 strncpy 或 strcpy:如果您完全了解它的作用,则 strncpy 在那里更安全。但在字符串处理应用程序代码中,依靠 strncpy 来防止缓冲区溢出就像戴了半个避孕套。

因此,您的 strdup-replacement 可能看起来像这样(更改定义顺序以使您保持悬念):

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 的特质是好是坏,在创建子字符串时是否需要共享缓冲区的不可变和/或写时复制字符串, ETC)。因此,尽管理论上您应该只从货架上取下一个,但最终每个项目都可以有一个。

于 2009-03-04T13:14:12.747 回答
4

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

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

    strcpy(buffer, input);

    ...
}

这是经常发生的令人惊讶的情况。

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

于 2009-03-04T12:07:43.310 回答
4

memcpy如果我已经计算了长度,我倾向于使用,尽管strcpy通常针对机器字进行优化,但感觉您应该为库提供尽可能多的信息,以便它可以使用最优化的复制机制。

但是对于您给出的示例,没关系-如果它会失败,它将在初始状态strlen,因此 strncpy 在安全方面不会给您带来任何好处(并且可能strncpy会更慢,因为它必须同时检查边界和对于 nul),以及memcpy和之间的任何区别strcpy都不值得投机地更改代码。

于 2009-03-04T12:01:48.037 回答
1

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

但实际上缓冲区不是空终止的。

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

于 2009-03-04T11:59:59.367 回答
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;
}
于 2009-05-09T06:48:28.340 回答
0

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

于 2009-03-04T12:16:20.280 回答
0

在您描述的情况下, strcpy 是一个不错的选择。只有当 s1 没有以 '\0' 结尾时,这个 strdup 才会遇到麻烦。

我会添加一条评论,说明为什么 strcpy 没有问题,以防止其他人(以及一年后的你自己)对它的正确性感到疑惑太久。

strncpy 通常看起来很安全,但可能会给您带来麻烦。如果源“字符串”比 count 短,它会用 '\0' 填充目标,直到达到 count。这可能对性能不利。如果源字符串长于 count,strncpy 不会将 '\0' 附加到目标。当您期望以 '\0' 结尾的“字符串”时,这势必会给您带来麻烦。所以 strncpy 也应该谨慎使用!

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

于 2009-03-04T12:49:56.627 回答
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 需要访问字符串中的每个元素。

可能还有其他问题......看,空终止并不总是一个坏主意。在某些情况下,为了计算效率或减少存储需求,它是有意义的。

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

于 2011-09-27T22:07:31.897 回答
0

这个答案使用size_tandmemcpy()快速简单strdup()

最好使用 type size_t,因为它是从 and 中返回和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 “字符串是由第一个空字符终止并包括第一个空字符的连续字符序列。......”

于 2014-04-21T17:02:34.093 回答
-1

您的代码非常低效,因为它两次遍历字符串来复制它。

一旦进入 strlen()。

然后再次在 strcpy() 中。

而且您不会检查 s1 是否为 NULL。

将长度存储在一些额外的变量中几乎没有任何成本,而对每个字符串运行两次以复制它是一个大罪。

于 2009-03-04T13:54:51.917 回答