6

考虑下面的程序

    char str[5];
    strcpy(str,"Hello12345678");
    printf("%s",str);

运行此程序时会出现分段错误。

但是当 strcpy 替换为以下时,程序运行良好。

strcpy(str,"Hello1234567");

所以问题是当尝试复制到 str 任何其他长度超过 5 个字符的字符串时它应该崩溃。

那么为什么它不会因为“Hello1234567”而崩溃,而只会因为“Hello12345678”而崩溃,即长度为 13 或超过 13 的字符串。

该程序在 32 位机器上运行。

4

8 回答 8

31

您应该对三种类型的标准行为感兴趣。

1/定义的行为。这将适用于所有符合要求的实现。自由地使用它。

2/实现定义的行为。如前所述,它取决于实现,但至少它仍然是定义的。实现需要记录他们在这些情况下所做的事情。如果您不关心可移植性,请使用它。

3/未定义的行为。任何事情都有可能发生。我们的意思是任何事情,包括你的整个计算机崩溃成一个赤裸裸的奇点并吞噬自己,你和你的大部分同事。永远不要使用这个。曾经!严重地!不要让我过来。

将超过 4 个字符和一个零字节复制到 achar[5]是未定义的行为。

说真的,你的程序为什么会因为 14 个字符而不是 13 个字符而崩溃并不重要,你几乎肯定会覆盖堆栈上的一些非崩溃信息,并且你的程序很可能会产生不正确的结果。事实上,崩溃更好,因为至少它可以阻止你依赖可能的不良影响。

将数组的大小增加到更合适的大小(char[14]在这种情况下使用可用信息)或使用其他一些可以处理的数据结构。


更新:

由于您似乎非常关心为什么额外的 7 个字符不会导致问题,但 8 个字符会导致问题,让我们设想一下输入时可能的堆栈布局main()。我说“可能”,因为实际布局取决于编译器使用的调用约定。由于 C 启动代码main()使用argcand调用,因此在为 a 分配空间后argv,位于 开头的堆栈可能如下所示:main()char[5]

+------------------------------------+
| C start-up code return address (4) |
| argc (4)                           |
| argv (4)                           |
| x = char[5] (5)                    |
+------------------------------------+

当您使用以下内容写入字节Hello1234567\0时:

strcpy (x, "Hello1234567");

to x,它会覆盖argcandargv但是,在从 回来时main(),没关系。专门Hello填充x1234填充argv567\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,它会覆盖返回地址的argcandargv 1 个字节,因此,当main()尝试返回 C 启动代码时,它会进入仙境:

+------------------------------------+ Overwrites with:
| C start-up code return address (4) |   '<NUL>'
| argc (4)                           |   '5678'
| argv (4)                           |   '1234'
| x = char[5] (5)                    |   'Hello'
+------------------------------------+

同样,这完全取决于编译器的调用约定。有可能不同的编译器总是将数组填充为 4 个字节的倍数,并且在您编写另外三个字符之前,代码不会在那里失败。即使是同一个编译器也可能在堆栈帧上分配不同的变量,以确保满足对齐要求。

这就是他们所说的未定义的意思:你不知道会发生什么。

于 2009-04-05T07:14:39.220 回答
7

您正在复制到堆栈,因此它取决于编译器在堆栈上放置的内容,以及需要多少额外数据才能使您的程序崩溃。

一些编译器可能生成的代码会在缓冲区大小上只有一个字节时崩溃——它的行为是未定义的。

我猜大小 13 足以覆盖返回地址或类似的东西,当你的函数返回时它会崩溃。但是另一个编译器或另一个平台可能/将会以不同的长度崩溃。

此外,如果您的程序运行较长时间,如果不那么重要的内容被覆盖,您的程序可能会以不同的长度崩溃。

于 2009-04-05T07:04:43.797 回答
5

对于 32 位 Intel 平台,解释如下。当您在堆栈上声明 char[5] 时,由于对齐,编译器实际上分配了 8 个字节。然后,函数通常具有以下序言:

push ebp
mov ebp, esp

这会将 ebp 注册表值保存在堆栈上,然后将 esp 寄存器值移动到 ebp 以使用 esp 值访问参数。这导致堆栈上的 4 个字节被 ebp 值占用。

在结语中,ebp 被恢复,但它的值通常仅用于访问堆栈分配的函数参数,因此在大多数情况下覆盖它可能不会造成伤害。

所以你有以下布局(堆栈在 Intel 上向下增长):8 字节用于数组,然后 4 字节用于 ebp,然后通常是返回地址。

这就是为什么您需要覆盖至少 13 个字节才能使程序崩溃的原因。

于 2009-04-05T08:00:31.277 回答
2

添加到上述答案:您可以使用Valgrind等工具测试此类错误。如果您使用的是 Windows,请查看此 SO thread

于 2009-04-05T07:24:55.310 回答
1

这取决于“str”数组之后的堆栈上的内容。在复制那么多字符之前,您只是碰巧不会践踏任何关键的东西。

所以这将取决于函数中的其他内容、您使用的编译器以及可能的编译器选项。

13 是 5 + 8,说明 str 数组后面有两个非关键字,然后是关键字(可能是返回地址)

于 2009-04-05T07:05:36.543 回答
1

这就是未定义行为 (UB) 的纯粹之美:它是未定义的。

你的代码:

char str[5];
strcpy(str,"Hello12345678");

写入 14 个字节/字符,str其中只能容纳 5 个字节/字符。这会调用 UB。

于 2009-04-05T07:05:56.103 回答
0

问:那么为什么它不会因为“Hello1234567”而崩溃,而只会因为“Hello12345678”而崩溃,即长度为 13 或大于 13 的字符串。

于 2009-04-05T07:42:18.327 回答
0

因为行为是未定义的。使用 strncpy。有关更多信息,请参阅此页面 http://en.wikipedia.org/wiki/Strcpy

strncpy 是不安全的,因为如果源字符串的长度 >= n,它不会添加 NULL 终止,其中 n 是目标字符串的大小。

char s[5];
strncpy(s,5,"test12345");
printf("%s",s); // crash

我们总是使用 strlcpy 来缓解这种情况。

于 2009-04-06T07:29:36.757 回答