Hello World
Spiga

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

2009-11-17 00:29 by 老赵, 19842 visits

周六老同学聚会,出门前随手从桌上抓起了《程序员的自我修养——链接、装载与库》在路上翻。自从武汉博文出版社的周筠老师送给我这本书后,我基本上还没怎么看过。对这本书第一感觉是“标题党”,主标题起大了,虽然经过解释之后并非无法理解,但还是不太喜欢。但书还是好书,已经看完大半,而且基本上会在近期找个方式推荐一把。不过现在我想细说的并不是推荐相关话题(如适合谁看,该怎么看,结合什么一起看等等),而是想指出书中还未被《勘误》收录的一个错误: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年在读研一的“同学”——所以我经常说,总有一些牛人让我汗颜。此话真的不假。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

108 条回复

  1. plpl[未注册用户]
    *.*.*.*
    链接

    plpl[未注册用户] 2009-11-17 00:40:00

    您发文章的时间集中在00:20-00:30间
    我记忆中不下10篇了
    呵呵
    支持
    能写写您从最普通的员工如何成长到现在这样的吗?
    早就想听听了,望您能成文!

  2. 老赵
    admin
    链接

    老赵 2009-11-17 01:09:00

    @plpl
    兄弟您一定搞错了,我到现在还是普通员工……

  3. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 01:14:00

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

    1. esp只是i386上的stack pointer

    2. return address 是由call指令本身压栈。没有单独的push return adress指令。

    3. 在i386上,参数的位置
    进入f时, esp指向的、栈顶的dword是返回地址没错,栈顶下的是参数没错。
    其他的说法就不一定正确了。


    3.1
    gcc通常会严格执行:push ebp, mov ebp esp,
    而msvc在函数体很小的情况下通常会把prologue部分优化。

    3.2
    并且还有double和C99的_LongLong类型。
    成员指针也有可能是8字节。


    3.3
    即使是i386,栈的生长方向也是可配置的……


    综上,要描述参数的位置……
    还是先抽象 —— 有一个sp为栈顶,栈顶下(避免栈的方向的困扰)第1、栈顶下第2、 。。。 并配合图。
    然后用i386的常用生长方向具体化……


    总的来说这本书的作者是有真才实学的,不像《高质量xxx编程指南》……
    可惜买不起…… 可惜没下到电子版…… 只在书店随便翻了翻……

  4. 蛙蛙王子
    *.*.*.*
    链接

    蛙蛙王子 2009-11-17 07:38:00

    老赵睡的确实很晚。

  5. xland
    *.*.*.*
    链接

    xland 2009-11-17 08:15:00

    一直想看着本书
    苦于知识不够
    一拖再拖

  6. 摆摊的
    *.*.*.*
    链接

    摆摊的 2009-11-17 08:15:00

    没想到老赵对数据结构和底层也有相当的研究,更佩服你明察秋毫,心细如丝的眼力和精神。学习了!

  7. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-17 08:19:00

    Jeffrey Zhao:
    @plpl
    兄弟您一定搞错了,我到现在还是普通员工……


    其实你应该叫老赵说说怎么从程序员走向架构师

  8. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-11-17 08:20:00

    上次去书店翻了一下书,基础太差,基本看不懂

  9. 横刀天笑
    *.*.*.*
    链接

    横刀天笑 2009-11-17 08:24:00

    没想到老赵是底层高层两不误啊,真是个全才,佩服佩服~~~

  10. 假正经哥哥
    *.*.*.*
    链接

    假正经哥哥 2009-11-17 08:54:00

    当时考数据结构的时候都是蒙的。。啊。怎么看得懂呢

  11. 我还是不懂
    *.*.*.*
    链接

    我还是不懂 2009-11-17 09:01:00

    温景良(Jason):

    Jeffrey Zhao:
    @plpl
    兄弟您一定搞错了,我到现在还是普通员工……


    其实你应该叫老赵说说怎么从程序员走向架构师


    对啊,介绍介绍如何从最初如何学习掌握,到后面工作中提高自己!

  12. Old
    *.*.*.*
    链接

    Old 2009-11-17 09:05:00

    老赵在逐渐释放其能量!哈哈

  13. 老赵
    admin
    链接

    老赵 2009-11-17 09:10:00

    假正经哥哥:当时考数据结构的时候都是蒙的。。啊。怎么看得懂呢


    这不是数据结构……

  14. 亚历山大同志
    *.*.*.*
    链接

    亚历山大同志 2009-11-17 09:16:00

    应该是在大学 操作系统 的课程里边会涉及到相应的内容

  15. 老赵
    admin
    链接

    老赵 2009-11-17 09:39:00

    亚历山大同志:应该是在大学 操作系统 的课程里边会涉及到相应的内容


    好像也不是操作系统,因为这个和操作系统关系也不大……

  16. CoolCode
    *.*.*.*
    链接

    CoolCode 2009-11-17 09:40:00

    Jeffrey Zhao:
    @plpl
    兄弟您一定搞错了,我到现在还是普通员工……


    那能说说您从技术人员成长为普通员工的故事吗?哧哧

  17. 老赵
    admin
    链接

    老赵 2009-11-17 09:59:00

    @水果阿生
    老苏当年用什么教材啊?

  18. 水果阿生
    *.*.*.*
    链接

    水果阿生 2009-11-17 09:59:00

    话说,这本书让我想起了我大学时候学习的编译原理教材。

  19. gussing
    *.*.*.*
    链接

    gussing 2009-11-17 10:01:00

    萝卜(也就是俞甲子)跟我一届的,确实是超牛的人。
    不过这本书呢,确实也是浅了点,前半部在<linker and loader>里都有提及,后半部分析c runtime就更浅了。。。

  20. 老赵
    admin
    链接

    老赵 2009-11-17 10:05:00

    话说linker and loader是2L。
    而现在这本是,linker, loader and library,是3L,多一个L。

  21. 老赵
    admin
    链接

    老赵 2009-11-17 10:05:00

    @gussing
    linker and loader没有看过……
    我感觉《链接》这本书对我还有些帮助,linker and loader就估计只能随便看看了。

  22. 水果阿生
    *.*.*.*
    链接

    水果阿生 2009-11-17 10:06:00

    @Jeffrey Zhao
    老苏当年本科的时候没去上课,考前跟老师要的题,所以不知道是什么教材。研究僧考试准备的时候因为要考中科院,就弄的中科大的教材。坦白讲,我觉的这书是计算机原理+编译原理+结合C语言的应用版,但是书确实不错,我在第三极等人下班的几个晚上站着把这书看完了。

  23. 老赵
    admin
    链接

    老赵 2009-11-17 10:09:00

    @水果阿生
    厉害厉害,佩服佩服

  24. gussing
    *.*.*.*
    链接

    gussing 2009-11-17 10:11:00

    @Jeffrey Zhao

    没错,多了一个c runtime的讲解,但很遗憾主选了vcrt而不是glibc(当然跟glibc太复杂不适合写成书也有关系)。

  25. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-17 10:51:00

    呃,,,,虽然没看这本书,,,
    但其实看了半天,没发现什么错误。。。


    +/-符号的似乎带来一些问题,但堆栈的生长方向不应是一个确定的东西。堆栈往哪边生长都是正常(用什么词?)的。从这个角度来说,无论+/-都可以说是对的。


    那个图?似乎也没错啊。。。


    指到缝隙上是一个习惯的问题。

    我们假设内存里有两个东西,A占一个字节,B占三个字节,我们在画图的时候应该把B画成三倍大小吧?
    像这样:

    /-------\<---a
    |   A   |<---a1
    |-------|<---b
    |       |<---b1
    |   B   |<---b2
    |       |<---b3
    \-------/
    


    我们要怎么描述A和B的地址呢?在上图中是选择a和b还是a1、b1还是a1、b2呢?习惯问题尔。。。。。

  26. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 10:56:00

    此书我也觉得它是标题党 我实在无法理解主标题和副标题究竟有何联系

    话说栈不一定是“从高地址向低地址”,只是某些CPU这么干。

  27. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 11:05:00

    高质量xxx编程指南 我觉得那书不错

  28. 老赵
    admin
    链接

    老赵 2009-11-17 11:06:00

    @Ivony...
    噢噢……不过书中谈到从高到低,就应该是+4+8咯。
    内存图这东西,的确就是个习惯,所以就看自然不自然吧……
    我还是觉得CSAPP那种不错的,呵呵。

  29. breeze
    *.*.*.*
    链接

    breeze 2009-11-17 11:21:00

    在参数之后的数据(包括参数)即使当前函数的活动记录,ebp固定在图中所示的位置,不随这个函数的执行而变化,相反地,esp始终指向栈顶,因此随着函数的执行,esp会不断变化。固定不变的ebp可以用来定位函数活动记录的各个数据。在ebp之前首先是这个函数的返回地址,它的地址是ebp - 4,再往前是压入栈中的参数,它们的地址分别是ebp - 8、ebp - 12等,视参数数量和大小而定……
    ------------------------------------------
    我想文章可能指的当前函数的参数变量,那么只是使ebp - (值),博主可能想说的调用这个函数时所带入的参数吧,那么地址就是ebp + (值),文章确实没有描述清楚。

  30. 老赵
    admin
    链接

    老赵 2009-11-17 11:24:00

    @breeze
    我也觉得作者应该不会犯这个错误……

  31. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 13:09:00

    亚历山大同志:应该是在大学 操作系统 的课程里边会涉及到相应的内容


    貌似应该是 汇编语言/计算机体系结构
    计算机组成原理可能也会有

  32. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 13:15:00

    @winter-cn
    我仔细看的部分是最后的测试题,每道都是鬼扯……
    最多有1、2个小题没有什么纰漏

    那本书的特色就是,粗看乍看不错。所以流传很广,害人很深。
    我一开始看也被蒙了。
    仔细一看满篇显摆,全是纰漏,没得一点严谨治学的态度。

  33. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 13:21:00

    OwnWaterloo:
    @winter-cn
    我仔细看的部分是最后的测试题,每道都是鬼扯……
    最多有1、2个小题没有什么纰漏

    那本书的特色就是,粗看乍看不错。所以流传很广,害人很深。
    我一开始看也被蒙了。
    仔细一看满篇显摆,全是纰漏,没得一点严谨治学的态度。


    其实这个问题要看以什么视角去看 我现在虽然觉得里面有些说法不算正确 但是我仍然觉得它们带给当时的我很多收获
    我后来看过TC++PL 看过Exceptional三卷本 看过Inside the C++ Object model 看过imperfect C++

    比起经典 高质量XXX自是差了很多 但是我认为没必要因此否定它

  34. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 13:32:00

    winter-cn:
    比起经典 高质量XXX自是差了很多 但是我认为没必要因此否定它


    写一本书,如果是真心想传道授业解惑,一定要有严谨的治学态度。
    你可以说不知道,可以说这是你的推测,可以说在什么地方可以找到参考资料或深层次的讨论。

    但《高》不是。
    它仅仅是为了显示自己很牛逼,将自己的胡乱猜测随意放进书里。
    就从这种态度上,这就是一本很垃圾的书。
    和前段时间被批评的一本js的书没什么两样。

  35. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 13:41:00

    @winter-cn
    我就仅举2个例子(还有很多很多,这2个特别明显的突出了林博士的写作态度 —— 吹嘘)。

    char* p = new char[len+1];
    ...
    delete [] p; // 因为char是基本类型,所以也可以delete
    


    p = new char[len + 1]; // 有NULL检测更好!
    


    两个地方的注释部分,都是偷鸡不成、牛皮吹爆的典型。
    没能达到他所希望的目的 —— 显示他学识丰富。
    反而暴露了他的无知,以及治学态度 —— 他根本对自己说的话不负丝毫责任,不作深入考究,就将自己的推测写进书里。


  36. 韦恩卑鄙 alias:v-zhewg
    *.*.*.*
    链接

    韦恩卑鄙 alias:v-zhewg 2009-11-17 13:44:00

    winter-cn:

    亚历山大同志:应该是在大学 操作系统 的课程里边会涉及到相应的内容


    貌似应该是 汇编语言/计算机体系结构
    计算机组成原理可能也会有


    为什么你们提到的课程 我都挂了呢?
    oh 我除了语文英语那年全挂了。。。
    计算机系的文科生

  37. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 14:01:00

    我觉得呢 水了这么多 还是上段代码支持下老赵比较好 x86上显然如老赵所述 栈是向下生长 大家闲的时候可以用VS在自己家的PC上跑跑下面的代吗看看:

    #include <stdio.h>
    int main()
    {
        int a = 0,b = 0,c=0;
        __asm
        {
            mov a,esp;
            push 0;
            mov b,esp;
            pop eax;
            mov c,ebp;
        }
        printf("esp(before push):%d\nesp(after push):%d\nebp:%d\n",a,b,c);
    }
    


    不过这个栈的生长方向 其实是依赖硬件的,换作是ARM CPU的话……噢,不好意思 ARM CPU没有栈这个概念 但是可以通过LDM和STM来实现相同的功能,所以栈的生长方向就归编译器说了算了。

    再所以老赵提的这个问题就不能算作者错,但是既然扯到了ebp,我确实也没听说过除了x86什么指令集有ebp,但是我没听过不等于没有,说不定哪个小工厂或者试验室生产的山寨机就搞了一个这样的指令集,咱也没办法。况且现在没有不等于以后没有,总之客观上,如果作者自己不觉得这是笔误,确实无法说这是一个错误。

  38. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 14:13:00

    OwnWaterloo:
    @winter-cn
    我就仅举2个例子(还有很多很多,这2个特别明显的突出了林博士的写作态度 —— 吹嘘)。

    char* p = new char[len+1];
    ...
    delete [] p; // 因为char是基本类型,所以也可以delete
    

    p = new char[len + 1]; // 有NULL检测更好!
    

    两个地方的注释部分,都是偷鸡不成、牛皮吹爆的典型。
    没能达到他所希望的目的 —— 显示他学识丰富。
    反而暴露了他的无知,以及治学态度 —— 他根本对自己说的话不负丝毫责任,不作深入考究,就将自己的推测写进书里。


    错误很多我知道 就我个人的经历 我觉得我并没有受它们的影响 至于作者抱着怎样的态度 我觉得评价事可以严厉 评价人还是应该nice一点

  39. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 14:49:00

    @winter-cn
    我被它影响过…… 所以怨念很深……
    广大程序员正在受它影响……

    那你觉得关于这2处注释,应该怎么评价才比较中肯呢?
    结合书中其他部分作者很多自负的话,我觉得这两处说他是牛皮吹爆也还算过得去吧……?

  40. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 14:59:00

    @OwnWaterloo
    考虑到广大程序员的普遍水平 呃......
    觉得他就是这么理解的 所以就写错了 仅此而已

    PS.这句也有问题?
    // 有NULL检测更好!

  41. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 15:04:00

    @winter-cn
    我的组里要是有"敢混用delete 和delete[]"的人在,我还真不敢用他……

    直到现在,你可以去看看诸多cpp的社区,chinaunix、csdn等,争论这个问题的人一票一票的。
    所以我说这本书害人甚深。

    p = new char[len + 1];
    assert( p ); // 如果能执行到这里,绝对是个有效的指针。
    // 否则就抛异常,离开正常执行路径了
    


    林博士所认识的C++、其实也就是VC++6.0而已。
    但他可不会这么说,他说的一定是"标准C++"。

  42. AutumnWinter
    *.*.*.*
    链接

    AutumnWinter 2009-11-17 15:13:00

    为什么不去看英文版的Linkers and Loaders ?

  43. 老赵
    admin
    链接

    老赵 2009-11-17 15:16:00

    @AutumnWinter
    因为对我来说没必要。

  44. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 15:19:00

    OwnWaterloo:
    @winter-cn
    我的组里要是有"敢混用delete 和delete[]"的人在,我还真不敢用他……

    直到现在,你可以去看看诸多cpp的社区,chinaunix、csdn等,争论这个问题的人一票一票的。
    所以我说这本书害人甚深。

    p = new char[len + 1];
    assert( p ); // 如果能执行到这里,绝对是个有效的指针。
    // 否则就抛异常,离开正常执行路径了
    

    林博士所认识的C++、其实也就是VC++6.0而已。
    但他可不会这么说,他说的一定是"标准C++"。


    关于new的问题 恰好说 我是做CE的 CE上面就是这样子的......

  45. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 15:28:00

    @winter-cn
    我不是说new一定、必须、像C++标准所说那样,得抛异常。
    确实存在一些不符合标准的C++实现。这是现实情况。

    但是,他可以说VC++6.0嘛,可以说WinCE嘛。
    可他在题目开篇就说了"标准C++、假设32位平台"。

    他分明就是不懂装懂。
    在他头脑中、C++语言、C++语言实现、C++方言、标准库、平台相关调用根本就是一体的,很有可能就是指的VC++6.0。

  46. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 15:34:00

    @OwnWaterloo
    以前听人说起谭老的书 仍然有人觉得不错的 至少技术上林博士比谭老强些吧。
    我觉得评价一本书你不能只看它犯了多少错误,高质量XXX你读了有收获就可以了

  47. a12345[未注册用户]
    *.*.*.*
    链接

    a12345[未注册用户] 2009-11-17 15:41:00

    如果记得没错,cpu指令、寻址等这些是计算机体系结构、汇编语言里的基础性内容。delete 删除数组将导致内存泄漏

  48. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 15:41:00

    @winter-cn
    谭浩强老师比他谦虚。
    对于谭浩强老师,我觉得有个评价是很中肯的:“扫盲”。

    指出书中的错误(而且是硬伤、不是笔误)这个行为,难道本身是错的?而且是一本流传甚广的书。
    你可以选择对他的错误隐忍,没问题。
    我选择将他的错误揭示出来,以免继续毒害广大的没有辨别能力的读者,我觉得我的作法也没有什么问题,不是这样吗?

    而且,这本书对我来说确实没有一点点收获。
    我凭什么要对它说哪怕一点点赞扬的词?

  49. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 15:51:00

    a12345:
    delete 删除数组将导致内存泄漏



    不是内存泄露那么简单。是未定义行为
    语言实现可以对这种作法采取任何应对措施。
    这是理论上的。

    从实际上来说,很少有运行库让delete,和delete[]调用不同的底层实现,char也确实又不需要析构。
    但万一出现这么一个运行库让delete,delete[]调用不同的底层实现呢?
    为什么要铤而走险,让自己的程序变得没有担保呢?


    我对这种做法的批评主要是从道德上的。
    别人提供给你一个接口、并撰有文档,告诉你应该怎么使用。
    为什么你要违反与接口提供者的约定、私自采用一些不符合接口规范的作法?
    而且,不为别的,仅仅是因为自己臆测、觉得可以这么做


    多敲一对方括号很难吗?类似的,多写一句va_end很难吗?
    为什么要为了显摆自己那点微薄的知识故意将它们去掉?


    玩火自焚。

  50. 老赵
    admin
    链接

    老赵 2009-11-17 15:53:00

    把这本书看完了,呵呵。
    兄弟们继续讨论,我在一边看着。:)

  51. 老赵
    admin
    链接

    老赵 2009-11-17 16:02:00

    上面一位朋友的评论嵌套出问题了,于是删了,不好意思啊。

  52. a12345[未注册用户]
    *.*.*.*
    链接

    a12345[未注册用户] 2009-11-17 16:27:00

    OwnWaterloo:

    a12345:
    delete 删除数组将导致内存泄漏



    不是内存泄露那么简单。是未定义行为
    语言实现可以对这种作法采取任何应对措施。
    这是理论上的。

    从实际上来说,很少有运行库让delete,和delete[]调用不同的底层实现,char也确实又不需要析构。
    但万一出现这么一个运行库让delete,delete[]调用不同的底层实现呢?
    为什么要铤而走险,让自己的程序变得没有担保呢?


    我对这种做法的批评主要是从道德上的...



    确实,c++陷阱很多,一不小心就会导致 未定义行为。不过正因为复杂,所以不少大牛可以写书赚钱

  53. winter-cn
    *.*.*.*
    链接

    winter-cn 2009-11-17 16:29:00

    OwnWaterloo:
    @winter-cn
    谭浩强老师比他谦虚。
    对于谭浩强老师,我觉得有个评价是很中肯的:“扫盲”。

    指出书中的错误(而且是硬伤、不是笔误)这个行为,难道本身是错的?而且是一本流传甚广的书。
    你可以选择对他的错误隐忍,没问题。
    我选择将他的错误揭示出来,以免继续毒害广大的没有辨别能力的读者,我觉得我的作法也没有什么问题,不是这样吗?

    而且,这本书对我来说确实没有一点点收获。
    我凭什么要对它说哪怕一点点赞扬的词?


    当然你可以揭出来 我主张的是对事不对人 就是说 我希望你的评论是“这个地方犯了一个非常幼稚的错误,标准明确规定.......而书中......” 而不是
    “作者真是愚蠢至极,......这根本就是炫耀自己的知识而已......为了几块钱稿费......”
    说实话看过的都知道,C++那spec写的真可谓张牙舞爪,所以做C++的大家的大部分知识都来自于实践和一些书籍,口口相传,理解错误也不是死罪。你说的错误当然是存在,但是"显摆自己那点微薄的知识"、"故意"这个评价恐怕还是不妥。

    一句话Be strict to ererything. Be kind to everyone.

  54. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-17 16:31:00

    OwnWaterloo:

    a12345:
    delete 删除数组将导致内存泄漏



    不是内存泄露那么简单。是未定义行为
    语言实现可以对这种作法采取任何应对措施。
    这是理论上的。

    从实际上来说,很少有运行库让delete,和delete[]调用不同的底层实现,char也确实又不需要析构。
    但万一出现这么一个运行库让delete,delete[]调用不同的底层实现呢?
    为什么要铤而走险,让自己的程序变得没有担保呢?


    我对这种做法的批评主要是从道德上的...




    虽然我觉得我说啥这位大神都能加以攻击,并仍然抱有这位大神心态有问题的“偏见”。但客观的说:


    不是内存泄露那么简单。是未定义行为。
    语言实现可以对这种作法采取任何应对措施。
    这是理论上的。

    从实际上来说,很少有运行库让delete,和delete[]调用不同的底层实现,char也确实又不需要析构。
    但万一出现这么一个运行库让delete,delete[]调用不同的底层实现呢?
    为什么要铤而走险,让自己的程序变得没有担保呢?


    我对这种做法的批评主要是从道德上的。
    别人提供给你一个接口、并撰有文档,告诉你应该怎么使用。
    为什么你要违反与接口提供者的约定、私自采用一些不符合接口规范的作法?
    而且,不为别的,仅仅是因为自己臆测、觉得可以这么做。


    这一段话我只能说完全赞成。

    当然,这个回复的最后两句是不认同的。

  55. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 16:38:00

    a12345:
    确实,c++陷阱很多,一不小心就会导致 未定义行为。不过正因为复杂,所以不少大牛可以写书赚钱


    学C++的赶快转行,节省脑细胞和书钱~_~

  56. 老赵
    admin
    链接

    老赵 2009-11-17 16:40:00

    @OwnWaterloo
    或者学好然后写书赚钱?

  57. a12345[未注册用户]
    *.*.*.*
    链接

    a12345[未注册用户] 2009-11-17 16:46:00

    @Jeffrey Zhao
    可能有点晚吧?现在需要用c++的地方越来越少了,学的人也会少吧。再过几年,会用c++的说不定成珍稀物种了

  58. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 16:54:00

    winter-cn:
    当然你可以揭出来 我主张的是对事不对人 就是说 我希望你的评论是“这个地方犯了一个非常幼稚的错误,标准明确规定.......而书中......” 而不是
    “作者真是愚蠢至极,......这根本就是炫耀自己的知识而已......为了几块钱稿费......”
    说实话看过的都知道,C++那spec写的真可谓张牙舞爪,所以做C++的大家的大部分知识都来自于实践和一些书籍,口口相传,理解错误也不是死罪。你说的错误当然是存在,但是"显摆自己那点微薄的知识"、"故意"这个评价恐怕还是不妥。



    作者是否真的是"显摆自己那点微薄的知识",以及是否"故意",这个推测可以交给心理学家和语言学家鉴定。
    但即使交给他们,得出的也是他们的看法,只是比较权威而已。
    在我看来,作者的意图就是这样……

    为什么要硬生生的写这么一句"因为char是... 所以可以... "?
    "有NULL检测更好"?

    我对他的评价不会改变,除非有什么新的情况发生……


    当然,你说的对,措辞要适当。
    其实我写了一些材料,大致叫《anti-高质量xxx》,里面就不是从批评作者的角度,而是从指出正确作法角度写的。估计那里面的措辞你会看得惯一些。。。
    写了一些,觉得文字软绵绵,就不高兴写了…… (其实是没时间、而且气消了……)

    不过嘛…… 一方面,这里是评论,不是嘛…… 所以口无遮拦了一些。
    另一方面,我也没有必要在cnblogs这里指出《高质量xxx》中的错误吧……
    我在这里提到这本书的唯一目的,就是批评这个作者治学态度的(看3楼评论)……
    而不是讲述这本书到底有什么错…… 相信也没多少人感兴趣……



    ISO C++确实不值得程序员花实现去看,那是写给做编辑器的家伙看的。
    当然,爱好者可以看看。
    除了ISO C++,还有这么多书籍啊?
    《the c++ programming langauge》林锐看过没?
    《c++ primer》林锐看过没?
    《effective c++》林锐又看过没?
    没有哪一本C++初级教程不会提到delete[],delete不能混用。effcpp更是提到这是未定义行为。
    只要教程提到异常,就不会放过new报告失败的方式。tc++pl、c++primer都有提到。
    他写书有参考这些资料吗?

    不是说不可以凭自己的感悟写书。
    可以写,明确说明"这是我的感悟"。
    他为什么要把自己的感悟,提升到"标准C++"? 他有这个资格么?

    所以,他治学态度确确实实是很糟糕。

  59. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-17 16:55:00

    话说绝大多数语言都有陷阱的,就是说句话都有误解的可能,最好的办法就是保持自己无知的态度,别去猜,去规范里面找答案。

  60. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 16:56:00

    更正一下,C++老豆的《the c++ programming language》算是C++圣经,但不能当作初级读物看……


  61. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 16:58:00

    Jeffrey Zhao:
    或者学好然后写书赚钱?


    真要写本好书,即使赚钱,也相当辛苦吧……

  62. 老赵
    admin
    链接

    老赵 2009-11-17 16:59:00

    @OwnWaterloo
    我随口一说的……在国内,别想用写书赚钱……真赚了点小钱,你名声基本上就臭掉了。

  63. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 16:59:00

    a12345:
    可能有点晚吧?现在需要用c++的地方越来越少了,学的人也会少吧。再过几年,会用c++的说不定成珍稀物种了


    会减少少,但是可能不会消亡。
    除非出现另一门抽象与效率兼顾的语言。

    个人看法,勿信、勿怪……

  64. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-17 17:02:00

    Jeffrey Zhao:
    @OwnWaterloo
    我随口一说的……在国内,别想用写书赚钱……真赚了点小钱,你名声基本上就臭掉了。




    但是名声臭掉不影响下本书的销量。。。。。。

  65. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 17:05:00

    Ivony...:
    虽然我觉得我说啥这位大神都能加以攻击,并仍然抱有这位大神心态有问题的“偏见”。但客观的说:

    这一段话我只能说完全赞成。

    当然,这个回复的最后两句是不认同的。



    我不期待你会认同我的任何观点,我也不是为了说服你的。

    你会居然赞同我的观点……
    我应该感到惊奇呢?还是荣幸呢?还是应该出门看看是否2012提前到了……

  66. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 17:07:00

    Ivony...:话说绝大多数语言都有陷阱的,就是说句话都有误解的可能,最好的办法就是保持自己无知的态度,别去猜,去规范里面找答案



    Ivony...这个号是多人共用?
    你是双子座的?
    或者是仙水式的奇才?

  67. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 17:30:00

    Jeffrey Zhao:
    我随口一说的……在国内,别想用写书赚钱……真赚了点小钱,你名声基本上就臭掉了。



    我见过最贵(没有之一)的一本(此处加不加C++定语都无妨……)书……
    http://www.china-pub.com/2022561

    估计这本外国书在国内也赚不到钱……
    相信这种价格的书都会买的人,是少数……

  68. 老赵
    admin
    链接

    老赵 2009-11-17 17:35:00

    @OwnWaterloo
    原版自然贵,自然没人买……

  69. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-11-17 17:45:00

    其实我想说,LZ的照片啥时候换的?怎么没看出减肥的成果?

  70. 老赵
    admin
    链接

    老赵 2009-11-17 17:50:00

    @Ivony...
    两小时前。
    减肥成果是显著滴,俺现在强壮滴很。
    只可惜我是属大脸猫的没办法……脸要对照着看比较明显。

  71. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-17 18:15:00

    说来惭愧,我面试时被问到的问题便是delete[]和delete有什么区别。我很想说delete不能用于数组(因为我是看TC++PL学习的)。可是面试官期待的答案的确是内存均可被解除分配,差别仅为析构函数……

  72. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-17 18:22:00

    老赵下次要让我给照相啦。。。我会给你照出脸部的层次感的。呵呵

  73. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 18:35:00

    装配脑袋:说来惭愧,我面试时被问到的问题便是delete[]和delete有什么区别。我很想说delete不能用于数组(因为我是看TC++PL学习的)。可是面试官期待的答案的确是内存均可被解除分配,差别仅为析构函数……


    我也有类似的经历,但不是delete,delete[]的问题。

    我差不多猜到他期待的答案是什么,而他期待的答案是错的……
    我就真没按他想的说……
    也没和他多解释……
    然后被拒了……

  74. 老赵
    admin
    链接

    老赵 2009-11-17 19:02:00

    装配脑袋:老赵下次要让我给照相啦。。。我会给你照出脸部的层次感的。呵呵


    脑袋什么都那么厉害,哎,崇拜……

  75. 老赵
    admin
    链接

    老赵 2009-11-17 19:18:00

    贴照片,及对比照,娃哈哈

  76. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-11-17 20:11:00

    Jeffrey Zhao:贴照片,及对比照,娃哈哈




    汗。。。。这是Pike Place的那头猪

  77. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-17 20:50:00

    hoodlum1980:
    你是不是太武断了一点,你确信他的理解是错的吗?
    另外以下两段注释有何问题?



    他是指的谁? 林锐?还是我的面试官?

    两段注释的问题,楼上有解释,回帖先看贴,谢谢。

  78. 骑着夕阳看着猪
    *.*.*.*
    链接

    骑着夕阳看着猪 2009-11-18 16:17:00

    装配脑袋:

    Jeffrey Zhao:贴照片,及对比照,娃哈哈




    汗。。。。这是Pike Place的那头猪





    bu za

  79. live41
    *.*.*.*
    链接

    live41 2009-11-20 17:51:00

    笑si我了,你还给书拍照..

  80. 老赵
    admin
    链接

    老赵 2009-11-20 22:42:00

    @live41
    呵呵,不拍照还能怎么搞……

  81. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-22 21:55:00

    Umm...一个月前,第一次在书店见到这本书的时候就想读了。稍微翻了几页都还OK。可惜刚过的这个月各种事情忙死(但几乎没产出,感觉就是白忙),都还没来得及顾上这事 T T
    想问一下已经读过这本书的人:它在讲解各个概念的时候,有没有在举例前以x86为前提?不限定适用场景的例子只能带来混乱,而如果书上有指定以x86为前提的话,

    PUSH
    Decrements the stack pointer and then stores the source operand on the top of the stack.


    IA-32手册如是说。x86硬件支持的调用栈的增长方向就是向低地址方向的,不可配置,不可变。如果书上没有说明是以x86为前提的话,那书上的例子就无法说是正确的了。
    ARM的话,虽然R13常用为stack pointer,但并不是一定要用它(不过用到Thumbz指令集的是要用R13为SP的);ARM上也没有专用的PUSH/POP指令,而是可以用各种LD/ST指令来模拟。这样当然就可以自由指定栈的增长方向,只要你指定的calling convention同时由OS和应用程序所遵守就OK。

    另外,关于栈的布局问题,不知道书上在提供例子之前有没有所用的calling convention?如果没有的话那就糟糕了……不指定calling convention根本就无从谈起到底什么offset是“第几个参数”。例如x86上MSVC提供的fastcall是把头两个32位或更小的参数放在ECX和EDX里,剩余的参数从右向左压栈(这里暂时不讨论浮点数参数的情况);而32位x86上的CLR中的JIT calling convention与fastcall相似,只不过剩余参数压栈顺序是从左到右。Delphi用的register calling convention是把头三个参数放在EAX、EDX,、ECX里,剩余参数从左到右压栈。所以我想知道书上有没有说清楚。

    我自己在画内存布局的图的时候是习惯把低地址画在“上面”,高地址画在“下面”。毕竟我用的调试器也是这样显示数据的(包括栈数据)。这样的话,“栈顶”就确实是在“上面”,而各种header啊啥的也都在“上面”。
    但这习惯跟很多人都相反,所以当我看到别人说“某某数据保存到某某对象的‘上面’”的时候会感到特别困惑。还是直接说“低地址”和“高地址”清晰些。

    MSVC在编译时,如果指定要优化,会把一些函数的frame pointer(在32位x86上也就是对EBP)的操作省略,称为frame pointer omission(FPO),对应的参数是/Oy。/O1、/O2和/Ox都隐含包括/Oy。
    在8086上,SP不能用于间接访问,所以mov ax, [sp]这种是不行的。BX、SI、DI和BP是index寄存器,可以用于间接访问;其中BP是专门用来做frame pointer用的。所以传统上BP都是得用到的。当然到80386之后,所有通用寄存器都可以用来间接访问,这个就不是问题了。
    但有些时候frame pointer是比较有用的。一个stack frame的底部在该frame存活的周期内都在固定的地址上,但其顶部就未必了。像CRT的alloca()函数可以在栈上申请空间;所谓“申请”也就是改一下stack pointer而已。当栈顶的地址可变时,局部变量和参数就无法从stack pointer出发以固定offset去访问到。与其让编译器去计算stack pointer变了之后的offset是多少,还不如直接让它生成代码根据frame pointer出发的offset来访问方便些。
    另外,x86上常规的使用EBP的方式使得调用栈形成了以stack frame为单元的链表,使调试变得方便,各种其它需要做stack walking的操作也变得方便。对于叶函数(不再调用别的函数的函数)而言,维护frame pointer链是冗余的,省略掉也不会有多少问题。

  82. 老赵
    admin
    链接

    老赵 2009-11-22 22:04:00

    @RednaxelaFX
    好久不见啊

  83. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-22 22:12:00

    Jeffrey Zhao:
    @RednaxelaFX
    好久不见啊


    嗯,收拾行李,路上,入职各种培训,上周末回南京去跟同学聚会还丢了手机,总之各种事情不甚顺……刚过的这周五才第一次commit我写的bugfix,都到这边快一个月了 T T

    一些原因使得我老本上不了网,而新本上没有导入feed,所以这段时间消息有点断线 T T

  84. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-22 22:40:00

    RednaxelaFX:
    想问一下已经读过这本书的人:它在讲解各个概念的时候,有没有在举例前以x86为前提?不限定适用场景的例子只能带来混乱,而如果书上有指定以x86为前提的话,

    PUSH
    Decrements the stack pointer and then stores the source operand on the top of the stack.


    IA-32手册如是说。x86硬件支持的调用栈的增长方向就是向低地址方向的,不可配置,不可变。如果书上没有说明是以x86为前提的话,那书上的例子就无法说是正确的了。



    关于x86上栈增长方向可配置,我可不是乱说的。
    我不会像Ixxx大牛一样张嘴打哈哈

    IA-32的手册你有看完吗? 还是只看了这一句?

    有兴趣去看看这里:
    http://bbs.chinaunix.net/viewthread.php?tid=1010016&extra=&page=6#message7540974


    我引用一下,防止链接不对:

    mik
    x86 的可以配置 stack 向上增长的:

    设置 stack segment descriptor 的 E 位, E 为 1 时:expand-down 这是 stack 的常用的方式
    E 为 0 时:expand-up 设为 0 时,使用 stack segment 与普通的 segment 一样


  85. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-22 23:09:00

    @OwnWaterloo
    罪过,我确实没完整的读过IA-32手册,对它的知识还不完整。这里也是冒着说错被拍砖的险把自己的理解写出来……不然不说出来的话错了还不知道更糟糕 T T

    不过在手册Vol 3A,3-14的部分有这么一段:

    For expand-down segments, the segment limit has the reverse function; the offset can range from the segment limit to FFFFFFFFH or FFFFH, depending on the setting of the B flag. Offsets less than the segment limit generate general-protection exceptions. Decreasing the value in the segment limit field for an expand-down segment allocates new memory at the bottom of the segment's address space, rather than at the top. IA-32 architecture stacks always grow downwards, making this mechanism convenient for expandable stacks.


    这段正好是讲expand-up和expand-down segment的。不知道实际上x86上expand-up的stack是啥样的呢?有例子不?

  86. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-22 23:59:00

    @RednaxelaFX
    关于这个应该是我搞错了。

    将x86和i386搞混了。

    x86、x86的说,要找台x86的机器还不容易…… 我肯定没有例子的……
    模拟器吧……?

    btw:谢谢你分享的手册~_~

  87. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-23 00:13:00

    @OwnWaterloo
    x86貌似只是以8086为基础的Intel的一系列体系结构的通称,包括80386(i386)、80486(i486)、Pentium(i586)以及后续版本,还有别的公司除的兼容版本,像Cyrix和AMD的。很长一段时间以来要是说支持平台包括x86的话,一般至少要兼容i386。新的AMD64和Intel64(不是IA-64)一般叫x86-64,所以一般说x86的时候都是指32位的版本。
    IA-32就是Intel对80386以后续x86系列32位体系结构的叫法。

  88. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-23 00:21:00

    @RednaxelaFX
    嗯,x86这个叫法很混乱。
    如果以软件对硬件的最低需求为准,说某个库支持i386,实际上支持i386以上的,也是很常见的。

    更常见的是说它支持x86,这其实就有点过了……
    很多时候最低需求其实是i386。

    硬件能兼容的最早软件,说某个硬件支持x86,应该就是指以前x86上的代码也能在这个硬件上跑吧?

    需要加以区别时我可能会想起用哪一个更准确,其他时候经常用错……
    上面应该是记混了。

    不过。。。要搜个x86的资料好像很困难……

  89. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-23 00:32:00

    @OwnWaterloo
    因为新的x86指令集总是老x86指令集的超集(只有非常少量的指令会在特殊情况下给出不同的结果),所以如果说支持i386的话,后面的i486、i586之类自然就是支持的。在Core 2上照样能跑针对i386编译的程序,不然我们现在很多程序在新机器上都动不了了
    要找32位x86的规范的话,读IA-32的手册没错的。
    64位有点怪,因为先是AMD出了x86-64的指令集(后来叫AMD64),Intel是从AMD那边买了使用权然后弄出IA-32e或者EMT-64,现在叫Intel64。这我就不太清楚看谁比较合适了……不过我是i饭,所以手册还是读Intel的版本 orz

  90. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-23 01:05:00

    @RednaxelaFX
    还是没找到比较权威的出处。 我已经尽力了……
    有提到x86栈生长可配置的人不少,mik是比较信得过的家伙。
    要不问问他?

  91. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-23 01:20:00

    @OwnWaterloo
    嗯拜托帮忙问问~我没有CU的帐号,懒……

  92. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-23 01:39:00

    @RednaxelaFX
    嗯,已经问了~_~

  93. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-25 00:01:00

    @RednaxelaFX
    结果已经出来了:


    2 年前的那个贴子里,我的回复确实是错误的。

    stack 是不能向上增长的。我在这里澄清一下。


    data segment descriptor 里的 E 标志,只是一个说明 limit 的计算方式:

    对于 expand-up 类型的 data segment 来说
    limit 定义上限。

    对于 expand-down 类型的 data segment 来说:
    limit 定义下限



  94. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-25 00:55:00

    @OwnWaterloo
    嗯,这点确认了之后我心里也稍微踏实些了。谢谢 ^_^

    ========================================================

    不过前面我问的问题没有读过这本书的人来帮忙解答一下么,关于书里有没有明确那几个点的?

  95. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-25 01:19:00

    @RednaxelaFX
    再看了看你的回帖。

    这书我就在书店随便翻了翻,买不起啊……
    这问题得问其他人……


    我只是发现msvc有时候不会生成保存ebp的代码。原来和/Oy有直接关系。
    学习了~_~

    那gcc上有相应的参数么?
    无论函数体多小,无论是-O2还是-O3,印象中都是要生成保存ebp的代码的。

  96. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-25 01:41:00

    @OwnWaterloo
    GCC 3.4.6和4.3.3的文档里都是这么写的:

    -fomit-frame-pointer
    Don't keep the frame pointer in a register for functions that don't need one. This avoids the instructions to save, set up and restore frame pointers; it also makes an extra register available in many functions. It also makes debugging impossible on some machines.

    On some machines, such as the VAX, this flag has no effect, because the standard calling sequence automatically handles the frame pointer and nothing is saved by pretending it doesn't exist. The machine-description macro FRAME_POINTER_REQUIRED controls whether a target machine supports this flag. See Register Usage.

    Enabled at levels -O, -O2, -O3, -Os.


    当然中间的版本也是……我只是正好抓到这俩链接而已。
    注意“functions that don't need one”。看来很多函数都是“need one”的范围内了 orz

  97. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-25 19:29:00

    @OwnWaterloo
    话说这个代码用GCC 4.3带上-fomit-frame-pointer编译:

    test_omit_frame_pointer.c
    
    int foo(int x, int y) {
      int z = x * y + x - y;
      return z;
    }
    
    int main() {
      foo(2, 3);
      
      return 0;
    }

    得到的代码就没有frame pointer:
    	.file	"test_omit_frame_pointer.c"
    	.text
    .globl _foo
    	.def	_foo;	.scl	2;	.type	32;	.endef
    _foo:
    	subl	$16, %esp
    	movl	24(%esp), %eax
    	incl	%eax
    	imull	20(%esp), %eax
    	subl	24(%esp), %eax
    	movl	%eax, 12(%esp)
    	movl	12(%esp), %eax
    	addl	$16, %esp
    	ret
    	.def	___main;	.scl	2;	.type	32;	.endef
    .globl _main
    	.def	_main;	.scl	2;	.type	32;	.endef
    _main:
    	leal	4(%esp), %ecx
    	andl	$-16, %esp
    	pushl	-4(%ecx)
    	pushl	%ecx
    	subl	$8, %esp
    	call	___main
    	pushl	$3
    	pushl	$2
    	call	_foo
    	addl	$8, %esp
    	movl	$0, %eax
    	addl	$8, %esp
    	popl	%ecx
    	leal	-4(%ecx), %esp
    	ret

  98. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-26 03:05:00

    @RednaxelaFX
    谢谢~
    我正准备转向xnix那套工具,但还很不熟悉……

    -fomit-frame-pointer
    It also makes debugging impossible on some machines


    这个,应该不包含i386吧?
    拿以前用vs(gdb一直不会用……)的经验来说,在release默认配置下调试也是用过的,release默认配置就使用/O2,以你上面提到的,/O2又会包含/Oy;也是可以看到很多东西的:调用栈、参数,好像auto变量的值是不对的。

    而我试了试在Debug默认配置中,启用/Oy,就几乎什么都能看到了……


    我猜想是不是这样,你看看对不对?
    1. 由一条指令的地址,就可以从符号表中查到该指令所在函数,以及该指令在函数中的位置——比如,是该函数的第几条指令。

    2. 知道函数中的位置,就可以计算出控制线到达该位置时,使用了消耗栈空间大小以及参数、返回地址在栈中的位置。
    毕竟,返回地址在进入函数时就保存在栈顶;执行时到某条指令前的栈操作的代码也是编译器生成的,应该可以计算出。

    3. 由返回地址,就可以得到caller中跳转到callee的地址,转第1步。

    4. 最开始指令地址由EIP得到。

    这样,整个调用栈就可以推导出来用于debugging了?

  99. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-27 01:39:00

    @OwnWaterloo
    在VS的VC那套环境中,调试信息都是保存在PDB(program database)文件里的。根据需要的不同,PDB文件中保存的数据也会有所不同。
    不过为了调试,基本上你会看到几种结构:数据类型信息(总大小,成员大小与偏移量等),函数地址与函数名字的映射关系,指令偏移量与源码行号间的映射,栈内偏移量与局部变量/参数/frame pointer/返回地址等的映射关系(这里可能附加变量存活区间的行号/指令偏移量信息),也有可能扩展到寄存器与变量的映射关系信息也保存。等等。
    每个函数在被调用时对应的栈帧的所有结构信息都是可以这样写到某个地方的,可以是在目标文件内,也可以像PDB文件一样放在外面。

    你可以试一下在VS里随便开个VC项目,在debug模式下把项目设置里C/C++里面的“调试信息格式”设为禁用,然后随便找个会执行经过的地方设个断点看看会怎样:程序就断不下来了,因为VS的调试器依赖于PDB文件提供调试信息。

    当调试器知道栈帧结构,而且栈帧里又保存了frame pointer的话,活动中的帧之前的每个栈帧都能很轻松被定位到,根据栈帧里保存的返回地址又可以知道前一个栈帧对应的是什么函数,然后可以知道前一个栈帧中返回地址的位置,再继续追溯上去,整个栈的状况就明了了。如果没有frame pointer,在没有调试信息的情况下就很难准确有效的追溯上去了。

    有个系列的blog可以一读,http://www.nynaeve.net/?p=91http://www.nynaeve.net/?p=97
    还有这个也很有趣,硬来:http://www.yosefk.com/blog/getting-the-call-stack-without-a-frame-pointer.html

  100. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-27 01:45:00

    RednaxelaFX:
    当调试器知道栈帧结构,而且栈帧里又保存了frame pointer的话 ...
    如果没有frame pointer,在没有调试信息的情况下就追溯不上去了...


    这是2种情况:frame pointer和pdb都存在;或者它们都不存在。

    另外2种情况。
    仅frame pointer存在,可以推算出整个调用栈,但参数与局部变量的值不可知。
    仅pdb文件存在,视其内容而定,最好的情况下,可以推算出整个调用栈上的所有内容。
    对吗?


  101. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-27 02:04:00

    另外2种情况。
    仅frame pointer存在,可以推算出整个调用栈,但参数与局部变量的值不可知。
    仅pdb文件存在,视其内容而定,最好的情况下,可以推算出整个调用栈上的所有内容。
    对吗?


    有些架构上有返回地址寄存器,但像x86就没有。这些硬件上的差异也可能影响讨论。
    限定在x86,前一条我觉得是对的,有FP可以知道整个调用栈中每个栈帧的分布,但每个栈帧内部的结构就无法猜了。后一条的话……如果整个程序里都没用过可变长栈帧,那就应该没问题,所以信息都能推出来;但如果用了可变长度参数列表,或者alloca()之类的,使得栈帧的大小在编译器无法确定的话,PDB里的信息就会不够用了——毕竟PDB里能保存的信息也就是编译时已知信息。

  102. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-27 02:15:00

    @RednaxelaFX
    其实我仅知道i386上的一些汇编…… 可以把我当作仅能看到i386那片天的青蛙……

    对,还有可变长参数和alloca这两种东西的存在,我考虑得很不仔细。

    alloca中最后一个a是什么意思?malloc是memory allocation,calloc是clean? clear? alloca中是auto
    其实这个问题不重要啦,我想问的是另外一个问题……
    (有些疑惑自己怎么都没想明白,好不容易逮住大牛了就得多问问^_^)
    这个问题就是……
    为什么c99标准支持运行时长度数组(我一直不喜欢叫它variable length array,这名字有歧义……),却不支持alloca?
    VLA和alloca难道不是一回事吗?
    VLA实现应该比alloca还麻烦一些吧? 比如,VLA需要在离开作用域时回收,alloca只要在函数返回时统一回收就可以了。

  103. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-27 03:03:00

    @RednaxelaFX
    或者换一种问法。


    c99是否是基于这样的考虑:VLA比alloca使用更容易?功能更强大?
    比如下面的代码:

    // 使用VLA
    void f(...)
    {
            ...
            for (int i=0;i<n;++i) {
                    size_t s = calculate_size( i );
                    char buf[ s ];
                    g( buf, s );
            }
            ...
    }
    
    // 使用alloca
    void f(...)
    {
            ...
            for (int i=0;i<n;++i) {
                    size_t s = calculate_size( i );
                    char* buf = alloca( s );
                    g( buf, s );
            }
            ...
            // 在f退出前,循环中alloca分配的内存不会回收
    }
    
    // 通常需要改为如下的形式:
    R h(size_t s)
    {
            char* buf = alloca( s );
            return g( buf , s );
    }
    void f(...)
    {
            ...
            for (int i=0;i<n;++i) {
                    size_t s = calculate_size( i );
                    h( s );
            }
            ...
    }
    

    所以c99只提供VLA、不提供alloca。


    那我的问题、或者说顾虑是:alloca的移植性真有传说中那么差吗?
    既然c99敢规定VLA,就可以猜测VLA能在各种平台下得到高效的支持 —— C语言标准应该不会将很低效的特性纳入其中。
    既然这样,alloca也应该能在各种平台下得到支持?
    或是我又有什么地方没有考虑到?


    这个问题很现实的……
    毕竟c89和c++目前都不支持VLA,但通常可以找到alloca。是否可以大胆使用alloca?
    为了一个临时的、生命周期非常短的buf去动用动态存储还是挺不爽的……

  104. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-27 03:45:00

    @OwnWaterloo
    GCC的文档说GCC版VLA跟C99版在细节上有细微的差别……嘛,总比VC到VC10也不支持VLA好。

    关于为什么不要用alloca(),可以参考C语言FAQ,http://www.c-faq.com/malloc/alloca.html
    里面还有一个附加链接:http://www.c-faq.com/malloc/alloca.glb.html

  105. OwnWaterloo
    *.*.*.*
    链接

    OwnWaterloo 2009-11-27 20:09:00

    @RednaxelaFX
    C语言FAQ里面对alloca的批评几乎是陈词滥调了。
    附加链接里面有一些新颖的说法: alloca可以被这样滥用,而VLA不行

    printf("%p %p %p %p %p\n", alloca(1), alloca(2), alloca(10), 
    	alloca(8), alloca(20));
    


    这确实是VLA的一个优势,避免alloca的一些混乱的用法。

    但其他的说法,我觉得仍然站不住脚。


    alloca cannot be written portably, [footnote] and is difficult to implement on machines without a conventional stack.


    那在这种机器上,如何实现VLA呢?

    还有这个链接上提到的一个问题:
    http://stackoverflow.com/questions/1018853/why-is-alloca-not-considered-good-practice

    alloca可能会引起栈溢出。那VLA不也同样会引起么?
    char overflow[(size_t)-1];
    这条理由不足以称为alloca的缺点,因为它的替代品同样需要考虑栈溢出的问题。


    可能需要研究一下VLA和alloca的实现到底有什么不同(尤其是不具有栈结构的机器上),才能清晰解释这个问题……


    谢谢你提供这些材料~

  106. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-11-27 21:08:00

    @OwnWaterloo
    我不是特别肯定纯基于栈的VLA与alloca()除了直接调整栈指针之外还有什么别的实现办法;主要问题是会不会需要在分配空间的过程中把一些状态(例如当前栈指针的值之类的)保存下来。就像在x86上要在API里提供一个调试断点函数的话,例如MSVC提供的__debugbreak(),那基本上就是用int 3指令来做,就这么一条路,虽然包装它的细节或许会不同。
    在体系结构没有提供对栈的直接支持时,自己通过代码实现一个调用栈出来是很常见的事。如果碰到硬件本身不直接支持frame pointer的状况,自己通过代码也是可以模拟出来的。所以你会看到有人说“为了实现XXX功能,我额外使用了YYY大小的栈空间”,就是弥补系统原生能力的不足。

    我也得多学习多接触不同的体系结构,实际去写点汇编来感受一下才行。现在我的视野还是太受x86的限制了,ARM编程的经验也非常少。

  107. lummar[未注册用户]
    *.*.*.*
    链接

    lummar[未注册用户] 2009-11-30 12:00:00

    确实应该是错误,而不是流派的问题,图跟描述对不上
    可以看一下函数头
    push ebp
    mov ebp, esp

    不管怎样,ebp最后应该指向old ebp或者old ebp的边缘
    ret addr
    ebp____old ebp
    saved regs
    local vars

  108. lummar[未注册用户]
    *.*.*.*
    链接

    lummar[未注册用户] 2009-11-30 12:01:00

    晕,怎么空格都被k掉了

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我