4

我在处理 C 字符串时遇到了这种奇怪的行为。这是 K&R 书中的一个练习,我应该在其中编写一个函数,将一个字符串附加到另一个字符串的末尾。这显然需要目标字符串分配足够的内存,以便源字符串适合。这是代码:

 /* strcat: Copies contents of source at the end of dest */
 char *strcat(char *dest, const char* source) {
  char *d = dest;
  // Move to the end of dest
  while (*dest != '\0') {
    dest++;
  } // *dest is now '\0'

  while (*source != '\0') {
    *dest++ = *source++;
  }
  *dest = '\0';
  return d;
}

在测试期间,我编写了以下内容,预计程序运行时会发生段错误:

int main() {
  char s1[] = "hello";
  char s2[] = "eheheheheheh"; 
  printf("%s\n", strcat(s1, s2));
}

据我了解, s1 分配了 6chars的数组, s2 分配了 13 的数组chars。我认为当strcat尝试以高于 6 的索引写入 s1 时,程序会出现段错误。相反,一切正常,但程序并没有干净地退出,而是:

helloeheheheheheh
zsh: abort      ./a.out

并以代码 134 退出,我认为这意味着中止。

为什么我没有收到段错误(或者如果字符串分配在堆栈上,则覆盖 s2)?这些字符串在内存中的什么位置(堆栈或堆)?

谢谢你的帮助。

4

6 回答 6

7

我认为当 strcat 尝试写入s1高于6程序的索引时会出现段错误。

在堆栈上分配的内存范围之外写入是未定义的行为。调用这种未定义的行为通常(但不总是)会导致段错误。但是,您不能确定是否会发生段错误。

维基百科链接很好地解释了它:

当发生未定义行为的实例时,就语言规范而言,任何事情都可能发生,也许什么都不会发生。

因此,在这种情况下,您可能会遇到段错误,程序可能会中止,或者有时它可能运行良好。或者,任何东西。没有办法保证结果。

这些字符串在内存中的什么位置(堆栈或堆)?

由于您已将它们声明为char []inside main(),因此它们是具有自动存储的数组,这实际上意味着它们在堆栈上。

于 2013-08-01T01:12:10.390 回答
2

编辑1:

我将尝试解释您如何为自己找到答案。我不确定实际发生了什么,因为这不是定义的行为(正如其他人所说),但你可以做一些简单的调试来弄清楚你的编译器实际上在做什么。

原始答案

我的猜测是它们都在堆栈上。您可以通过修改代码来检查这一点:

int main() {
  char c1 = 'X';
  char s1[] = "hello";
  char s2[] = "eheheheheheh"; 
  char c2 = '3';

  printf("%s\n", strcat(s1, s2));
}

c1并且c2将在堆栈上。知道您也可以检查是否s1s2是。

如果地址c1小于s1且地址s1小于c2则它在堆栈上。否则它可能在您的.bss部分中(这将是明智的做法,但会破坏递归)。

我依靠堆栈上的字符串的原因是,如果您在函数中修改它们,并且该函数调用自身,那么第二次调用将没有自己的字符串副本,因此无效。 .. 但是,编译器仍然知道这个函数不是递归的,并且可以将字符串放入其中,.bss所以我可能是错的。

假设我的猜测它在堆栈上是正确的,在你的代码中

int main() {
  char s1[] = "hello";
  char s2[] = "eheheheheheh"; 
  printf("%s\n", strcat(s1, s2));
}

"hello"(带有空终止符)被压入堆栈,然后是"eheheheheheh"(带有空终止符)。

它们都一个接一个地定位(这要归功于您编写它们的顺序的运气)形成一个您可以写入的内存块(但不应该!)......这就是为什么没有段错误,您可以通过打破之前printf并查看地址来看到这一点。

s2 == (uintptr_t)s1 + (strlen(s1) + 1)如果我是对的,应该是真的。

修改你的代码

int main() {
  char s1[] = "hello";
  char c = '3';
  char s2[] = "eheheheheheh"; 
  printf("%s\n", strcat(s1, s2));
}

c如果我是对的,应该看到被覆盖...

但是,如果我错了并且它在该.bss部分中,那么它们仍然可能是相邻的,并且您将在没有段错误的情况下覆盖它们。

如果你真的想知道,拆解它:

不幸的是,我只知道如何在 Linux 上做到这一点。尝试使用nm <binary> > <text file>.txt命令或objdump -t <your_binary> > <text file>.sym命令转储程序中的所有符号。这些命令还应该为您提供每个符号所在的部分。

在文件中搜索s1s2符号,如果没有找到它们应该意味着它们在堆栈上,但我们将在下一步中检查。

使用objdump -S your_binary > text_file.S命令(确保使用调试符号构建二进制文件),然后.S在文本编辑器中打开文件。

再次搜索s1ands2符号,(希望没有其他符号,我怀疑不是,但我不确定)。

如果您发现它们的定义后跟一个pushorsub %esp命令,那么它们就在堆栈中。如果您不确定它们的定义是什么意思,请将其发回此处,让我们看看。

于 2013-08-01T01:33:13.900 回答
1

没有段错误甚至覆盖,因为它可以使用第二个字符串的内存并且仍然起作用。甚至给出正确答案。中止是程序意识到有问题的标志。尝试颠倒您声明字符串的顺序,然后重试。恐怕不会那么愉快。

于 2013-08-01T01:10:53.157 回答
0

以下是您的程序没有崩溃的原因:

您的字符串被声明为数组(s1[] 和 s2[])。所以他们在堆栈上。恰好 s2[] 的内存正好在 s1[] 之后。因此,当调用 strcat() 时,它所做的只是将 s2[] 中的每个字符向前移动一个字节。堆栈作为堆栈是可读写的。所以没有限制你在做什么。

但我相信编译器可以自由地将 s1[] 和 s2[] 定位到它认为合适的位置,所以这只是一个快乐的意外。

现在让你的程序崩溃相对容易

  1. 在你的调用中交换 s1 和 s2:代替 strcat(s1, s2),做 strcat(s2, s1)。这应该会导致堆栈粉碎异常。
  2. 将 s1[] 和 s2[] 更改为 *s1 和 *s2。当您写入只读段时,这应该会导致段错误。
于 2013-08-01T03:06:54.053 回答
0
int main() {
  char s1[] = "hello";
  char s2[] = "eheheheheheh"; 
  printf("%s\n", strcat(s1, s2));
}

改为使用:

   int main() {
  char s1[20] = "hello";
  char s2[] = "eheheheheheh"; 
  printf("%s\n", strcat(s1, s2));
}
于 2013-08-01T01:06:01.800 回答
0

嗯..字符串都在堆栈中,因为堆仅用于动态分配内存和东西..

segfault 用于无效的内存访问,但是使用此数组,您只是在为数组编写超出范围(超出范围)的内容,因此在编写时我认为您不会遇到问题....因为在 C 中它实际上留给程序员以确保将事物保持在数组的范围内。

同样在阅读时,如果您使用指针 - 我认为也不会有问题,因为您可以继续阅读直到您想要的任何地方并使用先前长度的总和。但是,如果您使用 string.h 中提到的函数,它们会根据空字符“\0”的存在来决定在哪里停止操作——因此我认为您的函数有效!

但是终止也可能表明任何其他变量/可能已经存在于字符串位置旁边的东西可能已经被 char 值覆盖了......访问这些可能导致程序退出!

希望这会有所帮助....顺便问一下好问题!

于 2013-08-01T04:59:44.237 回答