通过调用栈排查段错误

   遇到过一个问题,一个函数的调用,会概率性的无法返回,出现段错误,从现象来看肯定是内存被踩,但当时是通过逐行检查代码来定位的。后面从新翻出这个问题,尝试通过调用栈来确认被踩内存的具体位置。下面用简单例子来记录这个过程。

1原理

首先放一张调用栈的结构(数据存放的位置有出入,仅作参考)
在这里插入图片描述
有两个重要的指针,EBP(Base Pointor)、ESP(Stack Pointor)
ESP: 调用栈是自高地址往低地址生长的,即最外层函数栈在高地址,每调用深一层函数,新的函数调用栈就会往低地址生长,而ESP就会始终指向调用栈的最顶部(已使用的最低地址)
EBP: 指向最深一层函数栈的基地址,即从EBP开始到ESP结束是一帧完整的最深函数调用栈。其它调用栈则是保存在下一栈的内存中。栈弹出时,EBP的值直接给ESP,新的EBP则从当前栈获取,这样便形成上一个调用栈的范围,当前栈丢失。

返回地址: 假如函数A调用了函数B,那么在自己的调用栈中必须保存调用完B后的返回地址,即调用完B后的下一 条指令地址。如果这个地址丢失就永远回不去了,情况再恶劣些,如果这个地址被篡改为非法地址(null或者操作系统保护的地址),那么程序直接就会 coredump,并抛出segment fault
栈内容: 除了保存EBP、RET、ADDR这些构成函数栈必须元素外, 调用栈的大部分内存是用来存储函数传参以及局部变量 的,有时一些莫名其妙的问题,也可通过查看各个参数来定位。

2实例

编写一个调用过程demo

#include <stdio.h>
void B(int data1, int data2, int data3)
{
	int b_s1 =5;
	int b_s2 =5;
	int b_s3 =7;
	printf("b_s1 + 7  value: %x, addr: %p\n",*(&b_s1 +7), &b_s1+7);
	printf("b_s1 value: %d, addr: %p\n", b_s1 , &b_s1);
	printf("b_s2 value: %d, addr: %p\n", b_s2 , &b_s2);
	printf("b_s3 value: %d, addr: %p\n", b_s3 , &b_s3);
	printf("data1 value: %d, addr: %p\n", data1, &data1);
	printf("data2 value: %d, addr: %p\n", data2,&data2);
	printf("data3 value: %d, addr: %p\n", data3, &data3);
	print("B end\n");
	//*(&b_s1 +7) = null;
}
void A(void)
{
	int a_s1 = 1;
	printf("a_s1 value: %d, addr: %p\n", a_s1 , &a_s1);
	B(2, 3, 4);
	print("A end\n");
}
int main(void)
{
	A();
	print("main end\n");
	while(1);
}

首先使用GDB获取三个调用栈(main、A、B)
进入gdb调试

获取三层函数的调用栈范围main->A->B
首先在三个函数最后一句打印处加断点,然后依次执行

Breakpoint 3, B (data1=2, data2=3, data3=4) at main.c:19
19 printf (“B end\n”);

(gdb) x /x $ebp
0xffffffffffffd130: Cannot access memory at address 0xffffffffffffd130
(gdb) x /x $esp
0xffffffffffffd100: Cannot access memory at address 0xffffffffffffd100
(gdb) c
Continuing.
B end

Breakpoint 1, A () at main.c:30
30 printf (“A end\n”);
(gdb) x /x $ebp
0xffffffffffffd150: Cannot access memory at address 0xffffffffffffd150
(gdb) x /x $esp
0xffffffffffffd140: Cannot access memory at address 0xffffffffffffd140
(gdb) c
Continuing.
A end

Breakpoint 2, main (argc=1, argv=0x7fffffffd258 “?\326\377\377\377\177”)
at main.c:36
36 printf (“Main end\n”);
(gdb) x /x $ebp
0xffffffffffffd170: Cannot access memory at address 0xffffffffffffd170
(gdb) x /x $esp
0xffffffffffffd160: Cannot access memory at address 0xffffffffffffd160

然后打印出这一区域的内存:

(gdb) x /32x 0x7fffffffd100

B:
0x7fffffffd100: 0xffffd14c 0x00000004 0x00000003 0x00000002
0x7fffffffd110: 0x00400877 0x00000007 0x00000006 0x00000005
0x7fffffffd120: 0xf7dea600 0x00007fff 0x00000000 0x00000000
0x7fffffffd130: 0xffffd150 0x00007fff 0x004006bb 0x00000000

0x00000004 0x00000003 0x00000002:为B函数的调用实参
0x00000007 0x00000006 0x00000005:为B函数的局部变量
0x00007ffffd150:上一栈帧(A函数)的EBP
0x004006bb:为A函数调用完B函数后的返回地址(可反汇编确认)

A:
0x7fffffffd140: 0xffffd170 0x00007fff 0x00000000 0x00000001
0x7fffffffd150: 0xffffd170 0x00007fff 0x004006db 0x00000000

0x00000001:A函数的临时变量
0x00007fffffffd170:上一栈帧(main函数)的EBP
0x004006db:为main函数调用完A函数后的返回地址(可反汇编确认)

main:
0x7fffffffd160: 0xffffd258 0x00007fff 0x00000000 0x00000001
0x7fffffffd170: 0x00000000 0x00000000 0xf7a32f45 0x00007fff

0x7fffffffd130: 0xffffd150 0x00007fff **0x004006bb
*0x7fffffffd140: 0xffffd170 0x00007fff 0x00000000 0x00000001

A函数的临时变量在A调用栈的*0x7fffffffd14处
而A函数调用完B函数的返回地址位于 0x7fffffffd138处
假如程序中,使用A函数局部变量a_s1的地址做如下操作:
*(&a_s1 - 3) = null
那么A函数在调用完B函数后,使用0x7fffffffd138保存的返回值时,就会访问 null ptr 出现段错误

附demo执行结果:
a_s1 addr :0x7fffffffd14c
b_s1+7 value :4006bb addr: 0x7fffffffd138
b_s1 value :5 addr :0x7fffffffd11c
b_s2 value :6 addr :0x7fffffffd118
b_s3 value :7 addr :0x7fffffffd114
data1 value :2 addr: 0x7fffffffd10c
data2 value :3 addr: 0x7fffffffd108
data3 value :4 addr: 0x7fffffffd104

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值