什么是栈溢出(C语言)?

17

代码:

int str_join(char *a,  const char *b) {
   int sz =0; 
   while(*a++) sz++;  
   char *st = a -1, c;  
   *st = (char) 32;
   while((c = *b++)) *++st = c;  
   *++st = 0;
   return sz;
}

....

char a[] = "StringA"; 
printf("string-1 length = %d, String a = %s\n", str_join(&a[0],"StringB"), a);

输出:

string-1长度为7,char *a = StringA StringB

*** stack smashing detected **** :/T02终止

Aborted (core dumped)

我不理解为什么会显示“stack smashing”?什么是*stack smashing? 还是我的编译器出错了吗?


2
可能是[Stack smashing detected]的重复问题(https://dev59.com/CnM_5IYBdhLWcg3wgzhl)。 - piyushj
3个回答

14

嗯,堆栈溢出或堆栈缓冲区溢出是一个相当详细的主题,在这里讨论,您可以参考此Wiki文章了解更多信息。

针对此处显示的代码,问题是您的数组a不足以容纳最终的连接结果。

因此,请说:

 while((c = *b++)) *++st = c;

你实际上正在访问超出边界的内存,这会引发 未定义行为。这就是你遇到“栈溢出”问题的原因,因为你试图访问不属于你进程的内存。

要解决这个问题,你需要确保数组a包含足够的空间来容纳第一个和第二个字符串连接在一起。简而言之,你需要提供一个更大的目标数组。


11
堆栈溢出意味着您已经在函数的本地变量存储空间之外写入("破坏"过/穿过)(在大多数系统和编程语言中,该区域称为“堆栈”)。 您还可能发现此类错误称为“堆栈溢出”和/或“堆栈下溢”。
在您的代码中,C 可能会将由 a 指向的字符串放在堆栈上。 在您的情况下,导致堆栈“破坏”的位置是当您将 st 递增超出原始 a 指针并写入其指向的位置时,您正在写入 C 编译器保证为分配给 a 的原始字符串保留的区域之外。
每当您在 C 中写入已经正确“保留”的内存区域之外时,这就是“未定义行为”(这只是意味着 C 语言/标准没有说明会发生什么):通常,您会覆盖程序内存中的其他信息(程序通常将其他信息紧挨着堆栈上的变量放置,例如返回地址和其他内部详细信息),或者您的程序尝试写入操作系统“允许”其使用的内存之外。无论哪种方式,程序通常都会崩溃,有时是立即明显的(例如,出现“分段错误”错误),有时则是非常隐蔽的,直到很久之后才变得明显。
在这种情况下,您的编译器正在使用特殊保护构建您的程序以检测此问题,因此您的程序会带有错误消息退出。如果编译器没有这样做,您的程序将尝试继续运行,但它可能会做错事和/或崩溃。
解决方案归结为需要显式告诉代码具有足够的内存来存储组合字符串。您可以通过显式指定“a”数组的长度足够长以容纳两个字符串之一来实现此目的,但通常仅适用于您预先知道需要多少空间的简单用途。对于通用解决方案,您可以使用类似 malloc 的函数从操作系统获取指向新内存块的指针,该内存块具有您需要/想要的大小,一旦计算出完整大小(只需记住在完成后调用 free 以释放从 malloc 和类似函数获取的指针)。

2

使用反汇编分析的最小复现示例

main.c

void myfunc(char *const src, int len) {
    int i;
    for (i = 0; i < len; ++i) {
        src[i] = 42;
    }
}

int main(void) {
    char arr[] = {'a', 'b', 'c', 'd'};
    int len = sizeof(arr);
    myfunc(arr, len + 1);
    return 0;
}

GitHub 上游

编译并运行:

gcc -fstack-protector-all -g -O0 -std=c99 main.c
ulimit -c unlimited && rm -f core
./a.out

执行失败:

*** stack smashing detected ***: terminated
Aborted (core dumped)

在Ubuntu 20.04,GCC 10.2.0上进行了测试。

在Ubuntu 16.04,GCC 6.4.0上,我可以使用-fstack-protector代替-fstack-protector-all来复现此问题。但是,当我根据Geng Jiawen的评论在GCC 10.2.0上进行测试时,它停止了崩溃。 man gcc澄清了,正如选项名称所建议的那样,-all版本会更积极地添加检查,因此可能会导致更大的性能损失:

-fstack-protector

发出额外的代码以检查缓冲区溢出,例如堆栈破坏攻击。这是通过向具有易受攻击对象的函数添加保护变量来完成的。这包括调用“alloca”的函数和具有大于或等于8个字节的缓冲区的函数。当进入函数时初始化保护变量,然后在退出函数时进行检查。如果保护检查失败,则打印错误消息并退出程序。只考虑实际在堆栈上分配的变量,优化掉的变量或在寄存器中分配的变量不计入。

-fstack-protector-all

与-fstack-protector类似,但保护所有函数。

反汇编

现在我们来看反汇编:

objdump -D a.out

其中包含:

int main (void){
  400579:       55                      push   %rbp
  40057a:       48 89 e5                mov    %rsp,%rbp

  # Allocate 0x10 of stack space.
  40057d:       48 83 ec 10             sub    $0x10,%rsp

  # Put the 8 byte canary from %fs:0x28 to -0x8(%rbp),
  # which is right at the bottom of the stack.
  400581:       64 48 8b 04 25 28 00    mov    %fs:0x28,%rax
  400588:       00 00 
  40058a:       48 89 45 f8             mov    %rax,-0x8(%rbp)

  40058e:       31 c0                   xor    %eax,%eax
    char arr[] = {'a', 'b', 'c', 'd'};
  400590:       c6 45 f4 61             movb   $0x61,-0xc(%rbp)
  400594:       c6 45 f5 62             movb   $0x62,-0xb(%rbp)
  400598:       c6 45 f6 63             movb   $0x63,-0xa(%rbp)
  40059c:       c6 45 f7 64             movb   $0x64,-0x9(%rbp)
    int len = sizeof(arr);
  4005a0:       c7 45 f0 04 00 00 00    movl   $0x4,-0x10(%rbp)
    myfunc(arr, len + 1);
  4005a7:       8b 45 f0                mov    -0x10(%rbp),%eax
  4005aa:       8d 50 01                lea    0x1(%rax),%edx
  4005ad:       48 8d 45 f4             lea    -0xc(%rbp),%rax
  4005b1:       89 d6                   mov    %edx,%esi
  4005b3:       48 89 c7                mov    %rax,%rdi
  4005b6:       e8 8b ff ff ff          callq  400546 <myfunc>
    return 0;
  4005bb:       b8 00 00 00 00          mov    $0x0,%eax
}

  # Check that the canary at -0x8(%rbp) hasn't changed after calling myfunc.
  # If it has, jump to the failure point __stack_chk_fail.
  4005c0:       48 8b 4d f8             mov    -0x8(%rbp),%rcx
  4005c4:       64 48 33 0c 25 28 00    xor    %fs:0x28,%rcx
  4005cb:       00 00 
  4005cd:       74 05                   je     4005d4 <main+0x5b>
  4005cf:       e8 4c fe ff ff          callq  400420 <__stack_chk_fail@plt>

  # Otherwise, exit normally.
  4005d4:       c9                      leaveq 
  4005d5:       c3                      retq   
  4005d6:       66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)
  4005dd:       00 00 00 

请注意,objdump人工智能模块自动添加了有用的注释。
如果你通过GDB多次运行此程序,你会发现:
- 栈末尾警卫每次都有不同的随机值。 - myfunc的最后一个循环正是修改栈末尾警卫地址的部分。
栈末尾警卫通过设置%fs:0x28来实现随机化,其中包含一个随机值,详见以下链接:
- https://unix.stackexchange.com/questions/453749/what-sets-fs0x28-stack-canary - Why does this memory address %fs:0x28 ( fs[0x28] ) have a random value? 如何调试? 请参见:Stack smashing detected

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接