【问题标题】:C strcpy() - evil?C strcpy() - 邪恶?
【发布时间】:2010-10-11 05:57:02
【问题描述】:

有些人似乎认为 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()。是否有任何逻辑优先于三个(忽略前两个的缓冲区检查)?

【问题讨论】:

  • 在多线程环境中,每个库函数自己处理锁定很少是合理的。
  • 由于 strlen 可能会出现段错误,或在不正确终止的 s1 上返回不合理的大值,因此您的 strdup 不安全。
  • 如果 'malloc' 改变了 's1',那么当你以后用 'strcpy' 复制到它时,不能保证缓冲区足够大。假设's1'是一个指向内存管理系统内部维护的字符串的指针——可能是最后一次调用'malloc'。

标签: c memcpy strcpy


【解决方案1】:

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

【讨论】:

  • 这当然是这里使用memcpy的原因,与安全无关。确定这里的 memcpy 是否实际上更快将是一个有趣的性能分析。和你一样,我认为它适用于大多数情况,但可能对于非常小的字符串,strcpy 最终可能会更快。
【解决方案2】:

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

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

因此,如果您点击n 到目前为止还没有从源字符串中找到'\0',您将不会在字符串中得到尾随的'\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)。我也会使用它们。

【讨论】:

  • strn* 函数没有通用模式。函数strncpy 用于将可能是零填充或零终止的字符串复制,零填充,到与最大字符串长度相同大小的缓冲区的情况。没有其他名为 strn* 的函数具有任何类似的语义。 strncat 函数用于不知道目标缓冲区中当前字符串的长度,但知道其中至少有一定数量的空间的情况。完全不同(恕我直言,可能性很小)的情况。
【解决方案3】:

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

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

【讨论】:

  • strncpy 也很危险,因为它不能保证目标字符串是 0 终止的!最好使用 strncpy_s,虽然我不确定这些函数是否是特定于 MS 的。
  • strncpy 只有在你传入正确的长度时才能保证你的安全。如果您正在寻找不是缓冲区开始的 dst,您仍然需要计算可用空间。这与使用 strlen 检查您的 strcpy 是否适合没有根本不同:它仍然是减法。但代码更少。
  • @jn - strncpy_s 是对 ISO/ANSI C (ISO/IEC TR 24731-1) 的提议补充,但 MS 并未以这种方式实现。
  • “字符串并不总是以空值结尾”是错误的陈述。根据定义,字符串是一个以 null 结尾的字符序列。如果您有一个没有空终止符的 char 缓冲区,则根据定义它不是字符串。
  • 除了在示例中我们知道字符串是空终止的,并且我们有足够的空间。使用 strncpy 作为一般规则很好,但 strcpy 在这种情况下是安全的。
【解决方案4】:

坦率地说,如果您在 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 文件。它让我们的生活变得更加简单。另一方面,这样的库编写起来相当简单,因此您可以自己编写一个,即使只是作为练习。

【讨论】:

  • 我目前的项目并没有大量使用字符串,但这是一个非常好的建议。我会记住这一点。而且,如果只是为了学习经验,我可能会采用“自己动手”的方式。
  • -1 库并不总是合适的,并且与问题无关。
  • +1。编程就是抽象来处理复杂性,但是很多 C 程序员似乎认为,因为它是 C,所以你必须尽可能接近裸机,甚至拒绝使用重复的样板代码进行一些函数调用进入一个小的包装函数。这可能适用于一些小型玩具程序或一些速度真正重要的狭窄部分(无论如何)。任何足够大的项目都会很快陷入内存管理地狱。仅仅因为它是 C 并不意味着你不能使用现代软件工程的原则。
  • @Tomas:库是用于进行字符串处理的 C 应用程序的正确选择。示例:qmail、postfix。
  • @ninjalj:我完全同意
【解决方案5】:

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

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

strlcpy 的使用存在反驳论点;维基百科页面指出

Drepper 认为 strlcpystrlcat 使截断错误更容易 程序员可以忽略,因此 可以引入比他们更多的错误 删除。*

但是,我相信这只会迫使知道他们在做什么的人添加手动 NULL 终止,以及手动调整 strncpy 的参数。使用 strlcpy 可以更轻松地避免缓冲区溢出,因为您未能 NULL 终止缓冲区。

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

【讨论】:

  • 作为一名 OS X 开发人员,我拥有所有这些有趣的函数,例如 strdup() 和 strcpy(),但在紧要关头编写自己看起来相当容易。
【解决方案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;
}

现在,字符串不会因你自己的过错而增长,你很安全。结果不会是 dup,但也不会是任何疯狂的溢出。

您提供的代码实际上是错误的可能性非常低(如果您在不支持任何线程)。这只是需要考虑的事情。

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 不会使您的函数线程安全。
  • '如果这个函数在内存分配成功(以及对 strlen 的调用)后被中断,字符串会增长,并且由于 strcpy 复制到 NULL 字节,因此出现缓冲区溢出情况。'嗯...
  • "如果另一个线程在 s1 指向的内存块中调用 "free" 会发生什么?您的代码将同样被破坏。我认为这不是一个好的论点。他编写代码时并没有考虑到多线程,而且你总是必须根据你的保证做出一些假设。
  • 您仍然可以使用 strncpy 出现缓冲区溢出:上面使用 strncpy 的代码可能不会以 NULL 终止(实际上,它永远不会以 NULL 终止 - 在我的平台上编译上述代码时,缓冲区来自malloc 用 '\0' 填充 - 如果不是,s2 就不会被 NULL 终止)
【解决方案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() 会解释它们,打开整架的蠕虫罐头。

【讨论】:

  • strncpy 旨在填充真正古老的 Unix(想想 1970 年代)目录条目中的文件名字段,这些字段最多 14 个字符,如果更短,则填充零。填充对于防止缓冲区末尾的信息泄漏很重要。这证明了 strncpy 的设计是合理的。
  • 调试时也很有用。如果您在缓冲区之后有保护页,那么 strncpy 的 0-fill 可确保即使您确实传递了错误的长度(好吧,受对齐舍入的影响),您会立即捕获,而不是仅在 src 字符串足够长时捕获。
  • snprintf对其格式字符串的解析增加了多少开销?
【解决方案8】:

我认为 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;

这些字符串抽象的问题在于,没有人可以就其中一个达成一致(例如,上面 cmets 中提到的 strncpy 的特性是好是坏,您是否需要共享缓冲区的不可变和/或写时复制字符串)创建一个子字符串等)。因此,尽管理论上您应该只从货架上取下一个,但最终每个项目都可以有一个。

【讨论】:

    【解决方案9】:

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

    但是对于您给出的示例,这并不重要 - 如果它会失败,它将在最初的 strlen 中,所以 strncpy 在安全方面不会给您带来任何好处(大概是 strncpy速度较慢,因为它必须同时检查边界和 nul),并且 memcpystrcpy 之间的任何差异都不值得投机地更改代码。

    【讨论】:

      【解决方案10】:

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

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

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

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

      【讨论】:

        【解决方案11】:

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

        但实际上缓冲区不是以 null 结尾的。

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

        【讨论】:

        • strlen() 计算 始终 具有终止空字符的 string 的长度。在charstrlen(3.14159) 等非空终止数组上使用strlen() 是简单的错误代码。当然,好的编译器会标记第二个。
        【解决方案12】:

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

        【讨论】:

        • 这些也是 strdupa() :-)
        【解决方案13】:

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

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

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

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

        【讨论】:

          【解决方案14】:
          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 需要访问字符串中的每个元素。

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

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

          【讨论】:

          • 你的回答没有意义。要么你为你的应用程序假设以 nul 结尾的字符串,要么你应该使用一个字符串库(即使它只是一个快速放在一起的 struct { size_t len; char str[]; } 和一些可以使用它们的函数)。为什么业务逻辑应该与您的代码处理字符串的方式完全相关?如果 nul 终止是危险的,那么 every str* 标准库函数也是危险的。
          【解决方案15】:
          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;
          }
          

          【讨论】:

            【解决方案16】:

            此答案使用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.1 1 “string 是一个连续的字符序列,由第一个空字符终止并包括第一个空字符。...”

            【讨论】:

              【解决方案17】:

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

              在 strlen() 中一次。

              然后再次在 strcpy() 中。

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

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

              【讨论】:

              • 既然函数没有被告知字符串有多长,那么如何避免双重遍历呢? AFAICS,没有办法,所以“非常低效”是不准确的。
              • 同意,效率低下。如果将已知长度传递给 memcpy(),则删除字符串的第二次扫描以查找 '\0'
              • 在 C 中如果有机会这样做,你应该缓存一个曾经确定的字符串长度(如果同时没有修改字符串)
              • 这就是为什么你应该使用帕斯卡风格的字符串 - struct { size_t len; char str[]; }
              猜你喜欢
              • 2011-06-29
              • 1970-01-01
              • 2010-10-02
              • 2016-11-18
              • 2011-01-02
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2011-04-23
              相关资源
              最近更新 更多