调试时必需的栈知识
栈(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'