Delphi的对象机制浅探 (100分)

  • 主题发起人 savetime
  • 开始时间
S

savetime

Unregistered / Unconfirmed
GUEST, unregistred user!
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}

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

===================
(完)
===================
 
写的非常好!
 
这么厉害啊!一个晚上就完成了!
 
高手就是高手,学习.
 
  楼主一个晚上完成,只能说佩服了!
 
厉害,不知 Delphi 的对象机制和 Java 、C# 相比,哪个个的效率更高?
 
写的非常好!
 
牛呀,学习 ,学习,在学习
 
小弟還有一事不知。
通過asm訪問類 的私有變量.
TA = class
private
FA: Integer;
public
procedure SetA(Value: Integer);
end;
var
Form1: TForm1;

implementation
{$R *.dfm}
procedure TForm1.Button1Click(Sender: TObject);
var
A: TA;
tmpInt: Integer;
begin
A := TA.Create;
tmpInt := 0;
A.SetA(100);
asm
MOV ECX, A.FA
//為什麼A.FA => 100 立即數就可以顯示為 100, 如果不改就顯示為其它值。
MOV tmpInt, ECX;
end;
ShowMessage(IntToStr(tmpInt));
A.Free;
end;
 
to 积步:

DELPHI 对“MOV ECX, A.FA”生成的代码实际上是以 Record 的类型生成的,这样 ECX 的值就是
变量 A 的地址 加上 TA.FA 的偏移值,结果是 ECX 变成堆栈上的一个值,所以不对。

A.FA 的实际地址是 A 指向的地址(也就是对象内存位置,而不是 A 的地址)加上 FA 相对于对象
头部的偏移地址。我凑出以下的代码,可以实现你要的结果:
var
A: TA;
tmpInt: Integer;
begin
A := TA.Create;
tmpInt := 0;
A.SetA(100);
asm
MOV EBX, A
MOV ECX, TA(EBX).FA
// 通知编译器 EBX 指向的是 TA class
MOV tmpInt, ECX;
end;
ShowMessage(IntToStr(tmpInt));
A.Free;
end;

我没写过汇编代码,所以不知道 DELPHI 会不会自动保护其他使用 EBX 的语句。如果你知道在混合
汇编的情况下如何使用寄存器请教我一下。

其实我真的不知道还有这种方法可以获得私有成员,有趣!
 
内容更正:

我发现自己在上文注释中的一个错误,在以下汇编第三行的“ push ecx ”我把它注释为
“保存 ecx (无用的语句)”,
更正为:
“分配局部变量 Obj 的堆栈空间”。

原来 add esp, -$4 花费 3 个字节的指令
而 push ecx 只要 1 个字节的指令,执行更快

大家现在知道我不是高手了吧,我是一边翻汇编手册,一边写注释的。我只能毫不谦虚地
说:我真的是初学者。高手看这些简单的汇编代码哪里需要花一个晚上。

希望大家关注内容,不要只是说“好”,重要的是“有没有错误”,这样才能提高。


出错的注释段:
=================================================
⊙ 测试目标:查看 TObject.Create 的编译器实现
=================================================
⊙ 测试代码及反汇编代码:
procedure Test
register;
var
Obj: TObject

begin
push ebp
前2句用于设置堆栈指针
mov ebp, esp
push ecx
保存 ecx (无用的语句) -> (更正为) 分配局部变量 Obj 的堆栈空间
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

 
to 积步,
我测试了在混合汇编的情况下修改寄存器时的实现,结果是:Delphi 会自动把嵌入汇编中修改了的寄存器备份在堆
栈中,所以可以随意使用 Delphi 允许的寄存器。
 
结论:真是精妙!一个对象的正常的创建(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 则会感觉对象的创建非常清晰。
-------------------------------------------------------------------------------
为什么要保存经常调用dl的值?
Dl主要用来表示是class级别调用还是对象级别调用,
class级别调用时,constructor会自动执行
System._ClassCreate,NewInstance,InitInstance,AfterConstruction等过程,
然后才是Constructor中的代码,
而对象级别调用时,只会执行Constructor中的代码。
 
to book523,

你说的是我在“⊙测试目标:以 object reference 和 class reference 调用构造函数的编译器实现”中的结果吧。我在文章中写的结论是:“object reference 方式的 constructor 调用,编译器尝试实现为 inherited 调用”

其实如果你看了测试代码的反汇编过程,就应该知道我的这个结论是错误的。Borland 在 Object Pascal Reference 中写的就是你说的“对象级别调用时,只会执行Constructor中的代码。”,可是事实上不是这样。

在形式如下的代码中
AComp := AComp.Create
Borland 先将 edx 所有 bit 设置为 1 ,也就是 dl 为 1,也就是仍然尝试沿用 class 级别的调用。可是你看 Delphi 的生成的汇编代码不知所云,所以根本就没有所谓的“对象级别调用”。我认为 Borland 应该对 AComp := AComp.Create 调用提示为语法错误。我实在是想不到什么时候会需要这种形式的调用。

我测试这样的调用方式是因为我经常在创建 Form 时忘了写 T 这一标识符:
ChildForm := ChildForm.Create(nil)
// 这里应该是 TChildForm.Create(nil)
我想知道这样的结果是什么。
 
高手就是高手,学习.
 
to savetime:
多謝指教。
以前也研究過匯編什麼東東的, 但是現在沒有多少時間研究。
 
procedure TForm1.Button1Click(Sender: TObject);
var
A: TA;
tmpInt: Integer;
begin
A := TA.Create;
tmpInt := 0;
A.SetA(100);
asm
MOV EAX, A;
MOV EAX, [EAX + 4];
//這樣也行 MOV EAX, [EAX] + 4 都是得到當前對象的第一變量
//MOV EAX, [EAX] + 8 訪問第二個變量, 依此類推
//MOV EAX, [EAX], 是指向VMT的指針
MOV tmpInt, EAX;
end;
ShowMessage(IntToStr(tmpInt));
A.Free;
end;

這樣也可以實現, 但是想不到savetime兄弟還有更高的招術, 厲害厲害!!
 
我看‘中国有救了‘,真有耐力!呵呵
心不静,做不了
 
to 积步,
我又从你这里学到一点汇编知识,知识就是这样积累出来的呀。

to everybody,
其实要看懂文中的汇编并不难,我做的这件工作只是 Delphi 的基础而已。读懂和会用根本上是两回事。
我现在正在分析 TWinControl 如何如何封装 Windows 的消息系统,二天过去,进展不大。相关的函数太多了,还有一些汇编夹在其中。我认为消息系统才是 VCL 的关键地方。整个程序的执行过程全在里面。
 

Similar threads

A
回复
0
查看
993
Andreas Hausladen
A
I
回复
0
查看
733
import
I
I
回复
0
查看
686
import
I
I
回复
0
查看
500
import
I
顶部