版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1344458
上面探讨了没有使用-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都是这样。