zoukankan      html  css  js  c++  java
  • 转:《链接、装载与库》里的一个错误:关于调用栈

    《链接、装载与库》里的一个错误:关于调用栈

    按照原文中描述做了一个PPT:

    每次执行push指令时,esp都会减4(因为栈是向低地址增长的),每次pop时esp都会加4。

    指令:push a

    指令:push b

    指令:

          1.把main方法当前指令的下一条指定地址(即return address))push到栈中。

          2.使用call指令调用目标函数体。

    指令:将ebp的当前值push到栈中,即saved ebp。

    指令:将esp的值赋给ebp,则意味着进入了foo方法的调用栈。

    指令:push c

    指令:pop c

    指令:将ebp的值赋给esp,意味着恢复foo调用时的栈顶。(这一句和之前没什么区别,具体不太懂?pop以后esp自然就和ebp一样了,懂得人可以给解答么?)

    图片同上

    指令:将栈顶的值赋给ebp(pop esp),即恢复main调用栈的栈底。    (原文写的是pop ebp,我认为是笔误)

    指令:ret,根据esp的位置,即栈顶获得之前保留的return address,继续执行。

    如果在foo方法中需要访问那些参数,则需要根据当前ebp中的值,再向高地址偏移后进行访问——因为高地址才是main方法的调用栈。也就是说,地址ebp + 8存放了foo方法的第1个参数,地址ebp + 12存放了foo方法的第2个参数,以此类推。那么地址ebp + 4存放了什么呢?它存放的是return address,即foo方法返回后,需要继续执行下去的main方法指令的地址。

    ///////////////////////////////  以下是原文  ////////////////////////////////////

    周六老同学聚会,出门前随手从桌上抓起了《程序员的自我修养——链接、装载与库》在路上翻。自从武汉博文出版社的周筠老师送给我这本书后,我基本上还没怎么看过。对这本书第一感觉是“标题党”,主标题起大了,虽然经过解释之后并非无法理解,但还是不太喜欢。但书还是好书,已经看完大半,而且基本上会在近期找个方式推荐一把。不过现在我想细说的并不是推荐相关话题(如适合谁看,该怎么看,结合什么一起看等等),而是想指出书中还未被《勘误》收录的一个错误:P288讲调用栈时,文字描述和配图上的问题。

    什么是调用栈呢?这里的“栈”和平时我们谈论的数据结构“栈”关系并不大。数据结构里的“栈”是一种先进后出的容器,日常生活中我们经常使用叠在一起的盘子进行类比。而计算机系统中的“栈”是一种有类似行为的内存区域,每次“压”进去和“弹”出来的都是数据。而这块内存区域,是在CPU进行过程调用时所“形成”,或者说是“使用”。与常见高级语言中的“方法”、“函数”不同,这里的“过程”并不是一种高级的抽象,它是CPU可以识别的概念,它的调用自有call指令与之应对。

    那么这个“栈”又是什么形态的呢?它是“从高地址向低地址”扩展的一块区域,使用ebp和esp分别作为单个过程的调用栈的栈底和栈顶:

             0xFFFFFFFF
    |                       |
    |                       |  高地址
    |                       |
               ...
    |                       |
    |                       |
    -------------------------
    |                       |
    |                       |  前一个过程的调用栈
    |                       |
    -------------------------  <-- 当前ebp(栈底)
    |                       |
    |                       |  当前过程的调用栈
    |                       |
    -------------------------  <-- 当前esp(栈顶)
    |                       |
    |                       |
               ...
    |                       |
    |                       |  低地址
    |                       |
            0x00000000

    以上是在32位体系结构中的内存地址示意图。每次执行push指令时,esp都会减4(因为栈是向低地址增长的),每次pop时esp都会加4。当方法main需要调用foo时,它的标准形式为:

    1. 在main方法的调用栈中,将foo的参数push到栈中。
    2. 把main方法当前指令的下一条指定地址(即return address))push到栈中。
    3. 使用call指令调用目标函数体。

    请注意,以上3步都处于main的调用栈,其中ebp保存其栈底,而esp保存其栈顶。而在foo方法body的标准形式为:

    1. 将ebp的当前值push到栈中,即saved ebp。
    2. 将esp的值赋给ebp,则意味着进入了foo方法的调用栈。
    3. 剩下的便是根据需要,保存(push)一些寄存器的值,或是计算过程中所需要的临时变量等等。

    而在foo方法调用完毕后,便执行前面阶段的逆操作:

    1. 恢复(pop)一些寄存器的值。
    2. 将ebp的值赋给esp,意味着恢复foo调用时的栈顶。
    3. 将栈顶的值赋给ebp(pop ebp),即恢复main调用栈的栈底。
    4. ret,根据esp的位置,即栈顶获得之前保留的return address,继续执行。

    可见,main方法先将foo方法所需的参数压入栈中,然后再改变ebp,进入foo方法的调用栈。因此,如果在foo方法中需要访问那些参数,则需要根据当前ebp中的值,再向高地址偏移后进行访问——因为高地址才是main方法的调用栈。也就是说,地址ebp + 8存放了foo方法的第1个参数,地址ebp + 12存放了foo方法的第2个参数,以此类推。那么地址ebp + 4存放了什么呢?它存放的是return address,即foo方法返回后,需要继续执行下去的main方法指令的地址。在著名的CSAPP中有这么一幅图,具体地展示了这一切:

    可惜,书中P228却是这样写的:

    在参数之后的数据(包括参数)即使当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录的各个数据。在ebp之前首先是这个函数的返回地址,它的地址是ebp - 4,再往前是压入栈中的参数,它们的地址分别是ebp - 8、ebp - 12等,视参数数量和大小而定……

    虽然书中的描述正确,但ebp + 4,ebp + 8,ebp + 12等地址都被错误的写成了ebp - 4,ebp - 8,ebp - 12。我估计这可能是由于栈是“从高地址向低地址扩充”的,此时的“之前”应该是“加”而不是“减”的缘故吧。这点的确不太符合人们“从小到大”的计数习惯,一时疏忽容易造成差错。此外,我认为书中配套的这幅示意图,似乎也有一些问题——至少不太妥当。如下:

    在这幅图中,ebp的指针指向old (saved) ebp和return address之间,那么ebp地址究竟算是保存了什么值呢?正确说来,ebp地址中保存的应该是old ebp,也就是说,在这幅图中“取值”是由高地址向低地址进行的——这不太自然。而更容易产生“歧义”的是,esp的指针指向了整个“活动记录”的最底部,这给人的感觉似乎是,如果谈起“esp”地址保存的值,则应该向“高地址”来获取(因为“低地址”已经没了)。这就和ebp产生矛盾了。可能正因为如此,我看这幅示意图时总觉得有些别扭,虽然我的确可以将ebp理解为“向下”取值,但总还是想着将ebp往下“掰一格”,使其和esp的方向“感觉上”保持统一。那么更好的做法是什么呢?个人认为,最不会产生歧义的方式,是像CSAPP那样(即前面的那幅图),将指针直接指向某个内存“块”,而不是某个“边界”。

    最后还有一个“问题”,那便是“Stack Frame”,即书中的“活动记录”的范围究竟是什么。在CSAPP中,一个Stack Frame是从ebp开始到esp范围的这块内存区域,但《链接》一书却认为它是从方法调用的参数开始到esp。这个区别从上面两幅图中也可以看出。我不知道孰对孰错,但是书中写的很清楚,因此这里肯定不是疏忽。我怀疑是不是“流派”之间的差异所致

    虽然《链接、装载与库》中有这么一个小问题,但瑕不掩瑜,它仍然是国内难得踏实的技术书籍。而且,这本书的作者“余甲子”是2006年在读研一的“同学”——所以我经常说,总有一些牛人让我汗颜。此话真的不假。

  • 相关阅读:
    zoj 3627#模拟#枚举
    Codeforces 432D Prefixes and Suffixes kmp
    hdu 4778 Gems Fight! 状压dp
    CodeForces 379D 暴力 枚举
    HDU 4022 stl multiset
    手动转一下田神的2048
    【ZOJ】3785 What day is that day? ——KMP 暴力打表找规律
    poj 3254 状压dp
    C++中运算符的优先级
    内存中的数据对齐
  • 原文地址:https://www.cnblogs.com/kira2will/p/4396917.html
Copyright © 2011-2022 走看看