调试时必需的栈知识

栈(stack)是程序存放数据的内存区域之一,其特征是LIFO(Last In First Out, 后进先出)式数据结构,即后放进的数据最先被取出。向栈中存储数据的操作称为PUSH(压入),从栈中取出数据称为POP(弹出)。在保存动态分配的自动变量时要使用栈。此外在函数调用时,栈还用于传递函数参数,以及用于保存返回地址和返回值。

 #include <stdio.h>
 #include <ctype.h>
 #include <stdlib.h>
 #define MAX (1UL << 20)
 
 typedef unsigned long long u64;
 typedef unsigned int u32;
 
 u32 max_addend = MAX;
 
 u64 sum_till_MAX(u32 n)
 {
     u64 sum;
     n++;
     sum = n;
 
     if (n < max_addend)
         sum += sum_till_MAX(n);
 
     return sum;
 }
 
 int main(int argc, char** argv)
 {
     u64 sum = 0;
 
     if ((argc == 2) && isdigit(*(argv[1])))
         max_addend = strtoul(argv[1], NULL, 0);
     
     if (max_addend > MAX || max_addend == 0) {
         fprintf(stderr, "Invalid number is specified\n");
         return 1;
     }
 
     sum = sum_till_MAX(0);
     printf("sum(0..%lu) = %llu\n", max_addend, sum);
 
     return 0;                                                          
 }

正常运行输入参数10,会计算1-10的和,如果不指定参数栈溢出导致Segmentation fault.

[root c++]#gcc -g -Wall -Werror -o sum sum.c
[root c++]#./sum 10
sum(0..10) = 55
[root c++]#
[root c++]#./sum
Segmentation fault (core dumped)

函数调用和栈的关系 —— 函数调用前后栈的变化情况

栈上依次保存了传给函数的参数、调用者的返回地址、上层栈帧指针和函数内部使用的自动变量。此外,处理有些函数时还会用栈来临时保存寄存器。每个函数都独自拥有这些信息,称为栈帧(stack frame)(gdb中可以通过f n,切换到对应的函数,执行info frame查看栈帧)。此时需要适当地设置表示栈帧起始地址的帧指针(FP)。此外,栈指针(SP)永远指向栈的顶端。

数组非法访问导致内存破坏

错误地操作数组导致的典型bug之一就是缓冲区溢出,即向我们分配的内存空间之外写入数据。特别是,如果这类bug发生在栈上的缓冲区中,就可能引发安全漏洞,因此出现了许多预防措施和应对措施,如通过指定缓冲区大小来编写安全的函数、源代码检查工具、编译器在构建时的报警等。即便如此,这种bug仍时有发生。

下面通过一个简单的例子进行分析:

#include <stdio.h>
#include <string.h>
 
char szInfo[] = "Oops! here is a buffer overflow, wcdj";
 
void func()                                             
{
    char buf[5];
    strcpy(buf, names);
}
 
int main()
{
    func();
    return 0;
}
[root c++]#vi stack.c
[root c++]#gcc -g -o stack stack.c
[root c++]#./stack
*** stack smashing detected ***: <unknown> terminated
Aborted (core dumped)

关于运行后报错信息stack smashing detected的详细解释:stack smashing detected
使用gdb调试core:

Type "apropos word" to search for commands related to "word"...
Reading symbols from stack...done.
[New LWP 3966]
Core was generated by `./stack'.
Program terminated with signal SIGABRT, Aborted.
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
51  ../sysdeps/unix/sysv/linux/raise.c: No such file or directory.
(gdb) bt
#0  __GI_raise (sig=sig@entry=6) at ../sysdeps/unix/sysv/linux/raise.c:51
#1  0x00007f6d82d66801 in __GI_abort () at abort.c:79
#2  0x00007f6d82daf897 in __libc_message (action=action@entry=do_abort,
    fmt=fmt@entry=0x7f6d82edc988 "*** %s ***: %s terminated\n") at ../sysdeps/posix/libc_fatal.c:181
#3  0x00007f6d82e5acd1 in __GI___fortify_fail_abort (need_backtrace=need_backtrace@entry=false,
    msg=msg@entry=0x7f6d82edc966 "stack smashing detected") at fortify_fail.c:33
#4  0x00007f6d82e5ac92 in __stack_chk_fail () at stack_chk_fail.c:29
#5  0x000055f8c95666e9 in func () at stack.c:10
#6  0x6c667265766f2072 in ?? ()
#7  0x6a646377202c776f in ?? ()
#8  0x00007f6d82d47b00 in __libc_start_main (main=0x55f8c95666eb <main>, argc=1, argv=0x7ffcffb3a0e8,
    init=0x6566667562206120, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7ffcffb3a0d8)
    at ../csu/libc-start.c:262
#9  0x000055f8c95665ca in _start ()

backtrace无法正确显示,在这种情况下backtrace是不可靠的。因为backtrace显示的内容很可能不是实际跟踪的内容。
在日常开发中如何定位和解决上面的问题呢?
添加编译选项 -fno-stack-protector后,重新编译二进制,然后调试运行产生的core

Type "apropos word" to search for commands related to "word"...
Reading symbols from stack...done.
/root/workspace/c++/core_stack_6979: No such file or directory.
(gdb) r
Starting program: /root/workspace/c++/stack

Program received signal SIGSEGV, Segmentation fault.
0x0000555555554667 in func () at stack.c:10
10  }
(gdb) bt
#0  0x0000555555554667 in func () at stack.c:10
#1  0x6566667562206120 in ?? ()
#2  0x6c667265766f2072 in ?? ()
#3  0x6a646377202c776f in ?? ()
#4  0x0000000000000000 in ?? ()
(gdb)

寻找错误地写入0x0000555555554667这个数据的地方。很重要的是需要怀疑数据是否为字符串的一部分,因为错误地将数据写入地址的典型情况之一就是字符串复制。由于字符串的输入长度很难预测,若缓冲区过小,再加上对输入字符串的长度检查不完善,就可能发生这种状况。

有时确定在哪里写入的错误数值不好确定,还可以借用objdump工具。

[root c++]#objdump -s  stack

stack:     file format elf64-x86-64

Contents of section .interp:
 0238 2f6c6962 36342f6c 642d6c69 6e75782d  /lib64/ld-linux-
 0248 7838362d 36342e73 6f2e3200           x86-64.so.2.
Contents of section .note.ABI-tag:
 0254 04000000 10000000 01000000 474e5500  ............GNU.
 0264 00000000 03000000 02000000 00000000  ................
Contents of section .note.gnu.build-id:
 0274 04000000 14000000 03000000 474e5500  ............GNU.
 0284 1442a4a1 a8933ae1 b7d9d38a 85a76dcb  .B....:.......m.
 0294 968ea179                                                     ...y                       
Contents of section .data:
 201000 00000000 00000000 08102000 00000000  .......... .....
 201010 00000000 00000000 00000000 00000000  ................
 201020 4f6f7073 21206865 72652069 73206120  Oops! here is a
 201030 62756666 6572206f 76657266 6c6f772c  buffer overflow,
 201040 20776364 6a00                         wcdj.
Contents of section .comment:
 0000 4743433a 20285562 756e7475 20372e35  GCC: (Ubuntu 7.5
 0010 2e302d33 7562756e 7475317e 31382e30  .0-3ubuntu1~18.0
 0020 34292037 2e352e30 00                 4) 7.5.0.

由于是小端方式,这里选中的地址时6566667562206120,选中的地址前面的地址是6c667265766f2072
可是地址0x0000555555554667这个画风突变,地址突然变化,反汇编看下这个地址是什么?

(gdb) disassemble func
Dump of assembler code for function func:
   0x000055555555464a <+0>: push   %rbp
   0x000055555555464b <+1>: mov    %rsp,%rbp
   0x000055555555464e <+4>: sub    $0x10,%rsp
   0x0000555555554652 <+8>: lea    -0x5(%rbp),%rax
   0x0000555555554656 <+12>:    lea    0x2009c3(%rip),%rsi        # 0x555555755020 <szInfo>
   0x000055555555465d <+19>:    mov    %rax,%rdi
   0x0000555555554660 <+22>:    callq  0x555555554520 <strcpy@plt>
   0x0000555555554665 <+27>:    nop
   0x0000555555554666 <+28>:    leaveq
=> 0x0000555555554667 <+29>:    retq
End of assembler dump.
(gdb) p (char *)0x0000555555554667
$8 = 0x555555554667 <func+29> "\303UH\211", <incomplete sequence \345\270>

通过objdump的分析结果可以看出,将全局字符串写入到了函数的返回地址中,导致函数返回信息异常,程序崩溃。

可以看到这个地址retq处是函数返回值,我们分析0x0000555555554667这里的值

(gdb) p szInfo
$64 = "Oops! here is a buffer overflow, wcdj"
(gdb) p &szInfo
$65 = (char (*)[38]) 0x555555755020 <szInfo>
(gdb) p/c  0x555555554661
$66 = 97 'a'
(gdb) p/c  0x55555555467a
$67 = 122 'z'