关于《程序员面试宝典5》中的一道堆栈题目

今天在读《程序员面试宝典5》,遇到这样一个关于栈的题目:
如下C++程序:
int  i=0x22222222;
char szTest[]="aaaa";     //a的ASCII码为61
func(i, szTest);               //函数原型为void func(int a, char *sz);
请问进入func函数时,参数在栈中的形式可以为以下哪一种?
A.                                                     B.
0x0013FCF0        0x61616161         0x0013FCF0         0x22222222
0x0013FCF4        0x22222222         0x0013FCF4        0x0013FCF8
0x0013FCF8        0x0013FCF8         0x0013FCF8        0x61616161
后面还有两个答案就省略了,书中给出的正确答案是A,并解释说参数压栈的时候,从右到左,即最右边的参数最先压栈,又应为Windows平台栈是从高地址向低地址生长的。
但是按照这个解释,我认为答案恰恰应该是B才对。
于是我自己在linux中写代码试了以下,linux上栈也是向低地址生长的。
测试程序:
#include <stdlib.h>
#include <stdio.h>

void func(int a, char *sz)
{
    return;
}

int main(void)
{
    int i=0x22222222;
    char szTest[]="aaaa";
    func(i, szTest);
    return 0;
}

编译后直接用gdb打印func刚刚进入时的栈内容,如下:

(gdb) x /4wx 0xbffff548
0xbffff548:     0xbffff578      0x08048481      0x22222222      0xbffff567
(gdb) x /4wx 0xbffff567
0xbffff567:     0x61616161      0x57910000      0x0484a035      0x00000008
实践证明,答案B是正确的。不过,会不会Windows上真是A呢? 我不想取windows下试了,就这样把,哈哈!
解释:
  0xbffff548位置开始:
    0xbffff578是调用者的栈基址
    0x08048481是func函数执行后的返回地址,就调用func函数那个位置的下一条指令;
    0x22222222是第一个参数,即i
    0xbffff567是第二个参数,即字符数据szTest, 因为是数组,所以它其实就是一个指针,指针指向的内容是0x61616161,就是字符串“aaaa”
 
 
继续研究linux下的情况,为了更详细的了解程序调用时候的栈相关操作,我们再写一个程序:
测试代码:
#include <stdlib.h>
#include <stdio.h>

int sum(int AA, int BB)
{
    return AA+BB;
}

int main(void)
{
    int AA=0xAAAA;
    int BB=0xBBBB;
    sum(AA,BB);
    return 0xABCD;
}

 

yang@yang-vmlinux:~/tmp/c$ gcc -g test.c          //编译(-g 表示编译生成gdb调试所需的额外信息)

yang@yang-vmlinux:~/tmp/c$ gdb a.out
GNU gdb (Ubuntu 7.7-0ubuntu3.1) 7.7
Copyright (C) 2014 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from a.out...done.
(gdb) disas main
Dump of assembler code for function main:
   0x080483fa <+0>:     push   %ebp                    //将当前栈基址压栈
   0x080483fb <+1>:     mov    %esp,%ebp            //将当前栈顶保存到EBP(开辟新的栈帧),此时EBP=ESP
   0x080483fd <+3>:     sub    $0x18,%esp            //这里为什么要减去0x18呢?我认为是为0xaaaa和0xbbbb以及sum函数的入参这四个数腾出空间,但是多出8个字节,是为了保险码?
   0x08048400 <+6>:     movl   $0xaaaa,-0x8(%ebp)    //将0xaaaa和0xbbbb两个立即数分别存储到相对EBP偏移-0x8和-0x4处
   0x08048407 <+13>:    movl   $0xbbbb,-0x4(%ebp)   
   0x0804840e <+20>:    mov    -0x4(%ebp),%eax        //将0xbbbb 存入EAX
   0x08048411 <+23>:    mov    %eax,0x4(%esp)        //将EAX,即0xbbbb,保存到ESP偏移0x4处
   0x08048415 <+27>:    mov    -0x8(%ebp),%eax        //将0xaaaa 存入EAX
   0x08048418 <+30>:    mov    %eax,(%esp)            //将EAX,即0xaaaa,保存到ESP偏移0x0处
   0x0804841b <+33>:    call   0x80483ed <sum>        //上面四步相当于参数压栈,最又的参数先压栈,压栈完毕后通过call调用sum, call同时会将返回地址压栈
   0x08048420 <+38>:    mov    $0xabcd,%eax            //将0xABCD存入EAX,作为函数main的返回值
   0x08048425 <+43>:    leave                        //leave 相当于“mov ebp, esp” 和 “pop ebp”两条指令的组合,leave执行后恢复到上一级函数的栈帧
   0x08048426 <+44>:    ret                            //使用ret返回
End of assembler dump.

(gdb) disas sum
Dump of assembler code for function sum:
   0x080483ed <+0>:     push   %ebp                    //将当前栈基址压栈
   0x080483ee <+1>:     mov    %esp,%ebp            //将当前栈顶保存到EBP(开辟新的栈帧),此时EBP=ESP
   0x080483f0 <+3>:     mov    0xc(%ebp),%eax        //取第二个参数,即0xbbbb
   0x080483f3 <+6>:     mov    0x8(%ebp),%edx        //取第一个参数,即0xaaaa
   0x080483f6 <+9>:     add    %edx,%eax           
   0x080483f8 <+11>:    pop    %ebp                    //应为sum函数里面ESP没有再改变过,所以直接使用pop ebp,执行后恢复到上一级main函数的栈帧
   0x080483f9 <+12>:    ret
End of assembler dump.


(gdb) break main                                    //我们设置两个断点跟踪看看
Breakpoint 1 at 0x8048400: file test.c, line 11.
(gdb) break sum
Breakpoint 2 at 0x80483f0: file test.c, line 6.



(gdb) r
Starting program: /home/yang/tmp/c/a.out

Breakpoint 1, main () at test.c:11
11          int AA=0xAAAA;                            //在第一个断点,main函数这里停住
(gdb) info register ebp
ebp            0xbffff578       0xbffff578
(gdb) info register esp
esp            0xbffff560       0xbffff560

(gdb) disas main
Dump of assembler code for function main:
   0x080483fa <+0>:     push   %ebp
   0x080483fb <+1>:     mov    %esp,%ebp
   0x080483fd <+3>:     sub    $0x18,%esp
=> 0x08048400 <+6>:     movl   $0xaaaa,-0x8(%ebp)    //从汇编代码看,是停在这里
   0x08048407 <+13>:    movl   $0xbbbb,-0x4(%ebp)
   0x0804840e <+20>:    mov    -0x4(%ebp),%eax
   0x08048411 <+23>:    mov    %eax,0x4(%esp)
   0x08048415 <+27>:    mov    -0x8(%ebp),%eax
   0x08048418 <+30>:    mov    %eax,(%esp)
   0x0804841b <+33>:    call   0x80483ed <sum>
   0x08048420 <+38>:    mov    $0xabcd,%eax
   0x08048425 <+43>:    leave
   0x08048426 <+44>:    ret
End of assembler dump


(gdb) c                                                //继续运行
Continuing.

Breakpoint 2, sum (AA=43690, BB=48059) at test.c:6    //停止在第二个断点
6           return AA+BB;
(gdb) disas sum
Dump of assembler code for function sum:
   0x080483ed <+0>:     push   %ebp
   0x080483ee <+1>:     mov    %esp,%ebp
=> 0x080483f0 <+3>:     mov    0xc(%ebp),%eax        //汇编代码停在这里
   0x080483f3 <+6>:     mov    0x8(%ebp),%edx       
   0x080483f6 <+9>:     add    %edx,%eax
   0x080483f8 <+11>:    pop    %ebp
   0x080483f9 <+12>:    ret
End of assembler dump.
(gdb) info r esp
esp            0xbffff558       0xbffff558            // ESP和EBP的值相同
(gdb) info r ebp
ebp            0xbffff558       0xbffff558
(gdb) x /8wx 0xbffff558                                //显示ESP指向的内存的内容
0xbffff558:     0xbffff578      0x08048420      0x0000aaaa      0x0000bbbb
                //main的栈基    //返回地址        //第一个参数    //第二个参数
0xbffff568:     0x0804843b      0xb7fbc000      0x0000aaaa      0x0000bbbb
                //未知            //未知            //变量AA        //变量BB

 

总结:
1. 调用一个函数时,先将堆栈原先的基址(EBP)入栈,以保存之前任务的信息。然后将栈顶指针的值赋给EBP,将之前的栈顶作为新的基址(栈底),然后再这个基址上开辟相应的空间用作被调用函数的堆栈。函数返回后,从EBP中可取出之前的ESP值,使栈顶恢复函数调用前的位置;再从恢复后的栈顶可弹出之前的EBP值,因为这个值在函数调用前一步被压入堆栈。这样,EBP和ESP就都恢复了调用前的位置,堆栈恢复函数调用前的状态。
2.操作栈的时候不仅可以用POP,PUSH,也可以直接用mov加偏移的方式直接操作ESP和EBP
posted @ 2017-02-18 22:55  泡面狂  阅读(1038)  评论(0编辑  收藏  举报