《coredump问题原理探究》Linux x86版3.5节栈布局之-fomit-frame-pointer编译选项

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/details/8526140

上面探讨了没有使用-fomit-frame-pointer编译选项的程序的栈桢规律,那么如果一个程序是通过-fomit-frame-pointer编译选项来编译,它运行时的栈桢规律有没有可能不同呢?

先看一个例子:

int func( int num )
{
     int result = num*num;
     result += num - 1;
     return result;
}
 
void test(int beg, int end )
{
     int a[10];
     for ( int i = 0; i < 10; i++ )
     {
         a[i] = beg + func( i ) + end;
     }
}
 
int main()
{
     test( 5, 100);
     return 0;
}

看一下它们的汇编:

gdb) disassemble func(int) 
Dump of assembler code for function _Z4funci:
   0x08048470 <+0>:     sub    $0x10,%esp
   0x08048473 <+3>:     mov    0x14(%esp),%eax
   0x08048477 <+7>:     imul   0x14(%esp),%eax
   0x0804847c <+12>:    mov    %eax,0xc(%esp)
   0x08048480 <+16>:    mov    0x14(%esp),%eax
   0x08048484 <+20>:    sub    $0x1,%eax
   0x08048487 <+23>:    add    %eax,0xc(%esp)
   0x0804848b <+27>:    mov    0xc(%esp),%eax
   0x0804848f <+31>:    add    $0x10,%esp
   0x08048492 <+34>:    ret    
End of assembler dump.
(gdb) disassemble test
Dump of assembler code for function _Z4testii:
   0x08048493 <+0>:     sub    $0x34,%esp
   0x08048496 <+3>:     movl   $0x0,0x30(%esp)
   0x0804849e <+11>:    jmp    0x80484c5 <_Z4testii+50>
   0x080484a0 <+13>:    mov    0x30(%esp),%eax
   0x080484a4 <+17>:    mov    %eax,(%esp)
   0x080484a7 <+20>:    call   0x8048470 <_Z4funci>
   0x080484ac <+25>:    mov    0x38(%esp),%edx
   0x080484b0 <+29>:    add    %eax,%edx
   0x080484b2 <+31>:    mov    0x3c(%esp),%eax
   0x080484b6 <+35>:    add    %eax,%edx
   0x080484b8 <+37>:    mov    0x30(%esp),%eax
   0x080484bc <+41>:    mov    %edx,0x8(%esp,%eax,4)
   0x080484c0 <+45>:    addl   $0x1,0x30(%esp)
   0x080484c5 <+50>:    cmpl   $0x9,0x30(%esp)
   0x080484ca <+55>:    setle  %al
   0x080484cd <+58>:    test   %al,%al
   0x080484cf <+60>:    jne    0x80484a0 <_Z4testii+13>
   0x080484d1 <+62>:    add    $0x34,%esp
   0x080484d4 <+65>:    ret    
End of assembler dump.

可见,通过-fomit-frame-pointer编译选项编译出来的程序没有

push   %ebp
mov    %esp,%ebp

pop    %ebp
ret    

这些开头和结尾的特征指令。而是一进来函数,就立马

   0x08048470 <+0>:     sub    $0x10,%esp   	// func函数

   0x08048493 <+0>:     sub    $0x34,%esp	// test函数

来进行分配局部变量空间了,而在退出时则

   0x0804848f <+31>:    add    $0x10,%esp		// func函数
   0x08048492 <+34>:    ret    

   0x080484d1 <+62>:    add    $0x34,%esp	// test函数
   0x080484d4 <+65>:    ret    

来回收局部变量空间和退出函数。

也就是说,函数桢指针的单链表规律在这种情况就不适用了。那么,在这种情况下,栈布局又会有什么规律呢?

在探索这种情况下的规律,先用函数桢指针单链表规律来假设一下在没有用-fomit-frame-pointer编译选项编译出来的程序在执行test到func时候,栈的变化:

1.      在test运行前,栈顶指向着main函数的返回地址ret1

2.      test函数执行,把桢指针fp1压入栈,并设置新的桢指针

3.      分配局部变量空间,假设也是分配0x34个字节

4.      压入func函数的参数(由于test只调用一个函数func,所以,直接把参数归并到局部变量空间分配,没有用push指令)

5.      用call指令调用func,往栈里压入test的返回地址ret2

如果用addr(X)表示X所在的地址,从上面6步来看,addr(fp1) –addr( fp2 ) = addr(ret1) – addr( ret2 ) = 0x34 + 4 + 4,刚好是分配的局部变量空间大小与桢指针大小,返回地址ret2大小的和。也就是说,即使不用单链表规律,只要看一下ret2所在函数分配的局部变量空间大小,压入的参数大小,桢指针大小,返回地址ret2的大小,用addr(ret2)加上这个局部变量空间,参数,桢指针大小,返回地址的大小就应该是addr(ret1)了。

那么,同样的,在使用了-fomit-frame-pointer的情况下,上面5步,就会少掉第2步,也就是说,addr( ret1 ) - addr(ret2)= 0x34 + 4(返回地址大小)。即相邻两个返回地址ret1, ret2, ret1 > ret2, 假设ret2所在的函数分配的局部变量空间为var_size, 在压入ret2前,压入参数的大小为par_size,那么,addr( ret1 ) - addr(ret2 )= var_size +par_size + 4(返回地址大小)。

下面验证一下,在func函数打断点:

(gdb) tbreak func
Temporary breakpoint 1 at 0x8048470
(gdb) r
Starting program: /home/buckxu/work/3/4/xuzhina_dump_c3_s4 

Temporary breakpoint 1, 0x08048470 in func(int) ()
 (gdb) bt          
#0  0x08048470 in func(int) ()
#1  0x080484ac in test(int, int) ()
#2  0x080484ec in main ()
(gdb) x /32x $esp
0xbffff448:     0x080484ac      0x00000000      0x437c43c4      0x43647f36
0xbffff458:     0x080497b8      0x08048552      0x00000001      0xbffff524
0xbffff468:     0xbffff52c      0x43647f4d      0x437c43c4      0x43cb75ec
0xbffff478:     0x0804850b      0x00000000      0x080484ec      0x00000005
0xbffff488:     0x00000064      0x4362f635      0x00000001      0xbffff524
0xbffff498:     0xbffff52c      0xb7ffef18      0x00000001      0x00000001
0xbffff4a8:     0x00000000      0x00000008      0x00000002      0x437c3ff4
0xbffff4b8:     0x00000000      0x00000000      0x00000000      0xdd2af16c

从上面可以看到,0x080484ac的地址是0xbffff448,而0x080484ec的地址是0xbffff480。0xbffff480-0xbffff448=0x38 = 0x34 + 4(返回地址大小)。和上面推断一致。

在-fomit-frame-pointer编译选项生成的程序里,栈布局有这样的规律:

两个相邻的返回地址ret1,ret2,其中ret1属于函数func1,ret2属于函数func2,且func1调用func2。当func2调用func3时,ret2被压入栈。其中func2的局部变量空间大小为var_size,func3压入栈中的参数大小为par_size,那么它们会满足下面的条件:

1. addr(ret1)-addr(ret2)= var_size + par_size + 4

2. info symbol ret1, info symbol ret2都能够显示出func1, func2

PS:这个规律是更加通用的,不止在x86平台下,在sparc,mips,arm都是这样。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏编程思想之异常处理

Java编程思想之通过异常处理错误

1.     异常分为被检查的异常和运行时异常,被检查的异常在编译时被强制要求检查。异常被用来错误报告和错误恢复,但很大一部分都是用作错误报告的。

7610
来自专栏老司机的技术博客

宝宝都能学会的python编程教程8:条件判断与循环

先公布上期编程练习的答案,没错,L是一个指向三个列表的二维元祖。 条件判断 实际的项目中条件判断可以说是使用最多的语法之一了,不管是最简单的判断还是负责的业务逻...

35750
来自专栏玄魂工作室

Python黑帽编程2.4 流程控制

Python黑帽编程2.4 流程控制 本节要介绍的是Python编程中和流程控制有关的关键字和相关内容。 2.4.1 IF …..ELSE 先上一段代码: ...

28840
来自专栏用户2442861的专栏

C++ STL源码实现以及分析之vector

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/haluoluo211/article/d...

46510
来自专栏增长技术

Swift体验2

使用if和switch做条件判断,使用for-in,for,while,do-while做循环 操作。括号中的条件或循环变量是可选的。括号的身体是必需的。

12930
来自专栏iOS开发随笔

iOS Swift基础语法(二)

11830
来自专栏从流域到海域

《笨办法学Python》 第30课手记

《笨办法学Python》 第30课手记 本节课讲if语句的嵌套,和c的差别仅仅是将else if简写成elif,其余类似。 原代码如下: people = 30...

20670
来自专栏琦小虾的Binary

map 学习(上)——C++中 map 的使用

map 学习(上)——C++中 map 的使用 欠下数据结构的债,迟早是要还的…… 最近写毕业论文过程中,需要用到哈希表的数据结构,此外空闲时间在刷 Leetc...

45160
来自专栏javathings

解释一下 HashMap 的工作原理

HashMap 是基于散列表的数据结构。所谓散列表,它通过键值对的方式存储数据,把 key 通过散列算法计算出一个存储地址,将 value 放入这个地址中。散列...

46710
来自专栏大闲人柴毛毛

深入理解JVM(七)——Class文件结构

什么是JVM的“无关性”? Java具有平台无关性,也就是任何操作系统都能运行Java代码。之所以能实现这一点,是因为Java运行在虚拟机之上,不同的操作系统...

34640

扫码关注云+社区

领取腾讯云代金券