C 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()
编写这个函数,它们都需要一个length参数。 我想知道人们对此的看法。 如果您认为strcpy()
在某些情况下是安全的,请说明。 如果你有充分的理由不在这种情况下使用strcpy()
,请给它 – 我想知道为什么在这种情况下使用strncpy()
或memcpy()
会更好。 如果你认为strcpy()
没问题,但不在这里,请解释一下。
基本上,我只是想知道为什么有些人在使用strcpy()
时会使用memcpy()
,而其他人则使用plain strncpy()
。 是否有任何逻辑可以优先选择三个(忽略前两个的缓冲区检查)?
memcpy
可以比strcpy
和strncpy
更快,因为它不必将每个复制的字节与’\ 0’进行比较,因为它已经知道复制对象的长度。 它可以用与Duff设备类似的方式实现,或者使用一次复制几个字节的汇编程序指令,如movsw和movsd
我在这里遵守规则。 让我引用它
最初将
strncpy
引入C库以处理目录条目等结构中的固定长度名称字段。 此类字段的使用方式与字符串不同:对于最大长度字段,尾部空值不是必需的,而将较短名称的尾随字节设置为空可确保有效的字段比较。 strncpy并非源于“有限的strcpy”,委员会倾向于认识到现有的做法而不是改变function以更好地适应这种用途。
出于这个原因,如果你到目前为止没有从源字符串中找到'\0'
,你将不会在字符串中得到一个尾随'\0'
。 滥用它很容易(当然,如果你知道这个陷阱,你可以避免它)。 正如引言所说,它不是设计为有限的strcpy。 如果没有必要,我宁愿不使用它。 在你的情况下,显然它的使用是没有必要的,你certificate了它。 为什么然后使用它?
一般来说,编程代码也是关于减少冗余。 如果你知道你有一个包含’n’个字符的字符串,为什么要告诉复制函数复制最多n
字符? 你做多余的检查。 它与性能有关,但更多关于一致的代码。 读者会问自己strcpy
可以做什么,可以跨越n
字符,这使得有必要限制复制,只是为了阅读手册,在这种情况下不会发生这种情况。 并且在代码的读者中发生混乱。
为了理性使用str-
, 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
)。 我会同样使用它们。
人们使用strncpy而不是strcpy的原因是因为字符串并不总是以空值终止,并且很容易溢出缓冲区(用strcpy为字符串分配的空间)并覆盖一些不相关的内存。
随着strcpy这种情况可能发生,strncpy这种情况永远不会发生。 这就是为什么strcpy被认为是不安全的原因。 邪恶可能有点强大。
坦率地说,如果你在C中做了很多字符串处理,你不应该问自己是否应该使用strcpy
或strncpy
或memcpy
。 您应该找到或编写一个提供更高级抽象的字符串库。 例如,跟踪每个字符串的长度,为您分配内存,并提供您需要的所有字符串操作。
这几乎肯定会保证你很少发生通常与C字符串处理相关的错误,例如缓冲区溢出,忘记终止带有NUL字节的字符串,等等。
该库可能具有以下function:
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文件。 它让我们的生活变得更加简单。 另一方面,这样的库写起来相当简单,所以你可以自己写一个,即使只是作为练习。
没人提到由Todd C. Miller和Theo de Raadt开发的 strlcpy
。 正如他们在论文中所说:
最常见的误解是
strncpy()
NUL终止目标字符串。 但是,如果源字符串的长度小于size参数,则这是正确的。 当将可能具有任意长度的用户输入复制到固定大小的缓冲区时,这可能是有问题的。 在这种情况下使用strncpy()
最安全的方法是传递一个小于目标字符串大小的方法,然后手动终止字符串。 这样,您可以保证始终拥有NUL终止的目标字符串。
关于使用strlcpy
存在反对意见; 维基百科页面记下了这一点
Drepper认为
strlcpy
和strlcat
使截断错误更容易被程序员忽略,从而可能引入比删除更多的错误。 *
但是,我认为这只会迫使人们知道他们正在做什么来添加手动NULL终止,除了手动调整strncpy
的参数。 使用strlcpy
可以更容易避免缓冲区溢出,因为您无法使NULL终止缓冲区。
另请注意,glibc或Microsoft库中缺少strlcpy
不应成为使用障碍; 您可以在任何BSD发行版中找到strlcpy
和朋友的来源,并且该许可证可能对您的商业/非商业项目很友好。 请参阅strlcpy.c
顶部的strlcpy.c
。
我个人认为,如果代码可以被certificate是有效的 – 并且如此快速地完成 – 这是完全可以接受的。 也就是说,如果代码很简单,因此显然是正确的,那么就可以了。
但是,您的假设似乎是在您的函数执行时,没有其他线程会修改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()
,因为它总会将输出填充到指定的长度。 这是一些历史性的决定,我认为这是非常不幸的,因为它严重恶化了表现。
考虑这样的代码:
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也是邪恶的。
为了真正保护自己免受此类编程错误的影响,您需要编写(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的特性是好还是坏,是否需要在创建子字符串时共享缓冲区的不可变和/或写时复制字符串等)。 因此,虽然理论上你应该只需要一个现成的,但每个项目最终可以使用一个。
如果我已经计算了长度,我倾向于使用memcpy
,虽然strcpy
通常经过优化以处理机器字,但是你觉得你应该为库提供尽可能多的信息,所以它可以使用最优的复制机制。
但是对于你给出的例子,没关系 – 如果它会失败,它将在最初的strlen
,所以strncpy不会在安全方面为你买任何东西(并且因为两者都需要,所以stribcpy会慢一些检查边界和nul),并且memcpy
和strcpy
之间的任何差异都不值得为推测性地更改代码。
当人们像这样使用它时,邪恶就来了(尽管下面是超简化的):
void BadFunction(char *input) { char buffer[1024]; //surely this will **always** be enough strcpy(buffer, input); ... }
这种情况经常令人惊讶。
但是,在你为目标缓冲区分配内存并且已经使用strlen来查找长度的任何情况下,strcpy和strncpy一样好。
strlen找到最后一个null终止位置。
但实际上缓冲区不会以空值终止。
这就是人们使用不同function的原因。
好吧,strcpy()并不像strdup()那样邪恶 – 至少strcpy()是标准C的一部分。
在你描述的情况下,strcpy是一个不错的选择。 如果s1没有以’\ 0’结尾,那么这个strdup只会遇到麻烦。
我会添加一个注释,说明为什么没有strcpy的问题,以防止其他人(和你自己一年后)对它的正确性长时间的疑问。
strncpy经常看似安全,但可能会让你陷入困境。 如果源“string”短于count,则用’\ 0’填充目标,直到达到count。 这可能对性能不利。 如果源字符串长于count,则strncpy不会向目标附加“\ 0”。 当你期望’\ 0’终止“字符串”时,这肯定会让你遇到麻烦。 所以strncpy也应谨慎使用!
如果我不使用’\ 0’终止字符串,我只会使用memcpy,但这似乎是一个品味问题。
char *strdup(const char *s1) { char *s2 = malloc(strlen(s1)+1); if(s2 == NULL) { return NULL; } strcpy(s2, s1); return s2; }
问题:
- s1未终止,strlen导致未分配内存的访问,程序崩溃。
- s1未终止,strlen同时不会导致从应用程序的另一部分访问未分配的内存访问内存。 它返回给用户(安全问题)或由程序的另一部分解析(出现heisenbug)。
- s1未终止,strlen导致系统无法满足的malloc,返回NULL。 strcpy传递NULL,程序崩溃。
- s1未终止,strlen导致malloc非常大,系统分配太多内存来执行手头的任务,变得不稳定。
- 在最好的情况下,代码效率低下,strlen需要访问字符串中的每个元素。
可能还有其他问题……看,空终止并不总是一个坏主意。 在某些情况下,为了提高计算效率或降低存储要求,这是有道理的。
对于编写通用代码,例如业务逻辑是否有意义? 没有。
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; }
这个答案使用size_t
和memcpy()
来实现快速简单的strdup()
。
最好使用type 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“ 字符串是由第一个空字符终止并包括第一个空字符的连续字符序列。……”
您的代码非常低效,因为它会在字符串中运行两次以复制它。
一旦进入strlen()。
然后再次在strcpy()中。
并且您不检查s1是否为NULL。
将长度存储在一些额外的变量中会使你无所事事,而在每个字符串中运行两次以复制它是一个重大的罪恶。