Delphi的对象机制浅探(转)

前几天开始阅读 VCL 源代码,可是几个基类的继承代码把我看得头大。在大富翁请教了几位仁兄后,我还是对Delphi对象的创建和方法调用原理不太清楚。最后只好临时啃了一下汇编,把Delphi对象操作的几个关键的方法勘察了一遍。

你可以通过以下链接知道我为什么要做这件事:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=2385681

这是我花费一个晚上的测试结果,更多的细节只能以后在学习中再去了解。

主要测试项目为:
⊙ 测试目标:查看 TObject.Create 的编译器实现
⊙ 测试目标:查看 constructor 函数中 inherited 的编译器实现
⊙ 测试目标:以 object reference 和 class reference 调用构造函数的编译器实现
⊙ 测试目标:考查 Object 和 Class 在调用 class method 时的编译器实现
⊙ 测试目标:考查 ShortString 返回值类型的函数没有赋值时编译器的实现


我把测试的细节记录在后文,一是自己留作参考,二是给对此有兴趣的朋友参考。其实更重要的是,大家可以帮忙检查我的分析有没有错误。我一直是用 Delphi 的组件拖放编程,真正的功底只是这几天阅读 Object Pascal Reference 和 VCL 得来的,汇编更是临时抱佛脚,所以错误难免。我清楚自己的水平,所以写下结论后非常担心。尽管如此,我的目的是为了学习,希望你发现错误后帮我指出来。

主要的结论是:
(*) TObject.Create确实是个空函数,Borland 并没有隐藏 TObject.Create 的代码。TObject.Create的汇编代码是由 constructor directive 指示编译器形成的,编译器对每个class 都一视同仁。
(*) dl 和 eax 是 constructor Create 实现的关键寄存器。Borland 将对象的创建过程设计得精妙而清晰(个人感觉,因为我不知道其他的语言比如C++是如何实现的)。
(*) 一个对象的正常的创建(Obj := TMyClass.Create)过程是这样的:
   1. 编译器保证第一个 constructor 调用之前 dl = 1
      编译器保证 inherited Create 调用之前 dl = 0
   2. dl = 1 时 编译器保证 Create 时 eax = pointer to class VMT
      dl = 0 时 编译器保证 Create 时 eax = pointer to current object
   3. 编译器保证任何层次的 constructor 调用后 eax = pointer to current object
   4. dl = 1 时 编译器保证 Create 调用 System._ClassCreate,并与 constructor 相同的方式使用 eax
      dl = 1 时 编译器保证 Create 调用 System._AfterConstruction,并且调用前后 eax = pointer to current object
      dl = 0 时 编译器保证 Create 不会调用 System._ClassCreate 
      dl = 0 时 编译器保证 Create 不会调用 System._AfterConstruction
   5. System._ClassCreate 中设置结构化异常处理,在 Create 即将结束时关闭结构化异常处理。
      如果出错则会(1)释放由编译器分配的内存(2)恢复堆栈至创建对象之前(3)调用 TSomeClass.Destroy。
(*) object reference 方式的 constructor 调用,编译器尝试实现为 inherited 调用,结果当然是错误。
(*) class method 的调用隐含参数 eax 为指向 VMT 的指针,不管是用 class 还是 object 方式调用,编译器都会正确地把指向 class VMT 的指针传递给 eax。


要读懂下文的测试过程,可能需要相关基础,推荐阅读 Object Pascal Reference 以下章节:
Parameter passing
Function results
Calling conventions (register缺省调用约定,constructor 和 destructor 函数必须采用 register 约定)
Inline assambly code
《Delphi的原子世界》非常值得一读。



以下是测试内容:

=================================================
⊙ 测试目标:查看 TObject.Create 的编译器实现
=================================================
⊙ 测试代码及反汇编代码:
procedure Test; register;
var
Obj: TObject;         
begin
          push ebp                    // 前2句用于设置堆栈指针
          mov ebp, esp
         push ecx                     //保存 ecx (无用的语句)
Obj := TObject.Create;
          mov dl, $01                 //设置 dl = 1,通知 TObject.Create 这是一次新建对象的调用
          mov eax, [$004010a0]         // 把指向 TObject class VMT 的指针存入 eax,
                                       //作为 TObject.Create 隐含的 Self 参数
          call TObject.Create          // 调用 TObject.Create 函数
          mov [ebp-$04], eax          // TObject.Create 返回新建对象的指针至 Obj
end;
          pop ecx                      // 恢复堆栈并返回
          pop ebp
          ret

⊙ TObject.Create 的反汇编代码:
                                      // 函数进入时 eax = pointer to VMT            (dl = 1)
                                                    eax = pointer to instance       (dl = 0)
                                      // 函数返回时 eax = pointer to instance
          test dl, dl                 // 检查 dl 是否 = 0
          jz +$08                     // dl = 0则跳至 @@1
          add esp, -$10                // 增加 16 字节的堆栈,每次调用 _ClassCreate 之前都会进行
                                      // 用于 System._ClassCreate 设置结构化异常处理 
          call @ClassCreate            // 调用 System._ClassCreate
        @@1:
          test dl, dl                  // 检查 dl 是否 = 0
          jz +$0f                      // dl = 0则跳到 end 结束过程
          call @AfterConstruction     // dl <> 0 则调用 System._AfterConstruction
                                       //(注意不是 TObject.AfterConstruction)
          pop dword ptr fs:[$00000000] ; fs:[0] 指向结构化异常处理的函数,此即取消最后一次的 try..except设置
                                      // 这个 try..except 在 System._ClassCreate 中创建
                                       // 用于在出错时自动恢复堆栈/释放内存分配/并调用 TObject.Free
          add esp, $0c                 // 恢复堆栈,注意只恢复了 12 字节的堆栈,还有4字节由上句 pop 了
          ret

注意:以上汇编代码中重复出现了 test dl,dl,说明 Borland 并没有特别对待 TObject.Create,TObject.Create确实是个空函数。TObject.Create的汇编代码是由 constructor directive 指示编译器形成的,编译器对每个class 都一视同仁。
注意:这段 TObject.Create 代码是在 PC 机上编译的结果,严格地说应该是在 Win32 操作系统上的实现之一。查看System._ClassCreate 就知道 Borland 还有其他的异常处理实现机制,产生的 TObject.Create 代码也不相同。

⊙ System._AfterContruction 函数的代码:
function _AfterConstruction(Instance: TObject): TObject;
begin
Instance.AfterConstruction;
Result := Instance;
end;

⊙ System._ClassCreate 函数的代码: //分配对象内存,初始化接口表
function _ClassCreate(AClass: TClass; Alloc: Boolean): TObject;
asm
        { ->    EAX = pointer to VMT      }
        { <-    EAX = pointer to instance }
        PUSH    EDX                    // 保存寄存器
        PUSH    ECX
        PUSH    EBX
        TEST    DL,DL                  // 如果 dl = 0 则不调用 TObject.NewInstance
        JL      @@noAlloc
        CALL    DWORD PTR [EAX] + VMTOFFSET TObject.NewInstance ; 调用 TObject.NewInstance
@@noAlloc:
{$IFNDEF PC_MAPPED_EXCEPTIONS}         // 设置 PC 架构的结构化异常处理
        XOR     EDX,EDX
        LEA     ECX,[ESP+16]
        MOV     EBX,FS:[EDX]
        MOV     [ECX].TExcFrame.next,EBX
        MOV     [ECX].TExcFrame.hEBP,EBP
        MOV     [ECX].TExcFrame.desc,offset @desc
        MOV     [ECX].TexcFrame.ConstructedObject,EAX   { trick: remember copy to instance }
        MOV     FS:[EDX],ECX
{$ENDIF}
        POP     EBX                     // 恢复寄存器
        POP     ECX
        POP     EDX
        RET

{$IFNDEF PC_MAPPED_EXCEPTIONS}          // 设置非 PC 架构的结构化异常处理
@desc:
        JMP     _HandleAnyException

{       destroy the object      }

        MOV     EAX,[ESP+8+9*4]
        MOV     EAX,[EAX].TExcFrame.ConstructedObject
        TEST    EAX,EAX
        JE      @@skip
        MOV     ECX,[EAX]
        MOV     DL,$81
        PUSH    EAX
        CALL    DWORD PTR [ECX] + VMTOFFSET TObject.Destroy
        POP     EAX
        CALL    _ClassDestroy
@@skip:
{       reraise the exception   }
        CALL    _RaiseAgain
{$ENDIF}
end;


==============================================================
⊙ 测试目标:查看 constructor 函数中 inherited 的编译器实现
==============================================================
⊙ 测试代码及反汇编代码:
type
TMyClass = class(TObject)
    constructor Create;
end;
constructor TMyClass.Create;
begin
    inherited; // 考查此句的实现
    Beep;
end;

procedure Test; register;
var
Obj: TMyClass;
begin
Obj := TMyClass.Create;
          mov dl, $01                 //class reference 时编译器设置 dl = 1
          mov eax, [$004600ec]        // 设置 eax 为指向 TMyClass 的 VMT pointer
          call TMyClass.Create        // 调用 TMyClass.Create
          mov [ebp-$04], eax          //保存 新建对象的指针
end;

constructor TMyClass.Create 的反汇编代码:
                                        // 函数进入时 eax = pointer to VMT            (dl = 1)
                                                      eax = pointer to instance       (dl = 0)
                                         // 函数返回时 eax = pointer to instance
begin
          push ebp                       // 这3句用于保存堆栈指针和创建堆栈
          mov ebp, esp
          add esp, -$08                  
          test dl, dl                    // 如果 dl = 0 则跳到 @ClassCreate 之后 @@1 处执行
          jz +$08
          add esp, -$10                  // 为 _ClassCreate 调用准备堆栈
          call @ClassCreate              // 调用 System._ClassCreate,执行完成后 eax = 新建对象的指针
       @@1:
          mov [ebp-$05], dl              // 将 dl 值保存到堆栈中的 1 字节中,因为后面的 inherited TObject.Create
                                        //可能会改变 edx 的值
          mov [ebp-$04], eax             //保存 eax 到堆栈, eax = pointer to instance
inherited;
          xor edx, edx                  // 将 edx 清零(dl = 0),以通知 TObject.Create 不用再调用
                                         // _ClassCreate 和 AfterConstructor (编译器实现)
          mov eax, [ebp-$04]            // 将 eax 的值还原为前面保存在堆栈的 eax 值
                                         // (这句是多余的,但在其它情况下可能必须执行此句)
          call TObject.Create           // 调用 TObject.Create 
Beep;
          call Beep                      // 继承类中 inherited 之后实现的功能
          mov eax, [ebp-$04]             // 将 eax 的值还原为前面保存在堆栈的 eax 值
          cmp byte ptr [ebp-$05], $00    ; (间接)检查 dl 是否 = 0
          jz +$0f                        // dl = 0 则跳过 _AfterConstruction 到 @@2 处
          call @AfterConstruction        // 调用 System._AfterConstruction
          pop dword ptr fs:[$00000000]  // 这2句恢复为 _ClassCreate 创建的堆栈空间
          add esp, $0c
       @@2:
          mov eax, [ebp-$04]             //返回 pointer to instance
end;
          pop ecx
          pop ecx
          pop ebp
          ret

结论:真是精妙!一个对象的正常的创建(Obj := TMyObj.Create, 与后面不正常的调用相对)过程是这样的:
   1. 编译器保证第一个 constructor 调用之前 dl = 1
      编译器保证 inherited Create 调用之前 dl = 0
   2. dl = 1 时 编译器保证 Create 时 eax = pointer to class VMT
      dl = 0 时 编译器保证 Create 时 eax = pointer to current object
   3. 编译器保证任何层次的 constructor 调用后 eax = pointer to current object
   4. dl = 1 时 编译器保证 Create 调用 System._ClassCreate,并与 constructor 相同的方式使用 eax
      dl = 1 时 编译器保证 Create 调用 System._AfterConstruction,并且调用前后 eax = pointer to current object
      dl = 0 时 编译器保证 Create 不会调用 System._ClassCreate 
      dl = 0 时 编译器保证 Create 不会调用 System._AfterConstruction
   5. System._ClassCreate 中设置结构化异常处理,在 Create 即将结束时关闭结构化异常处理。
      如果出错则会(1)释放由编译器分配的内存(2)恢复堆栈至创建对象之前(3)调用 TSomeClass.Destroy。

看上去有点繁杂,可是如果读懂了上面 TObject.Create 和 TMyObject.Create 则会感觉对象的创建非常清晰。



==================================================================================
⊙ 测试目标:以 object reference 和 class reference 调用构造函数的编译器实现
==================================================================================
⊙ static constructor 测试代码及反汇编代码 (省略了begin 和 end 后面的堆栈分配代码):
procedure Test; register;
var
Obj: TObject;         
begin
Obj := TObject.Create;
          mov dl, $01               // 采用 class reference 时编译器自动设置 dl = 1
          mov eax, [$004010a0]     // 把指向 TObject class VMT 的指针存入 eax,用于下一行调用
          call TObject.Create
          mov [ebp-$04], eax
Obj := Obj.Create;
          or edx, -$01             // 采用 object reference 时编译器自动设置 edx 的所有 bit 都为 1
          mov eax, [ebp-$04]        // 把 Obj 指针的所指的区域(即对象内存空间)存入 eax,用于下一行调用
          call TObject.Create       
          mov [ebp-$04], eax
end;

⊙ virtual constructor测试代码及反汇编代码 (省略了begin 和 end 后面的堆栈分配代码):
procedure Test; register;
var
Comp: TComponent;
begin
Comp := TComponent.Create(nil);
          xor ecx, ecx                    // 设置 参数 = nil
          mov dl, $01                     // 设置 dl = 1
          mov eax, [$00412eac]            // 设置 eax = class VMT pointer
          call TComponent.Create         // 调用 TComponent.Create
          mov [ebp-$04], eax              // 保存 新建的对象至 Comp
Comp := Comp.Create(nil);
          xor ecx, ecx                    // 同上
          or edx, -$01                    // 设置 edx 所有位为 1
          mov eax, [ebp-$04]              // 这句和下句 设置 ebx 为 TComponent class 的 VMT pointer
          mov ebx, [eax]                 // (如果 Comp 已经实例化了,则 ebx 的值是对的)
          call dword ptr [ebx+$2c]        // 可能是调用 TComponent.Create(Comp, -1, nil);
          mov [ebp-$04], eax              // 保存 新建的对象至 Comp
end;

结论:object reference 方式的 constructor 调用,编译器尝试实现为 inherited 调用,结果当然是错误。


=======================================================================
⊙ 测试目标:考查 Object 和 Class 在调用 class method 时的编译器实现
=======================================================================
⊙ 测试代码及反汇编代码 (省略了begin 和 end 后面的堆栈分配代码):
procedure Test; register;
var
Com: TComponent;
Str: String[255];
begin
Com := TComponent.Create(nil);
          xor ecx, ecx
          mov dl, $01
          mov eax, [$00412eac]             // eax = pointer to class VMT
          call TComponent.Create            
          mov [ebp-$04], eax
Str := Com.ClassName;
          lea edx, [ebp-$00000104]
          mov eax, [ebp-$04]                // eax = pointer to object
          mov eax, [eax]                   // eax = pointer to VMT
          call TObject.ClassName            
Str := TComponent.ClassName;
          lea edx, [ebp-$00000104]          // edx = address of Str
                                            ; ShortString 类型的返回值是以 var 类型的参数传递的
          mov eax, [$00412eac]              //eax = pointer to class VMT
          call TObject.ClassName
end;

结论:class method 的调用隐含参数 eax 为指向 VMT 的指针,不管是用 class 还是 object 方式调用,编译器都会正确地把指向 class VMT 的指针传递给 eax。


========================================================================
⊙ 测试目标:考查 ShortString 返回值类型的函数没有赋值时编译器的实现
========================================================================
procedure Test; register;
begin
TComponent.ClassName;
          lea edx, [ebp-$00000100]      //编译器会在堆栈中创建256 byte 的临时空间,以保证 edx 不会为非法值
          mov eax, [$00412eac]          
          call TObject.ClassName
end;

⊙ TObject.ClassName 函数代码:
class function TObject.ClassName: ShortString;
{$IFDEF PUREPASCAL}
begin
Result := PShortString(PPointer(Integer(Self) + vmtClassName)^)^;
end;
{$ELSE}
asm
        { ->    EAX VMT                         }
        {       EDX Pointer to result string    }
        PUSH    ESI
        PUSH    EDI
        MOV     EDI,EDX                 // EDX 是返回值串的指针
        MOV     ESI,[EAX].vmtClassName
        XOR     ECX,ECX
        MOV     CL,[ESI]                // 设置 result string 的 length
        INC     ECX
        REP     MOVSB
        POP     EDI
        POP     ESI
end;
{$ENDIF}

结论:这只是我想了解字符串返回值的传递方式。

===================
       (完)

转载于:https://www.cnblogs.com/keycode/archive/2010/10/13/1849859.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值