linux数据结构栈,Linux中踩栈浅析

调试时必需的栈知识

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

#include

#include

#include

#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

#include

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 ***: 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 , argc=1, argv=0x7ffcffb3a0e8,

init=0x6566667562206120, fini=, rtld_fini=, 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.

address.png

由于是小端方式,这里选中的地址时6566667562206120,选中的地址前面的地址是6c667265766f2072

可是地址0x0000555555554667这个画风突变,地址突然变化,反汇编看下这个地址是什么?

(gdb) disassemble func

Dump of assembler code for function func:

0x000055555555464a : push %rbp

0x000055555555464b : mov %rsp,%rbp

0x000055555555464e : sub $0x10,%rsp

0x0000555555554652 : lea -0x5(%rbp),%rax

0x0000555555554656 : lea 0x2009c3(%rip),%rsi # 0x555555755020

0x000055555555465d : mov %rax,%rdi

0x0000555555554660 : callq 0x555555554520

0x0000555555554665 : nop

0x0000555555554666 : leaveq

=> 0x0000555555554667 : retq

End of assembler dump.

(gdb) p (char *)0x0000555555554667

$8 = 0x555555554667 "\303UH\211",

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

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

(gdb) p szInfo

$64 = "Oops! here is a buffer overflow, wcdj"

(gdb) p &szInfo

$65 = (char (*)[38]) 0x555555755020

(gdb) p/c 0x555555554661

$66 = 97 'a'

(gdb) p/c 0x55555555467a

$67 = 122 'z'

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值