为什么 scanf() 在填充数组时不生成内存破坏错误。

Why doesn't scanf() generate memory clobbering errors when filling array.

给定一个包含 5 个元素的数组,众所周知,如果您使用 scanf() 恰好读入 5 个元素,则 scanf() 将填充该数组,然后通过放置一个空字符 '\0 来破坏内存' 进入第 6 个元素而不产生错误(我称它为第 6 个元素,但我知道它的内存不是数组的一部分)如此处所述:Null termination of char array

然而,当您尝试读入 6 个或更多元素时,会生成一个错误,因为 OS 检测到内存正在被破坏并且内核会发送一个信号。有人可以澄清为什么在上面的第一种内存破坏情况下没有产生错误吗?

示例代码:

// ex1.c
#include <stdio.h>
int main(void){
  char arr[5];
  scanf("%s", arr);
  printf("%s\n", arr);
  return 0;
}

编译,运行 并输入四个字符:1234。这会将它们正确地存储在数组中并且不会破坏内存。这里没有错误。

$ ./ex1
1234
1234

运行 再输入五个字符。这会破坏内存,因为 scanf() 在第 5 个元素之后在内存中存储了一个额外的 '\0' 空字符。没有产生错误。

$ ./ex1
12345
12345

现在输入我们希望破坏内存的六个字符。生成的错误看起来像(即我猜)它是内核发送的信号的结果,说我们刚刚以某种方式破坏了堆栈(本地内存)......为什么会为此内存破坏生成错误但是不是上面那个吗?

$ ./ex1
123456
123456
*** stack smashing detected ***: ./ex1 terminated
Aborted (core dumped)

无论我制作多大的数组,这似乎都会发生。

.Why is an error being generated for this memory clobbering but not for the previous one above?

因为第一次测试似乎只是因为运气不好。

在这两种情况下,arr 都被越界访问,因此代码调用了未定义的行为。这意味着代码可能会执行您期望的或不执行的操作,例如启动机器、格式化磁盘...

C 不测试内存访问,而是将其留给程序员。谁可以通过以下方式调用 scanf() 保存:

char arr[5];
scanf("%4s", arr); /* Stop scanning after 4th character. */

Stack Smashing这里其实是编译器检测缓冲区溢出的保护机制造成的errors.The编译器添加了已知值的保护变量(称为金丝雀)

在您的情况下,当输入字符串的大小大于 5 时会导致此变量损坏,从而导致 SIGABRT 终止程序。

您可以阅读更多关于 buffer overflow protection. But as @alk answered you are invoking Undefined Behavior

如果在这两种情况下您输入的字符多于缓冲区可以容纳的字符数,则行为是 undefined

堆栈粉碎检测机制通过使用canaries工作。当 canary 值被覆盖时,会生成 SIGABRT。它没有生成的原因可能是因为数组后至少有一个额外的内存字节(通常对象的末尾后一个是有效指针。但它不能使用存储到值——合法)。 本质上,当您输入 1 个额外的字符时,金丝雀不会被覆盖,但是当您出于某种原因输入 2 个字节时,它确实会被覆盖,从而触发 SIGABRT。

如果你在arr之后还有一些其他的变量比如:

#include <stdio.h>
int main(void){
  char arr[5];
  char var[128];
  scanf("%s", arr);
  printf("%s\n", arr);
  return 0;
}

然后,当您输入更多字节时,金丝雀可能不会被覆盖,因为它可能只是覆盖 var。从而延长了编译器对缓冲区溢出的检测。这是一个似是而非的解释。但是无论如何,如果你的程序溢出缓冲区,你的程序就是无效,你不应该依靠编译器的堆栈粉碎检测来拯救你。

实际上 如果我们声明一个大小为 5 的数组,那么我们也可以从这个数组中放置和访问数据,因为这个数组之外的内存是空的,我们可以做同样的事情直到这个内存空闲但是一旦它分配给另一个程序现在我们也是无法访问那里的数据