考虑以下计划
char str[5];
strcpy(str,"Hello12345678");
printf("%s",str);
运行此程序时会出现分段错误。
但是当用以下代替strcpy时,程序运行正常。
strcpy(str,"Hello1234567");
所以问题是当尝试复制到str超过5个字符长度的任何其他字符串时它应该崩溃。
那么为什么它不会因“Hello1234567”崩溃而只会崩溃为“Hello12345678”,即长度为13或超过13的字符串。
该程序在32位机器上运行。
答案 0 :(得分:31)
您应该感兴趣的标准行为有三种类型。
1 / 定义的行为。这将适用于所有符合要求的实现。自由使用。
2 / 实施定义的行为。如上所述,它取决于实施,但至少它仍然定义。需要实现来记录它们在这些情况下的作用。如果您不关心可移植性,请使用此选项。
3 / 未定义的行为。任何事情都可能发生。我们的意思是任何,直到并包括你的整个计算机崩溃成一个赤裸裸的奇点和吞咽自己,你和你的很大一部分同事。永远不要使用它。永远!认真!不要让我过来。
将超过4个字符和零字节复制到char[5]
是未定义的行为。
说真的,为什么你的程序崩溃了14个字符而不是13个,你几乎肯定会覆盖堆栈上的一些非崩溃信息,你的程序很可能会产生不正确的结果。事实上,崩溃更好,因为至少它会阻止你依赖可能的不良影响。
将数组的大小增加到更合适的位置(在这种情况下使用可用信息char[14]
)或使用其他可以处理的数据结构。
更新
因为你似乎非常关心找出为什么额外的7个字符不会导致问题但是8个字符的问题,让我们设想在输入main()
时可能的堆栈布局。我说“可能”,因为实际布局取决于编译器使用的调用约定。由于C启动代码使用main()
和argc
调用argv
,因此在为main()
分配空间后,char[5]
开头的堆栈可以看起来像这样:
+------------------------------------+
| C start-up code return address (4) |
| argc (4) |
| argv (4) |
| x = char[5] (5) |
+------------------------------------+
使用:
编写字节Hello1234567\0
时
strcpy (x, "Hello1234567");
到x
,它会覆盖argc
和argv
,但是从main()
返回时,这没关系。具体来说,Hello
填充x
,1234
填充argv
,567\0
填充argc
。如果您之后实际上没有尝试使用 argc
和/或argv
,那么你会没事的:
+------------------------------------+ Overwrites with:
| C start-up code return address (4) |
| argc (4) | '567<NUL>'
| argv (4) | '1234'
| x = char[5] (5) | 'Hello'
+------------------------------------+
但是,如果您将Hello12345678\0
(请注意额外的“8”)写入x
,它会覆盖argc
和argv
以及返回地址的一个字节,这样,当main()
尝试返回C启动代码时,它会进入仙境:
+------------------------------------+ Overwrites with:
| C start-up code return address (4) | '<NUL>'
| argc (4) | '5678'
| argv (4) | '1234'
| x = char[5] (5) | 'Hello'
+------------------------------------+
同样,这完全取决于编译器的调用约定。有可能一个不同的编译器总是将数组填充到4个字节的倍数,并且代码不会在那里失败,直到你写了另外三个字符。即使是相同的编译器也可能以不同的方式在堆栈帧上分配变量,以确保满足对齐。
这就是未定义的意思:你不知道会发生什么。
答案 1 :(得分:7)
您正在复制到堆栈,因此它取决于编译器在堆栈上放置的内容,以及需要多少额外数据才能使程序崩溃。
有些编译器可能会产生的代码只会在缓冲区大小上只有一个字节崩溃 - 它的行为是未定义的。
我猜大小13足以覆盖返回地址或类似的东西,当你的函数返回时它会崩溃。但是另一个编译器或其他平台可能/将以不同的长度崩溃。
如果一个不太重要的东西被覆盖,如果它运行的时间较长,你的程序可能会以不同的长度崩溃。
答案 2 :(得分:5)
对于32位Intel平台,解释如下。当你在堆栈上声明char [5]时,由于对齐,编译器确实分配了8个字节。然后,函数通常具有以下序言:
push ebp
mov ebp, esp
这会将ebp注册表值保存在堆栈中,然后将esp寄存器值移动到ebp中以使用esp值来访问参数。这导致堆栈上的4个字节被ebp值占用。
在结语中ebp已恢复,但其值通常仅用于访问堆栈分配的函数参数,因此在大多数情况下覆盖它可能不会受到伤害。
所以你有以下布局(堆栈在英特尔上向下增长):你的数组为8个字节,然后是ebp的4个字节,然后通常是返回地址。
这就是为什么你需要覆盖至少13个字节才能使你的程序崩溃。
答案 3 :(得分:2)
要添加上述答案:您可以使用Valgrind等工具测试此类错误。如果您使用的是Windows,请查看this SO thread。
答案 4 :(得分:1)
取决于“str”数组后堆栈上的内容。在复制那么多角色之前,你恰好不会踩踏任何关键词。
所以它将取决于函数中的其他内容,您使用的编译器以及可能的编译器选项。
13是5 + 8,表明在str数组之后有两个非关键字,然后是一些关键字(可能是返回地址)
答案 5 :(得分:1)
这是未定义行为的纯粹美(UB):它未定义。
您的代码:
char str[5];
strcpy(str,"Hello12345678");
将14个字节/字符写入str
,它只能容纳5个字节/字符。这会调用UB。
答案 6 :(得分:0)
问:那么为什么它不会因为“Hello1234567”崩溃而只会崩溃为“Hello12345678”,即长度为13或超过13的字符串。
答案 7 :(得分:0)
因为行为未定义。 使用strncpy。看到这个页面 http://en.wikipedia.org/wiki/Strcpy 了解更多信息。
strncpy是不安全的,因为如果源字符串的长度为&gt; = n,则不会添加NULL终止,其中n是目标字符串的大小。
char s[5];
strncpy(s,5,"test12345");
printf("%s",s); // crash
我们总是使用strlcpy来缓解这种情况。