C中strncpy的内存混淆

本周我的同事讨论了一个关于记忆的问题:

示例代码1:

int main() { #define Str "This is String." char dest[1]; char buff[10]; strncpy(dest, Str, sizeof(Str)); printf("Dest: %s\n", dest); printf("Buff: %s\n", buff); } 

输出:

 Dest: This is String. Buff: his is String. 

示例代码2:

 int main() { #define Str "This is String." char dest[1]; //char buff[10]; strncpy(dest, Str, sizeof(Str)); printf("Dest: %s\n", dest); //printf("Buff: %s\n", buff); } 

输出:

 Dest: This is String. *** stack smashing detected ***: ./test terminated Aborted (core dumped) 

我不明白为什么我在案例1中得到那个输出? 因为buff甚至没有在strncpy中使用,如果我注释变量buff它会检测到堆栈粉碎但是输出为dest。 同样为了buff我为什么得到输出为“他作为字符串”。

这是一个有趣的问题,我们都希望在某些时候理解这个问题。 此处出现的问题称为“缓冲区溢出” 。 此问题的副作用因系统而异(也称为未定义行为 )。 只是为了向您解释在您的情况下可能发生的事情让我们假设程序中变量的内存布局如下所示

在此处输入图像描述

注意上面的表示仅用于理解,并不显示任何体系结构的实际表示。 执行strncpy命令后,该内存区域的内容如下所示

在此处输入图像描述

现在当你打印buff时,你可以看到buf的起始地址现在有’h’。 printf开始打印它,直到找到超过buff内存区域的空字符。 因此,当你打印buf时,你会得到’他的字符串’。 但请注意,由于堆栈保护(系统/实现)依赖,程序1不会产生堆栈碎片错误。 因此,如果您在不包含此代码的系统上执行此代码,则程序1也将崩溃(您可以通过将Str增加到一个长字符串来测试它)。

在程序2的情况下,strncpy只是通过写入来自main的返回地址经过堆栈保护,因此你会崩溃。

希望这可以帮助。

PS以上所有描述均用于理解,并未显示任何实际的系统表示。

C标准以这种方式指定strncpy

7.24.2.4 strncpy函数

概要

 #include  char *strncpy(char * restrict s1, const char * restrict s2, size_t n); 

描述

strncpy函数从s2指向的数组复制不超过n字符(不会复制空字符后面的字符)到s1指向的数组。

如果在重叠的对象之间进行复制,则行为未定义。

如果s2指向的数组是一个短于n字符的字符串,则将空字符附加到s1指向的数组中的副本,直到写入所有n字符。

返回

strncpy函数返回s1的值。

这些语义被广泛误解: strncpy不是strcpy安全版本,如果源字符串长于n参数,则目标数组不会终止。

在您的示例中,此n参数大于目标数组的大小:行为未定义,因为字符写在目标数组的末尾之外。

您可以观察到这是第一个示例,因为buff数组在自动存储(也就是堆栈 )中的dest数组结束后由编译器定位并被strncpy覆盖。 编译器可以使用不同的方法,因此无法保证观察到的行为。

我的建议是永远不要使用这个function 。 其他C专家如Bruce Dawson也持有这样的观点: 已经停止使用strncpy了!

你应该支持一个不那么容易出错的函数,比如这个:

 // Utility function: copy with truncation, return source string length // truncation occurred if return value >= size argument size_t bstrcpy(char *dest, size_t size, const char *src) { size_t i; /* copy the portion that fits */ for (i = 0; i + 1 < size && src[i] != '\0'; i++) { dest[i] = src[i]; } /* null terminate destination unless size == 0 */ if (i < size) { dest[i] = '\0'; } /* compute necessary length to allow truncation detection */ while (src[i] != '\0') { i++; } return i; } 

您可以在示例中以这种方式使用它:

 int main(void) { #define Str "This is String." char dest[12]; // the size of the destination array is passed // after the pointer, just as for `snprintf` bstrcpy(dest, sizeof dest, Str); printf("Dest: %s\n", dest); return 0; } 

输出:

 This is a S 

strncpy(dest, Str, sizeof(Str));

你的dest只有一个字节,所以你在这里编写你不应该写的内存,这会调用未定义的行为。 换句话说,根据编译器如何实现这些东西,任何事情都可能发生。

写入buf的最可能原因是编译器在buf之后放置dest 。 因此,当你写过dest的边界时,你正在写给buf 。 当你注释掉buf时会导致崩溃。

但正如我之前所说,如果使用不同的编译器甚至不同版本的相同编译器,您可能会得到完全不同的行为。

简介:永远不要做任何调用未定义行为的事情。 在strncpy您应该使用sizeof(dest) ,而不是sizeof(src)并为目标分配足够的内存,以便源中的数据不会丢失。

堆栈中变量的位置是: –

 0. dest 1. buff 12. canary 16. Return address 

buff存在时,它保护金丝雀和返回地址免受损坏。

这是未定义的行为(将更多数据写入dest不是拟合)。 金丝雀在其中有一个特殊的随机值,它在函数启动时设置,并在执行返回指令之前进行测试。 这为缓冲区溢出增加了某种forms的保护。

未定义性质的例子是由于没有金丝雀,程序可能因“非法指令@ xxxxxx”而崩溃。 如果返回地址与变量位置分开,则程序可能表现正常。

在大多数当前的CPU上,堆栈通常会以负方向增长。 dest vs buff的位置也取决于编译器。 它可能已经将它们转换为圆形,或者如果(例如)你带走了第二个printf,编译器可能已经删除了dest的存储,因为它可能已经确定它没有被正确使用。