孔乙己之二----瞎扯是不对的

来源:互联网 发布:源码安装deepin wine 编辑:程序博客网 时间:2024/04/29 11:06
本文作者:sodme
本文出处:http://blog.csdn.net/sodme
声明: 本文可以不经作者同意, 任意复制, 转载, 但任何对本文的引用都请保留文章开始前三行的作者, 出处以及声明信息. 谢谢.

在前文中, 已经提到, 有时, 对于下标1的访问偶尔的时候却是正确的, 这很让人费解, 笔者当时在未经过大脑思考的情况下甚至还猜测是不是地址对齐引起的. 事后想来, 这种猜测未免太过儿戏了, 不仅不严谨, 而且还让人怀疑起自己的判断力, 其实, 问题并不复杂, 结果正确, 只是偶尔发生的, 其原因应该是那段内存没有初始化, 其值可能是个随机值, 而正好又随机到了正确的值. 但是, 本着追跟究底的精神, 笔者决定继续孔乙己下去, 通过对比分析C++和ASM代码来确定问题的原因所在, 也与读者一起了解对象以及对象的函数调用中所发生的诸多细节..

注: 本文所用c++及asm代码通过以下url访问或下载, 代码使用gcc 4.2, 在fc 4.0下编译. 为控制首页篇幅, 请通过以下url访问和下载代码(为顺利理解本文, 请务必结合源码阅读): http://sodme.dev.googlepages.com/kyj_02_code.txt

利用ASM跟踪程序时, 弄清楚堆栈变化是理清程序逻辑的基本前提, 为此, 我们看一下main函数中头部这些语句:
main:
    leal    4(%esp), %ecx
    andl    $-16, %esp
    pushl    -4(%ecx)
    pushl    %ebp
    movl    %esp, %ebp
    pushl    %ebx
    pushl    %ecx
    subl    $32, %esp

执行之后, 堆栈的状况:
        |--------|
esp-->  |        | -40
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        | -12
        |--------|
        |  ecx   | -8
        |--------|
        |  ebx   | -4
        |--------|
        |old ebp | <--ebp
        |--------|
        |old esp |
        |--------|
        |        |
        |--------|

进入真正的main函数执行体之前, 程序先申请了32个字节的空闲堆栈空间, 这段空间的作用下面会看到.

还是先从asm的main函数体开头的几条语句开始:
    movl    $8, (%esp)
    call    _Znwj
    movl    %eax, %ebx
    movl    %ebx, (%esp)
    call    _ZN7MyClassC1Ev
其中, _Znwj的功能, 是完成对象空间的申请, 申请空间时需要的参数会预先存在栈顶, 这里传的参数值是8, 这个值的大小等于sizeof(MyClass), 申请成功之后的对象首地址(也即this指针)会保存在eax中. 接着, 将this指针存入栈顶以用来调用类的构造函数_ZN7MyClassC1Ev. 对比下面对print函数调用的那几条asm代码来看, 构造函数的调用与类的其它成员函数(如print)一样, 除了调用时机上, 在其它方面, 并没有太大不同. 也就是说, 构造函数调用时, 也一样要传递this指针(貌似在说废话).

当调用完构造函数之后, 便开始第一次的print函数调用, 也就是执行这条C++语句: "pMyClass[0].print()", 它被翻译成了以下几条ASM语句:
    movl    %ebx, -12(%ebp)
    movl    -12(%ebp), %eax
    movl    %eax, (%esp)
    call    _ZN7MyClass5printEv
还是先看堆栈发生了怎样的变化:(在进入这段代码之前, ebx已经存放了对象的this指针)

        |--------|
esp-->  |  this  | -40
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |  this  | -12
        |--------|
        |  ecx   | -8
        |--------|
        |  ebx   | -4
        |--------|
        |old ebp | <--ebp
        |--------|
        |old esp |
        |--------|
        |        |
        |--------|

再接着, 进入print函数后, 执行了以下语句:
_ZN7MyClass5printEv:
    pushl    %ebp
    movl    %esp, %ebp
    pushl    %esi
    pushl    %ebx
    subl    $16, %esp

后, 堆栈变成了这样:
        |--------|
esp-->  |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |        |
        |--------|
        |  ebx   |
        |--------|
        |  esi   |  -52
        |--------|
ebp-->  |  ebp   |  -48
        |--------|
        |  eip   |  -44
        |--------|
        |  this  |  -40
        |--------|

执行以下语句:
    movl    8(%ebp), %eax
    movl    4(%eax), %esi
    movl    8(%ebp), %eax
    movl    (%eax), %ebx

之后, 此时, ebx中放的是this指针, 也即data1的地址, 而esi中放的是data2的地址, 即(&data1+4), 也即(this+4).

再往下的两条 call _ZNSolsEi 语句就分别是显示data1和data2值的了, 第一次调用时, 以ebx为参数, 第二次调用时, 以esi为参数.每次调用时, 在栈顶压的那个eax, 都分别是上次cout执行之后的返回值.

至此, 有关对象创建, 对象构造以及函数成员调用的整个过程我们都了解了, 但是, 还是没有解释数组下标越界调用的错误所在. 为了追查此问题, 我们回头再看main函数中对下标为1和下标为10000000两个数组元素的函数调用代码:
A:
    movl    -12(%ebp), %eax
    addl    $8, %eax
    movl    %eax, (%esp)
    call    _ZN7MyClass5printEv

B:
    movl    -12(%ebp), %eax
    addl    $80000000, %eax
    movl    %eax, (%esp)
    call    _ZN7MyClass5printEv

其中, A段代码是"pMyClass[1].print(); ", B段代码是: "pMyClass[10000000].print(); ". 将它们与""的调用代码:
    movl    %ebx, -12(%ebp)
    movl    -12(%ebp), %eax
    movl    %eax, (%esp)
    call    _ZN7MyClass5printEv

相比较后, 可以看到, 1和10000000对print()的调用, 实际上, 是把取出来的0的this指针作了一个加法, 以定位其相应的对象首地址. 但是, 在当前程序中, 由于我们只new了一个MyClass对象, 所以, 这些加出来的对象首地址实际上是不存在的, 是非法的. 那么, 对不存在对象的数据成员的引用也显然是非法的(由上面的分析可以看出, 数据成员的寻址实际上就是通过this+x来实现的). 至于其结果有时正确, 则也只能认为是碰巧而已了.


原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 左胸胸口疼痛怎么办 在外地上学学籍怎么办 偷渡后国内资产怎么办 手机系统太低怎么办 拍古装是短发怎么办 吞下一个李子核怎么办? 宝宝吞了荔枝核怎么办 美吉姆培训不过怎么办 1岁宝宝挑食怎么办 宝宝不愿意开口说话怎么办 自闭症孩子不爱学习怎么办 宝宝不独立走路怎么办 六个月宝宝不认人怎么办 小孩隔奶奶涨怎么办 小孩段奶奶涨怎么办 1岁半还不会说话怎么办 孩子嗓子哑了怎么办 小朋友嗓子哑了怎么办 4周岁宝宝拉肚子怎么办 小孩不肯拉小便怎么办 做销售不爱说话怎么办 我伤害了朋友怎么办 三岁发音不准怎么办 心里憋不住话怎么办 自己不长记性怎么办 孩子不愿意开口说话怎么办 孩子不爱开口说话怎么办 宝宝犟脾气不好怎么办 小孩说话不算话怎么办 孩子说话不算话怎么办 孩子故意不好好说话怎么办 小孩说话吐字不清楚怎么办 腿老是抽筋是怎么办 半夜睡觉脚抽筋怎么办 我不爱说话内向怎么办 小孩子吐字不清怎么办 宝宝前边头发少怎么办 宝宝咬嘴唇龅牙怎么办 小孩老是咬下唇怎么办 五月小孩掉下床怎么办 小孩说话夹舌头怎么办