日期:2014-05-16  浏览次数:20775 次

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

上面探讨了没有使用-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(re