ESPEBP是刚接触栈的时候就碰到的两个指针。对这两个我一直是处于一知半解状态。

错误认知:ESP是指向栈顶指针,EBP是指向栈底指针。

我这么认为已经很长时间了,而且自己觉得没问题。
直到今天看英文版的书,遇到mov eax,[ebp+10h]
我仔细想了一下,栈内肯定是栈底地址最大,要是ebp指向栈底,那么ebp+10h不是超出栈的地址范围了???
这一度让我的认知出现混乱。
这个就要涉及到栈的结构了,一直以为栈就是一个数据段,esp指头,ebp指尾。这里涉及到栈帧的概念,栈里面被分为一个个栈帧。
借用一个很火的网图
而ebp和esp的定义:

(1)ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
(2)EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。

ESP和EBP都是指向最上面一个栈帧的。ESP指向这个栈帧的栈顶,EBP指向这个栈帧的底部。(这么说来ESP指向栈顶地址也是正确的,但EBP指向栈底地址就有问题了。)
所以这里的ebp+10h,就指向了调用者的帧。

每个栈帧都有首地址,而ebp就是指向栈帧的首地址(注意,首地址和栈顶不同),函数层层调用的时候,刚开始ebp是存了父函数的首地址,当父函数调用子函数后,ebp则要存子函数的栈帧首地址,而子函数的栈帧首地址内可以存一个值,这个值还是父函数的栈帧首地址,这样,每当子函数调用完,就能回到父函数内,层层递推,最终回到main()函数,函数调用的过程由此实现。

关于ebp的详解:

%ebp叫帧指针,相信熟悉C指针的朋友看到名字时,对%ebp的工作原理就基本明白个七八分了。没错,既然叫帧指针,那就是用来存放各帧首地址的指针。

设想,当father函数要调用son函数时,需要对栈帧信息进行修改和维护,如何在son函数执行完后让CPU顺利的找到father的栈帧地址并成功返回呢?这就要在调用son之前做好充分的准备工作。比如,father栈帧有自己的帧首,在father函数执行时,%ebp就保存了这个帧首的地址值,或者说%ebp正指向帧首。当调用子函数son时,%ebp就会保存son的帧首地址,为了让son在返回时能够顺利更新%ebp,使得帧指针顺利指回到father的帧来,有必要在%ebp指向son帧首的同时,更改帧首空间内所保存的值为father帧首地址,也就是son的所谓“保存的%ebp”,或者说旧的%ebp值,父函数调用时%ebp的值。

区分下一个概念,一个存储单元空间,有两个属性:1、CPU访问这个存储单元需要依赖的地址值;2、这个存储单元所存储的数值,空间地址值和空间内存储的数值,区分这两个值是理解指针概念的基础。现在讨论函数的帧,每个帧都帧首,帧首作为存储单元空间,当然有标识自己的空间地址,同时空间里存了一个数值。栈帧结构恰恰巧妙的利用了这种概念,让%ebp始终保存当前调用函数的帧首地址,而当前帧首内又存储着父函数的帧首地址,以此类推,每一个当前调用函数的帧首内都保留着父函数的帧首地址,函数执行完成时都能顺利更新栈指针%ebp的值,一直可以推到main函数的帧首,通过栈指针%ebp的修改和被保存,就能确保栈帧结构的访问顺利进行。

以上是纯理论推导,一旦你真正看明白,那具体的汇编代码实现就会很容易弄懂了。函数调用在汇编中还会涉及到call、leave、ret等指令,其实都可以用更基本的指令进行描述。

为了便于讲解,我写了一段简单得不能再简单的函数调用事例ebpesp.c:

int son_add(int a, int b) {return a+b; }int father() {int a = 8;int b = 9;int sum = 0;sum = son_add(a, b);return sum; }

利用gcc 的-O2优化选项进行编译生成ebpesp.o的二进制文件(没有main函数所有不能编译成可执行文件,但汇编原理完全一样)

然后再反汇编代码,其中函数体部分如下:

00000000 <son_add>:0:   55                      push   %ebp1:   89 e5                 mov    %esp,%ebp3:   8b 45 0c            mov    0xc(%ebp),%eax6:   03 45 08            add    0x8(%ebp),%eax9:   c9                      leave  a:   c3                      ret    b:   90                      nop    0000000c <father>:c:   55                      push   %ebpd:   89 e5                 mov    %esp,%ebpf:   6a 02                  push   $0x911:   6a 01                push   $0x813:   e8 fc ff ff ff        call   14 <father+0x8>18:   5a                     pop    %edx19:   59                     pop    %ecx1a:   c9                     leave  1b:   c3                     ret

father函数从第三行push开始看起。两条push语句明显就是对参数进行压栈,先压9后压8,与c语言中的自右向左的原理一致,两个参数的值被成功压入栈。注意此时还是father执行阶段,因此参数所压的位置仍属于father的栈帧空间。接下来就是子函数调用call语句,call可以近似看成做如下操作:

call:push 返回地址,%esp

jmp 子函数地址

因此father中的call就可以翻译成更直观的汇编语句就是:(注意,18和0都是逻辑地址,这里只是为举例而写的伪汇编代码,在后面章节将详细描述。)

push $18,%esp

jmp $0

可见,两个参数入栈完成后,接着就是father函数的返回地址,返回到18这个地址,以便继续father代码的执行。到此为止,father函数的栈帧维护结束,函数调用的准备工作完成,可以通过跳转指令jmp跳转到son_add函数了。

我们发现,son_add第一句是push %ebp,理解这句很关键。想想,在这条语句之前,程序运行的是father函数,那么%ebp自然也保存的是father函数的帧首地址,直到执行到0,也没有谁修改过它,因此在还行push %ebp时,%ebp里仍然保存的是father帧首地址,现在对他进行压栈,于是push %ebp就使得该帧首地址就被顺利的放进了“返回地址”单元的下面(成为新栈顶,%esp就存储了其地址值),再由于这是运行son_add函数的第一条语句,因此该栈顶就作为son_add的帧首了,此时该帧首里面到是舒服的躺着father帧首的地址值,%ebp却并没有指向son_add函数的帧首,因此mov %esp,%ebp就是把当前这个帧首的地址值赋给%ebp,于是在son_add函数返回前,%ebp都作为当前帧指针不会变动了。

接下来的两句很有意思,mov 0xc(%ebp),%eax是对帧指针里的地址先增加0xC再取里面的值,增加12是啥意思?12刚好是4的倍数,也就是向上移动三个栈存储单元。根据栈结构图发现,%ebp作为帧首,向上移动一个单元是“返回地址”;向上移动两个单元是参数1,向上移动三个单元当然是参数2!也就是我们传给son_add的第二个参数9。因此这条汇编的意思是把9赋值给%eax寄存器。依次类推是不是还应该把参数1的这个8赋给另一个寄存器呢?编译器可没这么傻,你son_add不就是想做个加法么?直接add 0x8(%ebp),%eax,让%ebp寻找到参数1的地址位置,读取出8,然后直接和%eax的值相加,搞定!

好了,这个时候%eax寄存器就是存有加法结果的寄存器了,计算完成子函数需要返回了,于是先后执行leave和ret,先看leave的等价汇编代码:

> leave:movl %ebp,%esp
> pop   %ebp

这步在理解上稍显困难,主要是对出入栈的操作理解。movl %ebp,%esp这条语句,其实目的就是破坏子函数son_add栈帧结构。想想看,直接修改栈指针%esp,让他指向son_add的帧首,然后执行pop
%ebp,将帧首里的值赋给%ebp!回忆下帧首里存的是啥值啊?那当然是father帧首的地址值啊,这句目的就是让%ebp重新指回father栈帧的帧首!OK,son_add的帧首被弹出栈后,栈指针也不会再指向son_add帧首了,而是指向他的上面一个栈存储单元,那就是father帧的末尾:返回地址,leave的使命便完成。接下来就是ret,考虑到ret要完成函数调用的返回,还要维护栈帧的返回,我们可以猜测ret的等价汇编代码应该是:

> ret:jmp  (%esp)
> add  $4, %esp

因为此时%esp指向father帧的末尾,而该末尾里面又存储了father调用son_add函数后应该返回的地址(这里应该是18),因此就应该是将该地址取出,直接跳转到18,也就是call语句之后的语句,而子函数son_add的调用既然已经完成,根据个人的猜测,“返回地址”已失去意义,因此栈指针会加4返回。

至此,关于函数调用的栈帧原理就全部讲完了,如果你看懂了上面的论述,就能自然而然的推到出father其余部分的汇编含义,也能显而易见的弄明白大型C程序中每个函数都已push %ebp、mov %esp,%ebp开头以及leave和ret结尾,层层包裹,稳定和完美。

浅析栈指针ESP和帧指针EBP相关推荐

  1. 栈帧及EBP、ESP寄存器及出入栈的流程

    接下来咱们以一个例子来说明EBP.ESP寄存器的作用.由于刚是自己理解的,有错误可以提出,共同进步. 这个是栈帧的结构 以下是按调用约定__stdcall 调用函数test(int p1,int p2 ...

  2. GCC帧指针的开启与关闭以及反汇编测试

    编译阶段: -fno-omit-frame-pointer:开启帧指针 -fomit-frame-pointer:关闭帧指针 代码中添加: __attribute__((optimize(" ...

  3. 浅析C++中的this指针 通过空指针(NULL)可以正确调用一些类的成员函数?

    有下面的一个简单的类: class CNullPointCall { public:     static void Test1();     void Test2();     void Test3 ...

  4. 转:浅析C++中的this指针

    原文出处:http://blog.csdn.net/starlee/article/details/2062586 有下面的一个简单的类: class CNullPointCall { public: ...

  5. c语言栈指针移动原理,C指针原理(4)-ATamp;T汇编

    首先我们先用汇编编写一个helloworld,注意我们直接在汇编代码中调用C语言的printf函数将"hello,world\n" 输出在屏幕上. .section .data o ...

  6. 数组c语言与指针,浅析C语言数组与指针

    摘 要:数组和指针是C语言的两个最重要的概念,它们若结合起来使用,非常灵活,初学者往往感到无所适从,笔者根据多年的经验,利用典型实例和图表对指向一维数组的指针的定义和数组元素的引用.指针与自增自减运算 ...

  7. 四川大学软件学院 2017级系统级编程 复习知识点-很零碎的那种

    Cache Miss rate Conflict miss(thrash)抖动 capacity miss由于缓存的容量大小有限而引起的缓存缺失 miss panelty(缺失损失):缺失之后系统需要 ...

  8. Python源码剖析[19] —— 执行引擎之一般表达式(2)

    Python源码剖析 --Python执行引擎之一般表达式(2) 本文作者: Robert Chen(search.pythoner@gmail.com ) 3.2     Simple.py 前面我 ...

  9. 跟我一起玩《linux内核设计的艺术》第1章(四)——from setup.s to head.s,这回一定让main滚出来!(已解封)

    看到书上1.3的大标题,以为马上就要见着main了,其实啊,还早着呢,光看setup.s和head.s的代码量就知道,跟bootsect.s没有可比性,真多--这确实需要包括我在内的大家多一些耐心,相 ...

最新文章

  1. Oracle VM VirtualBox下各种视图切换
  2. MySql中的varchar类型
  3. 【Android】 -- 使用UncaughtExceptionHandler捕捉全局异常
  4. hdu3081 Marriage Match II(最大流)
  5. oracle近似查找,距离内的Oracle空间搜索
  6. 20120203 SVN安装 出现的问题处理
  7. ML《集成学习(五)XGBoost》
  8. CSDN-Markdown--基本语法功能效果
  9. linux清除占用端口,Linux中解除端口占用的方法
  10. 从零开始Android游戏编程(第二版) 第十章 游戏循环的设计
  11. Fiddler中文版汉化插件 0.1
  12. 斗地主+三人+叫地主+作弊(可设置)+积分属性+记牌器(可设置)
  13. 移动硬盘读取速度突然变慢?教你7个方法解决
  14. bi产品有哪些,商业智能产品
  15. 观察 | 家长焦虑,教培着急,暑期“培训热”今年还会持续吗?
  16. 为视图或函数指定的列名比其定义中的列多。
  17. 小米手机开发版、稳定版刷机和root等操作
  18. JavaScript下搭建ag-grid
  19. 【Python 实战】---- 批量修改文件名和将txt转excel
  20. 使Gradle构建更快 2016年2月5日奥列格Shelajev3评论 推特 inShare 70 上次我们谈到了构建系统,我们看着一些建议可能会使您的Maven构建更快。我们得到的结果是迷人的和对

热门文章

  1. Qt5 模拟鼠标点击
  2. SMB v1远程代码执行漏洞(CVE-2020-1301)复现
  3. 三极管Vbeo、Vceo的介绍
  4. 试题 算法训练 后缀数组——最长重复子串
  5. 赫耳墨斯与忒瑞西阿斯
  6. 3D角色硬表面建模技巧与思路分享
  7. OpenCV-图像颗粒感
  8. 功能测试--分享功能测试
  9. 2022山东健博会,食疗养生与滋补健康展,健康管理与精准医学展
  10. android 横向头像栏,Android实现个人资料页面头像背景模糊显示包(状态栏)