RELATEED CONSULTING
相关咨询
选择下列产品马上在线沟通
服务时间:9:30-18:00
你可能遇到了下面的问题
关闭右侧工具栏
Delphi的对象机制浅探
  • 作者:xiaoxiao
  • 发表时间:2020-12-23 10:58
  • 来源:未知

Delphi的对象机制浅探 savetime2k@yahoo.com 2004-1-3 前几天开始阅读 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} 结论:这只是我想了解字符串返回值的传递方式。 ===================        (完) ===================