考虑下面的程序
char str[5]; strcpy(str,"Hello12345678"); printf("%s",str);
运行此程序时会出现分段错误.
但是当strcpy被替换为以下时,程序运行正常.
strcpy(str,"Hello1234567");
所以问题是当尝试复制到str超过5个字符长度的任何其他字符串时它应该崩溃.
那么为什么它不会因为"Hello1234567"崩溃而只会崩溃为"Hello12345678",即长度为13或超过13的字符串.
该程序在32位机器上运行.
您应该感兴趣的标准行为有三种类型.
1/定义的行为.这将适用于所有符合要求的实现.自由使用.
2/实现定义的行为.如上所述,它取决于实施,但至少它仍然定义.需要实现来记录它们在这些情况下的作用.如果您不关心可移植性,请使用此选项.
3/未定义的行为.任何事情都可能发生.我们的意思是任何事情,包括你的整个计算机崩溃成一个赤裸裸的奇点并吞噬自己,你和你的大部分同事.永远不要使用它.永远!认真!不要让我过来.
将超过4个字符和零字节复制到a char[5]
是未定义的行为.
说真的,为什么你的程序崩溃了14个字符但不是13个,你几乎肯定会覆盖堆栈上的一些非崩溃信息,你的程序很可能会产生不正确的结果.事实上,崩溃更好,因为至少它会阻止你依赖可能不好的影响.
将数组的大小增加到更合适的位置(char[14]
在这种情况下使用可用信息)或使用其他可以应对的数据结构.
更新:
因为你似乎非常关心找出为什么额外的7个字符不会导致问题但是8个字符的问题,让我们设想一下输入时可能的堆栈布局main()
.我说"可能",因为实际布局取决于编译器使用的调用约定.由于C启动代码main()
使用argc
和调用,在为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");
对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' | 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) | '' | argc (4) | '5678' | argv (4) | '1234' | x = char[5] (5) | 'Hello' +------------------------------------+
同样,这完全取决于编译器的调用约定.有可能一个不同的编译器总是将数组填充到4个字节的倍数,并且代码不会在那里失败,直到你写了另外三个字符.即使是相同的编译器也可以不同地在堆栈帧上分配变量以确保满足对齐.
这就是未定义的意思:你不知道会发生什么.
您正在复制到堆栈,因此它取决于编译器放置在堆栈上的内容,以及需要多少额外数据才能使程序崩溃.
有些编译器可能会产生的代码只会在缓冲区大小上只有一个字节崩溃 - 它的行为是未定义的.
我猜大小13足以覆盖返回地址或类似的东西,当你的函数返回时它会崩溃.但是另一个编译器或其他平台可能/将以不同的长度崩溃.
如果程序运行时间过长,如果不重要的东西被覆盖,那么你的程序可能会以不同的长度崩溃.
对于32位Intel平台,解释如下.当你在堆栈上声明char [5]时,由于对齐,编译器确实分配了8个字节.然后,函数通常具有以下序言:
push ebp mov ebp, esp
这会将ebp注册表值保存在堆栈上,然后将esp寄存器值移动到ebp中,以便使用esp值来访问参数.这导致堆栈上的4个字节被ebp值占用.
在结语中,ebp被恢复,但其值通常仅用于访问堆栈分配的函数参数,因此在大多数情况下覆盖它可能不会受到伤害.
所以你有以下布局(堆栈在英特尔上向下增长):你的阵列有8个字节,然后是ebp的4个字节,那么通常是返回地址.
这就是为什么你需要覆盖至少13个字节来崩溃你的程序.