版权声明:本文为博主原创文章,未经博主允许不得转载。 https://cloud.tencent.com/developer/article/1344462
上面已经讨论当有局部变量空参数的函数时栈的布局,那么当函数具有局部变量和参数,那么栈布局又会怎样?
先看一个例子:
int func( int c, char* s, int off )
{
int a = 0x12345678;
int *p = &a;
int res = c + *( s + off );
return *p + res;
}
int main()
{
int b = 0x87654321;
return b + func( 0x100, "hello", 3 );
}
看一下main函数的汇编:
(gdb) disassemble main
Dump of assembler code for function main:
0x08048405 <+0>: push %ebp
0x08048406 <+1>: mov %esp,%ebp
0x08048408 <+3>: sub $0x1c,%esp
0x0804840b <+6>: movl $0x87654321,-0x4(%ebp)
0x08048412 <+13>: movl $0x3,0x8(%esp)
0x0804841a <+21>: movl $0x80484d4,0x4(%esp)
0x08048422 <+29>: movl $0x100,(%esp)
0x08048429 <+36>: call 0x80483d0 <_Z4funciPci>
0x0804842e <+41>: mov -0x4(%ebp),%edx
0x08048431 <+44>: add %edx,%eax
0x08048433 <+46>: leave
0x08048434 <+47>: ret
End of assembler dump.
从0x08048412 到0x08048422 的汇编语句可以看到,esp, esp+4,esp+8这三个单元依次存放着0x100,0x80484d4,3。而0x80484d4存放的内容可以如下获得:
(gdb) x /s 0x80484d4
0x80484d4 <__dso_handle+4>: "hello"
可见,刚好和依次传入func函数的参数0x100,”hello”, 3一样。可知正是那三个参数。
当执行
0x08048429 <+36>: call 0x80483d0 <_Z4funciPci>
进入func函数时,会把main函数的返回地址压入栈,这时,栈的布局应该如下:
esp:返回地址
esp+4:0x100
esp+8:”hello”的地址
esp+0xC:3
那么当func函数执行完它的开头特征指令
push %ebp
mov %esp,%ebp
栈的布局应该如下:
esp:main函数桢指针
esp+4:返回地址
esp+8:0x100
esp+0xC:“hello”的地址
esp+0x10:3
由于这时ebp和esp相等,而esp会用于push、pop、sub、add等操作来申请和释放局部变量空间。所以栈布局应该是ebp为基准。所以上面栈布局应该表述如下:
ebp:main函数桢指针
ebp+4:返回地址
ebp+8:0x100
ebp+0xC:“hello”的地址
ebp+0x10:3
在func打断点来看一下,是不是如此:
(gdb) tbreak func
Temporary breakpoint 1 at 0x80483d6
(gdb) r
Starting program: /home/buckxu/work/3/3/xuzhina_dump_c3_s3
Temporary breakpoint 1, 0x080483d6 in func(int, char*, int) ()
(gdb) x /8x $ebp
0xbffff4b4: 0xbffff4d8 0x0804842e 0x00000100 0x080484d4
0xbffff4c4: 0x00000003 0x0804844b 0xb7fbeff4 0x08048440
(gdb) info symbol 0x0804842e
main + 41 in section .text of /home/buckxu/work/3/3/xuzhina_dump_c3_s3
(gdb) x /s 0x080484d4
0x80484d4 <__dso_handle+4>: "hello"
果然如此。
由上面的探讨可知,函数的参数有如下规则:
1. 参数在栈上的排列和它的声明顺序是一致的。
2. 第一个参数是在ebp+8开始,第二个参数是放在ebp+0xC,其它依此类推。
到了这里,可以知道完整的栈布局应该如下图:
从图上还可以得到这样的规律:
如果两个函数桢指针fp1,fp2,fp1正好指向fp2的单元。那么它们满足这样的关系:
1. esp < fp1 < fp2
2. fp1的下一个单元的内容ret1肯定是返回地址,fp2的下一个单元的内容ret2也肯定是返回地址。即ret1,ret2都可以用info symbol命令来获取它们位于哪些函数里。
探讨完了函数参数的知识,在这里探究一下main函数的参数。虽然main函数在写的时候,可以是
int main()
也可以是
int main( int argc, char* argv[] )
实际上,main函数的原型是这样的:
int main( int argc, char* argv[], char* envp[ ] )
从上面的
(gdb) tbreak func
Temporary breakpoint 1 at 0x80483d6
(gdb) r
Starting program: /home/buckxu/work/3/3/xuzhina_dump_c3_s3
Temporary breakpoint 1, 0x080483d6 in func(int, char*, int) ()
(gdb) x /8x $ebp
0xbffff4b4: 0xbffff4d8 0x0804842e 0x00000100 0x080484d4
0xbffff4c4: 0x00000003 0x0804844b 0xb7fbeff4 0x08048440
可知,main函数桢指针是0xbffff4d8。现在看一下0xbffff4d8 +8,0xbffff4d8+0xC, 0xbffff4d8+0x10的内容是什么?
(gdb) x /4x 0xbffff4d8+8
0xbffff4e0: 0x00000001 0xbffff574 0xbffff57c 0xb7fdd6b0
由于程序运行没有参数,所以argc为1,而argv的值为0xbffff574,envp的值为0xbffff57c。
先看一下argv的内容:
(gdb) x /4x 0xbffff574
0xbffff574: 0xbffff6b9 0x00000000 0xbffff6e2 0xbffff6f3
(gdb) x /s 0xbffff6b9
0xbffff6b9: "/home/buckxu/work/3/3/xuzhina_dump_c3_s3"
正好和程序的路径一样。
再看一下envp的内容:
(gdb) x /4x 0xbffff57c
0xbffff57c: 0xbffff6e2 0xbffff6f3 0xbffff704 0xbffff714
(gdb) x /s 0xbffff6e2
0xbffff6e2: "XDG_SESSION_ID=2"
(gdb) x /s 0xbffff6f3
0xbffff6f3: "HOSTNAME=xuzhina"
(gdb) x /s 0xbffff704
0xbffff704: "SHELL=/bin/bash"
(gdb) x /s 0xbffff714
0xbffff714: "TERM=linux"
正好和
(gdb) shell set
COLUMNS=80
DIRSTACK=()
EUID=1000
GROUPS=()
HISTCONTROL=ignoredups
HISTSIZE=1000
HOME=/home/buckxu
HOSTNAME=xuzhina
HOSTTYPE=i686
PWD=/home/buckxu/work/3/3
QT_GRAPHICSSYSTEM_CHECKED=1
SHELL=/bin/bash
SHELLOPTS=braceexpand:hashall:interactive-comments
SHLVL=2
SSH_ASKPASS=/usr/libexec/openssh/gnome-ssh-askpass
SSH_AUTH_SOCK=/tmp/ssh-gORyjvO849/agent.849
SSH_TTY=/dev/pts/0
TERM=linux
UID=1000
USER=buckxu
XDG_RUNTIME_DIR=/run/user/buckxu
XDG_SESSION_ID=2
_=/bin/gdb
一样。