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

来源:互联网 发布:a*算法代码 编辑:程序博客网 时间:2024/04/29 03:10
本文作者: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来实现的). 至于其结果有时正确, 则也只能认为是碰巧而已了.


原创粉丝点击