当前位置:  开发笔记 > 编程语言 > 正文

C strcpy() - 邪恶?

如何解决《Cstrcpy()-邪恶?》经验,为你挑选了8个好方法。

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



1> dmityugov..:

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



2> Johannes Sch..:

我遵守这里的规则.让我引用它

strncpy最初被引入C库以处理诸如目录条目之类的结构中的固定长度名称字段.这些字段的使用方式与字符串不同:对于最大长度字段,尾部空值不是必需的,而将较短名称的尾随字节设置为空可确保有效的字段比较.strncpy并非源于"有限的strcpy",委员会倾向于认识到现有的做法,而不是改变功能以更好地适应这种用途.

因此,'\0'如果您到目前为止n未找到'\0'源字符串中的a ,则不会在字符串中得到尾随.滥用它很容易(当然,如果你知道这个陷阱,你可以避免它).正如引言所说,它并非设计为有限的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-(for C99).我会同样使用它们.


我不敢相信这个答案不是最高票.

3> 小智..:

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

随着strcpy这种情况可能发生,strncpy将永远不会发生.这就是为什么strcpy被认为是不安全的原因.邪恶可能有点强大.


"字符串不总是以空值终止"是一种错误的陈述.根据定义,字符串是一个空终止的字符序列.如果您有一个没有空终止符的char缓冲区,则根据定义它不是字符串.

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项目编写了一个,请参阅gwlib/octstr.h文件.它让我们的生活变得更加简单.另一方面,这样的库写起来相当简单,所以你可以自己写一个,即使只是作为练习.


+1.编程是关于处理复杂性的抽象,但很多C程序员似乎认为,因为它是C,你必须保持尽可能接近裸机,甚至拒绝用重复的样板代码进行一些函数调用进入一个小包装函数.这可能适用于一些小型玩具程序或速度真正重要的一些狭窄部分(无论如何).任何足够大小的项目都会很快在内存管理地狱中结束.仅仅因为它是C并不意味着你不能使用现代软件工程的原理.

5> Jared Oberha..:

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

最常见的误解是 strncpy()NUL终止目标字符串.但是,如果源字符串的长度小于size参数,则这是正确的.当将可能具有任意长度的用户输入复制到固定大小的缓冲区中时,这可能是有问题的.strncpy()在这种情况下使用最安全的方法 是传递一个小于目标字符串大小的方法,然后手动终止字符串.这样,您可以保证始终拥有NUL终止的目标字符串.

对于使用有反对意见strlcpy; 维基百科页面记下了这一点

Drepper认为,strlcpystrlcat作出更容易为程序员忽略截断误差,因而比他们删除可以引入更多的错误.*

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

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



6> Michael Trau..:

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

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


您不应该访问其他线程修改的任何缓冲区而不先将其锁定.使用strncpy不会使您的函数线程安全.
"如果另一个线程在s1指向的内存块中调用"free"会发生什么?" 你的代码将同样被打破.我认为这不是一个好的论据.他编写的代码并没有考虑到多个线程,你总是要根据自己的保证做出一些假设.

7> unwind..:

我同意.我建议不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旨在填充Really Ancient Unix(20世纪70年代)目录条目中的文件名字段,最多14个字符,如果更短则填充零.填充对于防止缓冲区末端的信息泄漏非常重要.这证明了strncpy的设计.

8> Steve Jessop..:

我认为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的特性是好还是坏,是否需要在创建子字符串时共享缓冲区的不可变和/或写时复制字符串等).因此,虽然从理论上讲你应该只需要一个现成的,但每个项目最终只需要一个.

推荐阅读
雯颜哥_135
这个屌丝很懒,什么也没留下!
DevBox开发工具箱 | 专业的在线开发工具网站    京公网安备 11010802040832号  |  京ICP备19059560号-6
Copyright © 1998 - 2020 DevBox.CN. All Rights Reserved devBox.cn 开发工具箱 版权所有