上面已经讨论当有局部变量空参数的函数时栈的布局,那么当函数具有局部变量和参数,那么栈布局又会怎样?
先看一个例子:
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
一样。