问题描述
我已经声明了两个相同大小的字符字符串(str1 和 str2)。之后,我通过gets() 函数读取一个字符串并将其存储在str1 上,然后将str1 复制到str2。当它们显示时,我意识到 str2 可以存储比其大小更多的字符?
这是我的代码:
#include<stdio.h>
#include<string.h>
void main()
{
char str1[20],str2[20];
printf("Enter the first string:");
gets(str1);
strcpy(str2,str1);
printf("First string is:%s\tSecond string is:%s\n",str1,str2);
}
这里的输出:
Enter the first string: Why can str2 store more characters than str1?
First string is:ore characters than str1? Second string is:Why can str2 store more characters than str1?
先谢谢大家
解决方法
查看带有注释的更新代码将确保某些内容实际存储在 str1
中并且内容不会溢出
#include <stdio.h>
#include <string.h>
// For EXIT_...
#include <stdlib.h>
int main() // Should be returning int
{
char str1[20],str2[20];
printf("Enter the first string:");
// Incorrect - see manual page - scanf(str1);
if (scanf("%19s",str1) == 1) { // Please read the manual page - this prevents buffer over runs and checks that something is stored in str1
strcpy(str2,str1);
printf("First string is:%s\tSecond string is:%s\n",str1,str2);
return EXIT_SUCCESS;
} else {
fprintf("Unable to read string\n");
return EXIT_FAILURE;
}
}
,
首先,正如评论部分已经指出的,您永远不应该在现代 C 代码中使用 gets
。那个函数is so dangerous that it has been removed from the ISO C standard。更安全的替代方法是 fgets
。
当您使用 str2
格式说明符打印 %s
时,printf
不会只打印 str2
数组的内容。它将打印它在内存中找到的所有内容,直到找到一个空终止字符。
由于数组 str2
不包含这样的空字符,它将继续打印它在内存中找到的所有内容,越过 str2
的边界,直到找到一个空字符(除非它事先崩溃)。由于您之前似乎已将字符串写入超过 str2
的边界(这是缓冲区溢出),因此它将打印该字符串,除非内存同时被其他内容覆盖。
我意识到 str2 可以存储比其大小更多的字符?
没有。发生的情况是超出一个数组的末尾写入了多余的字符,并且覆盖了另一个数组(或其他对象)的内容。 C 不强制要求对数组访问进行边界检查——如果你写到数组的末尾,你不会得到“IndexOutOfBounds”异常或类似的东西。
根据您的输出,以下是发生的情况 - str2
被分配到比 str1
低的地址,如下所示(地址值仅用于说明):
+---+
0x1000 str2: | | str2[0]
+---+
0x1001 | | str2[1]
+---+
0x1002 | | str2[2]
+---+
...
+---+
0x1013 | | str2[19]
+---+
0x1014 str1: | | str1[0]
+---+
0x1015 | | str1[1]
+---+
0x1016 | | str1[2]
+---+
...
+---+
0x1027 | | str1[19]
+---+
所以你做的第一件事是
gets( str1 );
并输入字符串 "Why can str2 store more characters than str1?"
,它是 45 个字符长。不幸的是,gets
只接收缓冲区的起始地址——它无法知道缓冲区的长度。因此,它很高兴地将字符串的 "ore characters than str1?"
部分紧跟在 str1
的末尾之后存储到内存中:
+---+
0x1000 str2: | | str2[0]
+---+
0x1001 | | str2[1]
+---+
0x1002 | | str2[2]
+---+
...
+---+
0x1013 | | str2[19]
+---+
0x1014 str1: |'W'| str1[0]
+---+
0x1015 |'h'| str1[1]
+---+
0x1016 |'y'| str1[2]
+---+
...
+---+
0x1027 |'m'| str1[19]
+---+
0x1028 |'o'| ???
+---+
0x1029 |'r'| ???
+---+
0x102a |'e'| ???
+---+
...
+---+
0x103f |'1'| ???
+---+
0x1040 |'?'| ???
+---+
0x1041 | 0 | ???
+---+
gets
还会写入一个 0 终止符来标记字符串的结尾。
接下来您要做的是调用 strcpy
将 str1
的内容复制到 str2
。与 gets
一样,strcpy
只获取源缓冲区和目标缓冲区的起始地址——它不知道任何一个缓冲区的长度。它依赖于源字符串中 0 终止符的存在来告诉它何时停止复制。因此,str1
的前 20 个字符被复制到 str2
,其余字符“溢出”回 str1
,覆盖原来的内容。在 strcpy
调用后,您会得到以下信息:
+---+
0x1000 str2: |'W'| str2[0]
+---+
0x1001 |'h'| str2[1]
+---+
0x1002 |'y'| str2[2]
+---+
...
+---+
0x1013 |' '| str2[19]
+---+
0x1014 str1: |'m'| str1[0]
+---+
0x1015 |'o'| str1[1]
+---+
0x1016 |'r'| str1[2]
+---+
0x1017 |'e'| str1[3]
+---+
...
+---+
0x1027 |' '| str1[19]
+---+
0x1028 |'s'| ???
+---+
0x1029 |'t'| ???
+---+
0x102a |'r'| ???
+---+
0x102b |'1'| ???
+---+
0x102c |'?'| ???
+---+
0x102d | 0 | ???
+---+
...
+---+
0x103f |'1'| ???
+---+
0x1040 |'?'| ???
+---+
0x1041 | 0 | ???
+---+
读取或写入数组末尾的行为未定义 - 语言标准不要求编译器或运行时环境以任何特定方式处理这种情况。一个实现可能在数组访问中添加边界检查代码,但我不知道有任何这样做。 只要您不覆盖任何“重要”的内容或尝试访问受保护的内存,您的代码就会看起来正常运行。但是,看似正常运行与实际正常运行并不相同。实际上,您正在破坏程序中的其他对象。您还可以覆盖堆栈帧的重要部分,这就是为什么像这样的缓冲区溢出是常见的恶意软件利用。
具体问题:
-
NEVER NEVER NEVER 出于任何原因使用
gets
- 它会在您的代码中引入一个故障点,如上所示。它在 C99 标准之后被弃用,并从 2011 标准的标准库中删除。改用fgets
:if ( fgets(str1,sizeof str1,stdin) ) { // do stuff with str1 }
-
main
的标准签名是-
int main( void )
-
int main( int argc,char **argv ) // or equivalent
void main()
列为有效签名,否则请使用上述两个之一(在您的情况下,第一个是合适的)。 -
您也可以使用 strncpy,它提供一个长度参数作为第三个参数。 这有助于避免写入越界。示例:
strncpy (str2,(size_t) 20); //fixed size 20